- Home
- >
- Mobile apps development
- >
- How to connect Angular with Firebase
Here we are continuing this tutorial with connecting Angular with Firebase. In the previous article of Angular 5, you get to know about Angular, Typescript and Angular CLI. As well as how to build your app with Angular 5. In that article, you learned App composition in Angular, Component communication and working with native events as well as Angular Forms.
To create a backend for your Angular 5 App. You need to create a demo project in Firebase and then connect Angular with Firebase.
Working with Backends
Here you are creating a backend for your Angular 5 app. Since you are not going to build server-side here. You can use Firebase for your API. If you actually do have your own API backend you can also configure your back-end in development server.
To do that create proxy.conf.json in the roof of the project and add this content there:
{
"/api": {
"target": "http://localhost:3000",
"secure": false
}
}
For every request from your app to its host. Which you remember is Webpack dev server. The /api root server should proxy the request to
http://localhost:3000/api. For that, to work, you also need to add one more thing to your app configuration; in package.json, as well as you need to replace the start command for your project.
[...]
"scripts": {
[...]
"start": "ng serve --proxy-config proxy.conf.json",
Now, you can run your project with yarn start or npm start and get proxy configuration in place. How can you work with the API from Angular? Angular gives you HttpClient. Let’s define your CardService for your current application:
import { Injectable } from '@angular/core';
import {HttpClient} from '@angular/common/http';
@Injectable()
export class CardService {
constructor(private http: HttpClient) { }
get() {
return this.http.get(`/api/v1/cards.json`);
}
add(payload) {
return this.http.post(`/api/v1/cards.json`, {text: trim(payload)});
}
remove(payload) {
return this.http.delete(`/api/v1/cards/${payload.id}.json`);
}
update(payload) {
return this.http.patch(`/api/v1/cards/${payload.id}.json`, payload);
}
}
So what does Injectable here mean? You already established that Dependency Injection helps you to inject your components with the services you use.But for getting access to your new service, you need to add it to the provider list in your AppModule :
[...]
import { CardService } from './services/card.service';
[...]
@NgModule({
[...]
providers: [CardService],
Now you can inject it in your AppComponent, for example:
import { CardService } from './services/card.service';
[...]
constructor(private cardService: CardService) {
cardService.get().subscribe((cards: any) => this.cards = cards);
}
Connecting Angular With Firebase
So let’s configure Firebase now, creating a demo project in Firebase and hitting the Add Firebase to your app button. Then, we copy credentials that Firebase shows us into the Environment files of your app, here: src/environments/.
export const environment = {
[...]
firebase: {
apiKey: "[...]",
authDomain: "[...]",
databaseURL: "[...]",
projectId: "[...]",
storageBucket: "[...]",
messagingSenderId: "[...]"
}
};
You need to add it both environment.ts and environment.prod.ts. and just to give you some understanding of what environment files are here, they are actually included in the project on compilation phase, and .prod. the part being defined by the –environment switch for ng serve or ng build. You can use values from that file in all parts of your projects and include them from environment.ts while Angular CLI takes care of providing content from the corresponding environment.your-environment.ts .
Add your Firebase support Libraries:
yarn add [email protected] angularfire2
yarn add v1.3.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[...]
success Saved lockfile.
success Saved 28 new dependencies.
[...]
Done in 40.79s.
And now just change your CardService to support Firebase:
import { Injectable } from '@angular/core';
import { AngularFireDatabase, AngularFireList, AngularFireObject } from 'angularfire2/database';
import { Observable } from 'rxjs/Observable';
import { Card } from '../models/card';
@Injectable()
export class CardService {
private basePath="/items";
cardsRef: AngularFireList<Card>;
cardRef: AngularFireObject<Card>;
constructor(private db: AngularFireDatabase) {
this.cardsRef = db.list('/cards');
}
getCardsList(): Observable<Card[]> {
return this.cardsRef.snapshotChanges().map((arr) => {
return arr.map((snap) => Object.assign(snap.payload.val(), { $key: snap.key }) );
});
}
getCard(key: string): Observable<Card | null> {
const cardPath = `${this.basePath}/${key}`;
const card = this.db.object(cardPath).valueChanges() as Observable<Card | null>;
return card;
}
createCard(card: Card): void {
this.cardsRef.push(card);
}
updateCard(key: string, value: any): void {
this.cardsRef.update(key, value);
}
deleteCard(key: string): void {
this.cardsRef.remove(key);
}
deleteAll(): void {
this.cardsRef.remove();
}
// Default error handling for all actions
private handleError(error: Error) {
console.error(error);
}
}
You see something interesting here, on the first model for the card being imported. So Let’s take a look at its composition.
export class Card {
$key: string;
text: string;
constructor(text: string) {
this.text = text;
}
}
So you are structuring your data with classes and, aside from your text, you add $key from Firebase. Let’s change your AppComponent to work with that service.
[...]
import { AngularFireDatabase } from 'angularfire2/database';
import {Observable} from 'rxjs/Observable';
import { Card } from './models/card';
[...]
export class AppComponent {
public cards$: Observable<Card[]>;
addCard(cardText: string) {
this.cardService.createCard(new Card(cardText));
}
constructor(private cardService: CardService) {
this.cards$ = this.cardService.getCardsList();
}
What is cards$? You mark your observable variables by adding $ to them to make sure you treat them as you should. So add your cards$ to the AppComponent template.
[...]
<app-card-list [cards]="cards$"></app-card-list>
But in return, you get this error in the console:
CardListComponent.html:3 ERROR Error: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.
Why so? you are getting observables from the Firebase. But your *ngFor in the CardList component waits for the array of objects, not observable of such arrays. so you can subscribe to that observable and assign it to a static array of cards, but there is a better option:
<app-card-list [cards]="cards$ | async"></app-card-list>
The asynchronous pipe, which is practically another syntax sugar that Angular gives to you. Subscribe to the Observable and return its current value as a result of the evaluation of your expression.
Reactive Angular – Ngrx
Let’s talk about your application state. Application state means all properties of your application that define its current behaviour as well as state literally. State is a single, Immutable data structure – the way Ngrx implements for you. Ngrx is an “RxJS powered state management library for Angular applications, inspired by Redux“.
“Redux is a pattern for managing application state.“ So it’s more like a set of conventions that allow you to answer the question of how your application should decide its needs to display some interface element. Or where it is supposed to store its session state after receiving it from the server.
Let’s see how it can be achieved. So you already know about State as well as its immutability. Which means you can’t change any of its properties after creating it. So this makes it all impossible to store your application state in your state. But not completely, every single state is immutable, but the store which is your way of accessing state is actually an observable of the state.
So State is a single value in any stream of store values. In order to change the app’s state not only, you need to make some Action that will take your current state but also replace it with a new one. Both are immutable but the second one is based on the first, so instead of mutating values on your State, you create a new State object. For that you use Reducers as pure functions, meaning that for any given State and Action as well as its payload reducer. It will return the same state as in any other call of that reducer function with same parameters.
Actions consist of action type and optional payload:
export interface Action {
type: string;
payload?: any;
}
For your task, let’s view how the action for adding a new card could be:
store.dispatch({
type: 'ADD',
payload: 'Test Card'
});
And also see a reducer for that:
export const cardsReducer = (state = [], action) => {
switch(action.type) {
case 'ADD':
return {...state, cards: [...cards, new Card(action.payload)]};
default:
return state;
}
}
This function is being called for every new Action event. So if you dispatch your ADD_CARD action, it’ll get into that case statement. What is happening there? You are returning your new State based on your previous State by using TypeScript spread syntax, so you don’t have to use something like Object.assign in most cases. You never should change your state outside of those case statements, because it will make life miserable as you waste time searching for the reason why your code is behaving unpredictably.
Let add Ngrx to your application. For that, run next in your console:
yarn add @ngrx/core @ngrx/store ngrx-store-logger
yarn add v1.3.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[...]
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 2 new dependencies.
├─ @ngrx/[email protected]
└─ @ngrx/[email protected]
└─ [email protected]
Done in 25.47s.
Now, add your Action definition ( app/actions/cards.ts ):
import { Action } from '@ngrx/store';
export const ADD = '[Cards] Add';
export const REMOVE = '[Cards] Remove';
export class Add implements Action {
readonly type = ADD;
constructor(public payload: any) {}
}
export class Remove implements Action {
readonly type = REMOVE;
constructor(public payload: any) {}
}
export type Actions
= Add
| Remove;
And your Reducer definition ( app/reducers/cards.ts ):
import * as cards from '../actions/cards';
import { Card } from '../models/card';
export interface State {
cards: Array<Card>;
}
const initialState: State = {
cards: []
}
export function reducer(state = initialState, action: cards.Actions): State {
switch (action.type) {
case cards.ADD:
return {
...state,
cards: [...state.cards, action.payload]
};
case cards.REMOVE:
const index = state.cards.map((card) => card.$key).indexOf(action.payload);
return {
...state,
cards: [...state.cards.slice(0, index), ...state.cards.slice(index+1)]
};
default:
return state;
}
}
Here you can see how you can use spreads as well as native TypeScript functions like map to drop the element off your list.
Let’s go one step further and make sure that if your application state will contain more than one type of data, you are composing it from a separate isolated state for each kind. So for that, use module resolution using ( app/reducers/index.ts ):
import * as fromCards from './cards';
import {ActionReducer, ActionReducerMap, createFeatureSelector, createSelector, MetaReducer} from '@ngrx/store';
import {storeLogger} from 'ngrx-store-logger';
import {environment} from '../../environments/environment';
export interface State {
cards: fromCards.State;
}
export const reducers: ActionReducerMap<State> = {
cards: fromCards.reducer
}
export function logger(reducer: ActionReducer<State>): any {
// default, no options
return storeLogger()(reducer);
}
export const metaReducers: MetaReducer<State>[] = !environment.production
? [logger]
: [];
/**
* Cards Reducers
*/export const getCardsState = createFeatureSelector<fromCards.State>('cards');
export const getCards = createSelector(
getCardsState,
state => state.cards
);
You also include a logger for your Ngrx in the development environment and create a selector function for your card array. So include it in your
AppComponent :
import { Component } from '@angular/core';
import { CardService } from './services/card.service';
import { Observable } from 'rxjs/Observable';
import { Card } from './models/card';
import * as fromRoot from './reducers';
import * as cards from './actions/cards';
import { Store } from '@ngrx/store';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
public cards$: Observable<Card[]>;
addCard(card: Card) {
this.store.dispatch(new cards.AddCard(card));
}
constructor(private store: Store<fromRoot.State>) {
this.cards$ = this.store.select(fromRoot.getCards);
}
}
So, you see how can you dispatch your actions using your store. But this code is still non-usable, as you don’t include your reducers (reducer as well as metaReducer) into your app. Let’s do it by changing your AppModule :
[...]
import { StoreModule } from '@ngrx/store';
import {reducers, metaReducers} from './reducers/index';
[...]
imports: [
[...]
StoreModule.forRoot(reducers, { metaReducers }),
[...]
And now it’s working. Kind of. Remember, you can use something like ngrx-store-localstorage to store your data the browser’s localStore, but how about working with APIs? Maybe you can add your previous API integration into your Reducer? But you can’t, as your Reducer function is supposed to be a pure function. So, “evaluation of the result does not cause any semantically observable side effect or output, such as mutation of mutable objects or output to I/O devices”… What can you do with that? The answer is actually right in that definition. Side-effects of Ngrx to the rescue.
Ngrx effects
So what is a side effect? Its piece of code that catches your Actions more or less the same way as your reducers do, but instead of changing something in your state, they actually send API requests and, on the result, dispatch new Actions.
As always, it’s simpler to show you than to tell you. Let’s make your new configuration support Firebase. For that, install the effects module:
yarn add @ngrx/effects
[...]
success Saved 1 new dependency.
└─ @ngrx/[email protected]
Done in 11.28s.
You will also add new actions to your Card Actions for loading support ( src/app/actions/cards.ts ):
[...]
export const LOAD = '[Cards] Load';
export const LOAD_SUCCESS = '[Cards] Load Success';
export const SERVER_FAILURE = '[Cards] Server failure';
[...]
export class Load implements Action {
readonly type = LOAD;
}
export class LoadSuccess implements Action {
readonly type = LOAD_SUCCESS;
constructor(public payload: any) {}
}
export class ServerFailure implements Action {
readonly type = SERVER_FAILURE;
constructor(public payload: any) {}
}
[...]
export type Actions
[...]
| Load
| LoadSuccess
| ServerFailure
So you have three new actions, one for loading the card list and two for dealing with successful and unsuccessful responses. Let’s implement your effects ( src/app/effects/cards.ts ):
import {Injectable} from '@angular/core';
import {Actions, Effect} from '@ngrx/effects';
import {CardService} from '../services/card.service';
import { of } from 'rxjs/observable/of';
import * as Cards from '../actions/cards';
import {exhaustMap, map, mergeMap, catchError} from 'rxjs/operators';
@Injectable()
export class CardsEffects {
@Effect()
loadCards$ = this.actions$
.ofType(Cards.LOAD).pipe(
mergeMap(action => {
return this.cardService.getCardsList().pipe(
map(res => new Cards.LoadSuccess(res)),
catchError(error => of(new Cards.ServerFailure(error))))}
)
);
@Effect({dispatch: false})
serverFailure$ = this.actions$
.ofType(Cards.SERVER_FAILURE).pipe(
map((action: Cards.ServerFailure) => action.payload),
exhaustMap(errors => {
console.log('Server error happened:', errors);
return of(null);
}));
constructor(
private actions$: Actions,
private cardService: CardService) {}
}
So you have injectable CardsEffects, which use the @Effect decorator for defining effects on top of your Actions and filtering only necessary actions by using the ofType operator. You may use ofType to create an effect that will be fired on multiple action types.
But for now, you only need two out of your three actions. For the Load action, you are transforming every action into a new observable on the result of your getCardList method call.
In the case of success, the observable will be mapped to a new action LoadSuccess with a payload of your request results, but in the case of error, you’ll return a single ServerFailure action. Mind the of operator there—it converts a single value or array of values to the observable.
So your Effects dispatch new Actions after making something that depends on the external system (your Firebase, to be precise). But within the same code, you see another effect, which handles the ServerFailure action using the decorator parameter dispatch: false.
What does this mean? As you can see from its implementation, it also maps your ServerFailure action to its payload and then displays this payload (your server error) to console.log. Clearly, in that case, you should not change state contents, so you don’t have to dispatch anything. And that’s how you make it work without any need for empty actions.
So, now that you’ve covered two of your three actions, let’s move on to LoadSuccess. From what you know so far, you are downloading a list of cards from the server and you need to merge them into your State. So you need to add it to your reducer ( src/app/reducers/cards.ts ):
[...]
switch (action.type) {
[...]
case cards.LOAD_SUCCESS:
return {
...state,
cards: [...state.cards, ...action.payload]
}
[...]
So same story as before, you open your object and card array in it by using the spread operator and join it with the spread payload (cards from the server, in your case ). Let’s add your new Load action to your AppComponent.
[...]
export class AppComponent implements OnInit {
public cards$: Observable<Card[]>;
addCard(card: Card) {
this.store.dispatch(new cards.AddCard(card));
}
constructor(private store: Store<fromRoot.State>) {
}
ngOnInit() {
this.store.dispatch(new cards.Load());
this.cards$ = this.store.select(fromRoot.getCards);
}
}
This should load your card from Firebase. Let’s take a look at the browser:
Something is not working. You are clearly dispatching the Action, as can be seen from your logs, but no server request is here for you. What’s wrong? you forget to load your effects to your AppModule. Let’s do that:
[...]
import { EffectsModule } from '@ngrx/effects';
import { CardsEffects } from './effects/cards.effects';
[...]
imports: [
[...]
EffectsModule.forRoot([CardsEffects])
So now back to the browser:
Now it’s working. So that’s how you integrate effects into loading data from the server. But you still need to send back there on your card creation. Also, make that works, For that change your CardService createCard method:
createCard(card: Card): Card {
const result = this.cardsRef.push(card);
card.$key = result.key;
return card;
}
And add an effect for the Adding card:
@Effect()
addCards$ = this.actions$
.ofType(Cards.ADD).pipe(
map((action: Cards.Add) => action.payload),
exhaustMap(payload => {
const card = this.cardService.createCard(payload);
if (card.$key) {
return of(new Cards.LoadSuccess([card]));
}
})
);
So if the card is to be created, it will get $key from Firebase and you will merge it into your card array. You also need to remove the case cards.ADD: branch from your reducer. try it in action:
For some reason, you are getting duplicated data on the card add operation. Try to figure out why. If you look closely at the console, you’ll see two LoadSuccess actions first being dispatched with your new card as it is supposed to be, the second one is being dispatched with both of your cards. If not in effect, where in your action is it being dispatched?
Your load effect on cards has this code:
return this.cardService.getCardsList().pipe(
map(res => new Cards.LoadSuccess(res)),
And your getCardsList is observable. So when you add a new card to your card collection, it is output. So either you don’t need to add that card on your own, or you need to use a take(1) operator in that pipe. It’ll take a single value and unsubscribe. But having live subscription seems more reasonable (presumably, you will have more than one user in the system), so let’s change your code to deal with the subscription.
Let’s add a non-dispatching element to your effect:
@Effect({dispatch: false})
addCards$ = this.actions$
.ofType(Cards.ADD).pipe(
map((action: Cards.Add) => action.payload),
exhaustMap(payload => {
this.cardService.createCard(payload);
return of(null);
})
);
Now you only need to change the reducer LoadSuccess to replace the cards, not combine them:
case cards.LOAD_SUCCESS:
return {
...state,
cards: action.payload
};
And now it’s working as it should:
So you can implement remove action the same way now. As you get data out of that subscription, you only have to implement the Remove effect.
Preparing for production
So let’s build your app for production use. And for that, let’s run the build command:
ng build --aot -prod
65% building modules 465/466 modules 1 active ...g/getting-started-ng5/src/styles.scssNode#moveTo was deprecated. Use Container#append.
Date: 2018-01-09T22:14:59.803Z
Hash: d11fb9d870229fa05b2d
Time: 43464ms
chunk {0} 0.657b0d0ea895bd46a047.chunk.js () 427 kB [rendered]
chunk {1} polyfills.fca27ddf9647d9c26040.bundle.js (polyfills) 60.9 kB [initial] [rendered]
chunk {2} main.5e577f3b7b05660215d6.bundle.js (main) 279 kB [initial] [rendered]
chunk {3} styles.e5d5ef7041b9b072ef05.bundle.css (styles) 136 kB [initial] [rendered]
chunk {4} inline.1d85c373f8734db7f8d6.bundle.js (inline) 1.47 kB [entry] [rendered]
So what’s happening here? You are building your application to static assets that could be served from any web server (if you want to serve from subdirectory ng build, have the option –base-href ). By using -prod, you are telling AngularCLI that you need the production build. And –aot is telling it that you like to have ahead-of-time compilation. In most cases, it is preferable, as it allows you to get the smaller bundle and faster code. Also, keep in mind that AoT is way too strict on your code quality, so it may produce errors that you haven’t seen before. Run the build earlier so it’s easier to fix.
If you want to develop an app with angular or any other technology and it seems difficult, there is no issue Codersera is here to help you by providing the top coder for your project:- Click here to Hire A Coder
And if you belong to the ones who are passionate about coding, Codersera can give you the opportunity to work on interesting and challenging projects. All you need to do is:- Click here to Apply As Coder
Source: InApps.net
Let’s create the next big thing together!
Coming together is a beginning. Keeping together is progress. Working together is success.