After adding user management, we can now move to the next user story that allows users to add stories to their favorites. Favorites data is stored in the Firebase Cloud Firestore. We’ll use AngularFire2 to interact with Firestore. The favorites page is only accessible to authenticated users, and we’ll use authentication guard to protect this page. After reading this chapter, you should know how to interact with Firebase Cloud Firestore.
Favorites Service
Favorites data is stored in the Firebase Cloud Firestore. Even though Cloud Firestore is still in beta at the time of writing, it’s still recommended for you to give it a try. If you have used MongoDB or other document-oriented databases before, you may find the concepts in Cloud Firestore are quite familiar. Cloud Firestore stores data in documents, while documents are stored in collections. A document is a lightweight record that contains fields, which map to values. Collections can be nested to create a hierarchical data structure.
The favorites data is stored in the collection favorites. Each document in this collection has the ID matching a user’s ID. These user documents have a subcollection items that contains documents for favorite items of the current user. Each document of the favorite items collection has the ID matching the item’s ID and a field timestamp representing the time when the favorite item is added. The timestamp is used to sort the favorite items to make sure newly added items appear first in the list. The path of a favorite item looks like /favorites/<user_id>/items/<item_id>.
AngularFire2 already has support for Cloud Firestore. We only need to import the module AngularFirestoreModule, then we can use the injected service AngularFirestore to interact with Cloud Firestore.
We start from the FavoritesService in Listing 10-1. The method collection returns the items collection for a user with a given userId. The method add adds a new item to the collection with a current timestamp. The method remove deletes an item from the collection. The method list returns the list of favorite items in the collection. orderBy('timestamp', 'desc') specifies that the items are sorted in descending order by timestamp.
import { Injectable } from '@angular/core';
import { AngularFirestore } from '@angular/fire/firestore';
Favorite in Listing 10-2 is the type to describe a favorite item. It contains two properties: itemId and timestamp.
export interface Favorite {
itemId: number;
timestamp: number;
}
Listing 10-2
Favorite
State Management
NgRx is used to manage the state related to favorites. Listing 10-3 shows the actions that can change state. The action Load means loading the current user’s favorite items. The action Add means adding an item to the favorite. The action Remove means removing an item from the favorite. These three actions all have two other actions to represent the results. The payload of these actions is straightforward.
readonly type = FavoritesActionTypes.RemoveSuccess;
constructor(public payload: number) {}
}
export class RemoveFailure implements Action {
readonly type = FavoritesActionTypes.RemoveFailure;
constructor(public payload: number) {}
}
export type FavoritesActions = Load | LoadSuccess | LoadFailure
| Add | AddSuccess | AddFailure
| Remove | RemoveSuccess | RemoveFailure;
Listing 10-3
Actions
The state, reducer function, and selectors are shown in Listing 10-4. The interface FavoritesItem describes the favorite items. The property loading specifies whether the action to modify it is still in progress. For example, when removing a favorite item, the property loading is updated to true. This property is useful for UI to display the loading indicators. The state for favorites also uses EntityState from @ngrx/entity, just like the feature items. When creating the EntityAdapter, we provide the sortComparer to sort by the timestamp.
In the reducer function, we use different functions of the adapter to update the state. For the action LoadSuccess, loaded favorite items are added to the entities store; for the actions Add and Remove, the property loading is set to true; for the action AddSuccess, the added favorite item is saved and the property loading is set to false; for the action RemoveFailure, only the property loading is set to false; for the actions RemoveSuccess and AddFailure, the favorite item is removed.
The function inFavorite returns a selector to check whether an item is in the favorite. The function getLoading returns a selector to check whether an item in in the loading status. The selector getFavoriteItems returns all favorite items.
import { FavoritesActions, FavoritesActionTypes } from '../actions/favorites';
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { createFeatureSelector, createSelector } from '@ngrx/store';
import * as fromAuth from '../../auth/reducers';
import { getItemEntities } from '../../reducers/items';
import { FavoritesService } from '../services/favorites.service';
The FavoritesEffects in Listing 10-5 uses FavoritesService to perform different actions. Because favorites data is stored for each user, we need to access the store to get the information of the current logged-in user. The effect load$ uses the method list to load all favorite items. Here we also need to dispatch an action itemsActions.Load to trigger the loading of those items. The effects add$ and remove$ use corresponding methods of FavoritesService and dispatch actions based on the results.
In the top stories page, we need to add a control for each item to allow a user to add a story to the favorite or remove a story from the favorite. The component FavoriteToggleComponent in Listing 10-6 displays different icons based on the item’s status in the favorites. The input property itemId is the id of the item to check. The property inFavorite specifies whether the item is in the favorite. The property loading specifies whether the action is still in progress. The EventEmitters toAdd and toRemove emit the item id when the user clicks the icon to toggle the favorite status.
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
@Component({
selector: 'app-favorite-toggle',
templateUrl: './favorite-toggle.component.html',
styleUrls: ['./favorite-toggle.component.scss']
})
export class FavoriteToggleComponent {
@Input() itemId: number;
@Input() inFavorite: boolean;
@Input() loading: boolean;
@Output() toAdd = new EventEmitter<number>();
@Output() toRemove = new EventEmitter<number>();
constructor() { }
add() {
this.toAdd.emit(this.itemId);
}
remove() {
this.toRemove.emit(this.itemId);
}
}
Listing 10-6
FavoriteToggleComponent
Listing 10-7 shows the template of the component FavoriteToggleComponent. The component displays different elements based on the values of input properties loading and inFavorite.
The component FavoriteToggleComponent is very simple as it only has plain input properties. The actual logic to deal with the store is encapsulated in the component FavoriteToggleContainerComponent in Listing 10-8. FavoriteToggleContainerComponent only has one input property itemId. isLoggedIn$ is the observable of the user’s login status using the selector fromAuth.getLoggedIn; inFavorite$ is the observable of the item’s favorite status using the function fromFavorites.inFavorite to create the selector; loading$ is the observable of the item’s loading status using the function fromFavorites.getLoading to create the selector. The methods add and remove dispatch actions with the item’s id.
import { Component, Input, OnInit } from '@angular/core';
import * as fromFavorites from '../../reducers';
import * as fromAuth from '../../../auth/reducers';
import { select, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { Add, Remove } from '../../actions/favorites';
Listing 10-9 shows the template of the component FavoriteToggleComponent. We simply use the pipe async to extract the values as properties for FavoriteToggleComponent.
<app-favorite-toggle *ngIf="isLoggedIn$ | async"
[itemId]="itemId"
[inFavorite]="inFavorite$ | async"
[loading]="loading$ | async"
(toAdd)="add()"
(toRemove)="remove()"></app-favorite-toggle>
Listing 10-9
Template of FavoriteToggleContainerComponent
Favorites Page
The page to show favorite items is very simple. In Listing 10-10, we use the selector fromFavorites.getFavoriteItems to get all favorite items. In the method ngOnInit, we dispatch the action Load to load all favorite items.
@Component({
selector: 'app-favorites-list',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './favorites-list.component.html',
styleUrls: ['./favorites-list.component.scss']
})
export class FavoritesListComponent implements OnInit {
The favorites page can only work when the user is logged in, so we can get the user id to retrieve the favorites data. If the user accesses the favorites page directly, we should redirect to the login page. This check can be done using authentication guards in Angular Router.
Listing 10-12 shows the AuthGuard to check for user login. AuthGuard implements the interface CanActivate from Angular Router. In the method canActivate, we use the selector fromAuth.getLoggedIn to get the login state. If the user is not logged in, we dispatch an action authActions.LoginRedirect to redirect the user to the login page.
After finishing the state management of favorites data, we still need to deal with the integration with authentication. When a user is logged in, we need to load the favorites data. This is done by dispatching the action Load in effect login$ of AuthEffects. When a user is logged out, we need to clear the favorites data. This is done by adding a new action Clear with the reducer function to clear all entities in the favorites; see the updated reducer function in Listing 10-15. The action Clear is dispatched in the effect logout$ of AuthEffects.
export function reducer(state = initialState, action: FavoritesActions) {
switch (action.type) {
case FavoritesActionTypes.Clear: {
return adapter.removeAll(state);
}
}
}
Listing 10-15
Remove all favorites data
Figure 10-1 shows the screenshot of the top stories page with the icon for favorites.
Summary
In this chapter, we implemented the user story to manage favorites, including adding stories into the favorites and removing stories from the favorites. Favorites data is stored in the Firebase Cloud Firestore. We implemented the service to interact with Firebase. We also created the page to list stories in the favorites. Since the favorites feature is only enabled when the user is logged in, we also discussed the integration with the authentication in this chapter. In the next chapter, we’ll implement the user story to share stories.