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

Ole Ersoy
8 min readNov 22, 2019

--

Image by xresch from Pixabay

Recent Version

The most recent of this article can be found here:

Introduction

We will be creating the NgRx demo application from scratch using the Firefly Semantics Slice Light Weight Reactive Application State Manager:

We will be doing this incrementally in phases, so that all the small implementation details are revealed.

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

As we are iterating through the implementation of the app we will review the parts of the design that are specific to state management with Slice.

This is the final result.

Open this in a separate tab and review the files we are describing for design details.

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

Click on dependencies and enter the following and accept the additional dependencies that need to be installed:

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

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 :

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. For an introduction to the approach see:

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 modeling 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:

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.

The application state service to check whether the user is authenticated:

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:

The service also has the methods for performing API queries:

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 :

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 }
}

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.

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 matInputplaceholder="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 :

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

That calls the AppStateService.setAuthenticationError method causing authenticationError$: Observable<boolean> to broadcast true . This in turns causes the UI to display the error.

Nav Item Presentation Component

Thesrc/app/core/nav-item.component.ts renders nav 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.

Create the app.component.html file:

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.

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:

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 .

App Module

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

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.

Summary

In this guide we reviewed the implementation of the NgRx demo application using the Firefly Semantics Slice 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.

--

--

Ole Ersoy

Founder of Firefly Semantics Corporation