Learn how to add authentication to your Angular app using reactive (ngrx) extensions.
Goals
We are tasked with adding authentication to our Angular application, and we will be using the @ngrx/store
for state management.
Here are our goals:
- Add authentication to our application using an authentication guard.
- Use
@ngrx/store
for state management. - Use
@ngrx/effects
to isolate our side effects. - Use
@ngrx/router-store
to bind the Angular router to our store. - Have fun!
Here is a screenshot of the completed application’s sign in page:
Source Code
You can download the source code and follow along or fork the repository on GitHub:
Live Demo
The authenticate use the email address foo@test.com and the password password.
ngrx
ngrx is a set of sweet reactive extensions for your Angular application. There are 3 basic building blocks for using ngrx, insipired by Redux:
- Store - singly state management repository.
- Actions - emitted to change the state.
- Reducers - pure functions that that transform the state.
Further, reactive extensions enable us to work with Observable
streams.
What is an Observable? Defined by RxJs:
Observables are lazy Push collections of multiple values.
Similar to a Promise
object with the exception that a promise is only resolved/rejected once.
An Observable
is a stream of objects that can be subscribed to.
Getting Started
Here are a few principles you need to know before we get started:
- CanActivate - this interface enables us to specify an authentication guard to protect routes in our application.
- Lazy-loading with Router - this allows us to lazy-load modules. Ideally our user is already authenticated and we just want to get them straight into our app. If not, we want to load the
UsersModule
and present the user with a sign in form. - @ngrx/store - If you are new to reactive programming I would suggest you check out Brian Troncone’s comprehensive guide to @ngrx/store. It’s a great read and well worth the time.
- @angular/material - We will be using Angular Material for the UI.
- @angular/flex-layout - We will be using Flex Layout for flex based layouts.
OK, let’s start a new Angular project using the Angular CLI.
$ npm install -g @angular/cli
$ ng new angular-reactive-authentication
Next, install the following packages:
- @angular/flex-layout
- @angular/material and @angular/animations
- @ngrx/core
- @ngrx/store
- @ngrx/effects
- @ngrx/router-store
- ngrx-store-freeze
- reselect
$ npm install @angular/flex-layout @angular/material @angular/animations @ngrx/core @ngrx/store @ngrx/effects @ngrx/router-store ngrx-store-freeze reselect --save
We’ll be using @ngrx/store
for the state management of our Angular application.
While the store provides a controlled state container, we will also use the @ngrx/router-store
that binds @angular/router
to our @ngrx/store
.
Additionally, we will isolate all of our side effects using @ngrx/effects
.
Angular Material requires a little bit of setup to get things working. First, we’ll import a pre-built theme in the styles.css file. I am going to use the indigo-pink theme:
@import '~@angular/material/core/theming/prebuilt/indigo-pink.css';
Then, add the Material icons to the index.html file:
<link
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
Structure
Let’s have a quick look of our application structure. Here is what my src directory looks like:
.
├── app
│ ├── app-routing.module.ts
│ ├── app.component.css
│ ├── app.component.html
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── app.reducers.ts
│ ├── core
│ │ ├── models
│ │ │ └── user.ts
│ │ ├── services
│ │ │ └── user.service.ts
│ │ └── util.ts
│ ├── not-found
│ │ ├── not-found.component.html
│ │ ├── not-found.component.scss
│ │ ├── not-found.component.spec.ts
│ │ └── not-found.component.ts
│ ├── shared
│ │ └── authenticated.guard.ts
│ └── users
│ ├── my-account
│ │ ├── my-account.component.html
│ │ ├── my-account.component.scss
│ │ ├── my-account.component.spec.ts
│ │ └── my-account.component.ts
│ ├── sign-in
│ │ ├── sign-in.component.html
│ │ ├── sign-in.component.scss
│ │ ├── sign-in.component.spec.ts
│ │ └── sign-in.component.ts
│ ├── sign-out
│ │ ├── sign-out.component.html
│ │ ├── sign-out.component.scss
│ │ ├── sign-out.component.spec.ts
│ │ └── sign-out.component.ts
│ ├── sign-up
│ │ ├── sign-up.component.html
│ │ ├── sign-up.component.scss
│ │ ├── sign-up.component.spec.ts
│ │ └── sign-up.component.ts
│ ├── users-routing.module.ts
│ ├── users.actions.ts
│ ├── users.effects.ts
│ ├── users.module.ts
│ └── users.reducers.ts
├── assets
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── sass
│ └── _mixins.scss
├── styles.css
├── test.ts
├── tsconfig.app.json
├── tsconfig.spec.json
└── typings.d.ts
Models and Services
Before we dive into ngrx and our controllers, let’s get a basic understanding of the User
model and UserService
.
The User
model is defined in src/app/core/models/user.ts:
export class User {
_id?: string;
email?: string;
firstName?: string;
lastName?: string;
password?: string;
}
Our User
model is pretty slim.
I included the _id
string property as I will eventually build out the UserService
to work with a REST API that uses MongoDb.
As such, this is the object id assigned by MongoDb.
Using the Angular CLI we can get started with our UserService
via:
$ ng g service core/services/user
Here is our UserService
defined in src/app/core/services/user.ts:
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/throw";
import { User } from "../models/user";
export const MOCK_USER = new User();
MOCK_USER._id = "1";
MOCK_USER.email = "foo@test.com";
MOCK_USER.firstName = "Foo";
MOCK_USER.lastName = "Bar";
MOCK_USER.password = "password";
/**
* The user service.
*/
@Injectable()
export class UserService {
/**
* True if authenticated
* @type
*/
private _authenticated = false;
/**
* Authenticate the user
*
* @param {string} email The user's email address
* @param {string} password The user's password
* @returns {Observable<User>} The authenticated user observable.
*/
public authenticate(email: string, password: string): Observable<User> {
// Normally you would do an HTTP request to determine to
// attempt authenticating the user using the supplied credentials.
if (email === MOCK_USER.email && password === MOCK_USER.password) {
this._authenticated = true;
return Observable.of(MOCK_USER);
}
return Observable.throw(new Error("Invalid email or password"));
}
/**
* Determines if the user is authenticated
* @returns {Observable<boolean>}
*/
public authenticated(): Observable<boolean> {
return Observable.of(this._authenticated);
}
/**
* Returns the authenticated user
* @returns {User}
*/
public authenticatedUser(): Observable<User> {
// Normally you would do an HTTP request to determine if
// the user has an existing auth session on the server
// but, let's just return the mock user for this example.
return Observable.of(MOCK_USER);
}
/**
* Create a new user
* @returns {User}
*/
public create(user: User): Observable<User> {
// Normally you would do an HTTP request to POST the user
// details and then return the new user object
// but, let's just return the new user for this example.
this._authenticated = true;
return Observable.of(user);
}
/**
* End session
* @returns {Observable<boolean>}
*/
public signout(): Observable<boolean> {
// Normally you would do an HTTP request sign end the session
// but, let's just return an observable of true.
this._authenticated = false;
return Observable.of(true);
}
}
Let’s review the UserService
class:
- First, I am importing the necessary classes and functions.
- Next, I have defined a
MOCK_USER
object that will be used in my service. As I indicated before, this will eventually integrate with a remote REST API using MongoDb and Mongoose, but in the meantime, we will just mock the service methods. - I decorate the
UserService
using theInjectable()
decorator so that theUserService
can be injected into myUserEffects
class. - A private boolean variabled named
_authenticated
will keep track of whether or not my user is authenticated. - The
authenticate()
method is invoked when the user is attempting to authenticate supplying their credentials. The method returns anObservable
of typeUser
. - The
authenticated()
method is used to determine if the user has an existing authenticated session in my application server (whatever backend you choose). Often times our user is coming back to our application, and they don’t want to be pestered with supplying their credentials each time. This method returns anObservable
that is aboolean
value; true if they are authenticated, or false if not. - The
authenticatedUser()
method is used to return anObservable
of our authenticatedUser
. - The
create()
method is invoked when the user is signing up for a new account. - The
signOut()
method terminates the user’s session.
Routing
Let’s set up the routing in our application. To begin, create a new src/app/app-routing.module.ts file:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
// components
import { NotFoundComponent } from './not-found/not-found.component';
const routes: Routes = [
{
path: 'users',
loadChildren: './users/users.module#UsersModule',
},
{
path: '',
pathMatch: 'full',
redirectTo: '/users/my-account',
},
{
path: '404',
component: NotFoundComponent,
},
{
path: '**',
redirectTo: '/404',
},
];
@NgModule({
exports: [RouterModule],
imports: [RouterModule.forRoot(routes)],
})
export class AppRoutingModule {}
Let’s review the AppRoutingModule
class:
- As always, we import the necessary classes, including the
NotFoundComponent
that will be displayed when a user accesses a route path that is invalid. - Next, we set up our
routes
array. - First, I lazy load the
UsersModule
. - Next, we redirect a root request (with no path) to the /users/my-account page. Likely you will want to redirect to another route. This is just for simplicity (not having to create an additional module for this example).
- Next, we set up a route for the “404” path.
- Then, we redirect all routes that are not explicitly defined, using the wildcard symbol (double asterisks), to the 404 page.
- Finally, we wire it all up using the
RouterModule.forRoot()
method. Keep in mind that theforRoot()
method should only be used in your base application routing.
Next, let’s create the UsersRoutingModule
class, which will specify the users module routing.
You will need to create a new users directory in your app directory first.
Here is our src/app/users/users-routing.module.ts file:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
// components
import { MyAccountComponent } from './my-account/my-account.component';
import { SignInComponent } from './sign-in/sign-in.component';
import { SignOutComponent } from './sign-out/sign-out.component';
import { SignUpComponent } from './sign-up/sign-up.component';
// routes
const routes: Routes = [
{
canActivate: [AuthenticationGuard],
path: 'my-account',
component: MyAccountComponent,
},
{
path: 'sign-in',
component: SignInComponent,
},
{
path: 'sign-out',
component: SignOutComponent,
},
{
path: 'sign-up',
component: SignUpComponent,
},
{
path: '**',
redirectTo: '/404',
},
];
@NgModule({
exports: [RouterModule],
imports: [RouterModule.forChild(routes)],
})
export class UsersRoutingModule {}
The UsersRoutingModule
is very similar to the AppRoutingModule
in that:
- As always, we import the necessary classes, including the
AuthenticationGuard
. We’ll take a look at that after we go through the ngrx store, actions and effects. - Next, we set up our routes. Note that the “my-account” route is protected using the
CanActivate
property, where we specify theAuthenticationGuard
class. There are also routes for “sign-in”, “sign-out”, and “sign-up”. We will not go through each component, but we will review theSignUpComponent
at the end. - Finally, we wire it all together in our
UsersRoutingModule
that is decorated withNgModule
. Note that we invoke theforChild()
method on theRouterModule
as this is not the root module.
OK, we have stubbed out our models, services and routing.
Let’s quickly go back into the src/app/app.module.ts file to add our services and routing to the AppModule
:
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
// routing
import { AppRoutingModule } from './app-routing.module';
// components
import { AppComponent } from './app.component';
import { NotFoundComponent } from './not-found/not-found.component';
// services
import { UserService } from './core/services/user.service';
@NgModule({
declarations: [AppComponent, NotFoundComponent],
imports: [AppRoutingModule, BrowserModule, BrowserAnimationsModule],
providers: [UserService],
bootstrap: [AppComponent],
})
export class AppModule {}
- First, I have added import statements for the following modules:
BrowserAnimationsModule
,AppRoutingModule
,NotFoundComponent
andUserService
. - Next, I added the
NotFoundComponent
to thedeclarations
array. - Then, I added the
AppRoutingModule
andFormsModule
to theimports
array. - Finally, I added the
UserService
to theproviders
array.
Next, we need to create a new UsersModule
.
To do that, let’s use the the Angular CLI:
$ ng g module users/users
Here is the UsersModule
class in src/app/users/users.module.ts:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
// @angular/flex-layout
import { FlexLayoutModule } from '@angular/flex-layout';
// @angular/material
import {
MdButtonModule,
MdCardModule,
MdIconModule,
MdInputModule,
MdProgressSpinnerModule,
MdMenuModule,
} from '@angular/material';
// components
import { MyAccountComponent } from './my-account/my-account.component';
import { SignInComponent } from './sign-in/sign-in.component';
import { SignOutComponent } from './sign-out/sign-out.component';
import { SignUpComponent } from './sign-up/sign-up.component';
// routing
import { UsersRoutingModule } from './users-routing.module';
// components constant
const components = [
MyAccountComponent,
SignInComponent,
SignUpComponent,
SignOutComponent,
];
@NgModule({
imports: [
CommonModule,
FlexLayoutModule,
FormsModule,
MdButtonModule,
MdCardModule,
MdIconModule,
MdInputModule,
MdProgressSpinnerModule,
MdMenuModule,
ReactiveFormsModule,
RouterModule,
UsersRoutingModule,
],
declarations: components,
exports: components,
})
export class UsersModule {}
Let’s review:
- First, we import the
FormsModule
and theReactiveFormsModule
classes. We’ll need these for our sign up and sign in forms. Add these to theimports
array. - Next, import the
RouterModule
and add it to theimports
array. - Then, import the
FlexLayoutModule
and add it to theimports
array. - Then, import all of the Angular Material modules that we will need. In my example I am importing the button, card, icon, input, progress spinner and menu modules. Again, add these to the
imports
array. - Then, import the components in our module. I created a
components
constant array to store reference to the array of components in the module. This is used in both thedeclarations
andexports
properties of theNgModule
decorator. - Finally, import the
UsersRoutingModule
and add this to theimports
array.
Phew - our routing is now configured and ready to go. So far we haven’t touched @ngrx, but we have our routing, modules and components set up and ready to go.
Store
To review, we created:
- The
User
model andUserService
service. - The
AppRoutingModule
andUsersRoutingModule
modules with our routing configurations. - The
UsersModule
for our /users module with our components, Angular Material and routing.
We also updated the AppModule
class with our components and routing.
Now, let’s add the StoreModule
and RouterStoreModule
to the src/app/app.module.ts file:
// @ngrx
import { RouterStoreModule } from '@ngrx/router-store';
import { StoreModule } from '@ngrx/store';
@NgModule({
// code ommitted
imports: [
AppRoutingModule,
BrowserModule,
FormsModule,
RouterStoreModule.connectRouter(),
StoreModule.provideStore(reducer, {
router: window.location.pathname + window.location.search,
}),
],
// code ommitted
})
export class AppModule {}
We import the RouterStoreModule
and StoreModule
, and then add them to the imports
array in the NgModule
decorator.
I have also optionally set the initial router
state.
Reducers
Reducers are pure functions that transform the state of our application.
What exactly does that mean?
- A pure function will always return the same value given the same input.
- We will define what the state of our application looks like by defining a
State
interface. - We will implement the
State
interface in a newinitialState
object, which is the default state that is given to our reducer function. - Our reducer function will transform the state of our application based on an action that is emitted.
The signature for the reducer function is:
function reducer(state: any = initialState, action: Actions): State
As you can see our reducer function accepts two parameters:
state
- the state of our application. The default value is theinitialState
.action
- the action that has been emitted. We will look more at actions next.
Before we implement our reducer function let’s declare what the state of my application will look like.
Create a new file: src/app/users/users.reducers.ts.
In the users.reducers.ts file we will first import the action, which we will create next, as well as the User
model.
Then, create the State
interface:
// import actions
import { Actions, ActionTypes } from './users.actions';
// import models
import { User } from '../core/models/user';
/**
* The state.
* @interface State
*/
export interface State {
// boolean if user is authenticated
authenticated: boolean;
// error message
error?: string;
// true if we have attempted existing auth session
loaded: boolean;
// true when loading
loading: boolean;
// the authenticated user
user?: User;
}
Then, create the initialState
object:
/**
* The initial state.
*/
const initialState: State = {
authenticated: false,
loaded: false,
loading: false,
};
By default we set the authenticated
, loaded
and loading
properties to false
.
Next, create the reducer function:
/**
* The reducer function.
* @function reducer
* @param {State} state Current state
* @param {Actions} action Incoming action
*/
export function reducer(state: any = initialState, action: Actions): State {
switch (action.type) {
case ActionTypes.AUTHENTICATE:
return Object.assign({}, state, {
loading: true,
});
case ActionTypes.AUTHENTICATED_ERROR:
return Object.assign({}, state, {
authenticated: false,
error: action.payload.error.message,
loaded: true,
});
case ActionTypes.AUTHENTICATED_SUCCESS:
return Object.assign({}, state, {
authenticated: action.payload.authenticated,
loaded: true,
user: action.payload.user,
});
case ActionTypes.AUTHENTICATE_ERROR:
case ActionTypes.SIGN_UP_ERROR:
return Object.assign({}, state, {
authenticated: false,
error: action.payload.error.message,
loading: false,
});
case ActionTypes.AUTHENTICATE_SUCCESS:
case ActionTypes.SIGN_UP_SUCCESS:
const user: User = action.payload.user;
// verify user is not null
if (user === null) {
return state;
}
return Object.assign({}, state, {
authenticated: true,
error: undefined,
loading: false,
user: user,
});
case ActionTypes.SIGN_OUT_ERROR:
return Object.assign({}, state, {
authenticated: true,
error: action.payload.error.message,
user: undefined,
});
case ActionTypes.SIGN_OUT_SUCCESS:
return Object.assign({}, state, {
authenticated: false,
error: undefined,
user: undefined,
});
case ActionTypes.SIGN_UP:
return Object.assign({}, state, {
authenticated: false,
error: undefined,
loading: true,
});
default:
return state;
}
}
In our reducer function we return a new state based on the incoming action.
In this example I am using Object.assign()
to return the transformed state object.
Next, let’s define some helper functions in our src/app/users/users.reducers.ts file.
Each function will accept the State
of our application and will return a specific value.
Add the following functions to the src/app/users/users.reducers.ts file:
/**
* Returns true if the user is authenticated.
* @function isAuthenticated
* @param {State} state
* @returns {boolean}
*/
export const isAuthenticated = (state: State) => state.authenticated;
/**
* Returns true if the authenticated has loaded.
* @function isAuthenticatedLoaded
* @param {State} state
* @returns {boolean}
*/
export const isAuthenticatedLoaded = (state: State) => state.loaded;
/**
* Return the users state
* @function getAuthenticatedUser
* @param {State} state
* @returns {User}
*/
export const getAuthenticatedUser = (state: State) => state.user;
/**
* Returns the authentication error.
* @function getAuthenticationError
* @param {State} state
* @returns {Error}
*/
export const getAuthenticationError = (state: State) => state.error;
/**
* Returns true if request is in progress.
* @function isLoading
* @param {State} state
* @returns {boolean}
*/
export const isLoading = (state: State) => state.loading;
/**
* Returns the sign out error.
* @function getSignOutError
* @param {State} state
* @returns {Error}
*/
export const getSignOutError = (state: State) => state.error;
/**
* Returns the sign up error.
* @function getSignUpError
* @param {State} state
* @returns {Error}
*/
export const getSignUpError = (state: State) => state.error;
Next, we will create a new file at: src/app/app.reducers.ts. I modeled this off of the @ngrx/example-app:
// reselect
import { createSelector } from 'reselect';
// @ngrx
import { ActionReducer, combineReducers } from '@ngrx/store';
import { compose } from '@ngrx/core/compose';
import { routerReducer, RouterState } from '@ngrx/router-store';
import { storeFreeze } from 'ngrx-store-freeze';
// environment
import { environment } from '../environments/environment';
/**
* Every reducer module"s default export is the reducer function itself. In
* addition, each module should export a type or interface that describes
* the state of the reducer plus any selector functions. The `* as`
* notation packages up all of the exports into a single object.
*/
import * as users from './users/users.reducers';
/**
* We treat each reducer like a table in a database.
* This means our top level state interface is just a map of keys to inner state types.
*/
export interface State {
router: RouterState;
users: users.State;
}
/**
* Because metareducers take a reducer function and return a new reducer,
* we can use our compose helper to chain them together. Here we are
* using combineReducers to make our top level reducer, and then
* wrapping that in storeLogger. Remember that compose applies
* the result from right to left.
*/
const reducers = {
router: routerReducer,
users: users.reducer,
};
// development reducer includes storeFreeze to prevent state from being mutated
const developmentReducer: ActionReducer<State> = compose(
storeFreeze,
combineReducers
)(reducers);
// production reducer
const productionReducer: ActionReducer<State> = combineReducers(reducers);
/**
* The single reducer function.
* @function reducer
* @param {any} state
* @param {any} action
*/
export function reducer(state: any, action: any) {
if (environment.production) {
return productionReducer(state, action);
} else {
return developmentReducer(state, action);
}
}
/**********************************************************
* Users Reducers
*********************************************************/
/**
* Returns the user state.
* @function getUserState
* @param {State} state Top level state.
* @return {State}
*/
export const getUsersState = (state: State) => state.users;
/**
* Returns the authenticated user
* @function getAuthenticatedUser
* @param {State} state
* @param {any} props
* @return {User}
*/
export const getAuthenticatedUser = createSelector(
getUsersState,
users.getAuthenticatedUser
);
/**
* Returns the authentication error.
* @function getAuthenticationError
* @param {State} state
* @param {any} props
* @return {Error}
*/
export const getAuthenticationError = createSelector(
getUsersState,
users.getAuthenticationError
);
/**
* Returns true if the user is authenticated
* @function isAuthenticated
* @param {State} state
* @param {any} props
* @return {boolean}
*/
export const isAuthenticated = createSelector(
getUsersState,
users.isAuthenticated
);
/**
* Returns true if the user is authenticated
* @function isAuthenticated
* @param {State} state
* @param {any} props
* @return {boolean}
*/
export const isAuthenticatedLoaded = createSelector(
getUsersState,
users.isAuthenticatedLoaded
);
/**
* Returns true if the authentication request is loading.
* @function isAuthenticationLoading
* @param {State} state
* @param {any} props
* @return {boolean}
*/
export const isAuthenticationLoading = createSelector(
getUsersState,
users.isLoading
);
/**
* Returns the sign out error.
* @function getSignOutError
* @param {State} state
* @param {any} props
* @return {Error}
*/
export const getSignOutError = createSelector(
getUsersState,
users.getSignOutError
);
/**
* Returns the sign up error.
* @function getSignUpError
* @param {State} state
* @param {any} props
* @return {Error}
*/
export const getSignUpError = createSelector(
getUsersState,
users.getSignUpError
);
Let’s review the app.reducers.ts file:
- First, we import the
createSelector()
function from the reselect project. Reselect is a selector library for Redux, which we can use with @ngrx to create functions that we will pass to thestore.select()
method. The use of reselect is optional. In place of the selectors thatcreateSelector()
creates you can simply hard code a string constant when usingstore.select()
- Next, we import several classes from the @ngrx packages.
- We import the
ActionReducer
interface and thecombineReducers()
function. We will use thecombineReducers()
function for our productionreducer
- We import the
compose()
function that we will use in development to ensure that our state is immutable. - We import the
routerReducer()
reducer function andRouterState
interface from the @ngrx/router-store package. - We import the
storeFreeze()
function that will be used in development. - Finally, we import the
environment
object to determine if we are running in production mode or not. - From there, we create the top level
State
interface and the top levelreducers
object. If you revisit the src/app/app.module.ts file you will note that we provide thereducers
to theStoreModule.provideStore()
method in ourimports
array. - Next, we create a
getUsersState()
helper method that returns the userState
. - Finally, we create reselect functions that we will pass to the store’s
select()
operator. We will see this implemented later in theAuthenticationGuard
class.
At first this may seem a bit complicated, but it makes it easy to extend and build upon this design.
We now have our store configured with our reducers. The next step is to define our actions as well as any side effects from those actions that will change the state of our application.
Actions
With our Store
, State
and reducer()
function set up, the next thing is to define our actions.
Broadly, we will need to perform the following actions:
- Authenticate the user from the sign in page.
- Determine if the user has an existing authenticated session.
- Terminate the session when the user signs out.
- Create a new user and authenticated session when the user signs in.
Using that as a base, let’s get started.
First, create a new file at: src/app/users/users.actions.ts.
To start, import the necessary classes and then create a new ActionTypes
object.
// import @ngrx
import { Action } from '@ngrx/store';
// import type function
import { type } from '../core/util';
// import models
import { User } from '../core/models/user';
export const ActionTypes = {
AUTHENTICATE: type('[users] Authenticate'),
AUTHENTICATE_ERROR: type('[users] Authentication error'),
AUTHENTICATE_SUCCESS: type('[users] Authentication success'),
AUTHENTICATED: type('[users] Authenticated'),
AUTHENTICATED_ERROR: type('[users] Authenticated error'),
AUTHENTICATED_SUCCESS: type('[users] Authenticated success'),
SIGN_OUT: type('[users] Sign off'),
SIGN_OUT_ERROR: type('[users] Sign off error'),
SIGN_OUT_SUCCESS: type('[users] Sign off success'),
SIGN_UP: type('[users] Sign up'),
SIGN_UP_ERROR: type('[users] Sign up error'),
SIGN_UP_SUCCESS: type('[users] Sign up success'),
};
First, note that I have imported the type()
function from the src/app/core/util.ts file.
This is a technique that I followed from the official @ngrx/example-app source code.
Here is our src/app/core/util.ts file with the type()
function:
const typeCache: { [label: string]: boolean } = {};
export function type<T>(label: T | ""): T {
if (typeCache[<string>label]) {
throw new Error(`Action type "${label}" is not unique"`);
}
typeCache[<string>label] = true;
return <T>label;
}
The type()
function enforces the uniqueness of each ActionTypes
property value.
This is to help you as a developer and is not required.
We then use the type()
function to declare our ActionTypes
object, which is a constant object with string key and value pairs.
If you look back to the reducer function you will note that we are switching on the incoming action based on the ActionTypes
that we imported at the top of the src/app/users/reducers.ts file.
Next, we need to create a class for each action that we want to emit that implements the Action
interface.
Before we do that, however, let’s look at the Action
interface:
export interface Action {
type: string;
payload?: any;
}
So, our task is to create classes that have a type property set to the ActionTypes
value and a constructor
function that will set the public payload
property.
Let’s add these classes to our src/app/users/users.actions.ts file:
/**
* Authenticate.
* @class AuthenticateAction
* @implements {Action}
*/
export class AuthenticateAction implements Action {
public type: string = ActionTypes.AUTHENTICATE;
constructor(public payload: {email: string, password: string}) {}
}
/**
* Checks if user is authenticated.
* @class AuthenticatedAction
* @implements {Action}
*/
export class AuthenticatedAction implements Action {
public type: string = ActionTypes.AUTHENTICATED;
constructor(public payload?: {token?: string}) {}
}
/**
* Authenticated check success.
* @class AuthenticatedSuccessAction
* @implements {Action}
*/
export class AuthenticatedSuccessAction implements Action {
public type: string = ActionTypes.AUTHENTICATED_SUCCESS;
constructor(public payload: {authenticated: boolean, user: User}) {}
}
/**
* Authenticated check error.
* @class AuthenticatedErrorAction
* @implements {Action}
*/
export class AuthenticatedErrorAction implements Action {
public type: string = ActionTypes.AUTHENTICATED_ERROR;
constructor(public payload?: any) {}
}
/**
* Authentication error.
* @class AuthenticationErrorAction
* @implements {Action}
*/
export class AuthenticationErrorAction implements Action {
public type: string = ActionTypes.AUTHENTICATE_ERROR;
constructor(public payload?: any) {}
}
/**
* Authentication success.
* @class AuthenticationSuccessAction
* @implements {Action}
*/
export class AuthenticationSuccessAction implements Action {
public type: string = ActionTypes.AUTHENTICATE_SUCCESS;
constructor(public payload: { user: User }) {}
}
/**
* Sign out.
* @class SignOutAction
* @implements {Action}
*/
export class SignOutAction implements Action {
public type: string = ActionTypes.SIGN_OUT;
constructor(public payload?: any) {}
}
/**
* Sign out error.
* @class SignOutErrorAction
* @implements {Action}
*/
export class SignOutErrorAction implements Action {
public type: string = ActionTypes.SIGN_OUT_SUCCESS;
constructor(public payload?: any) {}
}
/**
* Sign out success.
* @class SignOutSuccessAction
* @implements {Action}
*/
export class SignOutSuccessAction implements Action {
public type: string = ActionTypes.SIGN_OUT_SUCCESS;
constructor(public payload?: any) {}
}
/**
* Sign up.
* @class SignUpAction
* @implements {Action}
*/
export class SignUpAction implements Action {
public type: string = ActionTypes.SIGN_UP;
constructor(public payload: { user: User }) {}
}
/**
* Sign up error.
* @class SignUpErrorAction
* @implements {Action}
*/
export class SignUpErrorAction implements Action {
public type: string = ActionTypes.SIGN_UP_ERROR;
constructor(public payload?: any) {}
}
/**
* Sign up success.
* @class SignUpSuccessAction
* @implements {Action}
*/
export class SignUpSuccessAction implements Action {
public type: string = ActionTypes.SIGN_UP_SUCCESS;
constructor(public payload: { user: User }) {}
}
We have created a class for each ActionTypes
property with the corresponding payload object.
Finally, define a single Actions
type that can any of the Action
classes we just defined:
/**
* Actions type.
* @type {Actions}
*/
export type Actions =
| AuthenticateAction
| AuthenticatedAction
| AuthenticatedErrorAction
| AuthenticatedSuccessAction
| AuthenticationErrorAction
| AuthenticationSuccessAction
| SignUpAction
| SignUpErrorAction
| SignUpSuccessAction;
If you look back to your reducer function in src/app/users/users.reducers.ts you will note that the incoming action
argument is of type Actions
.
Effects
According to the @ngrx/effects
documentation:
In @ngrx/effects, effects are sources of actions.
Create a new file at: src/app/users/users.effects.ts:
import { Injectable } from "@angular/core";
// import @ngrx
import { Effect, Actions, toPayload } from "@ngrx/effects";
import { Action } from "@ngrx/store";
// import rxjs
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/debounceTime";
import "rxjs/add/operator/map";
import "rxjs/add/operator/switchMap";
// import services
import { UserService } from "../core/services/user.service";
// import models
import { User } from "../core/models/user";
// import actions
import {
ActionTypes,
AuthenticatedErrorAction,
AuthenticatedSuccessAction,
AuthenticationErrorAction,
AuthenticationSuccessAction,
SignOutErrorAction,
SignOutSuccessAction,
SignUpErrorAction,
SignUpSuccessAction
} from "./users.actions";
/**
* Effects offer a way to isolate and easily test side-effects within your
* application.
* The `toPayload` helper function returns just
* the payload of the currently dispatched action, useful in
* instances where the current state is not necessary.
*
* Documentation on `toPayload` can be found here:
* https://github.com/ngrx/effects/blob/master/docs/api.md#topayload
*
* If you are unfamiliar with the operators being used in these examples, please
* check out the sources below:
*
* Official Docs: http://reactivex.io/rxjs/manual/overview.html#categories-of-operators
* RxJS 5 Operators By Example: https://gist.github.com/btroncone/d6cf141d6f2c00dc6b35
*/
@Injectable()
export class UserEffects {
/**
* Authenticate user.
*/
@Effect()
public authenticate: Observable<Action> = this.actions
.ofType(ActionTypes.AUTHENTICATE)
.debounceTime(500)
.map(toPayload)
.switchMap(payload => {
return this.userService.authenticate(payload.email, payload.password)
.map(user => new AuthenticationSuccessAction({ user: user }))
.catch(error => Observable.of(new AuthenticationErrorAction({ error: error })));
});
/**
* Determine if the user is authenticated.
*/
@Effect()
public authenticated: Observable<Action> = this.actions
.ofType(ActionTypes.AUTHENTICATED)
.map(toPayload)
.switchMap(payload => {
return this.userService.authenticatedUser()
.map(user => new AuthenticatedSuccessAction({ authenticated: (user !== null), user: user }))
.catch(error => Observable.of(new AuthenticatedErrorAction({ error: error })));
});
/**
* Create a new user.
*/
@Effect()
public createUser: Observable<Action> = this.actions
.ofType(ActionTypes.SIGN_UP)
.debounceTime(500)
.map(toPayload)
.switchMap(payload => {
return this.userService.create(payload.user)
.map(user => new SignUpSuccessAction({ user: user }))
.catch(error => Observable.of(new SignUpErrorAction({ error: error })));
});
/**
* Terminate user session.
*/
@Effect()
public signOut: Observable<Action> = this.actions
.ofType(ActionTypes.SIGN_OUT)
.map(toPayload)
.switchMap(payload => {
return this.userService.signout()
.map(value => new SignOutSuccessAction())
.catch(error => Observable.of(new SignOutErrorAction({ error: error })));
});
/**
* @constructor
* @param {Actions }actions
* @param {UserService} userService
*/
constructor(
private actions: Actions,
private userService: UserService
) { }
}
Let’s review our import statements:
- First, import the
Injector
decorator so that ourUserEffects
class can be injected using the Angular dependency injection framework. - Then, import the
Effect
decorator. We will use this to decorate the functions that are sources of actions. - Then, import the
Actions
observable. This observable emits everyAction
that is dispatched in your application. - Then, import the
toPayload()
function. This function returns the payload for the current action. - Next, import the
Observable
class as well as some rxjs operators. - Next, import the
UserService
, which be injected into ourUserEffects
class. - Next, import the
User
model as some of our actions will return anObservable
of the authenticatedUser
. - Finally, we import all of the actions that we defined in our src/app/users/users.actions.ts module.
Let’s review the UserEffects
class:
- First, we decorate our
UserEffects
class using the@Injectable()
decorator. - Next, we define public properties for each effect, which is decorated using
@Effect()
. - Each property is an
Observable
of anAction
. - We use the
ofType()
operator to filter theActions
. - We then use the
map()
operator specifying thetoPayload()
helper function to get the payload for the current action. - Finally, we use the
switchMap()
operator to accept the payload and to return a newObservable
object. - Inside
switchMap()
we return theObservable
from the appropriate method in theUserService
class, which in turn, uses themap()
operator to emit a new action. In most cases, we are emitting the success action with acatch
block to emit a newObservable.of()
theError
. - Note that we provide a payload object for each action as required in the
constructor
of the action class that we defined in the src/app/users/users.actions.ts module.
I will also note that in some instances I have used the debounceTime()
rxjs operator.
The goal of this was to show the loading indicator briefly, for one second, while we invoke the UserService
methods to sign in or to sign up.
As our current service is just a mock, the indicator would likely never show, or show for a mere millisecond.
If you are implementing these effects in a production environment, be sure to remove the use of the debounceTime()
operators.
Authentication Guard
With our store, actions and effects configured we are ready to implement the AuthenticationGuard
. Guards enable us to protect routes by implementing the CanActivate
and CanLoad
interfaces.
To get started, create a new directory at: src/app/shared, and then create a new guard using the Angular CLI:
$ ng g guard shared/authentication
Here is our src/app/shared/authentication.guard.ts file:
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from "@angular/router";
// import rxjs
import { Observable } from "rxjs/Observable";
// import @ngrx
import { Store } from "@ngrx/store";
import { go } from "@ngrx/router-store";
// reducers
import {
isAuthenticated,
State
} from "../app.reducers";
/**
* Prevent unauthorized activating and loading of routes
* @class AuthenticatedGuard
*/
@Injectable()
export class AuthenticatedGuard implements CanActivate {
/**
* @constructor
*/
constructor(private store: Store<State>) {}
/**
* True when user is authenticated
* @method canActivate
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
// get observable
const observable = this.store.select(isAuthenticated);
// redirect to sign in page if user is not authenticated
observable.subscribe(authenticated => {
if (!authenticated) {
this.store.dispatch(go("/users/sign-in"));
}
});
return observable;
}
}
Let’s review:
- We import the
ActivatedRouteSnapshot
class,CanActivate
interface andRouterStateSnapshot
class. - Then, import the
Observable
class. OurcanActivate()
method is asynchronous. While our service is currently mocked, in the future, ourUserService
will implement an HTTP REST API, which by nature of HTTP, is asynchronous. As such, we will use Observables. - Next, import the
Store
as well as thego()
function. We will need access to theStore
to determine if the user is authenticated or not. And, thego()
function will enable us to update our application’s router state. - The
AuthenticationGuard
will implement theCanActivate
interface by implementing thecanActivate()
method. - The
canActivate()
method is provided the activateroute
snapshot as well as thestate
tree of activated routes. I am not using these in mycanActivate()
method because I am going to protect all routes that use theAuthenticationGuard
. - We inject the
Store
into our class using theconstructor()
function. - And then we use the
select()
method to return anObservable<boolean>
to determine if the user is authenticated. - I then
subscribe()
to the observable, and if the user is not authenticated, they are sent to the sign in page via the store’sdispatch()
method. - Finally, I return the observable.
One important thing to note: you must use the the store as the single and only source for state management in your application.
If you use the Angular RouterLink
directive or the Router.navigate()
methods your store will not know about the change in state.
This can cause all kinds of oddities.
So, when using the store, stick with the store.
Sign In Component
OK, if you made it this far, then I would suggest you pat yourself on the back and get another cup of coffee!
Let’s dig into the SignInComponent
.
To get started, create a new component using the Angular CLI:
ng g component users/sign-in
Here is our SignInComponent
:
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
// @ngrx
import { Store } from "@ngrx/store";
import { go } from "@ngrx/router-store";
// rxjs
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/takeWhile";
// actions
import { AuthenticateAction } from "../users.actions";
// reducers
import {
getAuthenticationError,
isAuthenticated,
isAuthenticationLoading,
State
} from "../../app.reducers";
/**
* /users/sign-in
* @class SignInComponent
*/
@Component({
templateUrl: "./sign-in.component.html",
styleUrls: ["./sign-in.component.scss"]
})
export class SignInComponent implements OnDestroy, OnInit {
/**
* The error if authentication fails.
* @type {Observable<string>}
*/
public error: Observable<string>;
/**
* True if the authentication is loading.
* @type {boolean}
*/
public loading: Observable<boolean>;
/**
* The authentication form.
* @type {FormGroup}
*/
public form: FormGroup;
/**
* Component state.
* @type {boolean}
*/
private alive = true;
/**
* @constructor
* @param {FormBuilder} formBuilder
* @param {Store<State>} store
*/
constructor(
private formBuilder: FormBuilder,
private store: Store<State>
) { }
/**
* Lifecycle hook that is called after data-bound properties of a directive are initialized.
* @method ngOnInit
*/
public ngOnInit() {
// set formGroup
this.form = this.formBuilder.group({
email: ["", Validators.required],
password: ["", Validators.required]
});
// set error
this.error = this.store.select(getAuthenticationError);
// set loading
this.loading = this.store.select(isAuthenticationLoading);
// subscribe to success
this.store.select(isAuthenticated)
.takeWhile(() => this.alive)
.filter(authenticated => authenticated)
.subscribe(value => {
this.store.dispatch(go("/users/my-account"));
});
}
/**
* Lifecycle hook that is called when a directive, pipe or service is destroyed.
* @method ngOnDestroy
*/
public ngOnDestroy() {
this.alive = false;
}
/**
* Go to the home page.
* @method home
*/
public home() {
this.store.dispatch(go("/"));
}
/**
* To to the sign up page.
* @method signUp
*/
public signUp() {
this.store.dispatch(go("/users/sign-up"));
}
/**
* Submit the authentication form.
* @method submit
*/
public submit() {
// get email and password values
const email: string = this.form.get("email").value;
const password: string = this.form.get("password").value;
// trim values
email.trim();
password.trim();
// set payload
const payload = {
email: email,
password: password
};
// dispatch AuthenticationAction and pass in payload
this.store.dispatch(new AuthenticateAction(payload));
}
}
Let’s review our SignInComponent
:
- First, we import the necessary classes from the @angular/core, @angular/forms, @ngrx/store, @ngrx/router-store and rxjs packages.
- Next, import the
AuthenticateAction
. We will dispatch this action when our user attempts to authenticate, providing the necessary payload object containing the user’s email and password. - Next, import the
getAuthenticationError()
,isAuthenticated()
andisAuthenticationLoading()
reselectors from the src/app/app.reducers.ts module. - Also, import our top level
State
interface from src/app/app.reducers.ts. - Note that the
SignInComponent
class implements both theOnDestroy
andOnInit
interfaces. As required for these interfaces, our component will implement thengOnDestroy()
andngOnInit()
lifecycle methods. - The public
error
property is anObservable
that we can subscribe to via theasync
pipe to show the user any error that has occured. For example, if their authentication attempt fails due to incorrect credentials. - The public
loading
property is anObservable
that we can subscribe to via theasync
pipe to show a loading indicator. - The public
form
property is theFormGroup
for the sign in form. - And, the private
alive
boolean flag indicates if our component is alive or destroyed. - The
formBuilder
andStore
are injected in ourconstructor()
function. - In the
ngOnInit()
method we build theFormGroup
using theformBuilder
, we set theerror
andloading
observables, and subscribe to theisAuthenticated()
selector. When the user has been successfully authenticated we go to the /users/my-account page. - The
ngOnDestroy()
method toggles the value of the privatealive
boolean to false to unsubscribe our observables. - The
home()
method goes to the home page. - The
signUp()
method goes to the sign up page. - Finnaly, the
submit()
method dispatches a newAuthenticateAction
action with the necessary payload.
Now, let’s put this together with our template in src/app/users/sign-in/sign-in.component.html:
<div class="flex-container" fxLayout="column">
<button (click)="home()" fxFlexAlign="center" class="logo-container">
<div class="logo"></div>
</button>
<div fxLayout="row" fxLayoutAlign="center" class="card-container">
<div fxFlex="90%" fxFlex.md="50%" fxFlex.lg="40%">
<md-card>
<md-card-title>Sign In</md-card-title>
<md-card-content>
<div *ngIf="error | async" class="error">{{ error | async }}</div>
<form
*ngIf="!(loading | async)"
(ngSubmit)="submit()"
[formGroup]="form"
novalidate
>
<div fxLayout="column">
<md-input-container fxFlex="100%">
<input
mdInput
required
placeholder="Email Address"
autocorrect="off"
autocapitalize="off"
spellcheck="off"
formControlName="email"
[class.invalid]="form.controls.email.errors && form.controls.email.dirty"
/>
</md-input-container>
<md-input-container fxFlex="100%">
<input
mdInput
required
type="password"
placeholder="Password"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="off"
formControlName="password"
[class.invalid]="form.controls.password.errors && form.controls.password.dirty"
/>
</md-input-container>
<button
md-raised-button
color="accent"
[disabled]="form.invalid"
type="submit"
>
Sign In
</button>
<a md-button (click)="signUp()">Sign Up with Email</a>
</div>
</form>
<div fxLayout="row" fxLayoutAlign="center">
<md-spinner *ngIf="loading | async"></md-spinner>
</div>
</md-card-content>
</md-card>
</div>
</div>
</div>
Our HTML template uses Flex Layout and Angular Material for the layout and UI. In general:
- I have a
.flex-container
element that wraps the content and provides some background. - There is a logo at the top, and when clicked on, will take you to the application’s home page.
- Beneath the logo we use the
md-card
component to contain the authentication form and buttons. - The form uses the
md-input-container
component along with themdInput
directive to style the form inputs. - I also use the
md-button
component to style the buttons. - Finally, an
md-spinner
is used to display an indeterminate loading indicator.
Now, let’s style our page using Sass in the src/app/users/users.component.scss file:
@import '~@angular/material/core/theming/palette';
@import '~@angular/material/core/theming/theming';
@import '../../../sass/mixins';
:host {
.flex-container {
background-color: mat-color($mat-grey, 100);
position: relative;
top: 0;
left: 0;
right: 0;
min-height: 100vh;
&::after {
position: absolute;
top: 0;
width: 100%;
height: 200px;
left: 0;
right: 0;
background-color: mat-color($mat-indigo, 500);
content: '';
box-shadow: 0 2px 4px 1px rgba(0, 0, 0, 0.14);
z-index: 0;
}
.logo-container,
.card-container {
z-index: 1;
}
button.logo-container {
background: transparent;
padding: 0 16px;
box-sizing: border-box;
cursor: pointer;
user-select: none;
outline: 0;
border: none;
display: inline-block;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
.logo {
height: 64px;
width: 64px;
margin: 48px auto;
background: url('http://placehold.it/64x64') contain;
@include retina {
background: url('http://placehold.it/128x128') 64px/64px;
}
}
}
.card-container {
md-card {
background-color: #fff;
margin-bottom: 16px;
md-card-title {
text-transform: uppercase;
text-align: center;
margin-bottom: 48px;
}
md-card-content {
margin-bottom: 16px;
.error {
margin: 16px 0;
color: mat-color($mat-pink, 500);
}
md-input-container {
margin-bottom: 16px;
}
button,
a {
margin-top: 16px;
}
}
}
}
}
}
A few things to note:
- We are importing Material’s palette library.
- We are importing Material’s theming, including the
mat-color()
function. This way I can use the color palettes that Material includes. - I have a placeholder graphic for the logo, which uses a
retina
mixin that is defined in the src/sass/_mixins.scss file.
Dev Tools
One helpful tool for debugging is the Redux DevTools Google Chrome browser extension. The DevTools enable us to:
- Inspect state.
- View state changes.
- Record, play and pause state changes in my application.
To get started download and install the Redex DevTool extension. Next, we will need to install the @ngrx/store-devtools package:
$ npm install @ngrx/store-devtools --save
Then, modify your AppModule
:
import { StoreDevtoolsModule } from "@ngrx/store-devtools";
@NgModule({
imports: [
StoreDevtoolsModule.instrumentOnlyWithExtension()
]
})
Note, you must import the StoreDevtoolsModule
after the StoreModule
.
There is a new tab in Chrome’s Developer Tools called Redux. Here is a screen shot showing the state change after attempting to load our application with not path.
Conclusion
A couple of things that I have learned:
- Read the docs.
- Refer to the example-app
- Do not try to circumvent the store. All routing and state changes must happen in the store.