Recreating the NgRx Demo App With the Firefly Semantics Slice State Manager | Guide

Ole Ersoy
Mar - 16  -  19 min

Introduction

We will be creating the NgRx demo application from scratch using the Firefly Semantics Slice Lightweight Reactive Application State Manager.

The NgRx Demo allows us to search the Google Books API and place books in our own collection.

We will be doing this incrementally in phases describing the Slice state management implementation details as we go along.

Final Stackblitz Demo

To play around with this demo log in with the username test and use any password.

Stackblitz

Go to Stackblitz and start a brand new Angular project.

Delete the hello.component.ts and remove it from app.module.ts correspondingly.

Remove the <hello name=”{{ name }}”></hello> from app.component.html.

Dependencies

The dependencies for this project are as follows:

  • @angular/material
  • @fireflysemantics/slice
  • @fireflysemantics/material-base-module
  • @ngneat/until-destroy
  • @types/nanoid
  • nanoid

These need to be installed using NPM via the command line in a desktop angular project.

Stackblitz will automatically pull in peer dependencies, so when Slice is declared as a dependency it automatically pulls in @types/nanoid and naonoid.

The dependency @ngneat/until-destroy is used to automatically unsubscribe when a component is destroyed.

Styling and Icons

Update src/styles.css with:

@import "~@angular/material/prebuilt-themes/indigo-pink.css";

Add the material icons cdn link to src/index.html:

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

Model

The folder src/app/model will hold our model types within index.ts:

export type Book = {
  id: string;
  volumeInfo: {
    title: string;
    subtitle: string;
    authors: string[];
    publisher: string;
    publishDate: string;
    description: string;
    averageRating: number;
    ratingsCount: number;
    imageLinks: {
      thumbnail: string;
      smallThumbnail: string;
    };
  };
};

export type Credentials = {
  username: string;
  password: string;
};

export type User = {
  name: string;
};

The Book model represents book instances we have retrieved via the Google Books API.

The Credentials are login credentials.

The User type represents an authenticated user.

Services

The folder src/app/services will contain all our services.

Application State Service

The application state service allows us to observe the state of:

  • Side navigation (Open or Closed)
  • User ( Null if not logged in)
  • Authentication Error

These have been modeled in a Firefly Semantics Slice Object Store. Click on this link for an introduction.

import { Injectable } from '@angular/core';
import {
  KeyObsValueReset,
  ObsValueReset,
  OStore,
  OStoreStart,
} from '@fireflysemantics/slice';

import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { User } from '../model';

//===================================
//Create the User Key for local storage
//===================================
export const USER_KEY = 'USER_KEY';

interface ISTART extends KeyObsValueReset {
  SIDENAV: ObsValueReset;
  USER: ObsValueReset;
  AUTHENTICATION_ERROR: ObsValueReset;
}

@Injectable({ providedIn: 'root' })
export class AppStateService {
  START: OStoreStart = {
    SIDENAV: { value: false, reset: false },
    USER: { value: null, reset: null },
    AUTHENTICATION_ERROR: { value: false, reset: false },
  };
  public OS: OStore<ISTART> = new OStore(this.START);

  //===================================
  //State parameters that we wish to observe
  //===================================
  public sideNavOpen$: Observable<boolean> = this.OS.S.SIDENAV.obs;
  public user$: Observable<User> = this.OS.S.USER.obs;
  public isLoggedIn$: Observable<boolean> = this.user$.pipe(map((u) => !!u));
  public authenticationError$: Observable<boolean> =
    this.OS.S.AUTHENTICATION_ERROR.obs;

  constructor() {
    const user = JSON.parse(localStorage.getItem(USER_KEY)) || null;
    this.OS.put(this.OS.S.USER, user);
  }

  /**
   * If the user is logged out just call
   * updateUser() without an argument.
   */
  updateUser(user?: User) {
    if (user) {
      this.OS.put(this.OS.S.USER, user);
      localStorage.setItem(USER_KEY, JSON.stringify(user));
    } else {
      this.OS.put(this.OS.S.USER, null);
      localStorage.setItem(USER_KEY, null);
    }
  }

  setAuthenticationError(e: boolean) {
    this.OS.put(this.OS.S.AUTHENTICATION_ERROR, e);
  }

  /**
   * Toggle the sidenav
   */
  toggleSidenav() {
    this.OS.put(this.OS.S.SIDENAV, !this.OS.snapshot(this.OS.S.SIDENAV));
  }
}

The interface ISTART is used to name the reactive state properties and it gives us autocomplete for these values on the store:

interface ISTART extends KeyObsValueReset {
    SIDENAV: ObsValueReset;
    USER: ObsValueReset;
    AUTHENTICATION_ERROR: ObsValueReset;
}

The START object provides the initial set of values for the reactive state:

START: OStoreStart = {
    SIDENAV: { value: false, reset: false },
    USER: { value: null, reset: null },
    AUTHENTICATION_ERROR: { value: false, reset: false }
};

The Object Store (OStore) is created like this:

public OS: OStore<ISTART> = new OStore(this.START);

For convenience we will provide typed Observable references within the app-state.service:

//===================================
//State parameters that we wish to observe
//===================================
public sideNavOpen$: Observable<boolean> = this.OS.S.SIDENAV.obs;
public user$: Observable<User> = this.OS.S.USER.obs;
public isLoggedIn$: Observable<boolean> = this.user$.pipe(map((u) => !!u));
public authenticationError$: Observable<boolean> = this.OS.S.AUTHENTICATION_ERROR.obs;

In the constructor we also load the user from local storage:

const user = JSON.parse(localStorage.getItem(USER_KEY)) || null;
this.OS.put(this.OS.S.USER, user);

If the user does not log out this will simulate the user remaining authenticated. The updateUser method is called by the login component when the user authenticates:

/**
 * If the user is logged out just call
 * updateUser() without an argument.
 */
updateUser(user?: User) {
if (user) {
    this.OS.put(this.OS.S.USER, user);
    localStorage.setItem(USER_KEY, JSON.stringify(user));
} else {
    this.OS.put(this.OS.S.USER, null);
    localStorage.setItem(USER_KEY, null);
    }
}

The setAuthenticationError allows us to indicate when the login does not succeed:

setAuthenticationError(e: boolean) {
   this.OS.put(this.OS.S.AUTHENTICATION_ERROR, e);
}

The toggleSidenav method is a utility for setting the sidenav state:

/**
 * Toggle the sidenav
 */
toggleSidenav() {
    this.OS.put(this.OS.S.SIDENAV, 
    !this.OS.snapshot(this.OS.S.SIDENAV));
}

Authentication Service

The auth.service.ts authenticates the user:

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { AppStateService } from './app-state.service';
import { Credentials } from '../model';

export type Creds = { password: string; username: string };

@Injectable({ providedIn: 'root' })
export class AuthService {
  constructor(private s: AppStateService, private r: Router) {}

  login({ username, password }: Credentials) {
    if (username !== 'test') {
      this.s.setAuthenticationError(true);
    } else {
      this.s.setAuthenticationError(false);
      this.s.updateUser({ name: username });
      this.r.navigate(['/']);
    }
  }

  logout() {
    this.s.updateUser();
    this.r.navigate(['/login']);
  }
}

This service uses the application state service to update the user on login:

this.state.updateUser({name:username});

And clear the user on logout:

this.state.updateUser();

Authentication Guard Service

The auth-guard.service.ts guards the application from being accessed when the user is not authenticated.

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { AppStateService } from './app-state.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private s: AppStateService, private router: Router) {}

  canActivate(): Observable<boolean> {
    return this.s.isLoggedIn$.pipe(
      switchMap((isLoggedIn) => {
        if (!isLoggedIn) {
          this.router.navigate(['/login']);
        }
        return of(isLoggedIn);
      })
    );
  }
}

If the user is not authenticated then the router navigates to /login:

return this.appState.isLoggedIn$.pipe(
   switchMap((isLoggedIn) => {
      if (!isLoggedIn) {
         this.router.navigate(['/login']);
}

Book Service

The book.service.ts contains the Entity Stores for book search results and the book collection.

For an introduction to Entity Stores see Introduction to the Firefly Semantics Slice Reactive Entity Store.

The service also has the methods for performing API queries:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import {
  map,
  tap,
  filter,
  debounceTime,
  distinctUntilChanged,
} from 'rxjs/operators';
import { Book } from '../model';
import { EStore } from '@fireflysemantics/slice';

@Injectable()
export class BookService {
  private API_PATH = 'https://www.googleapis.com/books/v1/volumes';

  public bookStore: EStore<Book> = new EStore();
  public bookCollection: EStore<Book> = new EStore();
  
  public books$: Observable<Book[]> = this.bookStore.observe();
  public collection$: Observable<Book[]> = this.bookCollection.observe();

  constructor(private http: HttpClient) {
    this.bookStore
      .observeQuery()
      .pipe(
        filter(Boolean),
        debounceTime(200),
        distinctUntilChanged(),
        tap(async (query: string) => {
          const bo: Observable<Book[]> = this.searchAPI(query);
          const books: Book[] = await bo.toPromise();
          this.bookStore.reset();
          this.bookStore.postA(books);
        })
      )
      .subscribe();
  }

  onSearch(query: string) {
    this.bookStore.query = query;
  }

  searchAPI(query: string): Observable<Book[]> {
    return this.http
      .get<{ items: Book[] }>(`${this.API_PATH}?q=${query}`)
      .pipe(map((books) => books.items || []));
  }

  getById(volumeId: string): Observable<Book> {
    return this.http.get<Book>(`${this.API_PATH}/${volumeId}`);
  }

  /**
   * Toggle the book collection with the `book` inttance.
   * @param book The book to remove or add.
   */
  toggleCollection(book: Book) {
    this.bookCollection.toggle(book);
  }
}

The book entity store is created like this:

public bookStore: EStore<Book> = new EStore();

And the entity store for the collection like this:

public bookCollection: EStore<Book> = new EStore();

We observe the books in the book entity store like this:

public books$: Observable<Book[]> = this.bookStore.observe();

The books$ observable is used by the search component to render books found by the Google Books API.

The collection$ is used to observe the book collection:

public collection$: Observable<Book[]> = this.bookCollection.observe();

This observable will be used by the collection component to render the books in our collection.

Listening For and Fetching Book Query Changes

When the user types in a search query we will store the query string on the bookStore.query property such that we can observe this property and react to changes in its state:

onSearch(query: string) {
    this.bookStore.query = query;
}

We implement the observation of this property in the constructor of the BookService:

constructor(private http: HttpClient) {
    this.bookStore.observeQuery().pipe(filter(Boolean),
      debounceTime(200),
      distinctUntilChanged(),
      tap(async (query: string) => {
        const bo: Observable<Book[]> = this.searchAPI(query);
        const books: Book[] = await bo.toPromise();
        this.bookStore.reset();
        this.bookStore.postA(books);
      })
    )
    .subscribe();
}

By calling this.bookStore.observeQuery() we create an observable that fires whenever the user creates keyup events in the search field.

We debounce these events so that we react to them at most every 200 milliseconds.

We then search the API for books that match the query, reset the store, and post the new search result to the store.

Posting the new results to the store will cause the books$ observable to emit the new search results and this will then be rendered by the search component.

The other methods in the service are for searching the Google Book API and toggling our book collection with Book instances:

toggleCollection(book:Book) {
    this.bookCollection.toggle(book);
}

Book Resolver Service

The book-resolver.service.ts makes the selected book available to the ViewBookComponent route. We wire it up like this:

{
    path: ':id',
    component: ViewBookComponent,
    resolve: { book: BookResolverService }
}
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, Router } from '@angular/router';
import { Book } from '../model';
import { BookService } from './book.service';

@Injectable()
export class BookResolverService implements Resolve<Book> {
  constructor(private router: Router, private bookService: BookService) {}
  async resolve(route: ActivatedRouteSnapshot) {
    const id = route.paramMap.get('id');
    let book = this.bookService.bookStore.findOneByID(id);

    if (!book) {
      try {
        book = await this.bookService.getById(id).toPromise();
        return book;
      } catch (err) {
        this.router.navigate(['/404']);
        return null;
      }
    }
    return book;
  }
}

The resolver first tries to lookup the book based on the provided :id parameter:

const id = route.paramMap.get('id');
let book = this.bookService.bookStore.findOneByID(id);

If it can't find the book it looks for it in the API:

book = await this.bookService.getById(id).toPromise();
return book;

If it still can't find it it will navigate to the NotFound component.

Pipes

Ellipsis Pipe

The src/app/pipes/ellipsis.pipe.ts abbreviates the title of the book if it is longer than 250 characters..

Commas Pipe

The src/app/pipes/commas.pipe.ts creates a string with correct grammar from the array of authors.

Components

Authors Presentation Component

The src/app/components/book/authors.component.ts renders the authors associated with a book.

Book Detail Presentation Component

The src/app/components/book/book-detail.component.ts renders the detailed display of a Book instance.

Collection Page Component

The src/app/components/book/collection.component.ts renders the book collection.

The collection rendered by the component is retrieved from the BookService like this:

this.books$ = this.bookService.
              bookCollection.observe().
              pipe(untilDestroyed(this));

Preview List Presentation Component

The src/app/components/book/preview-list.component.ts is used to preview books contained in the user collection and also to display books in the search result listing.

Preview Presentation Component

The src/app/components/book/preview.component.ts component renders a preview of a Book instance.

Search Smart Component

The src/app/components/book/search.component.ts binds the query to the bookStore.query parameter that we are observing in the BookService via this method.

search(query:string) {
    this.bookService.onSearch(query);
}

The search method is triggered when the user types in the search field. This is all that is needed to trigger the search. The search will update the books$ observable and then the search results are rendered by this part of the search component template:

<bc-book-preview-list
[books]="bookService.books$ | async">
</bc-book-preview-list>
Also note the template for the search field:
<mat-form-field>
<input matInput
placeholder="Search books"
[value]="bookService.bookStore.query"
(keyup)="search($event.target.value)" autocomplete=off>
</mat-form-field>

We are initializing the value of the field using the query string stored on the bookStore.query field. This way if we navigate away from the search component and come back it will remember our last query.

View Book Smart Component

The src/app/components/book/view-book.component.ts retrieves the book from the route:

this.book = this.route.snapshot.data['book'];

It then clears the bookStore active books:

this.bookService.bookStore.clearActive();
this.bookService.bookStore.addActive(this.book);
this.isSelectedBookInCollection$ = this.bookCollection$.pipe(map(() => this.bookCollection.contains(this.book)));

Then we add the book to the bookStore.active map of active books.

And finally we initialize the isSelectedBookInCollection$ observable so that we can notify the UI as to whether the currently active book is in the collection or not.

Login Component

The src/app/components/core/login.component.ts logs the user in via submit:

submit() {
if (this.form.valid) {
        this.authService.login(this.form.value);
    }
}

If there is an error the authService will notify the application of it by calling the AppStateService.setAuthenticationError method causing authenticationError$: Observable<boolean> to broadcast true:

if (username !== 'test') {
    this.state.setAuthenticationError(true);
}

This in turn causes the UI to display the error.

The component src/app/core/nav-item.component.ts renders navigation items.

Not Found Presentation Component

The src/app/components/core/not-found.component.ts is routed to when the user enters a path that is not valid, but more specifically it is used if the /books/:id path tries to access an :id that does not exist. The resolver will route to /404.

Side Nav Presentation Component

The component src/app/components/core/side-nav.component.ts renders the side navigation menu.

Toolbar Component

The component src/app/components/core/toolbar.component.ts renders the side toolbar.

Application Shell

The src/app/app.component.ts contains our application shell.

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AppStateService } from './services/app-state.service';
import { AuthService } from './services/auth.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(
    private router: Router,
    public s: AppStateService,
    public authService: AuthService
  ) {}

  signIn() {
    this.s.toggleSidenav();
    this.router.navigate(['login']);
  }
  logout() {
    this.s.toggleSidenav();
    this.authService.logout();
    this.router.navigate(['login']);
  }
}

The file app.component.html contains the application shell template:

<mat-sidenav-container fullscreen>
  <bc-sidenav [open]="s.sideNavOpen$ | async">
    <bc-nav-item (navigate)="signIn()" *ngIf="!(s.isLoggedIn$ | async)">
      Sign In
    </bc-nav-item>

    <bc-nav-item
      (navigate)="s.toggleSidenav()"
      *ngIf="s.isLoggedIn$ | async"
      routerLink="/books/collection"
      icon="book"
      hint="View your book collection"
    >
      My Collection
    </bc-nav-item>

    <bc-nav-item
      (navigate)="s.toggleSidenav()"
      *ngIf="s.isLoggedIn$ | async"
      routerLink="/books"
      icon="search"
      hint="Find your next book!"
    >
      Browse Books
    </bc-nav-item>

    <bc-nav-item (navigate)="logout()" *ngIf="s.isLoggedIn$ | async">
      Sign Out
    </bc-nav-item>
  </bc-sidenav>
  <bc-toolbar (openMenu)="s.toggleSidenav()"> Book Collection </bc-toolbar>
  <router-outlet></router-outlet>
</mat-sidenav-container>

When the user not signed in then the navigation item is shown via this *ngIf directive:

<bc-nav-item (navigate)="signIn()" *ngIf="!(state.isLoggedIn$ | async)">
   Sign In
</bc-nav-item>

Also note that the side navigation is gets the open and close state from the sideNavOpen$ observable:

<bc-sidenav [open]="state.sideNavOpen$ | async">

Modules

We will create a core.module.ts containing the core components used to boot the application. The book.module.ts is lazy loaded once the user authenticates and accesses the /books path.

Have a look at the Stackblitz demo to see the design of the various components.

Material Module

We will be getting all the Angular Material components from the module:

@fireflysemantics/material-base-module

The MaterialBaseModule needs to be imported into all the application modules, except for the app-routing.module.ts, since all the other modules use the various Angular Material components.

Book Module

The src/app/modules/book.module.ts is a lazy loaded module containing all the components used to search for and create a book collection.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { MaterialBaseModule } from '@fireflysemantics/material-base-module';
import { PipesModule } from '../pipes';

import { BookAuthorsComponent } from '../components/book/authors.component';
import { BookDetailComponent } from '../components/book/book-detail.component';
import { CollectionComponent } from '../components/book/collection.component';
import { BookPreviewListComponent } from '../components/book/preview-list.component';
import { BookPreviewComponent } from '../components/book/preview.component';
import { BookSearchComponent } from '../components/book/search.component';
import { ViewBookComponent } from '../components/book/view-book.component';

import { BookService } from '../services/book.service';
import { BookResolverService } from '../services/book-resolver.service';

export const routes: Routes = [
  { path: '', component: BookSearchComponent, pathMatch: 'full' },
  { path: 'collection', component: CollectionComponent },
  {
    path: ':id',
    component: ViewBookComponent,
    resolve: { book: BookResolverService },
  },
];

@NgModule({
  imports: [
    PipesModule,
    HttpClientModule,
    MaterialBaseModule,
    RouterModule.forChild(routes),
    CommonModule,
  ],
  declarations: [
    BookSearchComponent,
    BookPreviewListComponent,
    BookPreviewComponent,
    BookAuthorsComponent,
    BookDetailComponent,
    ViewBookComponent,
    CollectionComponent,
  ],
  providers: [BookService, BookResolverService],
})
export class BookModule {}

The module also contains the Routes for the the various components that the module bundles.

Pipes Module

The module src/app/modules/pipes.module.ts contains the pipes:

  • commas.pipe.ts: Used to format the authors component
  • ellipsis.pipe.ts: Used to abbreviate content

This module is imported into the books.module.ts.

Core Module

The core module contains all the custom components used to instantiate the application shell:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MaterialBaseModule } from '@fireflysemantics/material-base-module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { LoginComponent } from '../components/core/login.component';
import { NavItemComponent } from '../components/core/nav-item.component';
import { NotFoundComponent } from '../components/core/not-found.component';
import { SideNavComponent } from '../components/core/sidenav.component';
import { ToolbarComponent } from '../components/core/toolbar.component';

const COMPONENTS = [
  SideNavComponent,
  NavItemComponent,
  ToolbarComponent,
  LoginComponent,
  NotFoundComponent,
];

@NgModule({
  imports: [
    CommonModule,
    RouterModule,
    MaterialBaseModule,
    FormsModule,
    ReactiveFormsModule,
  ],
  declarations: COMPONENTS,
  exports: COMPONENTS,
})
export class CoreModule {}

App Routing Module

The app-routing.module.ts contains our main application routes. The books route handles loading lazy loading of the book.module.ts.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard } from './services/auth-guard.service';
import { LoginComponent } from './components/core/login.component';

const routes: Routes = [
  { path: '', redirectTo: '/books', pathMatch: 'full' },
  {
    path: 'books',
    loadChildren: () =>
      import('./modules/book.module').then((m) => m.BookModule),
    canActivate: [AuthGuard],
  },
  { path: 'login', component: LoginComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes, {})],
  exports: [RouterModule],
})
export class AppRoutingModule {}

App Module

The app.module imports the core.module, the app-routing.module, the Firefly Semantics Material Base Module and the Angular Forms Modules.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { CoreModule } from './modules/core.module';
import { AppRoutingModule } from './app-routing.module';
import { MaterialBaseModule } from '@fireflysemantics/material-base-module';

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    ReactiveFormsModule,
    CoreModule,
    AppRoutingModule,
    MaterialBaseModule,
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
})
export class AppModule {}

Application Component

Now we just need to ready our application component by dependency injecting the services it needs and declaring a the signIn and logout methods:

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AppStateService } from './services/app-state.service';
import { AuthService } from './services/auth.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(
    private router: Router,
    public s: AppStateService,
    public authService: AuthService
  ) {}

  signIn() {
    this.s.toggleSidenav();
    this.router.navigate(['login']);
  }
  logout() {
    this.s.toggleSidenav();
    this.authService.logout();
    this.router.navigate(['login']);
  }
}

The shell template looks like this:

<mat-sidenav-container fullscreen>
  <bc-sidenav [open]="s.sideNavOpen$ | async">
    <bc-nav-item (navigate)="signIn()" *ngIf="!(s.isLoggedIn$ | async)">
      Sign In
    </bc-nav-item>

    <bc-nav-item
      (navigate)="s.toggleSidenav()"
      *ngIf="s.isLoggedIn$ | async"
      routerLink="/books/collection"
      icon="book"
      hint="View your book collection"
    >
      My Collection
    </bc-nav-item>

    <bc-nav-item
      (navigate)="s.toggleSidenav()"
      *ngIf="s.isLoggedIn$ | async"
      routerLink="/books"
      icon="search"
      hint="Find your next book!"
    >
      Browse Books
    </bc-nav-item>

    <bc-nav-item (navigate)="logout()" *ngIf="s.isLoggedIn$ | async">
      Sign Out
    </bc-nav-item>
  </bc-sidenav>
  <bc-toolbar (openMenu)="s.toggleSidenav()"> Book Collection </bc-toolbar>
  <router-outlet></router-outlet>
</mat-sidenav-container>

And that’s it. Our application is ready. We hope you enjoyed this article and if you like Firefly Semantics Slice please star our Github Repository.

Material Icons

Within index.html we need to add our material icons:

<link
  href="https://fonts.googleapis.com/icon?family=Material+Icons"
  rel="stylesheet"
/>

<my-app>loading</my-app>

Summary

In this guide we reviewed the implementation of the NgRx demo application using the Firefly Semantics Slice Lightweight Reactive State Manager for web and mobile applications.

If you have any comments or suggestion for the guide please add them to the Slice Github Repository as an issue.

We would also love it if you could star the repository.