Part 3: Let’s build an Angular app using reactive programming with RxJS and ngrx.
Series
This post is part of a series on building a MEAN app using TypeScript with Angular Material and Reactive programming.
Source Code
You can download the source code and follow along or fork the repository on GitHub:
First, run the gulp tasks, then start the Node.js Express server.
$ gulp
$ chmod +x ./dist/bin/www
$ ./dist/bin/www
Then, serve the Angular client using the CLI:
$ ng serve
Goals
Our goals for this series are:
- Create a simple CRUD app similar to the Tour of Heros tutorial app for Angular.
- Create the REST API using Express written in TypeScript with Mongoose for persisting data to MongoDb.
- Use Angular Material and Angular Flex Layout for the UI.
- Use Reactive Extensions for JavaScript, specifically, RxJS and ngrx.
Project Structure
In the previous two parts of this series we built our server using Express and Mongoose written in TypeScript. Our TypeScript is transpiled into the root dist directory.
We also started setting things up in the root client directory, which contains our Angular client application. Let’s focus on building our application in the client directory using Angular Material, RxJS and ngrx.
├── client
│ ├── src
│ │ ├── app
│ │ │ ├── app-routing.module.ts
│ │ │ ├── app.component.html
│ │ │ ├── app.component.scss
│ │ │ ├── app.component.ts
│ │ │ ├── app.module.ts
│ │ │ ├── app.reducers.ts
│ │ │ ├── core
│ │ │ │ └── services
│ │ │ │ ├── heros.service.spec.ts
│ │ │ │ └── heros.service.ts
│ │ │ ├── heros
│ │ │ │ ├── heros-routing.module.ts
│ │ │ │ ├── heros.actions.ts
│ │ │ │ ├── heros.effects.ts
│ │ │ │ ├── heros.module.ts
│ │ │ │ ├── heros.reducers.ts
│ │ │ │ └── index
│ │ │ │ ├── index.component.html
│ │ │ │ ├── index.component.scss
│ │ │ │ ├── index.component.spec.ts
│ │ │ │ └── index.component.ts
│ │ │ ├── models
│ │ │ │ └── hero.ts
│ │ │ └── shared
│ │ │ ├── hero-create-dialog
│ │ │ │ ├── hero-create-dialog.component.html
│ │ │ │ ├── hero-create-dialog.component.scss
│ │ │ │ ├── hero-create-dialog.component.spec.ts
│ │ │ │ └── hero-create-dialog.component.ts
│ │ │ ├── heros-list
│ │ │ │ ├── heros-list.component.html
│ │ │ │ ├── heros-list.component.scss
│ │ │ │ ├── heros-list.component.spec.ts
│ │ │ │ └── heros-list.component.ts
│ │ │ ├── layout
│ │ │ │ ├── layout.component.html
│ │ │ │ ├── layout.component.scss
│ │ │ │ ├── layout.component.spec.ts
│ │ │ │ └── layout.component.ts
│ │ │ ├── shared.actions.ts
│ │ │ ├── shared.module.ts
│ │ │ ├── shared.reducers.ts
│ │ │ └── toolbar
│ │ │ ├── toolbar.component.html
│ │ │ ├── toolbar.component.scss
│ │ │ ├── toolbar.component.spec.ts
│ │ │ └── toolbar.component.ts
│ │ ├── environments
│ │ │ ├── environment.prod.ts
│ │ │ └── environment.ts
│ │ ├── index.html
│ │ ├── main.ts
│ │ ├── polyfills.ts
│ │ ├── styles.scss
│ │ └── tsconfig.app.json
│ ├── tsconfig.json
│ └── tslint.json
├── dist
├── gulpfile.js
├── gulpfile.ts
├── package.json
└── server
Review
- In the first part of the series we built a REST API using Express and Mongoose.
- We also wrote some tests using Mocha and Chai to verify that our API is working as expected.
- In the second part of the series we set everything up using Angular, Angular Material and Flex Layout.
In this final part of the series we will:
- Install and use RxJS and ngrx to build our application
- Refactor the toolbar to use reactive programming with RxJS and ngrx.
- Create a shared module with stateless components to make our application more modular.
- Create actions and a reducer function for opening and closing the sidebar.
- Create actions, effects and a reducer function for creating and removing heros.
- Add routing and navigation to our application using Angular’s router tied to the @ngrx/router-store.
- Open a dialog to create a hero.
- Create a module to list the heros
- Include a button to remove a hero.
Install RxJS and ngrx
The first thing we need to do is to install the Reactive Extensions for JavaScript (RxJS) library as well as several ngrx packages via npm:
$ npm install rxjs --save
$ npm install @ngrx/core --save
$ npm install @ngrx/effects --save
$ npm install @ngrx/router-store --save
$ npm install @ngrx/store --save
$ npm install reselect --store
$ npm install ngrx-store-freeze --save
If you are new to using RxJS and ngrx I would recommend that you check out another recent post I wrote where I created a simple authentication example app using RxJS and ngrx. You can also check out their documentation and more on their respective websites:
Refactor Toolbar
In the previous part of this series we installed and configured the Angular Material MdToolbarModule
module.
We also updated the client/src/app/app.component.html template to include a toolbar along with the title of our application.
Rather than continuing to build our application in a single HTML template, or module, we will start to modularize our application.
The first step is to move our toolbar into it’s own module.
I like to create a shared module for common modules, such as the toolbar, that will be used by our application. We can use the Angular CLI to generate the module for us:
$ ng g m shared
Let me explain what the command above does:
- The command we are executing is the ng command, for the Angular CLI.
- The first parameter to the command is g, for generate.
- The second paramter is m, for module
- The third parameter is the name of the module that we are generating, shared.
When we generate the module the CLI tells us that the module was created, but it provides us with a warning that we must provide the newly created module ourselves.
To do this, import the module into the client/src/app/app.module.ts file and then add the module to the array of imports
in the AppModule
decorator:
import { SharedModule } from './shared/shared.module';
@NgModule({
declarations: [AppComponent],
imports: [BrowserAnimationsModule, BrowserModule, SharedModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Note that the only item we added to the imports
property above was the SharedModule
module.
When you generated the SharedModule
, a new client/src/app/shared directory was created, along with a new file at client/src/app/shared/shared.module.ts.
After we generate components in our shared module we will declare them in this module file.
We will also likely use the exports
property to export components out of this module and into other modules in our application.
Let’s go ahead and create a new ToolbarComponent
component:
$ ng g c shared/toolbar
Let me break down the command again:
- We are executing the ng command for the Angular CLI.
- The first parameter, g, instructs the CLI to generate something.
- The second parameter, c, instructs the CLI to generate a new component.
- The fourth parameter instructs the CLI to generate a new component in our application’s root directory named toolbar within the client/src/app/shared directory.
Jump back to the client/src/app/shared/shared.module.ts file and modify the declarations
property in the decorator for our SharedModule
class to include the ToolbarComponent
:
import { ToolbarComponent } from './toolbar/toolbar.component';
@NgModule({
imports: [CommonModule],
declarations: [ToolbarComponent],
})
export class SharedModule {}
Note that above we included the ToolbarComponent
in the declarations
property array.
Before we go any further, we need to go back and do a little refactoring.
If you have been following along with each part in this series you will recall that we previously imported the MdToolbarModule
in our AppModule
.
Well, we are now going to move the toolbar out of the client/src/app/app.component.html template and into our new client/src/app/shared/toolbar/toolbar.component.html template.
Before we do that, we should remove the import of the MdToolbarModule
.
Remove the import statement on the top of the client/src/app/app.module.ts file, and then remove the MdToolbarModule
from the imports
array in the class decorator.
Here is what the AppModule
decorator should look like now:
@NgModule({
declarations: [AppComponent],
imports: [BrowserAnimationsModule, BrowserModule, SharedModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Next, remove the public title
string property from the AppComponent
class in client/src/app/app.component.ts.
and move it into our new ToolbarComponent
class:
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Output,
} from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-toolbar',
templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.scss']
})
export class ToolbarComponent {
@Output() public openSidenav = new EventEmitter<void>();
public title = 'Tour of Heros';
}
There are few other change to note:
- First, we are importing the
ChangeDetectionStrategy
enum, theEventEmitter
class as well as theOutput
decorator. - We are specifying the
changeDetection
property within the component toOnPush
. This will prevent Angular’s change detector from concerning itself with any changes that occur within this component, and all subsequent child components. Technically, what that means is that the change detection cycle is only executed once, during the component hydration phase. This is a good practice for stateless components. It also helps with the performance of our application. - Next, note that we have declared a public
openSidenav
property, which is anEventEmitter
. When the user clicks on the menu button within the toolbar we will emit this event. - Finally, we have moved the public
title
property out of theAppComponent
and into theToolbarComponent
.
Let’s update the client/src/app/shared/toolbar/toolbar.component.html template:
<md-toolbar>
<button md-icon-button (click)="openSidenav.emit()">
<md-icon>menu</md-icon>
</button>
{{ title }}
</md-toolbar>
In the ToolbarComponent
template:
- First, we are using Angular’s
<md-toolbar>
component to create a toolbar at the top of our application. - Within the toolbar we have a button with the
md-icon-button
directive. This will style our button appropriately for containing an icon. Further, we have an event binding for the click event, which will emit theopenSidenav
event that we declared as a public property of theToolbarComponent
. - We are also using Angular Material’s
<md-icon>
component to include the menu Material icon. - Finally, we use interpolation to output the public string
title
property value.
If you are using the Angular Language Service in your editor, you may have noted some errors being reported in our updated HTML template indicating that md-toolbar is an unknown element:
If you are unfamiliar with the Angular Language Service, check out my post on installing the Angular Language Service. It is an incredible tool for an Angular developer, and works with several popular editors.
The problem is that we removed the MdToolbarModule
module import from AppModule
, but we did not add it to our SharedModule
module.
Let’s go ahead and add a few Angular Material modules that we need, namely the MdIconModule
and the MdToolbarModule
to the SharedModule
module:
import { MdIconModule, MdToolbarModule } from '@angular/material';
@NgModule({
imports: [CommonModule, MdIconModule, MdToolbarModule],
declarations: [ToolbarComponent],
})
export class SharedModule {}
Then, export the ToolbarComponent
out of the SharedModule
module so that it is public, and can be used in other modules in our application:
@NgModule({
imports: [CommonModule, MdIconModule, MdToolbarModule],
declarations: [ToolbarComponent],
exports: [ToolbarComponent],
})
export class SharedModule {}
To export the component we include the class in the array of exports
in the class decorator for the SharedModule
in client/src/app/shared/shared.module.ts.
The ToolbarComponent
is now ready to be used by other modules in our application.
Actions
Let’s start diving into using reactive programming, specifically, using the ngrx library.
The first step is to define the actions for our shared module. Our shared module will have a single state, whether the sidenav is open or closed, which will be a boolean property. The actions on this state are pretty simple:
- open the sidenav
- close the sidenav.
Let’s create a new file at client/src/app/shared/shared.actions.ts:
import { Action } from '@ngrx/store';
export const SIDENAV_CLOSE = '[shared] Close Sidenav';
export const SIDENAV_OPEN = '[shared] Open Sidenav';
export class CloseSidenavAction implements Action {
readonly type = SIDENAV_CLOSE;
}
export class OpenSidenavAction implements Action {
readonly type = SIDENAV_OPEN;
}
export type Actions =
CloseSidenavAction
| OpenSidenavAction;
Let’s review:
- First, we import the
Action
interface from the @ngrx/store module. - Second, we define two string constant values to represent the actions:
SIDENAV_CLOSE
andSIDENAV_OPEN
. These must be unique values. - Third, we define two classes that implement the
Action
interface. Both classes contain a single readonly property namedtype
, which is the constant value we defined previously. - Fourth, we export the
Actions
type using the union operator.
Reducers
A reducer is a pure function that modifies the state of our application based on the action dispatched to the store. It is a pure function because we can expect the same value/behavior/output when provided the same input.
Create a new file: client/src/app/shared/shared.reducers.ts:
import { SIDENAV_CLOSE, SIDENAV_OPEN, Actions } from './shared.actions';
export interface State {
showSidenav: boolean;
}
const initialState: State = {
showSidenav: false,
};
export function reducer(state = initialState, action: Actions): State {
switch (action.type) {
case SIDENAV_CLOSE:
return {
...state,
...{
showSidenav: false,
},
};
case SIDENAV_OPEN:
return {
...state,
...{
showSidenav: true,
},
};
default:
return state;
}
}
export const getShowSidenav = (state: State) => state.showSidenav;
Let’s review the client/src/app/shared/shared.reducers.ts file:
- First, we import the constant values
SIDENAV_CLOSE
andSIDENAV_OPEN
, along with theActions
type from the shared.actions.ts file we previously created. - Second, we define our
State
interface. This will include all of the possible states for this module in our application. - Third, we define an
initialState
object that implements theState
interface. By default the sidenav will be closed. - The
reducer
function will accept two parameters: thestate
of the application, and theaction
being performed. The defaultstate
is theinitialState
. - Within the reducer function we switch on the readonly
type
property value that we defined for ourActions
. - Our reducer function will always return the
State
, defaulting to the currentState
. - Within the switch statement we have two cases, one for
SIDENAV_CLOSE
and another forSIDENAV_OPEN
. In each case we return a new state, assigning a new value to theshowSidenav
property. - Finally, we export a constant function named
getShowSidenav()
. This function will return the boolean value that indicates the state of our sidenav, whether it is open (true) or closed (false).
Now that we have our shared module reducer function defined, let’s create a single module within our application that will combine all of the module states for our application.
To do this, create a new file: client/src/app/app.reducers.ts:
import { createSelector } from 'reselect';
import { ActionReducer } from '@ngrx/store';
import { RouterState, routerReducer } from '@ngrx/router-store';
import { environment } from '../environments/environment';
import { compose } from '@ngrx/core/compose';
import { combineReducers } from '@ngrx/store';
import { storeFreeze } from 'ngrx-store-freeze';
import * as shared from './shared/shared.reducers';
export interface State {
router: RouterState;
shared: shared.State;
}
const reducers = {
router: routerReducer,
shared: shared.reducer,
};
const developmentReducer: ActionReducer<State> = compose(
storeFreeze,
combineReducers
)(reducers);
const productionReducer: ActionReducer<State> = combineReducers(reducers);
export function reducer(state: any, action: any) {
if (environment.production) {
return productionReducer(state, action);
} else {
return developmentReducer(state, action);
}
}
/**
* Shared Reducers
*/
export const getSharedState = (state: State) => state.shared;
export const getShowSidenav = createSelector(
getSharedState,
shared.getShowSidenav
);
There is a bit of ceremony here, but let’s quickly review:
- First, we import all of the necessary classes and functions from the various modules within the reselect and ngrx libraries.
- We then import everything from the shared/shared.reducers.ts module (file).
- We will use this to create a single
State
interface for our entire application. This includes therouter
state, which will be using the @ngrx/router-storeState
, as well as ourshared
state. - Finally, we define the
getShowSidenav
selector function using thecreateSelector
method.
LayoutComponent
The next step is to create a new LayoutComponent
.
This new component will serve as a wrapper, if you will, for the content of our application.
It will contain both the sidebar and the toolbar for the application, and we will use transclusion to embed our application’s content within the layout.
To get started, create the new component using the Angular CLI:
$ ng g c shared/layout
Our template is going to use two modules provided by Angular Material: the MdSidenavModule
and the MdButtonModule
.
Further, our template is also going to use the FlexLayoutModule
to easily implement a flex based layout.
So, let’s import these into the SharedModule
in client/src/app/shared/shared.module.ts:
import {
MdButtonModule,
MdIconModule,
MdSidenavModule,
MdToolbarModule
} from "@angular/material";
import { FlexLayoutModule } from "@angular/flex-layout";
@NgModule({
imports: [
CommonModule,
FlexLayoutModule
MdButtonModule,
MdIconModule,
MdSidenavModule,
MdToolbarModule
],
declarations: [
ToolbarComponent
],
exports: [
ToolbarComponent
]
})
export class SharedModule { }
Next, let’s update the client/src/app/shared/layout.component.html template that was generated by the CLI:
<md-sidenav-container>
<md-sidenav [opened]="open | async">
<md-list>
<md-list-item>
<button md-button (click)="heros()">Heros</button>
</md-list-item>
<md-list-item>
<button md-button (click)="add()">Add Hero</button>
</md-list-item>
</md-list>
</md-sidenav>
<app-toolbar (openSidenav)="openSidenav()"></app-toolbar>
<div fxLayout="column" fxLayoutAlign="start stretch">
<div fxLayout="row" fxLayoutAlign="center">
<div fxFlex="90%" fxFlex.md="66.6667%" fxFlex.gt-md="50%">
<ng-content></ng-content>
</div>
</div>
</div>
</md-sidenav-container>
Let’s review our new layout template:
- First, all of our content is wrapped using the
<md-sidenav-container>
element. - Next,
<md-sidenav>
is a direct child of the container element. We also have an input binding foropened
, whose value is bound to theopen
property within our component, which we will define shortly. Also note that we are using theasync
pipe since theopen
property is anObservable
. - Within the sidebare we are using the
<md-list>
element with a list of navigational buttons. Note that each native<button>
element has amd-button
directive, along with an event binding for the click event. We’ll implement theheros()
andadd()
methods shortly. - After the sidebar we implement our
<app-toolbar>
, binding to the outputopenSidenav
, which invokes theopenSidenav()
method. - I am using Flex Layout to specify a column, and then a row. This is enables us to have a simple and responsive layout of our content.
- Note the use of the
<ng-content>
element for transclusion. It’s a fancy word for taking any content that is between the opening<app-layout>
element and the closing</app-layout>
element to be transcluded (or inserted) into this placeholder. If you haven’t seen this before, don’t worry, it will make more sense soon.
With the template created, let’s update the LayoutComponent
class:
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from "rxjs/Observable";
import { Store } from "@ngrx/store";
import { go } from "@ngrx/router-store";
import { State, getShowSidenav } from "../../app.reducers";
import { OpenSidenavAction } from "../../shared/shared.actions";
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-layout',
templateUrl: './layout.component.html',
styleUrls: ['./layout.component.scss']
})
export class LayoutComponent implements OnInit {
public open: Observable<boolean>;
constructor(private store: Store<State>){}
ngOnInit() {
this.open = this.store.select(getShowSidenav);
}
public add() {
// placeholder
}
public heros() {
this.store.dispatch(go(["/heros"]));
}
public openSidenav() {
this.store.dispatch(new OpenSidenavAction());
}
}
As always, let’s review:
- First, we import the
ChangeDetectionStrategy
enum. - Next, we updated the
changeDetection
strategy for our component toOnPush
. As with ourToolbarComponent
, theLayoutComponent
is a stateless component. So, we can speed things up by telling Angular to not be concerned with any changes in our component after the initial hydration phase. - If you look back at our template we specified an input binding for the
<md-sidebar>
element, setting theopened
binding to the asynchronousopen
value. Here, we define the public property, after importing theObservable
class. The property’s generic type is set toboolean
. If the value is true, then our sidebar is open, and conversely, if the value is false, then the sidebar is not open, or closed. - We inject the ngrx
Store
into our component in theconstructor()
function. Note that theStore
generic type is set to our application’s combinedState
, which we imported from the client/src/app/app.reducers.ts module. - We have defined the
add()
method as a placeholder for adding a new hero. We will implement this later. - We also have a
heros()
method defined that will navigate to the /heros route using thego()
function, which we imported from the @ngrx/router-store module. - Finally, the
openSidenav()
method will use the store todispatch()
a new action, namely theOpenSidenavAction
that we defined in the client/src/app/shared/shared.actions.ts file.
Configure Store
We need to configure the store for our application.
To do this, we need to update the AppModule
:
import { StoreModule } from '@ngrx/store';
import { reducer } from './app.reducers';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserAnimationsModule,
BrowserModule,
SharedModule,
StoreModule.provideStore(reducer),
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Note that we invoke the static provideStore()
method on the StoreModule
, providing our application’s combined reducer
function from the client/src/app/app.reducers.ts module.
Hero
model
Our application is coming along nicely, and we have started using RxJS and ngrx. But, all we have is a sidenav and a button at this point.
So, what’s next? We need to build a model of our data.
If you recall from the first part of this series we built a REST API using Express and Mongoose.
We defined a collection in MongoDb named “Heros”, which has a single string property: name
.
So, similar to the model we defined for Mongoose, we will also define a model for our Angular application. I do see this as a bit of duplication, which we generally want to avoid, but this is the price we pay for having a client model and a server model.
Create a new client/src/app/models directory:
$ mkdir models
Then, create a new file: client/src/app/models/hero.ts:
export class Hero {
_id?: string;
name?: string;
}
Pretty simple, eh?
The _id
property is the MongoDb id for the document.
And, the name
property we defined to store the string
name of the hero.
HerosService
With our model defined, we now need to create a service in our Angular application that will use the Angular Http
service to create()
, delete()
, get()
, list()
and update()
our heros.
To get started, use the Angular CLI to generate a new service:
$ ng g s core/services/heros.service
Now, let’s modify the service at client/src/app/core/services/heros.service.ts:
import { Injectable } from '@angular/core';
import { Http, Response } from "@angular/http";
// rxjs
import { Observable } from "rxjs/Observable";
// models
import { Hero } from "../../models/hero";
@Injectable()
export class HerosService {
private readonly URL = "http://localhost:8080/api/heros"
constructor(
protected http: Http,
) {}
public create(hero: Hero): Observable<Hero> {
return this.http
.post(this.URL, hero)
.map(this.extractObject);
}
public delete(hero: Hero): Observable<Hero> {
return this.http
.delete(`${this.URL}/${hero._id}`)
.map(result => hero);
}
public get(id: string): Observable<Hero> {
return this.http
.get(`${this.URL}/${id}`)
.map(this.extractObject);
}
public list(): Observable<Array<Hero>> {
return this.http
.get(this.URL)
.map(response => response.json() || []);
}
public update(hero: Hero): Observable<Hero> {
return this.http
.put(`${this.URL}/${hero._id}`, hero)
.map(this.extractObject);
}
private extractObject(res: Response): Object {
const data: any = res.json();
return data || {};
}
}
Let’s review our new HerosService
:
- First, we import the
Injectable
interface from Angular. We decorate theHerosService
class usingInjectable()
. This enables the class to be injectable using Angular dependency injector (DI). - Next, we import the
Http
andResponse
classes. - Then, we import the
Observable
class from RxJS. Our methods will be returningObservable
objects for async operations. - Then, we import our newly created
Hero
model. - In the
constructor()
function we inject theHttp
service using Angular’s DI. - We then implement methods for our REST API’s verbs: DELETE, GET, PUT and POST. I’ve called these
delete()
,get()
,update()
andcreate()
, respectively. There is also alist()
method that GETs all of the hero documents in the MongoDb collection. - In each method we invoke the appropriate verb, supplying the necessary URL and data.
- Finally, I have an
extractData()
method which will safely extract any JSON data that is returned in the response object. If no data is returned, the method returns an empty object.
The next thing we need to do for the service to work is to specify the class in our AppModule
in the providers
array.
We also need to add the HttpModule
to the imports
array in AppModule
:
import { HttpModule } from '@angular/http';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserAnimationsModule,
BrowserModule,
HttpModule,
SharedModule,
StoreModule.provideStore(reducer),
],
providers: [HerosService],
bootstrap: [AppComponent],
})
export class AppModule {}
The HerosService
is now ready to be injected into our components as needed.
HerosModule
Create a new module in the application using the Angular CLI:
$ ng g m heros
Don’t forget to go back into the AppModule
and to import this new module:
import { HerosModule } from './heros/heros.module';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserAnimationsModule,
BrowserModule,
HerosModule,
HttpModule,
SharedModule,
StoreModule.provideStore(reducer),
],
providers: [HerosService],
bootstrap: [AppComponent],
})
export class AppModule {}
Note that we included the HerosModule
in the array of imports
.
Now, let’s update the generated client/src/app/heros/heros.module.ts file:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MdDialogModule, MdSnackBarModule } from '@angular/material';
import { SharedModule } from '../shared/shared.module';
@NgModule({
imports: [CommonModule, MdDialogModule, MdSnackBarModule, SharedModule],
declarations: [],
})
export class HerosModule {}
Here’s what we did:
- First, import the
MdDialogModule
andMdSnackbarModule
modules from @angular/material. We will be showing a dialog to create a new hero, and we will use the snackbar component to show the user a message when an error occurs. - Next, import the
SharedModule
. - Finally, we add these modules to the
imports
property for ourHerosModule
.
Heros Actions
Similar to the actions we defined for our shared module, we will also create actions for the heros module.
Create a new file: client/src/app/heros/heros.actions.ts:
import { Action } from '@ngrx/store';
import { Hero } from "../models/hero";
export const LOAD_HEROS = '[heros] Load heros';
export const LOAD_HEROS_ERROR = '[heros] Load heros error';
export const LOAD_HEROS_SUCCESS = '[heros] Load heros success';
export class LoadHerosAction implements Action {
readonly type = LOAD_HEROS;
}
export class LoadHerosErrorAction implements Action {
readonly type = LOAD_HEROS_ERROR;
constructor(public payload: { error: Error }) {}
}
export class LoadHerosSuccessAction implements Action {
readonly type = LOAD_HEROS_SUCCESS;
constructor(public payload: { heros: Hero[] }) {}
}
export type Actions =
LoadHerosAction
| LoadHerosErrorAction
| LoadHerosSuccessAction;
We have defined some initial actions to load the heros, along with actions for when there is an exception and when the action is successful.
Note that we have defined the constructor()
functions for the LoadHerosErrorAction
and LoadHerosSuccessAction
actions.
In each constructor()
function we defined a public parameter property named payload
, which is required.
The payload will either contain the Error
that was thrown, or the array of Hero
objects that were returned from the REST API.
Heros Reducer
Next, we need to define the reducer function for our actions in the heros module.
Create a new file: client/src/app/heros/heros.reducers.ts:
import {
LOAD_HEROS,
LOAD_HEROS_ERROR,
LOAD_HEROS_SUCCESS,
Actions,
} from './heros.actions';
import { Hero } from '../models/hero';
export interface State {
error?: Error;
heros: Hero[];
}
const initialState: State = {
heros: [],
};
export function reducer(state = initialState, action: Actions): State {
switch (action.type) {
case LOAD_HEROS:
return {
...state,
...{
error: undefined,
heros: [],
},
};
case LOAD_HEROS_ERROR:
return {
...state,
...{
error: action.payload.error,
},
};
case LOAD_HEROS_SUCCESS:
return {
...state,
...{
heros: action.payload.heros,
},
};
default:
return state;
}
}
export const getHeros = (state: State) => state.heros;
There are a few things to note:
- First, we import the
LOAD_HEROS
,LOAD_HEROS_ERROR
andLOAD_HEROS_SUCCESS
constant values along with theActions
type from the client/src/app/heros/heros.actions.ts module. - Next, we import the
Hero
model. - Then, we define the
State
interface for the heros module. In this case our state will have two properties:error
andheros
. Theerror
property is optional and is of typeError
. Theheros
property is required and is an array ofHero[]
objects. - Then, we define the
initialState
. Theerror
property isundefined
and ourheros
property is an empty array. - The
reducer()
function switches on the three actions:LOAD_HEROS
,LOAD_HEROS_ERROR
andLOAD_HEROS_SUCCESS
. - In each case we update the state as appropriate using the public
payload
property that we defined in theconstructor()
function for each action. - Finally, we have a
getHeros()
constant function that will return the array of heros in theState
. We will use this toselect()
the heros that have been asynchronously loaded from our REST API using theHerosService
we just created.
Now, we need to add the heros reducer to our client/src/app/app.reducers.ts module:
import * as heros from './heros/heros.reducers';
export interface State {
heros: heros.State;
router: RouterState;
shared: shared.State;
}
const reducers = {
heros: heros.reducer,
router: routerReducer,
shared: shared.reducer,
};
/**
* Heros Reducers
*/
export const getHerosState = (state: State) => state.heros;
export const getHeros = createSelector(getHerosState, heros.getHeros);
Note that I omitted some code above for simplicity. To add our heros reducers and state we:
- Import all exported members from the heros.reducers.ts module.
- Add a new
heros
property to theState
interface with a reference to theState
of our heros module. - Add a new
heros
property to thereducers
object with a reference to thereducer()
function defined in the heros.reducers.ts module.
Heros Effects
While we previously created actions and a reducer for our shared module to control the state of the sidenav, we didn’t have a need to implement any side effects. A side effect can be simply defined as something that we want to perform as a result of an action.
In this case we will invoke our REST API using the HerosService
for populating the array of heros
in our state.
Create a new file: client/src/app/heros/heros.effects.ts:
import { Injectable } from "@angular/core";
import { MdDialog, MdSnackBar } from "@angular/material";
import { Effect, Actions, toPayload } from "@ngrx/effects";
import { Action } from "@ngrx/store";
import { Observable } from "rxjs/Observable";
import { empty } from "rxjs/observable/empty";
import "rxjs/add/operator/catch";
import "rxjs/add/observable/of";
import "rxjs/add/operator/switchMap";
import { HerosService } from "../core/services/heros.service";
import {
LOAD_HEROS,
LOAD_HEROS_ERROR,
LoadHerosErrorAction,
LoadHerosSuccessAction
} from "./heros.actions";
@Injectable()
export class HeroEffects {
@Effect()
public loadHeros: Observable<Action> = this.actions
.ofType(LOAD_HEROS)
.map(toPayload)
.switchMap(payload => {
return this.herosService.list()
.map(heros => new LoadHerosSuccessAction({ heros: heros }))
.catch(error => Observable.of(new LoadHerosErrorAction({ error: error })));
});
@Effect()
public loadHerosError: Observable<Action> = this.actions
.ofType(LOAD_HEROS_ERROR)
.map(toPayload)
.switchMap(payload => {
this.mdSnackbar.open("Oops. Something went wrong.", null, {
duration: 1000
});
return empty();
});
constructor(
private actions: Actions,
private herosService: HerosService,
private mdDialog: MdDialog,
private mdSnackbar: MdSnackBar
) { }
}
Some things to note:
- First, we import all of the necessary classes, interfaces and methods.
- We import the
@injectable()
interface so theHeroEffects
class can be injected into theEffectsModule
. - We import the
MdDialog
andMdSnackbar
classes from @angular/material. This is so we can open a dialog and show a snackbar when something goes wrong. - We import the
Effect
decorator and theActions
observable from the @ngrx/effects module, along with thetoPayload()
function. TheEffect
decorator is used to decorate the properties in our effects class that will be invoked when an action is dispatched. TheActions
observable will be used to observe all actions that are dispatched to the store. We will use theofType()
method to filter out only the action that we want to implement an effect for. Finally, thetoPayload()
helper function will return just the payload of the currently dispatched action. - We import the
Action
interface from the @ngrx/store module as our effect properties will return anObservable
of anAction
. In most instances we will return a new action. For example, after loading our heros, if there is an exception then we will return a new instance of theLoadHerosErrorAction
. If the heros are loaded successfully, then we will return a new instance of theLoadHerosSuccessAction
with the payload of our array ofHero[]
objects specified. - We import the
empty()
observable. In the case of the error, our effect will return an observable that is empty. - We also import the
HerosService
so that we can interface with our REST API using the injectable service. - We have two public properties in our
HeroEffects
class:loadHeros
andloadHerosError
. Note that each property is a function that is decorated with the@Effect()
decorator. - As a side effect, we want to invoke the
HerosService.list()
method when theLOAD_HEROS
action is dispatched. - Further, as a side efect when an error occurs, we want to show a snackbar indicating that something went wrong. This is perhaps not the best error message, but at least we give some indication to our users that something went awry.
- Finally, we inject the necessary class instances into the
HeroEffects
class in theconstructor()
function.
Ok, now we need to import the EffectsModule
into the AppModule
, and then run()
each effect class.
In this instance, we are going to run()
our injectable HeroEffects
class.
As you build out your applications you will likely have multiple effects, for each child module of your application.
You will need to import each effect class and run()
each effect class in the AppModule
.
import { EffectsModule } from '@ngrx/effects';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserAnimationsModule,
BrowserModule,
EffectsModule.run(HeroEffects),
HerosModule,
HttpModule,
SharedModule,
StoreModule.provideStore(reducer),
],
providers: [HerosService],
bootstrap: [AppComponent],
})
export class AppModule {}
Note that we imported the EffectsModule
.
Then, in the imports
array for our AppModule
decorator we invoke the run()
static method, providing our injectable HeroEffects
class.
Our hero effects are now wired up.
IndexComponent
Before we get to configuring the routing and navigation for our application we need to create a component that will serve as the /heros index component.
As appropriate, we’ll name this IndexComponent
.
Using the CLI, generate the IndexComponent
in the heros module:
$ ng g c heros/index
Let’s update the generated client/src/app/heros/heros.component.html template:
<app-layout>
<h1>Heros</h1>
</app-layout>
We wrap our content using the <app-layout>
component.
If you recall we talked briefly about the idea of transclusion using the <ng-content>
component.
Now, we are seeing this in use.
Within the <app-layout>
element we have included a heading placeholder with the text “Heros”.
This header and text will be placed within the <app-layout>
template where we defined the <ng-content>
component.
We will later update this template to use a component to list the heros.
Now, let’s update the client/src/app/heros.component.ts component:
import { Component, OnInit } from '@angular/core';
import { Observable } from "rxjs/Observable";
import { Store } from "@ngrx/store";
import { State, getHeros } from "../../app.reducers";
import { Hero } from "../../models/hero";
import { LoadHerosAction } from "../heros.actions";
@Component({
selector: 'app-index',
templateUrl: './index.component.html',
styleUrls: ['./index.component.scss']
})
export class IndexComponent implements OnInit {
public heros: Observable<Array<Hero>>;
constructor(private store: Store<State>) { }
ngOnInit() {
this.heros = this.store.select(getHeros);
this.store.dispatch(new LoadHerosAction());
}
}
Our template is going to eventually need to obtain the list of heros, so we’ve gone ahead and implemented the code that will be required.
- First, we import the
Observable
class. The array ofheros
will be asynchronously retrieved from theHerosService
. - Next, we import the
Store
. - Then, we import our application’s combined
State
, as well as thegetHeros
selector function we previously created in the client/src/app/app.reducers.ts file. - Then, we import our
Hero
model. - Finally, we import the
LoadHerosAction
, which we previously defined in client/src/app/heros/heros.actions.ts. - Within the
IndexComponent
class we have defined a publicheros
property. This is anObservable
, whose generic type is defined as anArray
ofHero
objects. - We inject the
Store
into our component via theconstructor()
function. - Within the
ngOnInit()
lifecycle method we use our store to set theheros
property. Note that we use the store’sselect()
method, providing thegetHeros
function as the selector. - Finally, we
dispatch()
theLoadHerosAction
to our store.
Routing
Almost every Angular application will require some sort of routing for the user to navigate the application. As the documentation states:
The Angular Router enables navigation from one view to the next as users perform application tasks.
To get started, create a new file: client/src/app/app-routing.module.ts:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: 'heros',
loadChildren: './heros/heros.module#HerosModule',
},
{
path: '',
pathMatch: 'full',
redirectTo: '/heros',
},
{
path: '**',
redirectTo: '/404',
},
];
@NgModule({
exports: [RouterModule],
imports: [RouterModule.forRoot(routes)],
})
export class AppRoutingModule {}
Let’s review the AppRoutingModule
:
- First, we import the
NgModule
decorator and theRoutes
andRouterModule
.Routes
is simply a type that defines an array of Route interfaces to ensure the proper definition of ourroutes
array. And theRouterModule
is used for defining either our application’s root routes or child routes. - The
routes
array contains objects for each route that we are declaring. - The
path
property is a string that specifies the route matching DSL. - The
loadChildren
property references the module to lazy load. - The
pathMatch
specifies the path matching strategy. - The
redirectTo
property is a URI path fragment that replaces the currently matched segment.
We first define the routes for our application, and then we use the RouterModule.forRoot()
method to establish the root routes for our application.
Note that we are also lazy loading the HerosModule
using the loadChildren
property for the path heros.
When you first load the application and do not specify a path we will redirect to “/heros”. If the route path specified does not match any of the specified routes, we will redirect the user to a 404 page.
Now, create a new file client/src/app/heros/heros-routing.module.ts:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { IndexComponent } from './index/index.component';
const routes: Routes = [
{
path: '',
component: IndexComponent,
},
{
path: '**',
redirectTo: '/404',
},
];
@NgModule({
exports: [RouterModule],
imports: [RouterModule.forChild(routes)],
})
export class HerosRoutingModule {}
The heros module routing defines a single route for the IndexComponent
.
Again, we redirect to the 404 page when the route specified by the user is not found.
Note that we invoke the forChild()
method on the RouterModule
class in this instance.
This is because we are within a child module (/heros) and not at the root level, as required by the Angular router.
Next, we need to import the AppRoutingModule
into our AppModule
in client/src/app/app.module.ts.
We also need to import the RouterStoreModule
module from @ngrx/router-store:
import { AppRoutingModule } from './app-routing.module';
import { RouterStoreModule } from '@ngrx/router-store';
@NgModule({
declarations: [AppComponent],
imports: [
AppRoutingModule,
BrowserAnimationsModule,
BrowserModule,
EffectsModule.run(HeroEffects),
HttpModule,
HerosModule,
RouterStoreModule.connectRouter(),
SharedModule,
StoreModule.provideStore(reducer),
],
providers: [HerosService],
bootstrap: [AppComponent],
})
export class AppModule {}
We add the AppRoutingModule
to the array of imports
, and we also add the RouterStoreModule
to the array of imports
, invoking the connectRouter()
method.
The final step for our routing is to go back into the client/src/app/app.component.html template and to remove the existing contents and to include a single <router-outlet>
element:
<router-outlet></router-element>
When the router matches the URL to the /heros route and displays the IndexComponent
, the contents of the template will be placed after the <router-outlet>
element we have defined.
Demo
A couple of things should happen if we serve our application and test it:
- First, when we load the application without specifying a URI we should be redirected to the /heros path.
- Second, we should be able to see that the
list()
of heros is being loaded using our REST API.
Go ahead and use the CLI to serve the application:
$ ng serve
Here is a quick demo of our application so far:
Close Sidenav
But, you might notice an issue. While our sidenav opens, and we can close it by clicking on the container, we cannot open it again.
The issue is that we never toggle the boolean value to false, so the opened
input binding value on the <md-sidenav>
element is invalid.
We can fix this by using the backdropClick
output binding on the <md-sidenav-container>
in the client/src/app/shared/layout/layout.component.html template:
<md-sidenav-container
fullscreen
(backdropClick)="closeSidenav()"
></md-sidenav-container>
When the backdropClick
output event is emitted we invoke a new method named closeSidenav
in the LayoutComponent
:
import { CloseSidenavAction, OpenSidenavAction } from "../../shared/shared.actions";
export class LayoutComponent {
public closeSidenav() {
this.store.dispatch(new CloseSidenavAction());
}
}
The closeSidenav()
method simply dispatches the CloseSidenavAction
action, which we imported as well.
When the action is dispatched our reducer function will toggle the showSidenav
boolean property in the shared module State
to false
.
Create Actions
While our application will now list the heros in our MongoDb collection, we do not have any heros yet. And, that’s a pity.
So, I think the next logical step is to add the functionality to create a new hero. To do that, the first step is to create the necessary actions:
- Open the create hero dialog.
- Close the create hero dialog.
- Create a hero.
- A hero is successfully created.
- There is an error creating a hero.
Let’s add these new actions to the client/src/app/heros/heros.actions.ts module:
import { Action } from '@ngrx/store';
import { Hero } from "../models/hero";
export const CREATE_HERO = '[heros] Create hero';
export const CREATE_HERO_ERROR = '[heros] Create hero error';
export const CREATE_HERO_SUCCESS = '[heros] Create hero success';
export const CREATE_HERO_DIALOG_CLOSE = '[heros] Close create hero dialog';
export const CREATE_HERO_DIALOG_OPEN = '[heros] Open create hero dialog';
export const LOAD_HEROS = '[heros] Load heros';
export const LOAD_HEROS_ERROR = '[heros] Load heros error';
export const LOAD_HEROS_SUCCESS = '[heros] Load heros success';
export class CreateHeroAction implements Action {
readonly type = CREATE_HERO;
constructor(public payload: { hero: Hero }) {}
}
export class CreateHeroErrorAction implements Action {
readonly type = CREATE_HERO_ERROR;
constructor(public payload: { error: Error }) {}
}
export class CreateHeroSuccessAction implements Action {
readonly type = CREATE_HERO_SUCCESS;
constructor(public payload: { hero: Hero }) {}
}
export class CreateHeroDialogCloseAction implements Action {
readonly type = CREATE_HERO_DIALOG_CLOSE;
}
export class CreateHeroDialogOpenAction implements Action {
readonly type = CREATE_HERO_DIALOG_OPEN;
}
export class LoadHerosAction implements Action {
readonly type = LOAD_HEROS;
}
export class LoadHerosErrorAction implements Action {
readonly type = LOAD_HEROS_ERROR;
constructor(public payload: { error: Error }) {}
}
export class LoadHerosSuccessAction implements Action {
readonly type = LOAD_HEROS_SUCCESS;
constructor(public payload: { heros: Hero[] }) {}
}
export type Actions =
CreateHeroAction
| CreateHeroErrorAction
| CreateHeroSuccessAction
| CreateHeroDialogCloseAction
| CreateHeroDialogOpenAction
| LoadHerosAction
| LoadHerosErrorAction
| LoadHerosSuccessAction;
There are a couple of things to note:
- First, we define several new constant string values matching the actions that I outlined previously.
- Next, we define classes that implement the
Action
interface for each of the actions. - Each class has a readonly
type
property that is set to the appropriate constant string value. - As necessary, the class’s
constructor()
function is defined with the appropriatepayload
property object. - Finally, we add each class to the
Actions
type declaration.
Create Reducer
With our actions defined, our next step is to go to the client/src/app/heros/heros.reducers.ts module and to update the state for each action as necessary:
import {
CREATE_HERO,
CREATE_HERO_ERROR,
CREATE_HERO_SUCCESS,
LOAD_HEROS,
LOAD_HEROS_ERROR,
LOAD_HEROS_SUCCESS,
Actions,
} from './heros.actions';
import { Hero } from '../models/hero';
export interface State {
error?: Error;
hero?: Hero;
heros: Hero[];
}
const initialState: State = {
heros: [],
};
export function reducer(state = initialState, action: Actions): State {
switch (action.type) {
case CREATE_HERO:
return {
...state,
...{
error: undefined,
hero: undefined,
},
};
case CREATE_HERO_ERROR:
return {
...state,
...{
error: action.payload.error,
},
};
case CREATE_HERO_SUCCESS:
return {
...state,
...{
hero: action.payload.hero,
heros: [...state.heros, action.payload.hero],
},
};
case LOAD_HEROS:
return {
...state,
...{
error: undefined,
heros: [],
},
};
case LOAD_HEROS_ERROR:
return {
...state,
...{
error: action.payload.error,
},
};
case LOAD_HEROS_SUCCESS:
return {
...state,
...{
heros: action.payload.heros,
},
};
default:
return state;
}
}
export const getHero = (state: State) => state.hero;
export const getHeros = (state: State) => state.heros;
We have made several changes:
- First, we imported the new actions that we need. We don’t need to modify the state when opening or closing the dialog, so we don’t need to import those two actions.
- Next, we added a new
hero
property to theState
interface. This will store the current hero that we are editing. - Next, we added a case-statement for each new action.
- For the
CREATE_HERO
action we will reset theerror
andhero
properties toundefined
. - For the
CREATE_HERO_ERROR
action we will simply set theerror
property value to the error that is dispatched in the action. - For the
CREATE_HERO_SUCCESS
action we update thehero
property to the newly created hero, and we update ourheros
array. Note that I am using the array deconstructor to append the hero onto a new array. This is because theheros
object cannot be modified directly as it is frozen.
Create Dialog
Before we start coding up our side effects, we need to create a new HeroCreateDialogComponent
:
$ ng g c shared/hero-create-dialog
First, let’s update the client/src/app/shared/hero-create-dialog/hero-create-dialog.component.html template:
<div class="header">
<button md-dialog-close md-icon-button>
<md-icon>close</md-icon>
</button>
<h1 md-dialog-title>Create Hero</h1>
</div>
<md-dialog-content>
<form [formGroup]="form">
<md-input-container>
<input mdInput formControlName="name" placeholder="Name" />
</md-input-container>
</form>
</md-dialog-content>
<md-dialog-actions>
<button md-button (click)="submit()" color="primary">Create</button>
</md-dialog-actions>
If you are unfamiliar with Angular Material’s dialog component, I recommend you head over and read through the documentation.
In our dialog template:
- We create a new
div.header
element, which we will use to provide some styling and positioning to the close button. - We add a native
<button>
element with themd-dialog-close
directive. This will attach the necessary behavior to the button to close the dialog. We also use themd-icon-button
directive to give our button the necessary styling to appropriately display the icon. - We add a
h1
header using themd-dialog-title
directive for displaying the title text. - We use the
<md-dialog-content>
directive to wrap the dialog content. - We define a
<form>
with theformGroup
input binding to a public property within our component namedform
. This will be a newFormGroup
that we will define shortly. - We are using the
<md-input-container>
directive from Angular Material to wrap the native<input>
element, which will add style to our input using the Material design. - We have an input with the
mdInput
directive as required for Angular Materials input component. The<input>
element also specifies theformControlName
attribute with the valuename
, which will match up with our FormGroup definition. We also specify theplaceholder
attribute to provide a label for our input. - We use the
<md-dialog-actions>
directive to wrap the submit button. - We use a native
<button>
with an event binding for the click event to invoke thesubmit()
method. We will define this in theHeroCreateDialogComponent
next.
We are using Angular’s reactive forms module in this component.
Therefore, we need to import the ReactiveFormsModule
, as well as the FormsModule
, into our SharedModule
in client/src/app/shared/shared.module.ts file.
We also need to add the HeroCreateDialogComponent
to a new property named entryComponents
in our module decorator:
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
@NgModule({
entryComponents: [HeroCreateDialogComponent],
imports: [
CommonModule,
FlexLayoutModule,
FormsModule,
MdDialogModule,
MdButtonModule,
MdIconModule,
MdInputModule,
MdListModule,
MdSidenavModule,
MdToolbarModule,
ReactiveFormsModule,
],
declarations: [ToolbarComponent, HeroCreateDialogComponent, LayoutComponent],
exports: [ToolbarComponent, LayoutComponent],
})
export class SharedModule {}
Note that I omitted all of the imports at the top of the file, and only included the import of the ReactiveFormsModule
and FormsModule
.
Now, let’s move on to updating the HeroCreateDialogComponent
as necessary:
import {
ChangeDetectionStrategy,
Component,
OnInit
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { MdDialogRef } from "@angular/material";
import { Store } from "@ngrx/store";
import { State } from "../../app.reducers";
import { CreateHeroAction } from "../../heros/heros.actions";
import { Hero } from "../../models/hero";
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './hero-create-dialog.component.html',
styleUrls: ['./hero-create-dialog.component.scss']
})
export class HeroCreateDialogComponent implements OnInit {
public form: FormGroup;
constructor(
private formBuilder: FormBuilder,
private mdDialogRef: MdDialogRef<HeroCreateDialogComponent>,
private store: Store<State>
) { }
ngOnInit() {
this.form = this.formBuilder.group({
name: ["", Validators.required]
});
}
public submit() {
const hero: Hero = this.form.value;
this.store.dispatch(new CreateHeroAction({ hero: hero }));
this.mdDialogRef.close();
}
}
As always, let’s review:
- First, we import the
ChangeDetectionStrategy
enum. As this component is stateless, we save valuable compute resources by instructing Angular’s change detector to only check for changes a single time, when the component is hydrated. - Next, we import the
FormBuilder
,FormGroup
andValidators
classes from the @angular/forms module. We will need these to wire up our reactive form. - Next, import the
MdDialogRef
class. This will enable us to have a reference to the current dialog, enabling us to close the dialog after the user submits the form. - Next, we import the
Store
, along with our application’s combinedState
. - Next, we import the
CreateHeroAction
that we defined. - Finally, we import the
Hero
model. - Within the
HeroCreateDialogComponent
we declare a single publicform
property, which is aFormGroup
. - We inject the necessary class instances using our component’s
constructor()
function. - In the
ngOnInit()
lifecycle method we set the value of ourform
property using thegroup()
method. We also specify theValidators.required
method to ensure the name field value is not empty. - Finally, we have a
submit()
method which is invoked when the user clicks on the button in our template. We will first define a constanthero
, which is the value of the form. Then, wedispatch()
theCreateHeroAction
action to ourStore
, supplying the necessary payload object, which is an object with the singlehero
property. Then we invoke theclose()
method to close the dialog.
Let’s not forget to wire up our dialog to the “Add Hero” button in our sidebar.
If you recall, we created a placeholder add()
method in the LayoutComponent
class.
Let’s implement this now.
Import the CreateHeroDialogOpenAction
action and then update the add()
method in the LayoutComponent
class in client/src/app/shared/layout/layout.component.ts:
import { CreateHeroDialogOpenAction } from "../../heros/heros.actions";
export class LayoutComponent implements OnInit {
public add() {
this.store.dispatch(new CreateHeroDialogOpenAction());
}
}
The add()
method uses the store
to dispatch()
the CreateHeroDialogOpenAction
action.
Let’s also add some style to the HeroCreateDialogComponent
in the client/src/app/shared/hero-create-dialog/hero-create-dialog.component.scss file:
:host {
.header {
position: relative;
button[md-dialog-close] {
position: absolute;
top: -16px;
right: -16px;
}
}
md-dialog-content {
md-input-container {
min-width: 300px;
}
}
}
Create Hero Effects
While we defined the CreateHeroDialogOpenAction
action previously, we do not have a side effect for this.
So, nothing will happen when we click on the “Add Hero” button yet.
Let’s update the HeroEffects
class to include additional side effects for creating a hero in the client/src/app/heros/heros.effects.ts file:
import {
CREATE_HERO,
CREATE_HERO_ERROR,
CREATE_HERO_DIALOG_OPEN,
LOAD_HEROS,
LOAD_HEROS_ERROR,
CreateHeroErrorAction,
CreateHeroSuccessAction,
LoadHerosErrorAction,
LoadHerosSuccessAction
} from "./heros.actions";
import { HeroCreateDialogComponent } from "../shared/hero-create-dialog/hero-create-dialog.component";
export class HeroEffects {
@Effect()
public createHero: Observable<Action> = this.actions
.ofType(CREATE_HERO)
.map(toPayload)
.switchMap(payload => {
return this.herosService.create(payload.hero)
.map(hero => new CreateHeroSuccessAction({ hero: hero }))
.catch(error => Observable.of(new CreateHeroErrorAction({ error: error })));
});
@Effect()
public createHeroError: Observable<Action> = this.actions
.ofType(CREATE_HERO_ERROR)
.map(toPayload)
.switchMap(payload => {
this.mdSnackbar.open("Oops. Something went wrong.", null, {
duration: 1000
});
return empty();
});
@Effect()
public createHeroDialogOpen: Observable<Action> = this.actions
.ofType(CREATE_HERO_DIALOG_OPEN)
.map(toPayload)
.switchMap(payload => {
this.mdDialog.open(HeroCreateDialogComponent);
return empty();
});
}
Note, I have not included all of the effects that we previously defined, just the additional effects for the following actions: CREATE_HERO
, CREATE_HERO_ERROR
and CREATE_HERO_DIALOG_OPEN
.
A couple of other things to note:
- We import the constant
CREATE_HERO
,CREATE_HERO_ERROR
andCREATE_HERO_DIALOG_OPEN
string actions. - We also import the
CreateHeroErrorAction
andCreateHeroSuccessAction
actions. - We added three new properties that are decorated with the
@Effect()
decorator:createHero
,createHeroError
andcreateHeroDialogOpen
. - The
createHero
property will invoke thecreate()
method on theHeroService
instance to send the REST API request to create a new hero in MongoDb. Wemap()
each result to a newCreateHeroSuccessAction
action, supplying the necessary payload object with the requiredhero
parameter, which is the newly createdhero
object that is returned from the REST API. Wecatch()
any exceptions and return a newObservable.of()
theCreateHeroErrorAction
instance, again, supplying the necessary payload object with the requirederror
property. - The
createHeroError
property will show a snackbar to the user indicating that an error occurred. - The
createHeroDialogOpen
property willopen()
ourHeroCreateDialogComponent
dialog.
List Heros
While we can now add heros (hurray!), we still cannot see the list of our heros.
To do that, we’ll create a new HerosListComponent
component:
$ng g c shared/heros-list
Let’s start by modifying the generated client/src/app/shared/heros-list/heros-list.component.html template:
<h1>Heros</h1>
<md-list *ngIf="heros && heros.length > 0">
<md-list-item *ngFor="let hero of heros"> {{ hero.name }} </md-list-item>
</md-list>
<p \*ngIf="heros && heros.length === 0"><em>There are no heros. :(</em></p>
Here is a quick rundown of the template:
- We are using the Angular Material list component, which we will only include in the generated output when we have at least one hero using the
ngIf
directive. - Using the
ngFor
directive we iterate over the array ofheros
. For eachhero
we output the hero’sname
. - If there are no heros, we display a simple paragraph indicating as such.
Next, let’s implement the HerosListComponent
in client/src/app/shared/heros-list/heros-list.component.ts:
import {
ChangeDetectionStrategy,
Component,
Input
} from '@angular/core';
import { Hero } from "../../models/hero";
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-heros-list',
templateUrl: './heros-list.component.html',
styleUrls: ['./heros-list.component.scss']
})
export class HerosListComponent {
@Input() public heros: Hero[] = [];
}
This component might be the simplest yet:
- As this is another stateless component, we import the
ChangeDetectionStrategy
enum and set thechangeDetection
toOnPush
. This should start to feel familiar, and is good practice. - We also import the
Input
class to decorate theheros
property. Theheros
will be an input binding that will be provided in theIndexComponent
template. If you recall, we already wired up ourIndexComponent
to retreive the list of heros. - We import the
Hero
model, as we can expect that theheros
that are input into this component will be of typeHero
. - Finally, we declare the
heros
component as an array ofHero
objects. We set this to an empty array initially.
Now, let’s go back to our IndexComponent
template and include the list of heros in client/src/app/heros/index/index.component.html:
<app-layout>
<app-heros-list [heros]="heros | async"></app-heros-list>
</app-layout>
Note that we use the async
pipe to subscribe to the Observable
array of heros
.
Create Demo
Go ahead and serve the application using the CLI:
$ ng serve
Here’s a demo of adding a hero to our Tour of Heros application:
Pretty cool!
Remove Actions
Ok, we can now list the heros and create additional heros. Let’s now work on adding the functionality to delete a hero.
To get started, let’s declare the actions. This should start to be familiar.
We’ll define three actions:
- Remove a hero.
- Removing a hero resulting in an exception
- Removing a hero was successful.
Open the client/src/app/heros/heros.actions.ts file and add the following actions:
export const REMOVE_HERO = '[heros] Remove hero';
export const REMOVE_HERO_ERROR = '[heros] Remove hero error';
export const REMOVE_HERO_SUCCESS = '[heros] Remove hero success';
export class RemoveHeroAction implements Action {
readonly type = REMOVE_HERO;
constructor(public payload: { hero: Hero }) {}
}
export class RemoveHeroErrorAction implements Action {
readonly type = REMOVE_HERO_ERROR;
constructor(public payload: { error: Error }) {}
}
export class RemoveHeroSuccessAction implements Action {
readonly type = REMOVE_HERO_SUCCESS;
constructor(public payload: { hero: Hero }) {}
}
export type Actions =
CreateHeroAction
| CreateHeroErrorAction
| CreateHeroSuccessAction
| CreateHeroDialogCloseAction
| CreateHeroDialogOpenAction
| LoadHerosAction
| LoadHerosErrorAction
| LoadHerosSuccessAction
| RemoveHeroAction
| RemoveHeroErrorAction
| RemoveHeroSuccessAction;
Note that I excluded the existing actions we declared for loading and creating heros.
Here is what we did:
- First, we declare the
REMOVE_HERO
,REMOVE_HERO_ERROR
andREMOVE_HERO_SUCCESS
constant string values. - Second, we create three new action classes:
RemoveHeroAction
,RemoveHeroErrorAction
andRemoveHeroSuccessAction
. - Finally, we updated the
Actions
type with the additonal classes.
Remove Reducers
With our actions defined, let’s go to the client/src/app/heros/heros.reducers.ts file and add additional case statements for each action:
import {
CREATE_HERO,
CREATE_HERO_ERROR,
CREATE_HERO_SUCCESS,
CREATE_HERO_DIALOG_CLOSE,
CREATE_HERO_DIALOG_OPEN,
LOAD_HEROS,
LOAD_HEROS_ERROR,
LOAD_HEROS_SUCCESS,
REMOVE_HERO,
REMOVE_HERO_ERROR,
REMOVE_HERO_SUCCESS,
Actions,
} from './heros.actions';
import { Hero } from '../models/hero';
export interface State {
error?: Error;
hero?: Hero;
heros: Hero[];
}
const initialState: State = {
heros: [],
};
export function reducer(state = initialState, action: Actions): State {
switch (action.type) {
// code omitted
case REMOVE_HERO:
return {
...state,
...{
error: undefined,
hero: action.payload.hero,
},
};
case REMOVE_HERO_ERROR:
return {
...state,
...{
error: action.payload.error,
},
};
case REMOVE_HERO_SUCCESS:
return {
...state,
...{
heros: [...state.heros].filter(
(hero) => hero._id !== action.payload.hero._id
),
},
};
default:
return state;
}
}
export const getHeros = (state: State) => state.heros;
A few things to note:
- We import the action constant string values and action classes.
- We create a case statement that will update our state as necessary for each additional action.
- Note that we use the array deconstructor syntax to update the array of
heros
since the object is frozen and cannot be modified directly. We also use thefilter()
method to remove the hero that was successfully removed.
Remove Effects
And now let’s update the HeroEffects
class to include additional side effects for removing a hero.
When a hero is removed we want to send a DELETE request to our REST API to remove the specified hero from the collection in MongoDb.
To do that, we’ll invoke the delete()
method on the HerosService
:
export class HeroEffects {
// code omitted
@Effect()
public removeHero: Observable<Action> = this.actions
.ofType(REMOVE_HERO)
.map(toPayload)
.switchMap(payload => {
return this.herosService.delete(payload.hero)
.map(hero => new RemoveHeroSuccessAction({ hero: hero }))
.catch(error => Observable.of(new RemoveHeroErrorAction({ error: error })));
});
@Effect()
public removeHeroError: Observable<Action> = this.actions
.ofType(REMOVE_HERO_ERROR)
.map(toPayload)
.switchMap(payload => {
this.mdSnackbar.open("Oops. Something went wrong.", null, {
duration: 1000
});
return empty();
});
}
Note that I am only including the additional side effects, and I am not showing the importing of the necessary constant string values and action classes. Hopefully by this point you can easily figure out what you need to import.
Invoke Action
With our actions defined along with the updates to our reducer function and the additional side effects, we are now ready to implement the UI for removing a hero.
The first step is to update the client/src/app/shared/hero-list/hero-list.component.html template:
<h1>Heros</h1>
<md-list *ngIf="heros && heros.length > 0">
<md-list-item *ngFor="let hero of heros">
<button md-icon-button (click)="remove.emit(hero)">
<md-icon>remove_circle</md-icon>
</button>
{{ hero.name }}
</md-list-item>
</md-list>
<p \*ngIf="heros && heros.length === 0"><em>There are no heros. :(</em></p>
We have added a new <button>
that has an event binding for the click event that will emit()
the remove EventEmitter
, specifying the hero to remove.
Let’s add some styling in client/src/app/shared/hero-list/hero-list.component.scss:
@import '~@angular/material/theming';
:host {
md-list {
md-list-item {
width: 100%;
position: relative;
&:hover {
background-color: mat-color($mat-grey, 100);
}
button {
position: absolute;
right: 0;
}
}
}
}
We first import the theming sass file for Angular material, which contains the mat-color()
mixin as well as the color variables, including $mat-grey
. We have positioned the remove button so that it is on the right of each list item, and we highlight the list item when the cursor hovers over it.
The next step is to define the remove
property in the HeroListComponent
class in client/src/app/shared/hero-list/hero-list.component.ts:
export class HerosListComponent {
@Output() public remove = new EventEmitter<Hero>();
}
Note that the EventEmitter
generic type is a Hero
object.
We also imported the Output
decorator from @angular/core.
Finally, we need to add output binding in the client/src/app/heros/index/index.component.html template for the remove
event:
<app-layout>
<app-heros-list
[heros]="heros | async"
(remove)="remove(\$event)"
></app-heros-list>
</app-layout>
In our IndexComponent
we need to implement the remove
method that will dispatch()
the RemoveHeroAction
action:
export class IndexComponent implements OnInit {
public remove(hero: Hero) {
this.store.dispatch(new RemoveHeroAction({ hero: hero }));
}
}
Note that the $event
parameter that was in the template is typed to a Hero
object in the remove()
method.
If you recall, our EventEmitter
had a generic type declared a Hero
object, and in the HeroListComponent
template we specify the current hero
that we are going to remove within the ngFor
iterator.
That’s it. We can now remove heros.
Remove Demo
Go ahead and serve your application using the CLI:
$ ng serve
Redux DevTools
A great tool for developing using ngrx are the Redux devtools. To get started, install the @ngrx/store-devtools package using npm:
$ npm install @ngrx/store-devtools --save
Next, download and install the Redux Devtools Chrome extension.
Then, update the AppModule
to import the StoreDevtoolsModule
module:
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@NgModule({
imports: [
StoreModule.provideStore(rootReducer),
// Note that you must instrument after importing StoreModule
StoreDevtoolsModule.instrumentOnlyWithExtension({
maxAge: 5,
}),
],
})
export class AppModule {}
In the Chrome developer tools you will now have a new tab called “Redux”. You should see every action that is dispatched, and you can also inspect the current state of your application:
Download
If you haven’t already, you can download and run this entire application or fork a copy for your own use.
First, run the gulp tasks, then start the Node.js Express server.
$ gulp
$ chmod +x ./dist/bin/www
$ ./dist/bin/www
Then, serve the Angular client using the CLI:
$ ng serve