Jekyll2024-05-17T21:56:59+00:00https://wesleygrimes.com/feed.xmlWesley GrimesWes is a lead software engineer specializing in enterprise web apps. In his spare time, he loves mentoring new developers, tracking the weather, and contributing to open source projects like NgRx. If you follow him on Twitter you’re going to find a lot of pictures of food, sunsets, and tidbits about Angular.How to upgrade your Angular and NgRx Apps to v82019-06-14T15:00:00+00:002019-06-14T15:00:00+00:00https://wesleygrimes.com/angular/2019/06/14/how-to-upgrade-your-angular-and-ngrx-apps-to-v8Overview

Do you have an awesome application written with Angular v7 using NgRx v7, but have been feeling left out will all the mentions online and at conferences about Angular v8 and NgRx v8? Well, you are in luck! Today we will explore together, how to upgrade our applications to use Angular v8 using the Angular CLI tooling. We will also explore upgrading to NgRx v8. This will allow us to take advantage of the new features provided in NgRx v8. Included with NgRx v8 is a shiny set of creators, or type-safe factory functions, for actions, effects, and reducers.

This article has been adapted from an original post on Ultimate Courses.

Upgrading Dependencies

Upgrading Angular

The Angular team has provided a great website that walks through the process of upgrading in-depth. This website can be found at Angular Update Tool. We will touch on some of the information today.

The first step is the process is to upgrade our application to Angular v8. We will use the Angular CLI to manage this process for us.

This is the preferred method, as Angular has provided built-in migration scripts or schematics to alleviate some of the manual process involved had we just simply updated versions in our package.json.

Let’s start by running the following command in the terminal:

Update the Global Angular CLI version

npm install -g @angular/cli

Update the core framework and local CLI to v8

ng update @angular/cli @angular/core

Throughout this process, we might encounter issues with third-party libaries. In those instances, it is best to visit the GitHub issues and repositories for those libraries for resolution.

Upgrading NgRx

Now that we have upgraded our application to use Angular v8, let’s proceed with updating NgRx to v8. We will make use of the Angular CLI here as well.

Update NgRx to v8

ng update @ngrx/store

The prior command should update our package.json dependencies and run any NgRx-provided migrations to keep our application in working order.

Depending on your setup, ng update @ngrx/store may not automatically update the additional @ngrx/* libraries that you have installed. If this happens, the best course is to manually run npm install for each additional module in use with NgRx.

Examples are as follows:

npm install @ngrx/entity@latest
npm install @ngrx/effects@latest
npm install @ngrx/data@latest
npm install @ngrx/router-store@latest

NgRx Migration Guide

The NgRx team has provided a detailed migration guide for updating to NgRx v8. More information on upgrading to v8 of NgRx can be found here: V8 Update Guide

Learn by Example - a Fruit Store (NgRx v7)

One of the most popular ways to learn new methods, is through code examples. Let’s explore the following example of a simplified NgRx store that holds an array of Fruit objects.

Each Fruit object consists of three properties fruitId, fruitClass and fruitName.

interface Fruit {
    fruitId: number;
    fruitType: string;
    fruitName: string;
}

For example, if we had an orange, it might look something like this:

const orange: Fruit = {
    fruitId: 1,
    fruitType: 'citrus',
    fruitName: 'orange'
};

State

Exploring further, our State object in the NgRx store will contain properties like fruits, isLoading, and errorMessage.

  • fruits is defined as an array for Fruit objects
  • isLoading is a boolean to keep track of when the store is in the process of loading data from an external API.
  • errorMessage is a string property that is null unless an error has occurred while requesting data from an external API.

An example State interface might look like the following:

interface State {
    fruits: Fruit[];
    isLoading: boolean;
    errorMessage: string;
}

An example store with fruits loaded might look like the following:

const state: State = {
    fruits: [
        {
            fruitId: 1,
            fruitType: 'citrus',
            fruitName: 'orange'
        }
    ],
    isLoading: false,
    errorMessage: null
}

Actions

Following proper redux pattern guidance, we cannot directly update state, so we need to define a set of actions to work with our state through a reducer. Let’s imagine we have 3 actions for this example:

  • [App Init] Load Request - This action is intended to be dispatched from our UI layer to indicate we are requesting to load Fruit objects into our store. This action does not have a payload or props.

  • [Fruits API] Load Success - This action is intended to be dispatched from our effects when an [App Init] Load Request has been dispatched, an API has been called and successful response is received from the API. This action contains a payload or props object that includes the array of Fruit object to be loaded into our store.

  • [Fruits API] Load Failure - This action is intended to be dispatched from our effects when an [App Init] Load Request has been dispatched, an API has been called and failure response is received from the API. This action contains a payload or props object that includes the error message of our API request, so that it can be loaded into our store.

NgRx v7 Implementation

The actual NgRx v7 implementation of our actions might look something like the following:

import { Action } from '@ngrx/store';
import { Fruit } from '../../models';

export enum ActionTypes {
  LOAD_REQUEST = '[App Init] Load Request',
  LOAD_FAILURE = '[Fruits API] Load Failure',
  LOAD_SUCCESS = '[Fruits API] Load Success'
}

export class LoadRequestAction implements Action {
  readonly type = ActionTypes.LOAD_REQUEST;
}

export class LoadFailureAction implements Action {
  readonly type = ActionTypes.LOAD_FAILURE;
  constructor(public payload: { error: string }) {}
}

export class LoadSuccessAction implements Action {
  readonly type = ActionTypes.LOAD_SUCCESS;
  constructor(public payload: { fruits: Fruit[] }) {}
}

export type ActionsUnion = LoadRequestAction | LoadFailureAction | LoadSuccessAction;

NgRx v8 - Upgrading to createAction

It’s important to note, that while createAction is the hot new way of defining an Action in NgRx, the existing method of defining an enum, class and exporting a type union will still work just fine in NgRx v8.

Beginning with version 8 of NgRx, actions can be declared using the new createAction method. This method is a factory function, or a function that returns a function.

According to the official NgRx documentation, “The createAction function returns a function, that when called returns an object in the shape of the Action interface. The props method is used to define any additional metadata needed for the handling of the action. Action creators provide a consistent, type-safe way to construct an action that is being dispatched.”

In order to update to createAction, we need to do the following steps:

  1. Create a new export const for our action. If our action has a payload, we will also need to migrate to using the props method to define our payload as props.

Example for [App Init] Load Request

// before
export class LoadRequestAction implements Action {
  readonly type = ActionTypes.LOAD_REQUEST;
}
// after
export const loadRequest = createAction('[App Init] Load Request');

Example for [Fruits API] Load Success

// before
export class LoadSuccessAction implements Action {
  readonly type = ActionTypes.LOAD_SUCCESS;
  constructor(public payload: { fruits: Fruit[] }) {}
}
// after
export const loadSuccess = createAction('[Fruits API] Load Success', props<{fruits: Fruit[]}>());
  1. Remove the old action from the ActionTypes enum

  2. Remove the old action from the ActionsUnion

Our final migrated actions file might look something like this:

import { Action, props } from '@ngrx/store';
import { Fruit } from '../../models';

export const loadRequest = createAction('[App Init] Load Request');
export const loadFailure = createAction('[Fruits API] Load Failure', props<{errorMessage: string}>());
export const loadSuccess = createAction('[Fruits API] Load Success', props<{fruits: Fruit[]}>());

As we can see, this is a huge reduction in code, we have gone from 24 lines of code, down to 6 lines of code.

NgRx v8 - Dispatching createAction Actions

A final note is that we need to update the way we dispatch our actions. This is because we no longer need to create class instances, rather we are calling factory functions that return an object of our action.

Our before and after will look something like this:

// before 
this.store.dispatch(new featureActions.LoadSuccessAction({ fruits }))

// after
this.store.dispatch(featureActions.loadSuccess({ fruits }))

Reducer

Continuing with our example, we need a reducer setup to broker our updates to the store. Recalling back to the redux pattern, we cannot directly update state. We must, through a pure function, take in current state, an action, and return a new updated state with the action applied. Typically, reducers are large switch statements keyed on incoming actions.

Let’s imagine our reducer handles the following scenarios:

  • On [App Init] Load Request we want the state to reflect the following values:
    • state.isLoading: true
    • state.errorMessage: null
  • On [Fruits API] Load Success we want the state to reflect the following values:
    • state.isLoading: false
    • state.errorMessage: null
    • state.fruits: action.payload.fruits
  • On [Fruits API] Load Failure we want the state to reflect the following values:
    • state.isLoading: false
    • state.errorMessage: action.payload.errorMessage

NgRx v7 Implementation

The actual NgRx v7 implementation of our reducer might look something like the following:

import { ActionsUnion, ActionTypes } from './actions';
import { initialState, State } from './state';

export function featureReducer(state = initialState, action: ActionsUnion): State {
  switch (action.type) {
    case ActionTypes.LOAD_REQUEST: {
      return {
        ...state,
        isLoading: true,
        errorMessage: null
      };
    }
    case ActionTypes.LOAD_SUCCESS: {
      return {
        ...state,
        isLoading: false,
        errorMessage: null,
        fruits: action.payload.fruits
      };
    }
    case ActionTypes.LOAD_FAILURE: {
      return {
        ...state,
        isLoading: false,
        errorMessage: action.payload.errorMessage
      };
    }
    default: {
      return state;
    }
  }
}

NgRx v8 - Upgrading to createReducer

It’s important to note, that while createReducer is the hot new way of defining a reducer in NgRx, the existing method of defining a function with a switch statement will still work just fine in NgRx v8.

Beginning with version 8 of NgRx, reducers can be declared using the new createReducer method.

According to the official NgRx documentation, “The reducer function’s responsibility is to handle the state transitions in an immutable way. Create a reducer function that handles the actions for managing the state using the createReducer function.”

In order to update to createReducer, we need to do the following steps:

  1. Create a new const reducer = createReducer for our reducer.
  2. Convert our switch case statements into on method calls. Please note, the default case is handled automatically for us. The first parameter of the on method is the action to trigger on, the second parameter is a handler that takes in state and returns a new version of state. If the action provides props, a second optional input parameter can be provided. In the example below we will use destructuring to pull the necessary properties out of the props object.
  3. Create a new export function reducer to wrap our const reducer for AOT support.

Once completed, our updated featureReducer will look something like the following:

import { createReducer, on } from '@ngrx/store';
import * as featureActions from './actions';
import { initialState, State } from './state';
...
const featureReducer = createReducer(
  initialState,
  on(featureActions.loadRequest, state => ({ ...state, isLoading: true, errorMessage: null })),
  on(featureActions.loadSuccess, (state, { fruits }) => ({ ...state, isLoading: false, errorMessage: null, fruits })),
  on(featureActions.loadFailure, (state, { errorMessage }) => ({ ...state, isLoading: false, errorMessage: errorMessage })),
);

export function reducer(state: State | undefined, action: Action) {
  return featureReducer(state, action);
}

Effects

Because we want to keep our reducer a pure function, it’s often desirable to place API requests into side-effects. In NgRx, these are called Effects and provide a reactive, RxJS-based way to link actions to observable streams.

In our example, we will have an Effect that listens for an [App Init] Load Request Action and makes an HTTP request to our imaginary Fruits API backend.

  • Upon a successful result from the Fruits API the response is mapped to an [Fruits API] Load Success action setting the payload of fruits to the body of the successful response.

  • Upon a failure result from the Fruits API the error message is mapped to an [Fruits API] Load Failure action setting the payload of errorMessage to the error from the failure response.

NgRx v7 Implementation

The actual NgRx v7 implementation of our effect might look something like the following:

@Effect()
  loadRequestEffect$: Observable<Action> = this.actions$.pipe(
    ofType<featureActions.LoadRequestAction>(
      featureActions.ActionTypes.LOAD_REQUEST
    ),
    concatMap(action =>
      this.dataService
        .getFruits()
        .pipe(
          map(
            fruits =>
              new featureActions.LoadSuccessAction({
                fruits
              })
          ),
          catchError(error =>
            observableOf(new featureActions.LoadFailureAction({ errorMessage: error.message }))
          )
        )
    )
  );

NgRx v8 - Upgrading to createEffect

It’s important to note, that while createEffect is the hot new way of defining a reducer in NgRx, the existing method of defining a class property with an @Effect() decorator will still work just fine in NgRx v8.

Beginning with version 8 of NgRx, effects can be declared using the new createEffect method, according to the official NgRx documentation.

In order to update to createEffect, we need to do the following steps:

  1. Import createEffect from @ngrx/effects
  2. Remove the @Effect() decorator
  3. Remove the Observable<Action> type annotation
  4. Wrap this.actions$.pipe(...) with createEffect(() => ...)
  5. Remove the <featureActions.LoadRequestAction> type annotation from ofType
  6. Change the ofType input parameter from featureActions.ActionTypes.LOAD_REQUEST to featureActions.loadRequest
  7. Update the action calls to remove new and to use the creator instead of class instance. For example, new featureActions.LoadSuccessAction({fruits}) becomes featureActions.loadSuccess({fruits}).

Once completed, our updated loadRequestEffect will look something like the following:

  loadRequestEffect$ = createEffect(() => this.actions$.pipe(
        ofType(featureActions.loadRequest),
        concatMap(action =>
        this.dataService
            .getFruits()
            .pipe(
                map(fruits => featureActions.loadSuccess({fruits})),
                catchError(error =>
                    observableOf(featureActions.loadFailure({ errorMessage: error.message }))
                )
            )
        )
    )
  );

Full Video Walkthrough

If you would like to watch a full video walkthrough here you go.

Conclusion

This brings us to the end of this guide. Hopefully, you’ve been able to learn about upgrading your application to Angular v8 and NgRx v8. In addition, you should feel confident in taking advantage of some of the new features available in NgRx v8 to reduce the occurrence of what some might refer to as boilerplate. Happy updating and upgrading!

]]>
Wesley Grimes
Managing File Uploads With NgRx2019-04-01T10:00:00+00:002019-04-01T10:00:00+00:00https://wesleygrimes.com/angular/2019/04/01/managing-file-uploads-with-ngrx

In this article we will build a fully-functional file upload control, that is powered by Angular and is backed by an NgRx feature store. The control will provide the user with the following features:

  • The ability to upload files using the <input #file type="file" /> HTML element.
  • The ability to see an accurate upload progress via the reportProgress HttpClient option.
  • The ability to cancel in-process uploads

As an added bonus, we will briefly dive into building the server-side ASP.NET Core WebAPI Controller that will handle the file uploads.

Before We Get Started

In this article, I will show you how to manage file uploads using NgRx. If you are new to NgRx, then I highly recommend that you first read my article, NgRx - Best Practices for Enterprise Angular Applications. We will be using the techniques described in that article to build out the NgRx components for file uploads.

If you are new to Angular, then I recommend that you check out one of the following resources:

NPM Package Versions

For context, this article assumes you are using the following npm package.json versions:

  • @angular/*: 7.2.9
  • @ngrx/*: 7.3.0

Prerequisites

Before diving into building the file upload control, make sure that you have the following in place:

  1. An Angular 7+ application generated
  2. NgRx dependencies installed
  3. NgRx Store wired up in your application. e.g. Follow this guide

Create the Upload File Service

Let’s create a brand new service in Angular. This service will be responsible for handling the file upload from the client to the server backend. We will use the amazing HttpClient provided with Angular.

Generate the service

$ ng g service file-upload

Inject the HttpClient

Because we are using the HttpClient to make requests to the backend, we need to inject it into our service. Update the constructor line of code so that it looks as follows:

constructor(private httpClient: HttpClient) {}

Add a private field for API_BASE_URL

I typically store API base URLs in the src/environments area. If you’re interested in learning more about environments in Angular then check out this great article: Becoming an Angular Environmentalist

Let’s create a new private field named API_BASE_URL so that we can use this in our calls to the backend API.

One way to accomplish this would be to do the following:

import { environment } from 'src/environments/environment';
...
private API_BASE_URL = environment.apiBaseUrl;

Add a uploadFile public method

Let’s create a new public method named uploadFile to the service. The method will take in a parameter file: File and return an Observable<HttpEvent<{}>>.

Typically a get or post Observable<T> is returned from a service like this. However, in this situation we are going to actually return the raw request which is an Observable<HttpEvent<{}>>.

By returning a raw request we have more control over the process, to pass options like reportProgress and allow cancellation of a request.

public uploadFile(file: File): Observable<HttpEvent<{}>> {
  const formData = new FormData();
  formData.append('files', file, file.name);

  const options = {
    reportProgress: true
  };

  const req = new HttpRequest(
    'POST',
    `${this.API_BASE_URL}/api/file`,
    formData,
    options
  );
  return this.httpClient.request(req);
}

Completed File Upload Service

The completed file-upload.service.ts will look as follows:

import { HttpClient, HttpEvent, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class FileUploadService {
  private API_BASE_URL = environment.apiBaseUrl;

  constructor(private httpClient: HttpClient) {}

  public uploadFile(file: File): Observable<HttpEvent<{}>> {
    const formData = new FormData();
    formData.append('files', file, file.name);

    const options = {
      reportProgress: true
    };

    const req = new HttpRequest(
      'POST',
      `${this.API_BASE_URL}/api/file`,
      formData,
      options
    );
    return this.httpClient.request(req);
  }
}

Create the Upload File Feature Store

To keep your NgRx store organized, I recommend creating a separate Upload File Feature Store. Let’s bundle it all together in a module named upload-file-store.module.ts and keep it under a sub-directory named upload-file-store.

Create Feature Store Module

Create a feature store module using the following command:

$ ng g module upload-file-store --flat false

Create State Interface

Create a new file underneath the upload-file-store folder, named state.ts. The contents of the file will be as follows:

We are using a relatively new technique in that we will set up an enum to track the status. This enum will reflect the current state of the upload process. For more information on this method, check out Alex Okrushko’s article.

export enum UploadStatus {
  Ready = 'Ready',
  Requested = 'Requested',
  Started = 'Started',
  Failed = 'Failed',
  Completed = 'Completed'
}

export interface State {
  status: UploadStatus;
  error: string | null;
  progress: number | null;
}

export const initialState: State = {
  status: UploadStatus.Ready,
  error: null,
  progress: null
};

Create Feature Actions

If you would like to learn more about NgRx Actions, then check out the official docs.

Create a new file underneath the upload-file-store folder, named actions.ts. This file will hold the actions we want to make available on this store.

We will create the following actions on our feature store:

  • UPLOAD_REQUEST - This action is dispatched from the file upload form, it’s payload will contain the actual File being uploaded.

  • UPLOAD_CANCEL - This action is dispatched from the file upload form when the cancel button is clicked. This will be used to cancel uploads in progress.

  • UPLOAD_RESET - This action is dispatched from the file upload form when the reset button is clicked. This will be used to reset the state of the store to defaults.

  • UPLOAD_STARTED - This action is dispatched from the file upload effect, HttpClient when the API reports the HttpEventType.Sent event.

  • UPLOAD_PROGRESS - This action is dispatched from the file upload effect, HttpClient when the API reports the HttpEventType.UploadProgress event. The payload will contain the progress percentage as a whole number.

  • UPLOAD_FAILURE - This action is dispatched from the file upload effect when the API returns an error, or there is an HttpEventType.ResponseHeader or HttpEventType.Response with an event.status !== 200, or when an unknown HttpEventType is returned. The payload will contain the specific error message returned from the API and place it into an error field on the store.

  • UPLOAD_COMPLETED - This action is dispatched from the file upload effect when the API reports a HttpEventType.ResponseHeader or HttpEventType.Response event event.status === 200. There is no payload as the API just returns a 200 OK repsonse.

The final actions.ts file will look as follows:

import { Action } from '@ngrx/store';

export enum ActionTypes {
  UPLOAD_REQUEST = '[File Upload Form] Request',
  UPLOAD_CANCEL = '[File Upload Form] Cancel',
  UPLOAD_RESET = '[File Upload Form] Reset',
  UPLOAD_STARTED = '[File Upload API] Started',
  UPLOAD_PROGRESS = '[File Upload API] Progress',
  UPLOAD_FAILURE = '[File Upload API] Failure',
  UPLOAD_COMPLETED = '[File Upload API] Success'
}

export class UploadRequestAction implements Action {
  readonly type = ActionTypes.UPLOAD_REQUEST;
  constructor(public payload: { file: File }) {}
}

export class UploadCancelAction implements Action {
  readonly type = ActionTypes.UPLOAD_CANCEL;
}

export class UploadResetAction implements Action {
  readonly type = ActionTypes.UPLOAD_RESET;
}

export class UploadStartedAction implements Action {
  readonly type = ActionTypes.UPLOAD_STARTED;
}

export class UploadProgressAction implements Action {
  readonly type = ActionTypes.UPLOAD_PROGRESS;
  constructor(public payload: { progress: number }) {}
}

export class UploadFailureAction implements Action {
  readonly type = ActionTypes.UPLOAD_FAILURE;
  constructor(public payload: { error: string }) {}
}

export class UploadCompletedAction implements Action {
  readonly type = ActionTypes.UPLOAD_COMPLETED;
}

export type Actions =
  | UploadRequestAction
  | UploadCancelAction
  | UploadResetAction
  | UploadStartedAction
  | UploadProgressAction
  | UploadFailureAction
  | UploadCompletedAction;

Create the Feature Reducer

If you would like to learn more about NgRx Reducers, then check out the official docs.

Create a new file underneath the upload-file-store folder, named reducer.ts. This file will hold the reducer we create to manage state transitions to the store.

We will handle state transitions as follows for the aforementioned actions:

  • UPLOAD_REQUEST - Reset the state, with the exception of setting state.status to UploadStatus.Requested.

  • UPLOAD_CANCEL - Reset the state tree. Our effect will listen for any UPLOAD_CANCEL event dispatches so a specific state field is not needed for this.

  • UPLOAD_RESET - Reset the state tree on this action.

  • UPLOAD_FAILURE - Reset the state tree, with the exception of setting state.status to UploadStatus.Failed and state.error to the error that was throw in the catchError from the API in the uploadRequestEffect effect.

  • UPLOAD_STARTED - Set state.progress to 0 and state.status to UploadStatus.Started.

  • UPLOAD_PROGRESS - Set state.progress to the current action.payload.progress provided from the action.

  • UPLOAD_COMPLETED - Reset the state tree, with the exception of setting state.status to UploadStatus.Completed so that the UI can display a success message.

import { Actions, ActionTypes } from './actions';
import { initialState, State, UploadStatus } from './state';

export function featureReducer(state = initialState, action: Actions): State {
  switch (action.type) {
    case ActionTypes.UPLOAD_REQUEST: {
      return {
        ...state,
        status: UploadStatus.Requested,
        progress: null,
        error: null
      };
    }
    case ActionTypes.UPLOAD_CANCEL: {
      return {
        ...state,
        status: UploadStatus.Ready,
        progress: null,
        error: null
      };
    }
    case ActionTypes.UPLOAD_RESET: {
      return {
        ...state,
        status: UploadStatus.Ready,
        progress: null,
        error: null
      };
    }
    case ActionTypes.UPLOAD_FAILURE: {
      return {
        ...state,
        status: UploadStatus.Failed,
        error: action.payload.error,
        progress: null
      };
    }
    case ActionTypes.UPLOAD_STARTED: {
      return {
        ...state,
        status: UploadStatus.Started,
        progress: 0
      };
    }
    case ActionTypes.UPLOAD_PROGRESS: {
      return {
        ...state,
        progress: action.payload.progress
      };
    }
    case ActionTypes.UPLOAD_COMPLETED: {
      return {
        ...state,
        status: UploadStatus.Completed,
        progress: 100,
        error: null
      };
    }
    default: {
      return state;
    }
  }
}

Create the Feature Effects

If you would like to learn more about NgRx Effects, then check out the official docs.

Create a new file underneath the upload-file-store folder, named effects.ts. This file will hold the effects that we create to handle any side-effect calls to the backend API service. This effect is where most of the magic happens in the application.

Inject Dependencies

Let’s add the necessary dependencies to our constructor as follows:

constructor(
  private fileUploadService: FileUploadService,
  private actions$: Actions<fromFileUploadActions.Actions>
) {}

Add a new Upload Request Effect

Effects make heavy-use of RxJS concepts and topics. If you are new to RxJS then I suggest you check out the official docs

Let’s create a new effect in the file named uploadRequestEffect$.

A couple comments about what this effect is going to do:

  • Listen for the UPLOAD_REQUEST action and then make calls to the fileUploadService.uploadFile service method to initiate the upload process.

  • Use the concatMap RxJS operator here so that multiple file upload requests are queued up and processed in the order they were dispatched.

  • Use the takeUntil RxJS operator listening for an UPLOAD_CANCEL action to be dispatched. This allows us to short-circuit any requests that are in-flight.

  • Use the map RxJS operator to map specific HttpEvent responses to dispatch specific Actions that we have defined in our Store.

  • Use the catchError RxJS operator to handle any errors that may be thrown from the HttpClient.

The effect will look something like this:

@Effect()
uploadRequestEffect$: Observable<Action> = this.actions$.pipe(
  ofType(fromFileUploadActions.ActionTypes.UPLOAD_REQUEST),
  concatMap(action =>
    this.fileUploadService.uploadFile(action.payload.file).pipe(
      takeUntil(
        this.actions$.pipe(
          ofType(fromFileUploadActions.ActionTypes.UPLOAD_CANCEL)
        )
      ),
      map(event => this.getActionFromHttpEvent(event)),
      catchError(error => of(this.handleError(error)))
    )
  )
);

Add the getActionFromHttpEvent private method

For more information on listening to progress events, check out the official docs guide from here.

This method will be responsible for mapping a specific HttpEventType to a specific Action that is dispatched.

  • HttpEventType.Sent - This event occurs when the upload process has begun. We will dispatch an UPLOAD_STARTED action to denote that the process has begun.

  • HttpEventType.UploadProgress - This event occurs when the upload process has made progress. We will dispatch an UPLOAD_PROGRESS action with a payload of progress: Math.round((100 * event.loaded) / event.total) to calculate the actual percentage complete of upload. This is because the HttpClient returns an event.loaded and event.total property in whole number format.

  • HttpEventType.Response / HttpEventType.ResponseHeader - These events occur when the upload process has finished. It is important to note that this could be a success or failure so we need to interrogate the event.status to check for 200. We will dispatch the UPLOAD_COMPLETED action if event.status === 200 and UPLOAD_FAILURE if the event.status !== 200 passing the event.statusText as the error payload.

  • All Others (default case) - We treat any other events that may be returned as an error because they are unexpected behavior. We will dispatch a UPLOAD_FAILURE action with a payload of the event run through JSON.stringify.

private getActionFromHttpEvent(event: HttpEvent<any>) {
  switch (event.type) {
    case HttpEventType.Sent: {
      return new fromFileUploadActions.UploadStartedAction();
    }
    case HttpEventType.UploadProgress: {
      return new fromFileUploadActions.UploadProgressAction({
        progress: Math.round((100 * event.loaded) / event.total)
      });
    }
    case HttpEventType.ResponseHeader:
    case HttpEventType.Response: {
      if (event.status === 200) {
        return new fromFileUploadActions.UploadCompletedAction();
      } else {
        return new fromFileUploadActions.UploadFailureAction({
          error: event.statusText
        });
      }
    }
    default: {
      return new fromFileUploadActions.UploadFailureAction({
        error: `Unknown Event: ${JSON.stringify(event)}`
      });
    }
  }
}

Add the handleError private method

For more information on handling HttpClient errors, check out the official docs guide from here.

This method will be responsible for handling any errors that may be thrown from the HttpClient during requests. I am making use of a neat library from npm named serialize-error to give me a predictable error.message no matter what type of error is thrown.

Install the library as so:

$ npm install serialize-error
import serializeError from 'serialize-error';
...
private handleError(error: any) {
  const friendlyErrorMessage = serializeError(error).message;
  return new fromFileUploadActions.UploadFailureAction({
    error: friendlyErrorMessage
  });
}

Completed Feature Effect

The completed effect will look something like this:

import { HttpEvent, HttpEventType } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { Observable, of } from 'rxjs';
import { catchError, concatMap, map, takeUntil } from 'rxjs/operators';
import serializeError from 'serialize-error';
import { FileUploadService } from 'src/app/_services';
import * as fromFileUploadActions from './actions';

@Injectable()
export class UploadFileEffects {
  @Effect()
  uploadRequestEffect$: Observable<Action> = this.actions$.pipe(
    ofType(fromFileUploadActions.ActionTypes.UPLOAD_REQUEST),
    concatMap(action =>
      this.fileUploadService.uploadFile(action.payload.file).pipe(
        takeUntil(
          this.actions$.pipe(
            ofType(fromFileUploadActions.ActionTypes.UPLOAD_CANCEL)
          )
        ),
        map(event => this.getActionFromHttpEvent(event)),
        catchError(error => of(this.handleError(error)))
      )
    )
  );

  constructor(
    private fileUploadService: FileUploadService,
    private actions$: Actions<fromFileUploadActions.Actions>
  ) {}

  private getActionFromHttpEvent(event: HttpEvent<any>) {
    switch (event.type) {
      case HttpEventType.Sent: {
        return new fromFileUploadActions.UploadStartedAction();
      }
      case HttpEventType.UploadProgress: {
        return new fromFileUploadActions.UploadProgressAction({
          progress: Math.round((100 * event.loaded) / event.total)
        });
      }
      case HttpEventType.ResponseHeader:
      case HttpEventType.Response: {
        if (event.status === 200) {
          return new fromFileUploadActions.UploadCompletedAction();
        } else {
          return new fromFileUploadActions.UploadFailureAction({
            error: event.statusText
          });
        }
      }
      default: {
        return new fromFileUploadActions.UploadFailureAction({
          error: `Unknown Event: ${JSON.stringify(event)}`
        });
      }
    }
  }

  private handleError(error: any) {
    const friendlyErrorMessage = serializeError(error).message;
    return new fromFileUploadActions.UploadFailureAction({
      error: friendlyErrorMessage
    });
  }
}

Create the Feature Selectors

If you would like to learn more about NgRx Selectors, then check out the official docs.

Create a new file underneath the upload-file-store folder, named selectors.ts. This file will hold the selectors we will use to pull specific pieces of state out of the store. These are technically not required, but strongly encouraged. Selectors improve application performance with the use of the MemoizedSelector wrapper. Selectors also simplify UI logic.

We will create a selector for each significant property of the state. This includes the following properties:

  • state.status - Since this is an enum we will create a selector for each enum choice.
  • state.error
  • state.progress

The completed selectors file will look something like the following:

import {
  createFeatureSelector,
  createSelector,
  MemoizedSelector
} from '@ngrx/store';
import { State, UploadStatus } from './state';

const getError = (state: State): string => state.error;

const getStarted = (state: State): boolean =>
  state.status === UploadStatus.Started;

const getRequested = (state: State): boolean =>
  state.status === UploadStatus.Requested;

const getReady = (state: State): boolean => state.status === UploadStatus.Ready;

const getProgress = (state: State): number => state.progress;

const getInProgress = (state: State): boolean =>
  state.status === UploadStatus.Started && state.progress >= 0;

const getFailed = (state: State): boolean =>
  state.status === UploadStatus.Failed;

const getCompleted = (state: State): boolean =>
  state.status === UploadStatus.Completed;

export const selectUploadFileFeatureState: MemoizedSelector<
  object,
  State
> = createFeatureSelector<State>('uploadFile');

export const selectUploadFileError: MemoizedSelector<
  object,
  string
> = createSelector(
  selectUploadFileFeatureState,
  getError
);

export const selectUploadFileReady: MemoizedSelector<
  object,
  boolean
> = createSelector(
  selectUploadFileFeatureState,
  getReady
);

export const selectUploadFileRequested: MemoizedSelector<
  object,
  boolean
> = createSelector(
  selectUploadFileFeatureState,
  getRequested
);

export const selectUploadFileStarted: MemoizedSelector<
  object,
  boolean
> = createSelector(
  selectUploadFileFeatureState,
  getStarted
);

export const selectUploadFileProgress: MemoizedSelector<
  object,
  number
> = createSelector(
  selectUploadFileFeatureState,
  getProgress
);

export const selectUploadFileInProgress: MemoizedSelector<
  object,
  boolean
> = createSelector(
  selectUploadFileFeatureState,
  getInProgress
);

export const selectUploadFileFailed: MemoizedSelector<
  object,
  boolean
> = createSelector(
  selectUploadFileFeatureState,
  getFailed
);

export const selectUploadFileCompleted: MemoizedSelector<
  object,
  boolean
> = createSelector(
  selectUploadFileFeatureState,
  getCompleted
);

Update the Feature Module

We now need to update the feature module UploadFileStoreModule to wire-up the store.

The completed UploadFileStoreModule should look similar to this:

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { UploadFileEffects } from './effects';
import { featureReducer } from './reducer';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    StoreModule.forFeature('uploadFile', featureReducer),
    EffectsModule.forFeature([UploadFileEffects])
  ]
})
export class UploadFileStoreModule {}

Import this module where needed

Make sure to import this new UploadFileStoreModule where it is needed. In this example, we will import this into the AppModule as we do not have any lazy-loaded features.

Update your AppModule to import Store & Effects

Last, make sure that you update your AppModule to import the StoreModule.forRoot and EffectsModule.forRoot.

An updated AppModule may look as follows:

import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from 'src/environments/environment';
import { AppComponent } from './app.component';
import { UploadFileStoreModule } from './upload-file-store/upload-file-store.module';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    HttpClientModule,
    StoreModule.forRoot({}),
    EffectsModule.forRoot([]),
    StoreDevtoolsModule.instrument({
      maxAge: 25, // Retains last 25 states
      logOnly: environment.production // Restrict extension to log-only mode
    }),
    UploadFileStoreModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Let’s Review So Far

  • Up to this point, we have created a new FileUploadService that calls our backend API to upload a File object.

  • We have also created a new UploadFileStore feature store that provides Actions, a Reducer, Effects, and Selectors to manage the file upload process.

  • Last, the store has been imported into our AppModule for use.

Now that we have the foundation laid out for us we can turn our attention to the user interface and wire-up a new component to the UploadFileStore that we created to manage our process.

This will be the fun part!


Create the Upload File Component

Let’s start by creating a brand-new Component. This component will consist of the following elements:

  • An input element for the user to interact with to upload a file. The change event will dispatch the UploadFileStoreActions.UploadRequest() action

  • A progress percentage to connected to the UploadFileStoreSelectors.selectUploadFileProgress selector for real-time progress

  • A Cancel UPload button to dispatch the UploadFileStoreActions.UploadCancelRequest() action

  • An Upload Another File button to dispatch the UploadFileStoreActions.UploadResetRequest() action and allow for a new file upload

SIDE NOTE: This would be a good scenario to create a connected container with a dumb component, but for the brevity of this article I will show these combined as one. In the example repository, I will show both scenarios.

Generate the component

Click here for more details on using the powerful Angular CLI

$ ng g component upload-file

For simplicity of this article we will just display the progress percentage, this could easily be adapted to hook into the value property of a progress bar control, like the Angular Material library provides.

Update the component *.ts file

Inject the Store

We need to wire-up our store into this component for use. Let’s start by injecting the store into the constructor. The finished constructor should look something like this:

...
constructor(private store$: Store<fromFileUploadState.State>) {}

Wire-up our selectors from state

Let’s create six (6) public fields on the component. A good practice is to place $ as a suffix so that you know these are Observable and must be subscribed to in the template.

completed$: Observable<boolean>;
progress$: Observable<number>;
error$: Observable<string>;

isInProgress$: Observable<boolean>;
isReady$: Observable<boolean>;
hasFailed$: Observable<boolean>;

Let’s hook these up to the store in our ngOnInit life-cycle hook.

ngOnInit() {
  this.completed$ = this.store$.pipe(
    select(fromFileUploadSelectors.selectUploadFileCompleted)
  );

  this.progress$ = this.store$.pipe(
    select(fromFileUploadSelectors.selectUploadFileProgress)
  );

  this.error$ = this.store$.pipe(
    select(fromFileUploadSelectors.selectUploadFileError)
  );

  this.isInProgress$ = this.store$.pipe(
    select(fromFileUploadSelectors.selectUploadFileInProgress)
  );

  this.isReady$ = this.store$.pipe(
    select(fromFileUploadSelectors.selectUploadFileReady)
  );

  this.hasFailed$ = this.store$.pipe(
    select(fromFileUploadSelectors.selectUploadFileFailed)
  );
}

Wire-up our action dispatchers

Let’s add uploadFile, resetUpload, and cancelUpload methods to connect our button clicks to dispatch actions in the store.

uploadFile(event: any) {
  const files: FileList = event.target.files;
  const file = files.item(0);

  this.store$.dispatch(
    new fromFileUploadActions.UploadRequestAction({
      file
    })
  );

  // clear the input form
  event.srcElement.value = null;
}

resetUpload() {
  this.store$.dispatch(new UploadFileStoreActions.UploadResetAction());
}

cancelUpload() {
  this.store$.dispatch(new UploadFileStoreActions.UploadCancelAction());
}

Finished Component *.ts file

The finished component *.ts file should look similar to the following:

import { Component, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import * as fromFileUploadActions from 'src/app/upload-file-store/actions';
import * as fromFileUploadSelectors from 'src/app/upload-file-store/selectors';
import * as fromFileUploadState from 'src/app/upload-file-store/state';

@Component({
  selector: 'app-upload-file',
  templateUrl: './upload-file.component.html',
  styleUrls: ['./upload-file.component.css']
})
export class UploadFileComponent implements OnInit {
  completed$: Observable<boolean>;
  progress$: Observable<number>;
  error$: Observable<string>;
  isInProgress$: Observable<boolean>;
  isReady$: Observable<boolean>;
  hasFailed$: Observable<boolean>;

  constructor(private store$: Store<fromFileUploadState.State>) {}

  ngOnInit() {
    this.completed$ = this.store$.pipe(
      select(fromFileUploadSelectors.selectUploadFileCompleted)
    );

    this.progress$ = this.store$.pipe(
      select(fromFileUploadSelectors.selectUploadFileProgress)
    );

    this.error$ = this.store$.pipe(
      select(fromFileUploadSelectors.selectUploadFileError)
    );

    this.isInProgress$ = this.store$.pipe(
      select(fromFileUploadSelectors.selectUploadFileInProgress)
    );

    this.isReady$ = this.store$.pipe(
      select(fromFileUploadSelectors.selectUploadFileReady)
    );

    this.hasFailed$ = this.store$.pipe(
      select(fromFileUploadSelectors.selectUploadFileFailed)
    );
  }

  uploadFile(event: any) {
    const files: FileList = event.target.files;
    const file = files.item(0);

    this.store$.dispatch(
      new fromFileUploadActions.UploadRequestAction({
        file
      })
    );

    // clear the input form
    event.srcElement.value = null;
  }

  resetUpload() {
    this.store$.dispatch(new fromFileUploadActions.UploadResetAction());
  }

  cancelUpload() {
    this.store$.dispatch(new fromFileUploadActions.UploadCancelAction());
  }
}

Update the component *.html template

We are going to add five (5) major parts to our upload file component.

Add the input field

There is no upload file button, rather we will make use of the built-in input component and hook to the change event. Any time a file is added to the form this event will fire. We also only want to display this form if we are accepting new files to be uploaded, i.e. it has failed or it is ready. We will use the *ngIf structural directive to help here referencing our isReady$ and hasFailed$ observables.

<div class="message" *ngIf="(isReady$ | async) || (hasFailed$ | async)">
  <input #file type="file" multiple (change)="uploadFile($event)" />
</div>

Add the progress message

This message will be displayed when the progress is greater than or equal to 0% and the UploadStatus is Failed. We will use *ngIf to only display if it’s in this state using the isInProgress$ selector value. We will set the text of the progress message to the progress$ selector value.

<div class="message" *ngIf="(isInProgress$ | async)">
  <div style="margin-bottom: 14px;">Uploading... %</div>
</div>

Add the Cancel Upload button

This button will utilize the *ngIf to only display if the upload is in progress using the isInProgress$ selector value. The click event will trigger the dispatch of the UploadCancelAction.

<div class="message" *ngIf="(isInProgress$ | async)">
  <button (click)="cancelUpload()">Cancel Upload</button>
</div>

Add the Reset Upload button

This button will utilize the *ngIf to only display if the upload is complete using the completed$ selector value. The click event will trigger the dispatch of the UploadResetAction.

<div class="message" *ngIf="(completed$ | async)">
  <h4>
    File has been uploaded successfully!
  </h4>
  <button (click)="resetUpload()">Upload Another File</button>
</div>

Add the Error message

This button will utilize the *ngIf to only display if hasFailed$ selector value returns true. The actual error message is pulled from the error$ selector value.

<div class="message error" *ngIf="(hasFailed$ | async)">
  Error: 
</div>

Finished Component *.html file

<div class="message" *ngIf="(isReady$ | async) || (hasFailed$ | async)">
  <input #file type="file" multiple (change)="uploadFile($event)" />
</div>

<div class="message" *ngIf="(isInProgress$ | async)">
  <div style="margin-bottom: 14px;">Uploading... %</div>
</div>

<div class="message" *ngIf="(isInProgress$ | async)">
  <button (click)="cancelUpload()">Cancel Upload</button>
</div>

<div class="message" *ngIf="(completed$ | async)">
  <h4>
    File has been uploaded successfully!
  </h4>
  <button (click)="resetUpload()">Upload Another File</button>
</div>

<div class="message error" *ngIf="(hasFailed$ | async)">
  Error: 
</div>

Add some styles to our Component *.css file

For formatting let’s add a few simple classes to our component stylesheet:

.message {
  margin-bottom: 15px;
}

.error {
  color: red;
}

Add the Component to our AppComponent

For the purposes of this article we will add our new UploadFileComponent component to our AppComponent. The template will look as follows:

<app-upload-file></app-upload-file>

(Bonus Feature) Back-end REST Endpoint

For a full mock back-end server checkout my [repository here:

For those of you brave souls that have made it this far… You might be asking what the backend API endpoint looks like. Well, here’s an example ASP.NET Core Controller offered free of charge ;-)

public class FileController : ControllerBase
{
    [HttpPost("")]
    public async Task<IActionResult> Post(List<IFormFile> files)
    {
        try
        {
            foreach (var file in files)
            {
                Console.WriteLine($"Begin Uploaded File: {file.FileName}");

                //simulate upload
                Task.Delay(5000).Wait();

                Console.WriteLine($"Finished Uploaded File: {file.FileName}");
            }

            return Ok();
        }
        catch (Exception ex)
        {
            return BadRequest($"Unable to upload file(s).");
        }
    }
}

GitHub Example Repository

I always like to provide working code examples that follow the article. You can find this articles companion application at the following repository:

Conclusion

It’s important to remember that I have implemented these best practices in several “real world” applications. While I have found these best practices helpful, and maintainable, I do not believe they are an end-all-be-all solution to your NgRx projects; it’s just what has worked for me. I am curious as to what you all think? Please feel free to offer any suggestions, tips, or best practices you’ve learned when building enterprise Angular applications with NgRx and I will update the article to reflect as such. Happy Coding!


Additional Resources

I would highly recommend enrolling in the Ultimate Angular courses, especially the NgRx course. It is well worth the money and I have used it as a training tool for new Angular developers. Follow the link below to signup.

Ultimate Courses: Expert online courses in JavaScript, Angular, NGRX and TypeScript

]]>
Wesley Grimes
A Deep Dive into Angular’s NgOnDestroy2019-03-29T10:34:51+00:002019-03-29T10:34:51+00:00https://wesleygrimes.com/angular/2019/03/29/making-upgrades-to-angular-ngondestroy

This article is a continuation of an Angular Hot Tip tweet that I sent out earlier this week. It became widely popular and generated quite a discussion. The concepts explored in this article reflect that discussion, so you should probably take some time and go check it out here:

As an extension of the above mentioned tweet, we will discuss limitations with how and when ngOnDestroy is called. We will also discuss ways to overcome those limitations. If you are new to Angular, or new to lifecycle methods in Angular, then I suggest you check out the official docs here.


NPM Package Versions

For context, this article assumes you are using the following npm package.json versions:

  • @angular/*: 7.2.9

A Brief Primer On NgOnDestroy

Before we dig too deep, let’s take a few minutes and review ngOnDestroy.

NgOnDestroy is a lifecycle method that can be added by implementing OnDestroy on the class and adding a new class method named ngOnDestroy. It’s primary purpose according to the Angular Docs is to “Cleanup just before Angular destroys the directive/component. Unsubscribe Observables and detach event handlers to avoid memory leaks. Called just before Angular destroys the directive/component.”

A Leaky MyValueComponent

Let’s imagine that we have a component named MyValueComponent that subscribes to a value from MyService in the ngOnInit method:

import { Component, OnInit } from '@angular/core';
import { MyService } from './my.service';

@Component({
  selector: 'app-my-value',
  templateUrl: './my-value.component.html',
  styleUrls: [ './my-value.component.css' ]
})
export class MyValueComponent implements OnInit {
  myValue: string;

  constructor(private myService: MyService) {}

  ngOnInit() {
      this.myService.getValue().subscribe(value => this.myValue = value);
  }
}

If this component is created and destroyed multiple times in the lifecycle of an Angular application, each time it’s created the ngOnInit would be called creating a brand new subscription. This could quickly get out of hand, with our value being updated exponentially. This is creating what is called a “memory leak”. Memory leaks can wreak havoc on the performance of an application and in addition add unpredictable or unintended behaviors. Let’s read on to learn how to plug this leak.

Fixing the Leak on MyValueComponent

To fix the memory leak we need to augment the component class with an implementation of OnDestroy and unsubscribe from the subscription. Let’s update our component adding the following:

  • Add OnDestroy to the typescript import
  • Add OnDestroy to the implements list
  • Create a class field named myValueSub: Subscription to track our subscription
  • Set this.myValueSub equal to the value of this.myService.getValue().subscription
  • Create a new class method named ngOnDestroy
  • Call this.myValueSub.unsubscribe() within ngOnDestroy if a subscription has been set.

The updated component will look something like this:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { MyService } from './my.service';

@Component({
  selector: 'app-my-value',
  templateUrl: './my-value.component.html',
  styleUrls: [ './my-value.component.css' ]
})
export class MyValueComponent implements OnInit, OnDestroy {
  myValue: string;
  myValueSub: Subscription;

  constructor(private myService: MyService) {}

  ngOnInit() {
      this.myValueSub = this.myService.getValue().subscribe(value => this.myValue = value);
  }

  ngOnDestroy() {
    if (this.myValueSub) {
        this.myValueSub.unsubscribe();
    }
  }
}

Moving Beyond Memory Leaks

Great! Now you have some background on ngOnDestroy and how cleaning up memory leaks is the primary use case for this lifecycle method. But what if you want to take it a step further and add additional cleanup logic? How about making server-side cleanup calls? Maybe preventing user navigation away?

As you read on we will discuss three methods to upgrade your ngOnDestroy for optimum use.


Hot Tip #1 - Making NgOnDestroy Async

As with other lifecycle methods in Angular, you can modify ngOnDestroy with async. This will allow you to make calls to methods returning a Promise. This can be a powerful way to manage cleanup activities in your application. As you read on we will explore an example of this.

Adding logic to call AuthService.logout from ngOnDestroy

Let’s pretend that you need to perform a server-side logout when MyValueComponent is destroyed. To do so we would update the method as follows:

  • Add AuthService to your imports
  • Add AuthService to your constructor
  • Add async in front of the method name ngOnDestroy
  • Make a call to an AuthService to logout using the await keyword.

Your updated MyValueComponent will look something like this:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { MyService } from './my.service';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-my-value',
  templateUrl: './my-value.component.html',
  styleUrls: [ './my-value.component.css' ]
})
export class MyValueComponent implements OnInit, OnDestroy {
  myValue: string;
  myValueSub: Subscription;

  constructor(private myService: MyService, private authService: AuthService) {}

  ngOnInit() {
      this.myValueSub = this.myService.getValue().subscribe(value => this.myValue = value);
  }

  async ngOnDestroy() {
    if (this.myValueSub) {
        this.myValueSub.unsubscribe();
    }

    await this.authService.logout();
  }
}

Tada! Now when the component is destroyed an async call will be made to log the user out and destroy their session on the server.

Hot Tip #2 - Ensure Execution During Browser Events

Many developers are surprised to learn that ngOnDestroy is only fired when the class which it has been implemented on is destroyed within the context of a running browser session.

In other words, ngOnDestroy is not reliably called in the following scenarios:

  • Page Refresh
  • Tab Close
  • Browser Close
  • Navigation Away From Page

This could be a deal-breaker when thinking about the prior example of logging the user out on destroy. Why? Well, most users would simply close the browser session or navigate to another site. So how do we make sure to capture or hook into that activity if ngOnDestroy doesn’t work in those scenarios?

Decorating ngOnDestroy with HostListener

TypeScript decorators are used throughout Angular applications. More information can be found here in the official TypeScript docs.

To ensure that our ngOnDestroy is executed in the above mentioned browser events, we can add one simple line of code to the top of ngOnDestroy. Let’s continue with our previous example of MyValueComponent and decorate ngOnDestroy:

  • Add HostListener to the imports
  • Place @HostListener('window:beforeunload') on top of ngOnDestroy

Our updated MyValueComponent will look something like this:

import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { MyService } from './my.service';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-my-value',
  templateUrl: './my-value.component.html',
  styleUrls: [ './my-value.component.css' ]
})
export class MyValueComponent implements OnInit, OnDestroy {
  myValue: string;
  myValueSub: Subscription;

  constructor(private myService: MyService, private authService: AuthService) {}

  ngOnInit() {
      this.myValueSub = this.myService.getValue().subscribe(value => this.myValue = value);
  }

  @HostListener('window:beforeunload')
  async ngOnDestroy() {
    if (this.myValueSub) {
        this.myValueSub.unsubscribe();
    }

    await this.authService.logout();
  }
}

Now our ngOnDestroy method is called both when the component is destroyed by Angular AND when the browser event window:beforeunload is fired. This is a powerful combination!

More about HostListener

@HostListener() is an Angular decorator that can be placed on top of any class method. This decorator takes two arguments: eventName and optionally args. In the above example, we are passing window:beforeunload as the DOM event. This means that Angular will automatically call our method when the DOM event window:beforeunload is fired. For more information on @HostListener check out the official docs.

If you want to use this to prevent navigation away from a page or component then:

  • Add $event to the @HostListener arguments
  • Call event.preventDefault()
  • Set event.returnValue to a string value of the message you would like the browser to display

An example would look something like this:

@HostListener('window:beforeunload', ['$event'])
async ngOnDestroy($event) {
  if (this.myValueSub) {
    this.myValueSub.unsubscribe();
  }

  await this.authService.logout();

  $event.preventDefault();
  $event.returnValue = 'A message.';
}

PLEASE NOTE: This is not officially supported by Angular! OnDestroy and ngOnDestroy suggest that there is no input argument on ngOnDestroy allowed. While unsupported, it does in fact still function as normal.

More about window:beforeunload

window:beforeunload is an event fired right before the window is unloaded. More details can be found in the documentation here: https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event.

A couple points to be aware of:

  • This event is currently supported in all major browsers EXCEPT iOS Safari.

  • If you need this functionality in iOS Safari then consider reviewing this Stack Overflow thread.

  • If you are using this event in an attempt to block navigation away you must set the event.returnValue to a string of the message you would like to display. More details in this example.

Conclusion

I realize that some of the tips recommended in this article are not mainstream and may generate some concern. Please remember as always to try these out and see if they fit for what you are doing in your application. If they work great! If not, then it’s ok to move on.

If you have any comments or questions feel free to contact me on Twitter


Additional Resources

I would highly recommend enrolling in the Ultimate Angular courses. It is well worth the money and I have used it as a training tool for new and experienced Angular developers. Follow the link below to signup.

Ultimate Courses: Expert online courses in JavaScript, Angular, NGRX and TypeScript

]]>
Wesley Grimes
Angular Routing - Best Practices for Enterprise Applications2019-02-24T10:34:51+00:002019-02-24T10:34:51+00:00https://wesleygrimes.com/angular/2019/02/24/angular-routing-best-practices-for-enterprise-applications

Before We Get Started

This article is not intended to be a tutorial on routing in Angular. If you are new to Routing in Angular then I highly recommend you check out one of the the following resources:

Background

The following represents a pattern that I’ve developed at my day job after building several enterprise Angular applications. While most online tutorials do a great job laying out the fundamentals, I had a hard time locating articles that showed recommended conventions and patterns for large and scalable applications.

With this pattern you should have a clean and concise organization for all routing related concerns in your applications.

Prerequisites

For context, this article assumes you are using the following version of Angular:

  • Angular v7.2.6

Best Practice #1 - Create a top-level Routes array file

The official Angular docs recommend creating a full-blown app-routing.module.ts for your top-level routing. I have found this extra layer to be unnecessary in most cases.

HOT TIP: Only register top-level routes here, if you plan to implement feature modules, then the child routes would live underneath the respective feature.routes.ts file. We want to keep this top-level routes file as clean as possible and follow the component tree structure.

Let’s go with the following approach:

  1. Create a new file named app.routes.ts in the root src/app directory. This file will hold our top-level Routes array. We will come back later throughout the article and fill this in. For now, let’s scaffold it with the following contents:

     import { Routes } from '@angular/router';
    
     export const AppRoutes: Routes = [];
    
  2. Register AppRoutes in the app.module.ts file.

    • Import AppRoutes from app.routes.ts.
    • Import RouterModule from @angular/router.
    • Add RouterModule.forRoot(AppRoutes) to your imports array

    Your updated app.module.ts will look similar to the following:

     import { NgModule } from '@angular/core';
     import { BrowserModule } from '@angular/platform-browser';
     import { RouterModule } from '@angular/router';
     import { AppComponent } from './app.component';
     import { AppRoutes } from './app.routes';
    
     @NgModule({
       declarations: [AppComponent],
       imports: [BrowserModule, RouterModule.forRoot(AppRoutes)],
       providers: [],
       bootstrap: [AppComponent]
     })
     export class AppModule {}
    

Best Practice #2 - Create a feature-level Routes array file

In similar fashion to how we constructed the app.routes.ts we will create a feature.routes.ts to list out the individual routes for this feature module. We want to keep our routes as close to the source as possible. This will be in keeping with a clean code approach, and having a good separation of concerns.

  1. Create a new file named feature/feature.routes.ts where feature matches the name of your feature.module.ts prefix. This file will hold our feature-level Routes array. Keeping in mind that you would replace Feature with the actual name of your module, let’s scaffold it with the following contents:

     import { Routes } from '@angular/router';
    
     export const FeatureRoutes: Routes = [];
    
  2. Register FeatureRoutes in the feature/feature.module.ts file. We will make use of the RouterModule.forChild import so that these routes are automatically registered with lazy loading.

    • Import FeatureRoutes from feature.routes.ts.
    • Import RouterModule from @angular/router.
    • Add RouterModule.forChild(FeatureRoutes) to your imports array

    Your updated feature/feature.module.ts will look similar to the following:

     import { CommonModule } from '@angular/common';
     import { NgModule } from '@angular/core';
     import { RouterModule } from '@angular/router';
     import { FeatureRoutes } from './feature.routes';
    
     @NgModule({
       declarations: [],
       imports: [CommonModule, RouterModule.forChild(FeatureRoutes)]
     })
     export class FeatureModule {}
    

    An example of a feature.routes.ts file with child route(s) may look like the following:

     import { Routes } from '@angular/router';
     import { FeatureOneComponent } from './feature-one.component';
     import { FeatureSpecificCanActivateGuard } from './_guards';
    
     export const FeatureOneRoutes: Routes = [
       {
         path: '',
         pathMatch: 'full',
         redirectTo: 'feature-one-component'
       },
       {
         path: 'feature-one-component',
         component: FeatureOneComponent,
         canActivate: [FeatureSpecificCanActivateGuard]
       }
     ];
    

Best Practice #3 - Add Lazy Loaded Features to top-level Routes file

Lazy loading is the concept of deferring load of code assets (javascript, styles) until the user actually needs to utilize the resources. This can bring large performance increases to perceived load times of your application as the entire code set doesn’t have to download on first paint.

Angular provides a nice way to handle this with the loadChildren option for a given route. More information can be found in the official Angular docs.

Once you’ve created your app.routes.ts and *.routes.ts files, you need to register any feature modules that you want to load lazily.

Per Feature Module…

Update the AppRoutes array in the app.routes.ts file to include a new route the feature:

import { Routes } from '@angular/router';

export const AppRoutes: Routes = [
  {
    path: 'feature',
    loadChildren: './feature/feature.module#FeatureModule'
  }
];

By adding the above route to the array, when the user requests /feature in the browser, Angular lazy loads the module using the path given and then automatically registers any routes defined in the feature.routes.ts FeatureRoutes array using the RouterModule.forChild import.

For each additional feature module, you would add another item to the AppRoutes array. If you have multiple features, it might look something like the following:

import { Routes } from '@angular/router';

export const AppRoutes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'feature-one'
  },
  {
    path: 'feature-one',
    loadChildren: './feature-one/feature-one.module#FeatureOneModule'
  },
  {
    path: 'feature-two',
    loadChildren: './feature-two/feature-two.module#FeatureTwoModule'
  }
];

Best Practice #4 - Keep Router Guards Organized

Here are a few tips to keep your router guards organized. These are just guidelines, but I have found them to be very helpful.

Name Your Guards Well

Guards should use the following naming convention:

  • File Name: name.function.guard.ts
  • Class Name: NameFunctionGuard

Each part being identified as:

  • name - this is the name of your guard. What are you guarding against?
  • function - this is the function your guard will be attached to. Angular supports CanActivate, CanActivateChild, CanDeactivate, and Resolve.

An example of an Auth Guard that is attached to the CanActivate function would be named as follows:

  • File Name: auth.can-activate.guard
  • Class Name: AuthCanActivateGuard

Group under _guards folder

Organize all top-level guards under a folder named src/app/_guards. I have seen apps dump guards in the top level directory and this is messy, especially if you end up with more than a few guards.

Use Barrel Exports

The jury is still out on whether or not using barrel exports is officially considered a “best practice” or even supported by the Angular style guide. However, I am a big fan of the clean organization this provides. This method is offered as a suggestion.

Make sure that src/app/_guards has a nice and clean index.ts barrel export. Barrel exports are simply index.ts files that group together and export all public files from a directory. An example is as follows:

export * from './auth.can-activate.guard';
export * from './require-save.can-deactivate.guard';

Without Barrel Exporting:

import { AuthCanActivateGuard } from 'src/app/_guards/auth.can-activate.guard';
import { RequireSaveCanDeactivateGuard } from 'src/app/_guards/require-save.can-deactivate.guard';

With Barrel Exporting:

import { AuthCanActivateGuard, RequireSaveCanDeactivateGuard } from 'src/app/_guards';

An example application with a _guards directory would look as follows:

Organize Feature-Specific Route Guards

If you have guards that are only used in a particular FeatureRoutes array, then store these routes underneath a folder named _guards underneath your feature folder. Make sure to follow the same naming conventions defined above, as well as barrel exporting.

  • Place guards under a folder named _guards underneath your feature folder
  • Make sure to create a barrel export index.ts for clean importing

An example feature directory with _guards would look as follows:

Finished Application Structure

A completed application structure should look something like the following:


Example GitHub Repository

I have created a demonstration repository on GitHub. Feel free to fork, clone, and submit PRs.

https://github.com/wesleygrimes/angular-routing-best-practices

Conclusion

It’s important to remember that I have implemented these best practices in several “real world” applications. While I have found these best practices helpful, and maintainable, I do not believe they are an end-all be-all solution to organizing routes in projects; it’s just what has worked for me. I am curious as to what you all think? Please feel free to offer any suggestions, tips, or best practices you’ve learned when building enterprise Angular applications with routing and I will update the article to reflect as such. Happy Coding!

]]>
Wesley Grimes
Angular courses for you and your team, a review of Ultimate Courses2019-02-22T10:34:51+00:002019-02-22T10:34:51+00:00https://wesleygrimes.com/angular/2019/02/22/angular-courses-for-you-and-your-team-a-review-of-ultimate-courses

As a senior developer in a small-to-medium sized software firm, I am often tasked with training new developers, or seasoned developers in new technologies. I am always on the lookout for ways to ease the burden and standardize the process for all parties involved.

One-on-one training and instructor-led training sessions are great, but not everyone has the resources to do this, and often times our current workloads and “deliverables” prevent us from setting aside a week (or more) to devote to training on new topics. Most of you reading this are well aware of the mainstream online training offerings that exist. Pluralsight and Lynda come to mind.

While these are fantastic resources, it’s often hard to find Angular training courses that teach on the latest and greatest versions of front-end libraries and frameworks. In this article, I will explore, Ultimate Courses, the offerings created and curated by Todd Motto (Google Developer Expert and Angular extraordinaire).


Let’s Review The Packages

For Angular development, Ultimate Courses offers two packages to choose from: Angular Kickstart Package and Angular Ultimate Package. Let’s quickly review the differences.

Angular Kickstart Package

If your team has previous TypeScript experience, then this is the package I would recommend. It includes:

  • Angular Fundamentals
  • Angular Pro

Angular Ultimate Package

Learning Angular, for most developers, is not just as simple as learning the frameworks features, conventions and tooling. For most, it requires getting up to speed on TypeScript, a powerful, typed superset of JavaScript. Teaching developers TypeScript is a must for any online solution that I recommend, and thankfully Ultimate Courses’ Angular Ultimate Package has you covered here. It includes:

  • Angular Fundamentals
  • Angular Pro
  • TypeScript Basics
  • TypeScript Masterclass
  • NGRX Store + Effects

Individual Courses Available

Courses can be purchased in packages as stated above, however, they can also be purchased individually as needed which may make sense for some scenarios.

Team Licensing Available

If you are working with a team of developers, Ultimate Courses offers user licensing with discounts as the user count grows. This is a great option for teams of developers learning Angular.


Angular Fundamentals

This course starts out from the high-level and slowly dives deeper into the basic building blocks of an Angular single page application. The content is separated into the following sections:

  • Architecture, setup, source files
  • ES5 to ES6 and TypeScript refresher
  • Getting started
  • Template fundamentals
  • Rendering flows
  • Component Architecture and Feature Modules
  • Services, Http and Observables
  • Template-driven Forms, Inputs and Validation
  • Component Routing

I won’t dive too deep into each these sections, but I will say for an introductory course, this offering does a fantastic job of giving you just enough information to be dangerous (in a good way), while not overwhelming first-time Angular developers.


Angular Pro

This course takes the concepts learned in Angular Fundamentals and goes deep, way deep. The topics covered in this course are vital to learn as any Angular app that grows in complexity will almost always need to handle these situations. I appreciate Todd’s attention to detail. Topics covered include:

  • Advanced Components — including dynamic component creation
  • Directives
  • Pipes
  • Reactive Forms — This is a good one as the best practice for Angular forms now-a-days is considered Reactive Forms.
  • Routing — this includes a nice deep drive into lazy loading modules, a method to speed up initial load times of large applications
  • Unit Testing — A must have for distributed teams and complex applications. Todd walks through need-to-know topics around unit testing with built-in Angular tooling.
  • Dependency Injection and Zones
  • Statement Management with Rx — although I recommend NgRx

TypeScript Basics

This course is an introduction to TypeScript. Developers coming from C# will appreciate this course in particular. In addition, this course can be purchased separately from the package if you are building with TypeScript. Topics include:

  • Overview, setup and source files
  • ES6/7 and TypeScript
  • Primitive Types
  • Special Types
  • Type Aliases and Assertions
  • Diving into Interfaces
  • Classes, Properties and Inheritance

TypeScript Masterclass

Just as with any language, there are folks that use the basics and are off to the races. There are some cases, however, where you need to dig deep and really understand what’s happening. If you are building Angular or NodeJS libraries, then this course is probably for you. Topics include:

  • Understanding and Typing “this”
  • Type Queries
  • Mapped Types
  • Exploring Type Guards
  • Advanced Types and Practices
  • Generics and Overloads
  • Exploring Enums
  • Declaration Files
  • tsconfig and Compiler Options

NGRX Store + Effects

Personally, I have been using NgRx for a long time. I no longer build Angular applications without it! So for me, I was very curious to learn how Todd explains this topic. Redux is a complicated pattern for first-time developers to grasp, but it really is a must for creating quality applications.

In the Angular realm, the Redux pattern is implemented in several libraries, the most popular being NgRx and NGXS. For those of you new to Redux, redux is a pattern for managing global state in an application. It was originally developed at Facebook, and since has taken off and is widely used through most modern front-end frameworks. NgRx is, by far, the most widely used Angular redux library. As such, Ultimate Courses has chosen to focus it’s offerings on NgRx. As we focus on this course, I must say upfront, I was pleasantly surprised and impressed with Todd’s approach to teaching NgRx. The course has been so well received, in fact, that even Mike Ryan (NgRx Core Team/Google Developer Expert) recommends this course as the best way to get started!

Course Walkthrough

The course starts out by walking through what exactly state management is, how redux accomplishes that, and how JavaScript presents challenges with mutation.

Once you have grasped the concept of state management using the Redux pattern, the course then has you build your very own vanilla Redux store using plain TypeScript. Realizing then, that NgRx is built on top of these concepts, it’s an easy transfer into learning NgRx.

After having built a vanilla redux store, the course then walks through the process of setting up a store using the tools provided by NgRx. The course walks you through creating actions, reducers, selectors, effects. The course then walks through the process of structuring lists of entities using the Entity pattern.

Even folks with some NgRx experience will find this course helpful as it does a deep-dive into more advanced concepts like routing with the store, preloading state, and unit testing your NgRx store.

Below is a detailed list of the topics covered in this course:

  • Redux Architecture
  • Writing our own Redux Store
  • Architecture: ngrx/store and components
  • Core Essentials
  • Effects and Entities
  • Router State Composition
  • Extending our State Tree
  • Entity patterns, CRUD operations
  • Routing via Dispatch
  • State preload and protection via Guards
  • Observables and Change Detection
  • Unit Testing

Conclusion

After taking these courses, and comparing other available options, I can safely recommend the Angular Ultimate Package for teams looking to get into Angular Enterprise development. Todd’s down-to-earth approach of explaining complex concepts, makes these courses both fun and educational. As an added bonus, Todd does the voice-overs himself so you get to learn Angular with a British accent. Win-Win-Win.

10 out of 10 Recommend

More Information on Ultimate Courses

Ultimate Courses: Expert online courses in JavaScript, Angular, NGRX and TypeScript Expert online courses in JavaScript, Angular, NGRX and TypeScript. Join 50,000 others mastering new technologies with Ultimate Courses

]]>
Wesley Grimes
Automatically Remove All Unused Imports in a TypeScript Project2019-02-14T19:34:51+00:002019-02-14T19:34:51+00:00https://wesleygrimes.com/angular/2019/02/14/how-to-use-tslint-to-autoremove-all-unused-imports-in-a-typescript-project

The Problem

Recently, I had a need arise to programatically and recursively traverse through all *.ts files in a given project and remove all unused TypeScript imports. At the time this article was written, there was no way to do this within Visual Studio Code without opening each individual *.ts file and hitting CTRL + Shift + O on Windows/Linux. After some research, and much appreciated help from Twitter colleagues, I found a solution that works. This should work for Angular, React, Vue.js, or any plain TypeScript project.

The Solution

We will use the tslint command line tool, in conjuction with the tslint-etc rules, to automatically detect and remove all unused imports in the directory, recursively. If you have a large project, the process can take some time to run. It is important to double-check all files for correctness once the fix process is complete.

Install Required Tools

Install the follow node packages required to for this process to work.

$ npm install -g typescript tslint tslint-etc

Create a temporary tslint config file

Create a new file named tslint-imports.json in the root of your project. This creates a hyper-focused tslint process that will only check for unused declarations. It is important to note that this will throw tslint errors on unused imports, parameters and variables. We are only using this for the --fix process. As such, the tslint-etc rules only autofix unused imports.

This file needs the following contents:

{
  "extends": [
    "tslint-etc"
  ],
  "rules": {
    "no-unused-declaration": true
  }
}

Run the autofix process

The following command will traverse, recursively through all *.ts files in the project and remove the unused imports. It save the files automatically in place.

BE CAREFUL! This process is only reversible if you are using a source control solution like git or svn, where you can revert changes.

$ tslint --config tslint-imports.json --fix --project .

Double-Check Your Files

At this point, I would highly recommend you double-check your files for correctness. This tool worked on all but 2 of my 195 *.ts files. Two of the components were incorrectly updated. I was able to spot this by running an ng build --prod as it was an Angular application. You could run a manual tslint --project . if your project doesn’t use a build tool.

Resources

]]>
Wesley Grimes
Building an AOT Friendly Dynamic Content Outlet in Angular2019-02-05T19:40:51+00:002019-02-05T19:40:51+00:00https://wesleygrimes.com/angular/2019/02/05/building-an-aot-friendly-dynamic-content-outlet

Overview — Dynamic Content Outlet

Have you ever needed to dynamically load content or components in your Angular applications? How about in a way that the built-in structural directives  (*ngIf*, *ngSwitch) just don’t provide? Are you also in need of the optimization benefits of using Ahead-of-Time compilation?

Well, I have good news for you…(And no you don’t have to be Chuck Norris!) If you stay tuned, I will help you get a solution up and running that will provide a solid way to choose from and load dynamically, at run-time, a set of predefined modules & components in your application.

This article is assumes you are building an Angular 6+ application generated using the Angular CLI. For information on using the Angular CLI check out the official documentation.

This arose out of a business need for the company that I work for. What’s important to note here is that many articles and examples exist on loading content dynamically in Angular, but none that I found worked reliably when compiling Angular with the — prod or — aot flags enabled. The good news is that what I describe in this article works fantastically with Ahead-of-Time compiling.

What We’re Going To Do

We’re going to build a special module with a dynamic component outlet that can be included and used anywhere in your application. The only requirement is that you register, upfront, an array mapping your dynamic components to their parent modules. You will also add these modules to the lazyModules property in your angular.json file. By doing so, the compiler will pre-compile these modules. The compiler then splits them off into separate minified chunks and makes them available to the SystemJS loader at runtime, with AOT.


Let’s Build Our Dynamic Content Outlet

Assuming that you have an existing Angular 6+ CLI generated project let’s run through the following steps to scaffold the necessary parts that make up this new Dynamic Content Outlet.

Generate the Dynamic Content Outlet Module

Generate a new module named DynamicContentOutletModule by running the following command in your shell of choice:

$ ng g m dynamic-content-outlet

We will come back later to this module and wire things up.

Build the Dynamic Content Outlet Registry

Create a new file underneath the newly created folder src/app/dynamic-content-outlet named dynamic-content-outlet.registry.ts. This will serve as the placeholder for array mapping component name to module path and module name. For now, it will be an empty array as follows.

interface RegistryItem {
  componentName: string;
  modulePath: string;
  moduleName: string;
}

/**
 * A registry array of Component Name to details
 * that must be updated with each new component
 * that you wish to load dynamically.
 */

export const DynamicContentOutletRegistry: RegistryItem[] = [];

Build the Dynamic Content Outlet Error Component

Create a new file underneath the folder src/app/dynamic-content-outlet/dynamic-content-outlet-error.component.ts. This will serve as the component to be rendered anytime an error occurs attempting to load a dynamic component. You can customize the template property to use any custom styles or layout that you may have. The errorMessage input must stay the same and will be fed with the actual details of the error that occurred while attempting to dynamically render your component.

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-dynamic-content-outlet-error-component',
  template: `
    <div></div>
  `
})
export class DynamicContentOutletErrorComponent {
  @Input() errorMessage: string;
  constructor() {}
}

Build the Dynamic Content Outlet Service

Create a new file underneath the folder src/app/dynamic-content-outlet/dynamic-content-outlet.service.ts.

  • This service encapsulates the logic that loads dynamic components using SystemJS and renders them into the Dynamic Content Outlet.
  • It uses the DynamicContentOutletRegistry to lookup the module by componentName.
  • It also makes use of a new static property that we will add later on to each module we wish to dynamically load named dynamicComponentsMap. This allows us to get the type literal for the given componentName so that the resolveComponentFactory can instantiate the correct component. You might ask why we didn’t just add a fourth property to the DynamicContentOutletRegistry, well this is because if we import the type in the registry, then it defeats the purpose of lazy loading these modules as the the type will be included in the main bundle.
  • If an error occurs, a DynamicContentOutletErrorComponent is rendered instead with the error message included.
import {
  ComponentFactoryResolver,
  ComponentRef,
  Injectable,
  Injector,
  NgModuleFactoryLoader,
  Type
} from '@angular/core';
import { DynamicContentOutletErrorComponent } from './dynamic-content-outlet-error.component';
import { DynamicContentOutletRegistry } from './dynamic-content-outlet.registry';

type ModuleWithDynamicComponents = Type<any> & {
  dynamicComponentsMap: {};
};

@Injectable()
export class DynamicContentOutletService {
  constructor(
    private componentFactoryResolver: ComponentFactoryResolver,
    private moduleLoader: NgModuleFactoryLoader,
    private injector: Injector
  ) {}

  async GetComponent(componentName: string): Promise<ComponentRef<any>> {
    const modulePath = this.getModulePathForComponent(componentName);

    if (!modulePath) {
      return this.getDynamicContentErrorComponent(
        `Unable to derive modulePath from component: ${componentName} in dynamic-content.registry.ts`
      );
    }

    try {
      const moduleFactory = await this.moduleLoader.load(modulePath);
      const moduleReference = moduleFactory.create(this.injector);
      const componentResolver = moduleReference.componentFactoryResolver;

      const componentType = (moduleFactory.moduleType as ModuleWithDynamicComponents)
        .dynamicComponentsMap[componentName];

      const componentFactory = componentResolver.resolveComponentFactory(
        componentType
      );
      return componentFactory.create(this.injector);
    } catch (error) {
      console.error(error.message);
      return this.getDynamicContentErrorComponent(
        `Unable to load module ${modulePath}.
                Looked up using component: ${componentName}. Error Details: ${
          error.message
        }`
      );
    }
  }

  private getModulePathForComponent(componentName: string) {
    const registryItem = DynamicContentOutletRegistry.find(
      i => i.componentName === componentName
    );

    if (registryItem && registryItem.modulePath) {
      // imported modules must be in the format 'path#moduleName'
      return `${registryItem.modulePath}#${registryItem.moduleName}`;
    }

    return null;
  }

  private getDynamicContentErrorComponent(errorMessage: string) {
    const factory = this.componentFactoryResolver.resolveComponentFactory(
      DynamicContentOutletErrorComponent
    );
    const componentRef = factory.create(this.injector);
    const instance = <any>componentRef.instance;
    instance.errorMessage = errorMessage;
    return componentRef;
  }
}

Build the Dynamic Content Outlet Component

Create a new file underneath the folder src/app/dynamic-content-outlet/dynamic-content-outlet.component.ts. This component takes an input property named componentName that will call the DynamicContentOutletService.GetComponent method passing into it componentName. The service then returns an instance of that rendered and compiled component for injection into the view. The service returns an error component instance if the rendering fails for some reason. The component listens for changes via the ngOnChanges life-cycle method. If the @Input() componentName: string; is set or changes it automatically re-renders the component as necessary. It also properly handles destroying the component with the ngOnDestroy life-cycle method.

import {
  Component,
  ComponentRef,
  Input,
  OnChanges,
  OnDestroy,
  ViewChild,
  ViewContainerRef
} from '@angular/core';
import { DynamicContentOutletService } from './dynamic-content-outlet.service';

@Component({
  selector: 'app-dynamic-content-outlet',
  template: `
    <ng-container ##container></ng-container>
  `
})
export class DynamicContentOutletComponent implements OnDestroy, OnChanges {
  @ViewChild('container', { read: ViewContainerRef })
  container: ViewContainerRef;

  @Input() componentName: string;

  private component: ComponentRef<{}>;

  constructor(private dynamicContentService: DynamicContentOutletService) {}

  async ngOnChanges() {
    await this.renderComponent();
  }

  ngOnDestroy() {
    this.destroyComponent();
  }

  private async renderComponent() {
    this.destroyComponent();

    this.component = await this.dynamicContentService.GetComponent(
      this.componentName
    );
    this.container.insert(this.component.hostView);
  }

  private destroyComponent() {
    if (this.component) {
      this.component.destroy();
      this.component = null;
    }
  }
}

Finish Wiring Up Parts To The Dynamic Content Outlet Module

Make sure your src/app/dynamic-content-outlet/dynamic-content-outlet.module.ts file looks like the following:

import { CommonModule } from '@angular/common';
import {
  NgModule,
  NgModuleFactoryLoader,
  SystemJsNgModuleLoader
} from '@angular/core';
import { DynamicContentOutletErrorComponent } from './dynamic-content-outlet-error.component';
import { DynamicContentOutletComponent } from './dynamic-content-outlet.component';
import { DynamicContentOutletService } from './dynamic-content-outlet.service';

@NgModule({
  imports: [CommonModule],
  declarations: [
    DynamicContentOutletComponent,
    DynamicContentOutletErrorComponent
  ],
  exports: [DynamicContentOutletComponent],
  providers: [
    {
      provide: NgModuleFactoryLoader,
      useClass: SystemJsNgModuleLoader
    },
    DynamicContentOutletService
  ]
})
export class DynamicContentOutletModule {}

Let’s Use Our New Dynamic Content Outlet

Phew! Take a deep breath and grab a cup of coffee (french press fair trade organic dark roast). The hard work is behind you. Next we will go through the process of actually putting this new module into play!

For any component that you would like dynamically rendered you need to do the following four steps. These steps must be followed exactly.

1. Prepare your module for dynamic import

  • Confirm that the component is listed in the entryComponents array in the module that the component is a part of.

  • Add to the module, a new static property called dynamicComponentsMap. This allows us to get the type literal for the given componentName so that the resolveComponentFactory can instantiate the correct component.

You might ask why we didn’t just add a fourth property to the DynamicContentOutletRegistry named componentType; well this is because if we import the type in the registry, then it defeats the purpose of lazy loading these modules as the the type will be included in the main bundle.

A prepared module might look as follows:

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { DynamicMultipleOneComponent } from './dynamic-multiple-one.component';
import { DynamicMultipleTwoComponent } from './dynamic-multiple-two.component';

@NgModule({
  declarations: [MySpecialDynamicContentComponent],
  imports: [CommonModule],
  entryComponents: [MySpecialDynamicContentComponent]
})
export class MySpecialDynamicContentModule {
  static dynamicComponentsMap = {
    MySpecialDynamicContentComponent
  };
}

2. Add your dynamic component(s) to the registry

For any component that you would like dynamically rendered, add a new entry to the DynamicContentOutletRegistry array in src/app/dynamic-content-outlet/dynamic-content-outlet.registry.ts.

The following properties must be filled out:

  • componentName: This should match exactly the name of the Component you wish to load dynamically.

  • modulePath: The absolute path to the module containing the component you wish to load dynamically. This is only the path to the module and does NOT include moduleName after a #.

  • moduleName: This is the exact name of the module.

Example Component Mapping

{
  componentName: 'MySpecialDynamicContentComponent',
  modulePath: 'src/app/my-special-dynamic-content/my-special-dynamic-content.module',
  moduleName: 'MySpecialDynamicContentModule'
},

3. Add your dynamic modules to the lazyModules array

In your angular.json update the projects > ** > architect > build > options > lazyModules array and add an item for each module that you added to the registry in order for the Angular AOT compiler to detect and pre-compile your dynamic modules. If you have multiple projects in a folder, make sure you add this for the correct project you are importing and using dynamic modules in. The updated file will look similar to this:

{
  ...
  "projects": {
    "angular-dynamic-content": {
      ...
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            ...
            "lazyModules": ["src/app/my-special-dynamic-content/my-special-dynamic-content.module"]
          },
        }
      }
    }
  }
}

Wire up the Dynamic Content Outlet Module

Up to this point you have created your dynamic content outlet module and registered your components to be available in the outlet. The last thing we need to do is wire up our new DynamicContentOutletModule to be used in our application. In order to do so you need to:

  1. Add your new DynamicContentOutletModule to the imports array of any feature module or the main AppModule of your Angular application.

Example of addition to the imports array

@NgModule({
...
imports: [
   ...
   DynamicContentOutletModule
  ],
  ...
})
export class AppModule {}
  1. Add the following tag to the template of the parent component that you would like to render the dynamic content in:
<app-dynamic-content-outlet [componentName]="'MyComponent'">
</app-dynamic-content-outlet>

This is very similar in nature to Angular’s built-in <router-outlet>/</router-outlet> tag.

  1. Happy ng serve --prod ing!

Real-World Complex Example

If you are interested in a more in-depth real-world example, then check out the Github Repository which will demonstrate the following:

  • Dynamic modules with multiple components
  • Demonstrating the use of on-the-fly component changes
  • Demonstrating that the scoped styles are loaded dynamically for each component

GitHub Repository Example https://github.com/wesleygrimes/angular-dynamic-content

Conclusion

Hopefully you have found this solution helpful. Here is the full GitHub repository example for you to clone and play around with. PR’s are welcome, appreciated, encouraged and accepted!

Additional Resources

I would highly recommend enrolling in the Ultimate Angular courses. It is well worth the money and I have used it as a training tool for new Angular developers. Follow the link below to signup.

Ultimate Courses: Expert online courses in JavaScript, Angular, NGRX and TypeScript

Special Thanks

I want to take a moment and thank all those I was able to glean this information from. I did not come up with all this on my own, but I was able to get a working solution by combining parts from each of these articles!

]]>
Wesley Grimes
NgRx — Best Practices for Enterprise Angular Applications2018-05-30T15:00:00+00:002018-05-30T15:00:00+00:00https://wesleygrimes.com/angular/2018/05/30/ngrx-best-practices-for-enterprise-angular-applications

Before We Get Started

This article is not intended to be a tutorial on NgRx. There are several great resources that currently exist, written by experts much smarter than me. I highly suggest that you take time and learn NgRx and the redux pattern before attempting to implement these concepts.

Background

The following represents a pattern that I’ve developed at my day job after building several enterprise Angular applications using the NgRx library. I have found that most online tutorials do a great job of helping you to get your store up and running, but often fall short of illustrating best practices for clean separation of concerns between your store feature slices, root store, and user interface.

With the following pattern, your root application state, and each slice (property) of that root application state are separated into a RootStoreModule and per feature MyFeatureStoreModule.

Prerequisites

This article assumes that you are building an Angular v7 CLI generated application.

Installing NgRx Dependencies

Before we get started with generating code, let’s make sure to install the necessary NgRx node modules from a prompt:

$ npm install @ngrx/{store,store-devtools,entity,effects}

Best Practice #1 — The Root Store Module

Create a Root Store Module as a proper Angular NgModule’s that bundle together NgRx store logic. Feature store modules will be imported into the Root Store Module allowing for a single root store module to be imported into your application’s main App Module.

Suggested Implementation

  1. Generate RootStoreModule using the Angular CLI:
$ ng g module root-store --flat false --module app.module.ts
  1. Generate RootState interface to represent the entire state of your application using the Angular CLI:
$ ng g interface root-store/root-state

This will create an interface named RootState but you will need to rename it to State inside the generated .ts file as we want to later on utilize this as RootStoreState.State

PLEASE NOTE: You will come back later on and add to this interface each feature module as a property.

Best Practice #2 — Create Feature Store Module(s)

Create feature store modules as proper Angular NgModule’s that bundle together feature slices of your store, including state, actions, reducer, selectors, and effects. Feature modules are then imported into your RootStoreModule. This will keep your code cleanly organizing into sub-directories for each feature store. In addition, as illustrated later on in the article, public actions, selectors, and state are name-spaced and exported with feature store prefixes.

Naming Your Feature Store

In the example implementation below we will use the feature name MyFeature, however, this will be different for each feature you generate and should closely mirror the RootState property name. For example, if you are building a blog application, a feature name might be Post.

Entity Feature Modules or Standard Feature Modules?

Depending on the type of feature you are creating you may or may not benefit from implementing NgRx Entity. If your store feature slice will be dealing with an array of type then I suggest following the Entity Feature Module implementation below. If building a store feature slice that does not consist of a standard array of type, then I suggest following the Standard Feature Module implementation below.

Suggested Implementation — Entity Feature Module

  1. Generate MyFeatureStoreModule feature module using the Angular CLI:
$ ng g module root-store/my-feature-store --flat false --module root-store/root-store.module.ts
  1. Actions — Create an actions.ts file in the app/root-store/my-feature-store directory:
import { Action } from @ngrx/store;
import { MyModel } from '../../models';

export enum ActionTypes {
  LOAD_REQUEST = '[My Feature] Load Request',
  LOAD_FAILURE = '[My Feature] Load Failure',
  LOAD_SUCCESS = '[My Feature] Load Success'
}

export class LoadRequestAction implements Action {
  readonly type = ActionTypes.LOAD_REQUEST;
}

export class LoadFailureAction implements Action {
  readonly type = ActionTypes.LOAD_FAILURE;
  constructor(public payload: { error: string }) {}
}

export class LoadSuccessAction implements Action {
  readonly type = ActionTypes.LOAD_SUCCESS;
  constructor(public payload: { items: MyModel[] }) {}
}

export type Actions = LoadRequestAction | LoadFailureAction | LoadSuccessAction;
  1. State — Create a state.ts file in the app/root-store/my-feature-store directory:
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { MyModel } from '../../models';

export const featureAdapter: EntityAdapter<MyModel> = createEntityAdapter<
  MyModel
>({
  selectId: model => model.id,
  sortComparer: (a: MyModel, b: MyModel): number =>
    b.someDate.toString().localeCompare(a.someDate.toString())
});

export interface State extends EntityState<MyModel> {
  isLoading?: boolean;
  error?: any;
}

export const initialState: State = featureAdapter.getInitialState({
  isLoading: false,
  error: null
});
  1. Reducer — Create a reducer.ts file in the app/root-store/my-feature-store directory:
import { Actions, ActionTypes } from './actions';
import { featureAdapter, initialState, State } from './state';

export function featureReducer(state = initialState, action: Actions): State {
  switch (action.type) {
    case ActionTypes.LOAD_REQUEST: {
      return {
        ...state,
        isLoading: true,
        error: null
      };
    }
    case ActionTypes.LOAD_SUCCESS: {
      return featureAdapter.addAll(action.payload.items, {
        ...state,
        isLoading: false,
        error: null
      });
    }
    case ActionTypes.LOAD_FAILURE: {
      return {
        ...state,
        isLoading: false,
        error: action.payload.error
      };
    }
    default: {
      return state;
    }
  }
}
  1. Selectors — Create a selectors.ts file in the app/root-store/my-feature-store directory:
import {
  createFeatureSelector,
  createSelector,
  MemoizedSelector
} from '@ngrx/store';

import { MyModel } from '../models';

import { featureAdapter, State } from './state';

export const getError = (state: State): any => state.error;

export const getIsLoading = (state: State): boolean => state.isLoading;

export const selectMyFeatureState: MemoizedSelector<
  object,
  State
> = createFeatureSelector<State>('myFeature');

export const selectAllMyFeatureItems: (
  state: object
) => MyModel[] = featureAdapter.getSelectors(selectMyFeatureState).selectAll;

export const selectMyFeatureById = (id: string) =>
  createSelector(
    this.selectAllMyFeatureItems,
    (allMyFeatures: MyModel[]) => {
      if (allMyFeatures) {
        return allMyFeatures.find(p => p.id === id);
      } else {
        return null;
      }
    }
  );

export const selectMyFeatureError: MemoizedSelector<
  object,
  any
> = createSelector(
  selectMyFeatureState,
  getError
);

export const selectMyFeatureIsLoading: MemoizedSelector<
  object,
  boolean
> = createSelector(
  selectMyFeatureState,
  getIsLoading
);
  1. Effects — Create an effects.ts file in the app/root-store/my-feature-store directory with the following:
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { Observable, of as observableOf } from 'rxjs';
import { catchError, map, startWith, switchMap } from 'rxjs/operators';
import { DataService } from '../../services/data.service';
import * as featureActions from './actions';

@Injectable()
export class MyFeatureStoreEffects {
  constructor(private dataService: DataService, private actions$: Actions) {}

  @Effect()
  loadRequestEffect$: Observable<Action> = this.actions$.pipe(
    ofType<featureActions.LoadRequestAction>(
      featureActions.ActionTypes.LOAD_REQUEST
    ),
    startWith(new featureActions.LoadRequestAction()),
    switchMap(action =>
      this.dataService.getItems().pipe(
        map(
          items =>
            new featureActions.LoadSuccessAction({
              items
            })
        ),
        catchError(error =>
          observableOf(new featureActions.LoadFailureAction({ error }))
        )
      )
    )
  );
}

Suggested Implementation — Standard Feature Module

  1. Generate MyFeatureStoreModule feature module using the Angular CLI:
$ ng g module root-store/my-feature-store --flat false --module root-store/root-store.module.ts
  1. Actions — Create an actions.ts file in the app/root-store/my-feature-store directory:
import { Action } from '@ngrx/store';
import { User } from '../../models';

export enum ActionTypes {
  LOGIN_REQUEST = '[My Feature] Login Request',
  LOGIN_FAILURE = '[My Feature] Login Failure',
  LOGIN_SUCCESS = '[My Feature] Login Success'
}

export class LoginRequestAction implements Action {
  readonly type = ActionTypes.LOGIN_REQUEST;
  constructor(public payload: { userName: string; password: string }) {}
}

export class LoginFailureAction implements Action {
  readonly type = ActionTypes.LOGIN_FAILURE;
  constructor(public payload: { error: string }) {}
}

export class LoginSuccessAction implements Action {
  readonly type = ActionTypes.LOGIN_SUCCESS;
  constructor(public payload: { user: User }) {}
}

export type Actions =
  | LoginRequestAction
  | LoginFailureAction
  | LoginSuccessAction;
  1. State — Create a state.ts file in the app/root-store/my-feature-store directory:
import { User } from '../../models';

export interface State {
  user: User | null;
  isLoading: boolean;
  error: string;
}

export const initialState: State = {
  user: null,
  isLoading: false,
  error: null
};
  1. Reducer — Create a reducer.ts file in the app/root-store/my-feature-store directory:
import { Actions, ActionTypes } from './actions';
import { initialState, State } from './state';

export function featureReducer(state = initialState, action: Actions): State {
  switch (action.type) {
    case ActionTypes.LOGIN_REQUEST:
      return {
        ...state,
        error: null,
        isLoading: true
      };
    case ActionTypes.LOGIN_SUCCESS:
      return {
        ...state,
        user: action.payload.user,
        error: null,
        isLoading: false
      };
    case ActionTypes.LOGIN_FAILURE:
      return {
        ...state,
        error: action.payload.error,
        isLoading: false
      };
    default: {
      return state;
    }
  }
}
  1. Selectors — Create a selectors.ts file in the app/root-store/my-feature-store directory:
import {
  createFeatureSelector,
  createSelector,
  MemoizedSelector
} from '@ngrx/store';

import { User } from '../../models';

import { State } from './state';

const getError = (state: State): any => state.error;

const getIsLoading = (state: State): boolean => state.isLoading;

const getUser = (state: State): any => state.user;

export const selectMyFeatureState: MemoizedSelector<
  object,
  State
> = createFeatureSelector<State>('myFeature');

export const selectMyFeatureError: MemoizedSelector<
  object,
  any
> = createSelector(
  selectMyFeatureState,
  getError
);

export const selectMyFeatureIsLoading: MemoizedSelector<
  object,
  boolean
> = createSelector(
  selectMyFeatureState,
  getIsLoading
);

export const selectMyFeatureUser: MemoizedSelector<
  object,
  User
> = createSelector(
  selectMyFeatureState,
  getUser
);
  1. Effects — Create an effects.ts file in the app/root-store/my-feature-store directory with the following:
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { Observable, of as observableOf } from 'rxjs';
import { catchError, map, startWith, switchMap } from 'rxjs/operators';
import { DataService } from '../../services/data.service';
import * as featureActions from './actions';

@Injectable()
export class MyFeatureStoreEffects {
  constructor(private dataService: DataService, private actions$: Actions) {}

  @Effect()
  loginRequestEffect$: Observable<Action> = this.actions$.pipe(
    ofType<featureActions.LoginRequestAction>(
      featureActions.ActionTypes.LOGIN_REQUEST
    ),
    switchMap(action =>
      this.dataService
        .login(action.payload.userName, action.payload.password)
        .pipe(
          map(
            user =>
              new featureActions.LoginSuccessAction({
                user
              })
          ),
          catchError(error =>
            observableOf(new featureActions.LoginFailureAction({ error }))
          )
        )
    )
  );
}

Suggested Implementation — Entity and Standard Feature Modules

Now that we have created our feature module, either Entity or Standard typed above, we need to import the parts (state, actions, reducer, effects, selectors) into the Angular NgModule for the feature. In addition, we will create a barrel export in order to make imports in our application components clean and orderly, with asserted name-spaces.

  1. Update the app/root-store/my-feature-store/my-feature-store.module.ts with the following:
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { MyFeatureStoreEffects } from './effects';
import { featureReducer } from './reducer';

@NgModule({
  imports: [
    CommonModule,
    StoreModule.forFeature('myFeature', featureReducer),
    EffectsModule.forFeature([MyFeatureStoreEffects])
  ],
  providers: [MyFeatureStoreEffects]
})
export class MyFeatureStoreModule {}
  1. Create an app/root-store/my-feature-store/index.ts barrel export. You will notice that we import our store components and alias them before re-exporting them. This in essence is “name-spacing” our store components.
import * as MyFeatureStoreActions from './actions';
import * as MyFeatureStoreSelectors from './selectors';
import * as MyFeatureStoreState from './state';

export { MyFeatureStoreModule } from './my-feature-store.module';

export { MyFeatureStoreActions, MyFeatureStoreSelectors, MyFeatureStoreState };

Best Practice #1 — The Root Store Module (cont.)

Now that we have built our feature modules, let’s pick up where we left off in best practice ##1 and finish building out our RootStoreModule and RootState.

Suggested Implementation (cont.)

  1. Update app/root-store/root-state.ts and add a property for each feature that we have created previously:
import { MyFeatureStoreState } from './my-feature-store';
import { MyOtherFeatureStoreState } from './my-other-feature-store';

export interface State {
  myFeature: MyFeatureStoreState.State;
  myOtherFeature: MyOtherFeatureStoreState.State;
}
  1. Update your app/root-store/root-store.module.ts by importing all feature modules, and importing the following NgRx modules: StoreModule.forRoot({}) and EffectsModule.forRoot([]):
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { MyFeatureStoreModule } from './my-feature-store/';
import { MyOtherFeatureStoreModule } from './my-other-feature-store/';

@NgModule({
  imports: [
    CommonModule,
    MyFeatureStoreModule,
    MyOtherFeatureStoreModule,
    StoreModule.forRoot({}),
    EffectsModule.forRoot([])
  ],
  declarations: []
})
export class RootStoreModule {}
  1. Create an app/root-store/selectors.ts file. This will hold any root state level selectors, such as a Loading property, or even an aggregate Error property:
import { createSelector, MemoizedSelector } from '@ngrx/store';
import { MyFeatureStoreSelectors } from './my-feature-store';

import { MyOtherFeatureStoreSelectors } from './my-other-feature-store';

export const selectError: MemoizedSelector<object, string> = createSelector(
  MyFeatureStoreSelectors.selectMyFeatureError,
  MyOtherFeatureStoreSelectors.selectMyOtherFeatureError,
  (myFeatureError: string, myOtherFeatureError: string) => {
    return myFeature || myOtherFeature;
  }
);

export const selectIsLoading: MemoizedSelector<
  object,
  boolean
> = createSelector(
  MyFeatureStoreSelectors.selectMyFeatureIsLoading,
  MyOtherFeatureStoreSelectors.selectMyOtherFeatureIsLoading,
  (myFeature: boolean, myOtherFeature: boolean) => {
    return myFeature || myOtherFeature;
  }
);
  1. Create an app/root-store/index.ts barrel export for your store with the following:
import { RootStoreModule } from './root-store.module';
import * as RootStoreSelectors from './selectors';
import * as RootStoreState from './state';
export * from './my-feature-store';
export * from './my-other-feature-store';
export { RootStoreState, RootStoreSelectors, RootStoreModule };

Wiring up the Root Store Module to your Application

Now that we have built our Root Store Module, composed of Feature Store Modules, let’s add it to the main app.module.ts and show just how neat and clean the wiring up process is.

  1. Add RootStoreModule to your application’s NgModule.imports array. Make sure that when you import the module to pull from the barrel export:
import { RootStoreModule } from './root-store';
  1. Here’s an example container component that is using the store:
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { MyModel } from '../../models';
import {
  RootStoreState,
  MyFeatureStoreActions,
  MyFeatureStoreSelectors
} from '../../root-store';

@Component({
  selector: 'app-my-feature',
  styleUrls: ['my-feature.component.css'],
  templateUrl: './my-feature.component.html'
})
export class MyFeatureComponent implements OnInit {
  myFeatureItems$: Observable<MyModel[]>;
  error$: Observable<string>;
  isLoading$: Observable<boolean>;

  constructor(private store$: Store<RootStoreState.State>) {}

  ngOnInit() {
    this.myFeatureItems$ = this.store$.select(
      MyFeatureStoreSelectors.selectAllMyFeatureItems
    );

    this.error$ = this.store$.select(
      MyFeatureStoreSelectors.selectUnProcessedDocumentError
    );

    this.isLoading$ = this.store$.select(
      MyFeatureStoreSelectors.selectUnProcessedDocumentIsLoading
    );

    this.store$.dispatch(new MyFeatureStoreActions.LoadRequestAction());
  }
}

Finished Application Structure

Once we have completed implementation of the above best practices our Angular application structure should look very similar to something like this:

 ├── app
 │ ├── app-routing.module.ts
 │ ├── app.component.css
 │ ├── app.component.html
 │ ├── app.component.ts
 │ ├── app.module.ts
 │ ├── components
 │ ├── containers
 │ │    └── my-feature
 │ │         ├── my-feature.component.css
 │ │         ├── my-feature.component.html
 │ │         └── my-feature.component.ts
 │ ├── models
 │ │    ├── index.ts
 │ │    └── my-model.ts
 │ │    └── user.ts
 │ ├── root-store
 │ │    ├── index.ts
 │ │    ├── root-store.module.ts
 │ │    ├── selectors.ts
 │ │    ├── state.ts
 │ │    └── my-feature-store
 │ │    |    ├── actions.ts
 │ │    |    ├── effects.ts
 │ │    |    ├── index.ts
 │ │    |    ├── reducer.ts
 │ │    |    ├── selectors.ts
 │ │    |    ├── state.ts
 │ │    |    └── my-feature-store.module.ts
 │ │    └── my-other-feature-store
 │ │         ├── actions.ts
 │ │         ├── effects.ts
 │ │         ├── index.ts
 │ │         ├── reducer.ts
 │ │         ├── selectors.ts
 │ │         ├── state.ts
 │ │         └── my-other-feature-store.module.ts
 │ └── services
 │      └── data.service.ts
 ├── assets
 ├── browserslist
 ├── environments
 │ ├── environment.prod.ts
 │ └── environment.ts
 ├── index.html
 ├── main.ts
 ├── polyfills.ts
 ├── styles.css
 ├── test.ts
 ├── tsconfig.app.json
 ├── tsconfig.spec.json
 └── tslint.json

Fully Working Example — Chuck Norris Joke Generator

I have put together a fully working example of the above best practices. It’s a simple Chuck Norris Joke Generator that has uses @angular/material and the http://www.icndb.com/ api for data.

Github

https://github.com/wesleygrimes/angular-ngrx-chuck-norris

Stackblitz

You can see the live demo at https://angular-ngrx-chuck-norris.stackblitz.io and here is the Stackblitz editor:

angular-ngrx-chuck-norris - StackBlitz


Conclusion

It’s important to remember that I have implemented these best practices in several “real world” applications. While I have found these best practices helpful, and maintainable, I do not believe they are an end-all be-all solution to organizing NgRx projects; it’s just what has worked for me. I am curious as to what you all think? Please feel free to offer any suggestions, tips, or best practices you’ve learned when building enterprise Angular applications with NgRx and I will update the article to reflect as such. Happy Coding!


Additional Resources

I would highly recommend enrolling in the Ultimate Angular courses, especially the NgRx course. It is well worth the money and I have used it as a training tool for new Angular developers. Follow the link below to signup.

Ultimate Courses: Expert online courses in JavaScript, Angular, NGRX and TypeScript

]]>
Wesley Grimes