Recreating the NgRx Demo App With the Firefly Semantics Slice State Manager
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 componentellipsis.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.