Author avatar

Hristo Georgiev

UI State Management with Redux in Angular 4

Hristo Georgiev

  • Dec 15, 2018
  • 53 Min read
  • 111,591 Views
  • Dec 15, 2018
  • 53 Min read
  • 111,591 Views
Front-End JavaScript

Introduction

Despite the numerous advances in building web interfaces in the last few years, the control of the DOM and the application User Interface (UI) is still highly dependent on jQuery, a 10-year-old library. Although this is not necessarily a bad thing, jQuery was initially built for different purposes than it's used for nowadays. As a result, modern uses have started to cause issues. Front-end applications have become increasingly complex, with all kinds of unrelated components, encapsulated views and other elements trying to interact together.

In this guide, we'll explore using Redux to mitigate the current challenges in controlling the UI of an Angular 2 application. You will learn how to represent some of the logic for controlling the UI layout in reducer functions and see an example use case.

Taming your application UI layout

Since Redux came out, state management for front-end apbplications went through a revolution. My team and I have field-tested Redux with Angular 2 and the resulting productivity boost has been tremendous.

Redux has not only allowed us to ship faster, but it increased the overall maintainability of our codebase by encapsulating most of the crucial logic in a single place and providing an easy to test architecture.

We liked Redux so much that we wanted to control everything with it. One of our most recent projects was building a very UI-intensive application and we decided to experiment by giving the reducers in the applicaiton a little bit more resposibility than just controlling data.

We found out that "reduxifying" the UI leads to numerous benefits and makes controlling the flexibility . It made previously difficult use cases a breeze to implement.

Three key benefits of using Redux for your UI layout
  • Maintain the state of your UI such as keeping the sidebar opened or closed when changing routes.
  • Control the UI from any point of the application without worrying about how components are related or how to inject a specific service.
  • Chain UI layout-specific actions with other events such as saving data server-side or changing a route

Setup

The examples below will be done with ng-bootstrap since it's one of the most popular libraries with Angular 2 components for Bootstrap 4. You can also implement these examples with other component libraries, such as Material Design, by following the same design principles and making small adjustments to the code so that it works with the API of the corresponding library.

Dependencies

Below are the packages you need to install in order to start working:

Redux

Bootstrap

Installing

To ensure a smooth setup, we will use Angular CLI to initialize the application architecture. Make sure you have it installed globally before you proceed.

In your terminal, write the following commands to initialize your Angular 2 app:

1$ ng new redux-layout-tutorial-app
2$ cd new redux-layout-tutorial-app
bash
1$ yarn add [email protected]
bash

To add Bootstrap's assets to your project, open angular-cli.json in your app root and add the following:

1apps: [
2  {
3   //..
4   "styles": [
5        "../node_modules/bootstrap/dist/css/bootstrap.css"
6    ],
7    //...
8    "environments": {
9    //...
10    "scripts": [
11      "../node_modules/jquery/dist/jquery.js",
12      "../node_modules/tether/dist/js/tether.js",
13      "../node_modules/bootstrap/dist/js/bootstrap.js"
14    ]
15  }
json

This will let your angular-cli locate the JavaScript and CSS files of your Bootstrap installation and add them in the build of the project.

Next, add ng-bootstrap to your dependencies:

1$ yarn add @ng-bootstrap/ng-bootstrap
bash

And include the NgbModule in your app's root module (located in app.module.ts):

1import {NgbModule} from "@ng-bootstrap/ng-bootstrap";
2
3@NgModule({
4    //..
5    imports: [
6        NgbModule.forRoot()
7    ],
8    //..
9})
ts

Setting up the application store and meta reducer

Next, we are going to make a bare minimum implementation of a Redux architecture that will serve as a foundaton of all the use cases that will be later implemented in this guide.

Start off by adding the core dependencies for the Redux application store:

1$ yarn add @ngrx/core
2$ yarn add @ngrx/store
bash

For asynchronous events such as pagination and loading bars, in the layout of the application, there needs to be a middleware:

1$ yarn add @ngrx/effects
bash

To make selection of the state fast an efficient, add reselect. We are going to use reselect's createSelector function to create efficient selectors that are memoized and only recompute when arguments change.

1$ yarn add reselect
bash

To make development more convenient and easier to debug, add a store logger, which will log to the console every action and the new state of the state.

1$ yarn add ngrx-store-logger
bash

To structure the application's files properly, all the redux-related files will stay in src/app/common directory.

1$ mkdir src/app/common
bash

Creating the layout state

Create common/layout directory which is going to contain all actions, effects, and the reducer of the layout sate.

1$ mkdir src/app/common/layout
2$ cd src/app/common/layout
bash

In the directory create three files for the layout state:

1$ touch layout.actions.ts
bash

layout.actions.ts

The layout actions will be dispatched every time when an user action is made (closing and opening sidebar, opening a modal and so on) or when certain events happen (window resizing).

1import { Action } from "@ngrx/store";
2
3/*
4 Layout actions are defined here
5 */
6
7export const LayoutActionTypes = {};
8
9/*
10 The action classes will be added here once they are defined
11*/
12export type LayoutActions = null;
ts

layout.reducer.ts

1$ touch layout.reducer.ts
bash

The reducer of the layout will handle all changes of the application layout and create a new state every time the UI has to change.

1import * as layout from "./layout.actions";
2
3export interface State {
4  /*
5   The description of the different parts of the layout go here
6  */
7}
8
9const initialState: State = {
10  /*
11    The initial values of the layout state will be initialized here
12   */
13};
14
15/*
16  The reducer of the layout state. Each time an action for the layout is dispatched,
17  it will create a new state for the layout.
18 */
19export function reducer(
20  state = initialState,
21  action: layout.LayoutActions
22): State {
23  switch (action.type) {
24    default:
25      return state;
26  }
27}
ts

Creating the meta reducer

With the UI state ready, the last step is to add the meta reducer, which will eventually be bootstrapped with the StoreModule provided by @ngrx/store. If you are not very familiar with Redux and the role of the meta reducer, read here:

1$ touch src/app/common/index.ts
bash
1/*
2  Import createSelector from reselect to make selection of different parts of the state fast efficient
3 */
4import { createSelector } from "reselect";
5/*
6  Import the store logger to log all the actions to the console
7 */
8import { storeLogger } from "ngrx-store-logger";
9
10/*
11 Import the layout state
12 */
13
14import * as fromLayout from "./layout/layout.reducer";
15import { compose } from "@ngrx/core";
16import { combineReducers } from "@ngrx/store";
17
18export interface AppState {
19  reducer: {
20    layout: fromLayout.State;
21  };
22}
23export const reducers = {
24  layout: fromLayout.reducer
25};
26
27const developmentReducer: Function = compose(storeLogger(), combineReducers)(
28  reducers
29);
30
31export function metaReducer(state: any, action: any) {
32  return developmentReducer(state, action);
33}
34
35/**
36 * Layout selectors
37 */
38
39export const getLayoutState = (state: AppState) => state.reducer.layout;
ts

Finally, add the metaReducer to the StoreModule in the imports array of the root module:

1import { StoreModule } from "@ngrx/store";
2import { metaReducer } from "./common/index";
3//...
4
5@NgModule({
6  //...
7  imports: [
8    //Provide the application reducer to the store.
9    StoreModule.forRoot({ reducer: metaReducer })
10  ]
11  //...
12})
13export class AppModule {}
ts

"Smart" containers and "dumb" components

If you are fimilar with Redux, you would know that there are two types of components - presentational components and container components.

When building the UI state, it's best to keep the logic inside directives in order to keep the logic DRY. You don't have to write the same logic for a sidebar in every container component in your application.

Another possibility is to keep the logic in the container and only in exceptional cases there's a need to put the logic inside a component that represents a UI element.

In this guide, the container component will be the AppComponent. In order to make the state of the application accessible in it and be able to dispatch actions, you have to import layout.actions and the root state:

1import { Component } from "@angular/core";
2import { Store } from "@ngrx/store";
3import { Observable } from "rxjs";
4/**
5 * Import the root state in order to select parts of it.
6 */
7import * as fromRoot from "./common/index";
8/*
9 * Import the layout actions to make dispatching from the component possible.
10 */
11import * as layout from "./common/layout/layout.actions";
12
13@Component({
14  selector: "app-root",
15  templateUrl: "./app.component.html",
16  styleUrls: ["./app.component.css"]
17})
18export class AppComponent {
19  constructor(private store: Store<fromRoot.AppState>) {}
20}
ts

Modals

The easiest way to implement a modal in the state is to keep its name as an identifier. Since only one modal can be opened at a time in the UI view (unless you're trying to do some kind of black magic), every modal can be referenced by a modalName.

Let's start with the actions. An user can open and close a modal, so let's add actions for that:

Adding to the state

layout.actions.ts

1export const LayoutActionTypes = {
2  OPEN_MODAL: "[Layout] Open modal",
3  CLOSE_MODAL: "[Layout] Close modal"
4};
5
6/*
7  Modal actions
8 */
9export class OpenModalAction implements Action {
10  type = LayoutActionTypes.OPEN_MODAL;
11  constructor(public payload: string) {}
12}
13
14export class CloseModalAction implements Action {
15  type = LayoutActionTypes.CLOSE_MODAL;
16  constructor(public payload: string) {}
17}
18
19export type LayoutActions = CloseModalAction | OpenModalAction;
ts

Let's go ahead and implement how modal actions will be handled in the layout reducer: layout.reducer.ts

1import * as layout from "./layout.actions";
2
3export interface State {
4  openedModalName: string;
5}
6
7const initialState: State = {
8  openedModalName: null
9};
10
11export function reducer(
12  state = initialState,
13  action: layout.LayoutActions
14): State {
15  switch (action.type) {
16    /*
17      Modal cases
18     */
19    case layout.LayoutActionTypes.OPEN_MODAL: {
20      const name = action.payload;
21      return Object.assign({}, state, {
22        openedModalName: name
23      });
24    }
25
26    case layout.LayoutActionTypes.CLOSE_MODAL: {
27      return Object.assign({}, state, {
28        openedModalName: null
29      });
30    }
31    default:
32      return state;
33  }
34}
35
36export const getOpenedModalName = (state: State) => state.openedModalName;
ts

The currently modal's name will be stored in the openedModalName which will be set and unset according to the dispatched action. A selector getOpenedModalName is needed to easily access the openedModalName property within the state.

In index.ts, add a selector to access the openedModalName property from the application state: index.ts

1export const getLayoutState = (state: AppState) => state.layout;
2
3//...
4export const getLayoutOpenedModalName = createSelector(
5  getLayoutState,
6  fromLayout.getOpenedModalName
7);
ts

Usage

To see how it works, let's create a sample modal:

1$ ng g component template-modal
bash

template-modal.component.ts

1import {
2  Component,
3  ChangeDetectionStrategy,
4  Output,
5  ViewChild,
6  EventEmitter,
7  Input,
8  ElementRef
9} from "@angular/core";
10import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap";
11
12@Component({
13  selector: "template-modal",
14  templateUrl: "template-modal.component.html"
15})
16export class TemplateModalComponent {
17  private modalName: string = "templateFormModal";
18  private modalRef: NgbModalRef;
19
20  @ViewChild("content") _templateModal: ElementRef;
21
22  @Input()
23  set modalState(_modalState: any) {
24    if (_modalState == this.modalName) {
25      this.openModal();
26    } else if (this.modalRef) {
27      this.closeModal();
28    }
29  }
30
31  @Output() onCloseModal = new EventEmitter<any>();
32
33  constructor(private modalService: NgbModal) {}
34
35  openModal() {
36    this.modalRef = this.modalService.open(this._templateModal, {
37      backdrop: "static",
38      keyboard: false,
39      size: "sm"
40    });
41  }
42
43  closeModal() {
44    this.modalRef.close();
45  }
46}
ts

The name of the currently-selected modal is coming from the container and is obtained through the modalState input of the component. If the name matches with the modalName (templateFormModal), then the modal is opened through the modalService.

Conversely, onCloseModal is used to emit to the container that te user has clicked to close the modal.

template-modal.component.html

1<template #content="" let-c="close" let-d="dismiss">
2  <div class="modal-header">
3    <div class="row">
4      <div class="col-sm-10">
5        Template Modal
6      </div>
7      <div class="col-sm-2">
8        <button class="close" type="button" aria-label="Close" (click)="d('Cross click'); onCloseModal.emit()"><span aria-hidden="true">×</span></button>
9      </div>
10    </div>
11  </div>
12  <div class="modal-body">
13    Modal with Redux using a template. You can put anything here
14  </div>
15  <div class="modal-footer">
16    <div class="btn-group">
17      <button class="btn btn-warning" type="button" (click)="d('Cross click'); onCloseModal.emit()">Close</button>
18    </div>
19  </div>
20</template>
html

Every time the user attempts to close the modal, onCloseModal directly emits from the template to the container. In case you have issues understanding how the modal works, you can check the standard implementation of a ng-bootstrap modal without Redux.

In the container there need to be handlers for getting the openedModalName from the state and for dispatching an action for closing a modal:

app.component.ts

1export class AppComponent {
2  public openedModalName$: Observable<any>;
3
4  constructor(private store: Store<fromRoot.AppState>) {
5    // Use the selector to directly get the opened modal name from the state
6    this.openedModalName$ = store.select(fromRoot.getLayoutOpenedModalName);
7  }
8
9  //Dispatch an action to open a modal
10  handleOpenModal(modalName: string) {
11    this.store.dispatch(new layout.OpenModalAction(modalName));
12  }
13
14  handleCloseModal() {
15    this.store.dispatch(new layout.CloseModalAction());
16  }
17}
ts

As you can see, you can reuse the handleOpenModal and handleCloseModal. No matter how many modals your container has, the only thing that needs to be specified is the modalName of the modal you would like to see opened.

app.component.html

1<!-- Use the async pipe to get the latest broadcasted value of on observable as an input in the component  -->
2<template-modal [modalState]="this.openedModalName$ | async" (onCloseModal)="handleCloseModal()"></template-modal>
3<button class="btn btn-outline-primary" (click)="handleOpenModal('templateFormModal')">Open modal with template</button>
4
5<!-- Don't forget to add this: -->
6<template ngbModalContainer></template>
html

In this case handleOpenModal is dispatched by clicking a button, but it can also be dispatched as an output from another component, a directive,a service or an effect. The possibilities are endless.

modal example

Sidebar(s)

The most generic representation of a sidebar in an application comes down to whether the sidebar is opened or not. The state will have a property that denotes whether a sidebar is opened or closed using a boolean value. In case there are two sidebars (or more, depends on what kind of sorcery your're doing), there will be a property in the state for each.

For the user to start interacting with the sidebar, there need to be actions for opening and closing:

layout.actions.ts

1export const LayoutActionTypes = {
2  //Left sidenav actions
3  OPEN_LEFT_SIDENAV: "[Layout] Open LeftSidenav",
4  CLOSE_LEFT_SIDENAV: "[Layout] Close LeftSidenav",
5  //Right sidenav actions
6  OPEN_RIGHT_SIDENAV: "[Layout] Open RightSidenav",
7  CLOSE_RIGHT_SIDENAV: "[Layout] Close RightSidenav"
8};
9
10export class OpenLeftSidenavAction implements Action {
11  type = LayoutActionTypes.OPEN_LEFT_SIDENAV;
12
13  constructor() {}
14}
15export class CloseLeftSidenavAction implements Action {
16  type = LayoutActionTypes.CLOSE_LEFT_SIDENAV;
17
18  constructor() {}
19}
20export class OpenRightSidenavAction implements Action {
21  type = LayoutActionTypes.OPEN_RIGHT_SIDENAV;
22
23  constructor() {}
24}
25
26export class CloseRightSidenavAction implements Action {
27  type = LayoutActionTypes.CLOSE_RIGHT_SIDENAV;
28
29  constructor() {}
30}
31
32export type LayoutActions =
33  | CloseLeftSidenavAction
34  | OpenLeftSidenavAction
35  | CloseRightSidenavAction
36  | OpenRightSidenavAction;
ts

As mentioned, the states of the sidebar will be represented with booleans. In this case, the left sidenav will be open by default, but there can be logic that checks if the window size is small enough to dispatch a CloseLeftSidenavAction to close it:

layout.reducer.ts

1import * as layout from './layout.actions';
2
3export interface State {;
4 leftSidebarOpened:boolean;
5 rightSidebarOpened:boolean;
6}
7
8const initialState: State = {
9 leftSidebarOpened:true,
10 rightSidebarOpened:false
11};
12
13
14export function reducer(state = initialState, action: layout.LayoutActions ): State {
15 switch (action.type) {
16   case layout.LayoutActionTypes.CLOSE_LEFT_SIDENAV: {
17     return Object.assign({}, state, {
18       leftSidebarOpened: false
19     });
20   }
21   case layout.LayoutActionTypes.OPEN_LEFT_SIDENAV: {
22     return Object.assign({}, state, {
23       leftSidebarOpened: true
24     });
25   }
26   case layout.LayoutActionTypes.CLOSE_RIGHT_SIDENAV: {
27     return Object.assign({}, state, {
28       rightSidebarOpened: false
29     });
30   }
31   case layout.LayoutActionTypes.OPEN_RIGHT_SIDENAV: {
32     return Object.assign({}, state, {
33       rightSidebarOpened: true
34     });
35   }
36
37   default:
38     return state;
39 }
40}
41
42export const getLeftSidenavState = (state:State) => state.leftSidebarOpened;
43export const getRightSidenavState = (state:State) => state.rightSidebarOpened;
ts

In the root of the state, add selectors to access the states of the sidebars: index.ts

1//...
2
3export const getLeftSidenavState = (state: State) => state.leftSidebarOpened;
4export const getRightSidenavState = (state: State) => state.rightSidebarOpened;
ts

Usage

Instead of putting the logic in each of the sidebar components, it can be combined within a structural directive that closes and opens the corresponding sidebar depening on the state of the application.

1$ ng g directive sidebar-watch
bash

sidebar-watch.directive.ts

1import {
2  Directive,
3  ElementRef,
4  Renderer,
5  OnInit,
6  AfterViewInit,
7  AfterViewChecked
8} from "@angular/core";
9import { Store } from "@ngrx/store";
10import * as fromRoot from "../common/index";
11let $ = require("jquery");
12
13@Directive({ selector: "[sidebarWatch]" })
14export class SidebarWatchDirective implements OnInit {
15  constructor(
16    private el: ElementRef,
17    private _store: Store<fromRoot.AppState>
18  ) {}
19
20  /*
21  Doing the checks on ngOnInit makes sure the DOM is fully loaded and the
22  elements are available to be selected
23  */
24  ngOnInit() {
25    /*
26    Watch for the left sidebar state
27    */
28    this._store.select(fromRoot.getLayoutLeftSidenavState).subscribe(state => {
29      if (this.el.nativeElement.className == "left-sidebar") {
30        if (state) {
31          $("#main-content").css("margin-left", "300px");
32          $(this.el.nativeElement).css("width", "300px");
33        } else {
34          $("#main-content").css("margin-left", "0");
35          $(this.el.nativeElement).css("width", "0");
36        }
37      }
38    });
39
40    /*
41    Watch for the right sidebar state
42    */
43    this._store.select(fromRoot.getLayoutRightSidenavState).subscribe(state => {
44      /*
45      You can use classes (addClass/removeClass) instead of using jQuery css(), or you
46      can go completely vanilla by using selectors such as windiw.getElementById(). .
47      */
48      if (this.el.nativeElement.className == "right-sidebar") {
49        console.log("test");
50        if (state) {
51          $("#fade").addClass("fade-in");
52          $("#rightBar-body").css("opacity", "1");
53          $("body").css("overflow", "hidden");
54          $(this.el.nativeElement).css("width", "60%");
55        } else {
56          $("#fade").removeClass("fade-in");
57          $("#rightBar-body").css("opacity", "0");
58          $("body").css("overflow", "auto");
59          $(this.el.nativeElement).css("width", "0");
60        }
61      }
62    });
63  }
64}
ts

The directive checks ElementRef's nativeElement, which make the DOM properties of the template accessible in the component. Once it knows which sidebar it is being applied to, the directive checks whether the corresponding state (LeftSidenavbarState or RightSidenavbarState, respectively) is true or false. Then the directive uses jQuery to manipulate the corresponding elements in the layout. The use of jQuery to select and directly change the DOM element's style properties is optional and you can use addition and removal of classes or by using plain JavaScript.

Following the same logic, there can be a directive for toggling the sidebars:

1 $ ng g directive sidebar-toggle
bash

sidebar-toggle.directive.ts

1/**
2 * Created by Centroida-2 on 1/22/2017.
3 */
4import {
5  Directive,
6  Input,
7  ElementRef,
8  Renderer,
9  HostListener
10} from "@angular/core";
11import { Store } from "@ngrx/store";
12import * as fromRoot from "../common/index";
13import * as layout from "../common/layout/layout.actions";
14@Directive({
15  selector: "[sidebarToggle]"
16})
17export class SidebarToggleDirective {
18  public leftSidebarState: boolean;
19  public rightSidebarState: boolean;
20  @Input() sidebarToggle: string;
21
22  @HostListener("click", ["$event"])
23  onClick(e) {
24    /*
25    Left sidenav toggle
26    */
27    if (this.sidebarToggle == "left" && this.leftSidebarState) {
28      this._store.dispatch(new layout.CloseLeftSidenavAction());
29    } else if (this.sidebarToggle == "left" && !this.leftSidebarState) {
30      this._store.dispatch(new layout.OpenLeftSidenavAction());
31    }
32
33    /*
34    Right sidenav toggle
35    */
36    if (this.sidebarToggle == "right" && this.rightSidebarState) {
37      this._store.dispatch(new layout.CloseRightSidenavAction());
38    } else if (this.sidebarToggle == "right" && !this.rightSidebarState) {
39      this._store.dispatch(new layout.OpenRightSidenavAction());
40    }
41  }
42
43  constructor(
44    private el: ElementRef,
45    private renderer: Renderer,
46    private _store: Store<fromRoot.AppState>
47  ) {
48    this._store.select(fromRoot.getLayoutLeftSidenavState).subscribe(state => {
49      this.leftSidebarState = state;
50    });
51
52    this._store.select(fromRoot.getLayoutRightSidenavState).subscribe(state => {
53      this.rightSidebarState = state;
54    });
55  }
56}
ts

The directive has an @Input sidebarToggle which can be either left or right , depending on which sidebar the directive has to control. Every time the user clicks on the element to which the directive is attached, the @HostListener('click') catches the event and checks the state of the sidebar of the store and dispatches the corresponding action.

To demonstrate how everything comes together, let's make two sidebars:

1$ ng g component left-sidebar
bash

left-sidebar.component.ts

1import { Component } from "@angular/core";
2
3@Component({
4  selector: "left-sidebar",
5  templateUrl: "left-sidebar.component.html",
6  styleUrls: ["./sidebar.styles.css"]
7})
8export class LeftSidebarComponent {
9  constructor() {}
10}
ts

left-sidebar.component.html

1<section sidebarWatch class="left-sidebar">
2</section>
html
1$ ng g component right-sidebar
bash

right-sidebar.component.ts

1import { Component } from "@angular/core";
2
3@Component({
4  selector: "right-sidebar",
5  templateUrl: "right-sidebar.component.html",
6  styleUrls: ["./sidebar.styles.css"]
7})
8export class RightSidebarComponent {
9  constructor() {}
10}
ts

right-sidebar.component.html

1<section sidebarWatch class="right-sidebar">
2  <button class="btn btn-primary" sidebarToggle="right">Close Right Sidebar</button>
3</section>
html

Using the sidebarWatch directive is quite straightforward. Just put in the topmost elements of the sidebars.

The sidebarToggle needs to be put in a button (although you can put it anywhere you like) and it needs to have the left or right value assigned to it.

To make the elements look and feel like sidebars, there needs to be some additional CSS:

1$ touch src/app/components/sidebar.styles.css
bash

sidebar.styles.css

1.left-sidebar,
2.right-sidebar {
3  transition: width 0.3s;
4  height: 100%;
5  position: fixed;
6  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
7}
8
9.left-sidebar {
10  background: #909090;
11}
12.right-sidebar {
13  overflow-y: auto !important;
14  overflow-x: hidden !important;
15  right: 0;
16  z-index: 999 !important;
17  background: #212121;
18}
css

In the root component of the application, put the sidebars on top all the content in a div with a class main-content: app.component.html

1<div id="fade" class="fade-in"></div>
2<left-sidebar></left-sidebar>
3<right-sidebar></right-sidebar>
4<div id="main-content">
5  <button class="btn btn-primary" sidebarToggle="left">Toggle Left Sidebar</button>
6  <button class="btn btn-primary" sidebarToggle="right">Toggle Right Sidebar</button>
7
8  <!-- ... -->
9</div>
10<!-- ... -->
html

The div with id fade will be used for the fade effect when the right sidebar is opened. Add the styles to the component styles:

app.component.css

1.fade-in {
2  position: absolute;
3  min-height: 100% !important;
4  top: 0;
5  right: 0;
6  bottom: 0;
7  left: 0;
8  background: rgba(0, 0, 0, 0.5);
9  width: 100%;
10  transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s;
11}
css

With this setup, the sidebars are truly container-agnostic. Any element can be made a sidebar through a directive and be toggled from any point of the UI layout. There is also flexibility if there's a requirement to add additional components such as bottom bars or top bars.

sidebars example

Dismissable Alerts

Including alerts in the application state gives control when, where and how alerts can appear. Since alerts are dependent on either server-side or user actions, their place in the application state is well-deserved.

Unlike other examples in this guide, "reduxifying" alerts is somehwat easy - they can be simply represented as a local collection of items that can be added and removed from the state.

By default, an alert would have two attributes: message and type. Here is how the model of a alert would look like:

1export class Alert {
2  message: string;
3  type: string;
4}
ts

First, let's add actions for removing and adding alerts:

layout.actions.ts

1export const LayoutActionTypes = {
2  ADD_ALERT: "[Layout] add alert",
3  REMOVE_ALERT: "[Layout] remove alert"
4  //...
5};
6
7//...
8export class AddAlertAction implements Action {
9  type = LayoutActionTypes.ADD_ALERT;
10  constructor(public payload: Object) {}
11}
12
13export class RemoveAlertAction implements Action {
14  type = LayoutActionTypes.REMOVE_ALERT;
15  constructor(public payload: Object) {}
16}
17//...
18
19export type LayoutActions = AddAlertAction | RemoveAlertAction;
ts

Second, let's add the alerts slice in the layout state:

layout.reducer.ts

1import * as layout from "./layout.actions";
2
3export interface State {
4  //...
5  alerts: Array<Object>;
6}
7
8const initialState: State = {
9  //...
10  alerts: []
11};
12
13export function reducer(
14  state = initialState,
15  action: layout.LayoutActions
16): State {
17  switch (action.type) {
18    case layout.LayoutActionTypes.ADD_ALERT: {
19      return Object.assign({}, state, {
20        alerts: [...state.alerts, action.payload]
21      });
22    }
23    case layout.LayoutActionTypes.REMOVE_ALERT: {
24      return Object.assign({}, state, {
25        /*
26         Alerts are filtered by message content, but for real-world usage, an 'id' field would be more suitable.
27        */
28        alerts: state.alerts.filter(
29          alert => alert["message"] !== action.payload["message"]
30        )
31      });
32    }
33    //...
34    default:
35      return state;
36  }
37}
38
39//...
40/*
41 If you add more attributes to the alerts such as 'position' or 'modelType',
42 there can be more selectors added that can filter the collection and allow
43 only certain to be displayed in designated places in the application.
44*/
45export const getAlerts = (state: State) => state.alerts;
ts

Finally, let's add a selector for alerts in the root:

index.ts

1//...
2export const getLayoutAlertsState = createSelector(
3  getLayoutState,
4  fromLayout.getAlerts
5);
ts

That's it. Now alerts are part of the application state. But how are they going to be used? Let's find out:

Usage

With the tools given, making dismissible alerts requires a very small amount of code since ng-bootstrap already offers an implementation. Thus, the only thing that is required is a reusable component to be made that can be used in various places in the application:

1$ touch src/app/alerts-list.component.ts
bash

alerts.component.ts

1import { Component, Input, EventEmitter, Output } from "@angular/core";
2
3@Component({
4  selector: "alerts-list",
5  templateUrl: "alerts-list.component.html"
6})
7export class AlertsListComponent {
8  @Input() alerts: any;
9  @Output() closeAlert = new EventEmitter();
10
11  constructor() {}
12}
ts

The component accepts an array of alerts and outputs the alert which the user decides to close.

1$touch src/app/alerts-list.component.html
bash

alerts.component.html

1<p *ngFor="let alert of alerts">
2  <ngb-alert [type]="alert.type" (close)="closeAlert.emit(alert)">{{ alert.message }}</ngb-alert>
3</p>
html

Don't forget to add the component to the root module:

app.module.ts

1import { AlertsListComponent } from "./components/alerts-list.component";
2///...
3
4@NgModule({
5  declarations: [AlertsListComponent]
6  //...
7})
8export class AppModule {}
ts

Next, the logic in the container component has to be implemented to select alerts and dispatch events.

app.component.ts

1import { Component, OnInit } from "@angular/core";
2import { Store } from "@ngrx/store";
3import { Observable } from "rxjs";
4import * as fromRoot from "./common/index";
5import * as layout from "./common/layout/layout.actions";
6
7@Component({
8  selector: "app-root",
9  templateUrl: "./app.component.html"
10})
11export class AppComponent implements OnInit {
12  public alerts$: Observable<any>;
13
14  constructor(private store: Store<fromRoot.AppState>) {
15    this.alerts$ = store.select(fromRoot.getLayoutAlertsState);
16  }
17
18  addAlert(alert) {
19    this.store.dispatch(new layout.AddAlertAction(alert));
20  }
21
22  onCloseAlert(alert: Object) {
23    this.store.dispatch(new layout.RemoveAlertAction(alert));
24  }
25}
ts

To demonstrate how alerts look, the template will have two buttons for opeaning different types of alerts:

app.component.html

1<div id="fade" class="fade-in"></div>
2<left-sidebar></left-sidebar>
3<right-sidebar></right-sidebar>
4
5<div id="main-content">
6    <!-- List of alerts goes here -->
7    <alerts-list [alerts]="alerts$ | async (closeAlert)="onCloseAlert($event)"></alerts-list>
8
9    <!-- Buttons for creating alerts -->
10    <button class="btn btn-danger" (click)="addAlert({type: 'danger', message: 'This is a danger alert'})">Add a danger alert</button>
11    <button class="btn btn-success" (click)="addAlert({type: 'success', message: 'This is a success alert'})">Add a success alert</button>
12</div>
html

alert example

In a real-world scenario, alerts can be created when a server returns certain results. For example, in the snippet below, an AddAlertAction is called once the application resolves a server-side request:

1  @Effect() deleteStudent = this._actions.ofType(student.ActionTypes.DELETE_STUDENT)
2    .switchMap((action) => this._service.delete(action.payload)
3    )
4    .mergeMap(
5      () => {
6        return Observable.from([
7          new DeleteStudentSuccessAction(),
8          /* Chain actions - once the server successfully
9           deletes some model, create an alert from it.
10          */
11          new layout.AddAlertAction({type:'success', message: 'Student successfully deleted!')
12        ])
13      .catch(() => {
14             new layout.AddAlertAction({type:'danger', message: 'An error ocurred.'})
15             return Observable.of( new DeleteStudentFailureAction()
16            })
17        );
18      }
19    );
ts

Window size

Having the window size available in the application store can make Redux useful for numerous use cases, especially for making responsive UI changes, device-specific actions or dynamic changes of the CSS (using NgClass or NgStyle ).

To make the window size usable in the application state, it has to be updated every time the window is resized. Let's add an action for that:

layout.actions.ts

1import { Action } from "@ngrx/store";
2
3export const LayoutActionTypes = {
4  // Add indow resize action
5  RESIZE_WINDOW: "[Layout] Resize window"
6};
7
8export class ResizeWndowAction implements Action {
9  type = LayoutActionTypes.RESIZE_WINDOW;
10  constructor(public payload: Object) {}
11}
12
13export type LayoutActions = ResizeWndowAction;
ts

To implement the window size in the UI state, there need to be two attributes added to the state - windowWidth and windowHeight:

layout.reducer.ts

1import * as layout from "./layout.actions";
2
3export interface State {
4  //...
5  windowHeight: number;
6  windowWidth: number;
7}
8
9const initialState: State = {
10  //...
11  windowHeight: window.screen.height,
12  windowWidth: window.screen.width
13};
14
15export function reducer(
16  state = initialState,
17  action: layout.LayoutActions
18): State {
19  switch (action.type) {
20    /*
21     Window resize case
22     */
23    case layout.LayoutActionTypes.RESIZE_WINDOW: {
24      const height: number = action.payload["height"];
25      const width: number = action.payload["width"];
26      return Object.assign({}, state, {
27        windowHeight: height,
28        windowWidth: width
29      });
30    }
31    //...
32
33    default:
34      return state;
35  }
36}
37
38export const getWindowWidth = (state: State) => state.windowWidth;
39export const getWindowHeight = (state: State) => state.windowHeight;
ts

The inital state is set by using window.screen.height and window.screen.width, which get the values of the window's size when the state is initialized. The WindowResizeAction payload contains an object with the height and the width of the resized window: {width:number , height:number}.

There are numerous ways to listen for window resize changes, but perhaps one of the most convenient and conventional ones is to put a host attribute in the application's root component decorator (Appcomponent). Since the listener is attached to the root component, ResizeWindowAction will be dispatched regardless of which part in the application the user is.

app.component.ts

1import { Component, OnInit } from "@angular/core";
2import { Store } from "@ngrx/store";
3import { Observable } from "rxjs";
4import * as fromRoot from "./common/index";
5import * as layout from "./common/layout/layout.actions";
6
7@Component({
8  selector: "app-root",
9  templateUrl: "./app.component.html",
10  styleUrls: ["./app.component.css"],
11  /*
12   Add this to your AppComponent to listen for window resize events
13   */
14  host: {
15    "(window:resize)": "onWindowResize($event)"
16  }
17})
18export class AppComponent implements OnInit {
19  //...
20  constructor(private store: Store<fromRoot.AppState>) {
21    //...
22  }
23
24  ngOnInit() {}
25
26  onWindowResize(event) {
27    this.store.dispatch(
28      new layout.ResizeWndowAction({
29        width: event.target.innerWidth,
30        height: event.target.innerHeight
31      })
32    );
33  }
34}
ts

The host listens for window resize events and calls the onWindowResize method with the event as a parameter. The method gets the new sizes using the event.target property and dispatches a ResizeWindowAction with the new values.

Usage

The most obiquitous case for using the window size is responsiveness. For example, suppose the left sidebar has to automatically close if the window width is lower than 768px (iPad width). Doing this with Redux is quite simple - just add an if statement to the corresponding case:

layout.reducer.ts

1//...
2
3export function reducer(
4  state = initialState,
5  action: layout.LayoutActions
6): State {
7  switch (action.type) {
8    //...
9    case layout.LayoutActionTypes.RESIZE_WINDOW: {
10      const height: number = action.payload["height"];
11      const width: number = action.payload["width"];
12
13      // If the width is lower than 768px, assign false. Otherwise don't change the state
14      const leftSidebarState = width < 768 ? false : state.leftSidebarOpened;
15
16      return Object.assign({}, state, {
17        windowHeight: height,
18        windowWidth: width,
19        leftSidebarOpened: leftSidebarState
20      });
21    }
22  }
23  //...
24}
ts

The equivalent jQuery operation would be quite frustrating. However, with Redux, simply adding a simple ternary operator in the reducer does the trick.

With Redux, all the logic is isolated in a single place, and it is easy to debug and test all your implementations.

window resize example

Server-side Pagination

Even though pagination is not strictly part of an application's UI layout, it is an integral part of the application's UI that can be implemented with Redux. The goal of implementing server-side pagination is to utilize the application store as much as possible and achieve flexibility with the least code possible.

GiantBomb API

To illustrate how Redux pagination works, we will use the GiantBomb API as the source of information. We will fetch the games stored in the GiantBomb database, and then we will paginate the results. The pagination will be controlled by the application state.

First, create a separate directory for games:

1$ mkdir src/app/common/games
bash
1 $ touch src/app/common/games.actions.ts
bash

games.actions.ts

1import { type } from "../util";
2import { Action } from "@ngrx/store";
3export const GameActionTypes = {
4  /*
5  Because the games collection is asynchronous, there need to be actions to handle
6  each of the stages of the request.
7  */
8  LOAD: "[Games] load games",
9  LOAD_SUCCESS: "[Games] successfully loaded games",
10  LOAD_FAILURE: "[Games] failed to load games"
11};
12
13export class LoadGamesAction implements Action {
14  type = GameActionTypes.LOAD;
15  constructor(public payload: any) {}
16}
17
18export class LoadGamesFailedAction implements Action {
19  type = GameActionTypes.LOAD_FAILURE;
20
21  constructor() {}
22}
23export class LoadGamesSuccessAction implements Action {
24  type = GameActionTypes.LOAD_SUCCESS;
25  constructor(public payload: any) {}
26}
27
28export type GameActions =
29  | LoadGamesAction
30  | LoadGamesFailedAction
31  | LoadGamesSuccessAction;
ts

Redux has a convention for loading asynchronous results. It does it by using three actions - LOAD , LOAD_SUCCESS and LOAD_FAILURE. The last two get dispatched when the middleware resolves the server-side request.

To figure out how to construct the state of the paginated games entities, let's see what a pagination needs:

  1. Number of current pages
  2. Total amount of items
  3. Collection of the items currently displayed
  4. (optional) Number of items per page and number of visible pages

Having this in mind, here's how the state interface should look:

1export interface State {
2  loaded: boolean;
3  loading: boolean;
4  entities: Array<any>;
5  count: number;
6  page: number;
7}
ts

Let's see how the full implementation looks like:

1$ touch src/app/common/games.reducer.ts
bash

games.reducer.ts

1import { createSelector } from "reselect";
2import * as games from "./games.actions";
3
4export interface State {
5  loaded: boolean;
6  loading: boolean;
7  entities: Array<any>;
8  count: number;
9  page: number;
10}
11
12const initialState: State = {
13  loaded: false,
14  loading: false,
15  entities: [],
16  count: 0,
17  page: 1
18};
19
20export function reducer(
21  state = initialState,
22  action: games.GameActions
23): State {
24  switch (action.type) {
25    case games.GameActionTypes.LOAD: {
26      const page = action.payload;
27
28      return Object.assign({}, state, {
29        loading: true,
30        /*
31         If there is no page selected, use the page from the initial state
32         */
33        page: page == null ? state.page : page
34      });
35    }
36
37    case games.GameActionTypes.LOAD_SUCCESS: {
38      const games = action.payload["results"];
39      const gamesCount = action.payload["number_of_total_results"];
40
41      return Object.assign({}, state, {
42        loaded: true,
43        loading: false,
44        entities: games,
45        count: gamesCount
46      });
47    }
48
49    case games.GameActionTypes.LOAD_FAILURE: {
50      return Object.assign({}, state, {
51        loaded: true,
52        loading: false,
53        entities: [],
54        count: 0
55      });
56    }
57    default:
58      return state;
59  }
60}
61/*
62 Selectors for the state that will be later
63 used in the games-list component
64 */
65export const getEntities = (state: State) => state.entities;
66export const getPage = (state: State) => state.page;
67export const getCount = (state: State) => state.count;
68export const getLoadingState = (state: State) => state.loading;
ts

Every time LOAD is called from the GamesActions, the page number is contained within the action's payload and it is then assigned to the state. What's left is to find a way to query the server using page from the games state. To do this, the state has to be added to the application store:

index.ts

1import * as fromGames from "./games/games.reducer";
2//...
3export interface AppState {
4  layout: fromLayout.State;
5  games: fromGames.State;
6}
7
8export const reducers = {
9  layout: fromLayout.reducer,
10  games: fromGames.reducer
11};
12
13//...
14/*
15 Games selectors
16 */
17export const getGamesState = (state: AppState) => state.games;
18export const getGamesEntities = createSelector(
19  getGamesState,
20  fromGames.getEntities
21);
22export const getGamesCount = createSelector(getGamesState, fromGames.getCount);
23export const getGamesPage = createSelector(getGamesState, fromGames.getPage);
24export const getGamesLoadingState = createSelector(
25  getGamesState,
26  fromGames.getLoadingState
27);
ts

getGamesPage will be used to obtain the current page and send it as a parameter in the query to the service.

1$ touch src/app/common/games.service.ts
bash

games.service.ts

1import { Injectable, Inject } from "@angular/core";
2import { Response, Http, Headers, RequestOptions, Jsonp } from "@angular/http";
3import { Store } from "@ngrx/store";
4import * as fromRoot from "../index";
5
6@Injectable()
7export class GamesService {
8  public page: number;
9
10  constructor(private jsonp: Jsonp, private store: Store<fromRoot.AppState>) {
11    /*
12    Get the page from the games state
13     */
14    store.select(fromRoot.getGamesPage).subscribe(page => {
15      this.page = page;
16    });
17  }
18
19  /*
20  Get the list of games. GiantBomb requires a jsnop request with a token. You can use this token
21  as a present from me, the author, and use it in moderation!
22   */
23  query() {
24    let pagination = this.paginate(this.page);
25    let url = `http://www.giantbomb.com/api/games/?api_key=b89a6126dc90f68a87a6fe1394e64d7312b242da&?&offset=${
26      pagination.offset
27    }&limit=${pagination.limit}&format=jsonp&json_callback=JSONP_CALLBACK`;
28    return this.jsonp.request(url, { method: "Get" }).map(res => {
29      return res["_body"];
30    });
31  }
32  /**
33   * This function converts a page to a pagination
34   * query.
35   *
36   * @param page
37   *
38   * @returns {{offset: number, limit: number}}
39   */
40
41  paginate(page: number) {
42    let beginItem: number;
43    let endItem: number;
44    // Items per page are hardcoded, but you can make them dynamic by adding another parameter
45    let itemsPerPage: number = 10;
46    if (page == 1) {
47      beginItem = 0;
48    } else {
49      beginItem = (page - 1) * itemsPerPage;
50    }
51    return {
52      offset: beginItem,
53      limit: itemsPerPage
54    };
55  }
56}
ts

The currently selected page is taken from the state and passed through paginate. paginate is a utility function that converts the current page to offset and limit parameters in accordance with the GiantBomb API requirements for paginating results.

Next, let's implement the middleware that will be used to call the service and dispatch SUCCESS or FAILURE actions.

1$ touch src/app/common/games.effects.ts
bash

games.effects.ts

1import "rxjs/add/operator/map";
2import "rxjs/add/operator/catch";
3import "rxjs/add/operator/switchMap";
4import { Observable } from "rxjs/Observable";
5import { Injectable } from "@angular/core";
6import * as games from "./games.actions";
7import { Actions, Effect } from "@ngrx/effects";
8import { GamesService } from "./games.service";
9import { LoadGamesSuccessAction } from "./games.actions";
10import { LoadGamesFailedAction } from "./games.actions";
11
12@Injectable()
13export class GameEffects {
14  constructor(private _actions: Actions, private _service: GamesService) {}
15
16  @Effect()
17  loadGames$ = this._actions
18    .ofType(games.GameActionTypes.LOAD)
19    .switchMap(() =>
20      this._service.query().map(games => {
21        return new LoadGamesSuccessAction(games);
22      })
23    )
24    .catch(() => Observable.of(new LoadGamesFailedAction()));
25}
ts

Lastly, import the EffectsModule from ngrx/effects and run the effects and add GamesService as a provider:

app.module.ts

1import { EffectsModule } from "@ngrx/effects";
2import { GameEffects } from "./common/games/games.effects";
3import { GamesService } from "./common/games/games.service";
4//...
5@NgModule({
6  //...
7  imports: [
8    //...
9    EffectsModule.run(GameEffects)
10  ],
11  providers: [GamesService],
12  bootstrap: [AppComponent]
13})
14export class AppModule {}
ts

This implementation of pagination provides a great deal of convenience and efficiency - the application state is used to both to represent the state in the client and also give instructions to the server what results to fetch.

Usage

To demostrate the requirements for making a reusable and paginatable list component and to see the pagination in action, we will implement a games-list component.

As mentioned earlier, four "slices" of a state need to be present for pagination to be possible:

  1. Collection of entities
  2. Total number of entities
  3. Current page
  4. Loading/Loaded status

Let's create the template of the games-list component first:

1$ ng g component games-list
bash

games-list.component.ts

1import { Component, OnInit, Input, EventEmitter, Output } from "@angular/core";
2
3@Component({
4  selector: "games-list",
5  templateUrl: "games-list.component.html"
6})
7export class GamesListComponent {
8  /*
9  The minimim required inputs of a list component using redux
10  */
11  @Input() games: any;
12  @Input() count: number;
13  @Input() page: number;
14  @Input() loading: boolean;
15  /*
16   Emit and event when the user clicks on another page
17  */
18  @Output() onPageChanged = new EventEmitter<number>();
19
20  constructor() {}
21}
ts

games-list.component.html

1<div class="container" *ngIf="games">
2  <table class="table table-hover">
3    <thead class="thead-inverse" >
4    <tr>
5      <th>Name</th>
6    </tr>
7    </thead>
8    <tbody>
9    <tr *ngFor="let game of games" >
10      <td>{{game?.name}}</td>
11    </tr>
12    </tbody>
13  </table>
14  <ngb-pagination [collectionSize]="count" [(page)]="page" (pageChange)="onPageChanged.emit($event)" [maxSize]="10" [disabled]="loading"></ngb-pagination>
15</div>
html

Declare the component in the application module:

app.module.ts

1import {GamesListComponent} from "./components/games-list.component";
2//..
3@NgModule({
4  //...
5  declarations: [
6    //...
7    GamesListComponent,
8  ],
9  //...
10})
ts

GamesListComponent uses the ngbPagination component which comes in the ng-bootstrap library. The component gets the @Inputs to render a pagination and the pageChange event triggers the onPageChanged @output to emit to the container component.

Next, let's modify the container component (AppComponent in this case).

To have a working pagination, the container component needs to:

  1. Provide the parts of the state needed as inputs for the GamesListComponent
  2. Have a method for handling the onPageChanged output.

app.component.ts

1import * as games from "./common/games/games.actions";
2//...
3
4@Component({
5  selector: "app-root",
6  templateUrl: "./app.component.html",
7  styleUrls: ["./app.component.css"]
8})
9export class AppComponent implements OnInit {
10  //...
11  public games$: Observable<any>;
12  public gamesCount$: Observable<number>;
13  public gamesPage$: Observable<number>;
14  public gamesLoading$: Observable<boolean>;
15
16  constructor(private store: Store<fromRoot.AppState>) {
17    /*
18    Select all the parts of the state needed for the GamesListComponent
19    */
20    this.games$ = store.select(fromRoot.getGamesEntities);
21    this.gamesCount$ = store.select(fromRoot.getGamesCount);
22    this.gamesPage$ = store.select(fromRoot.getGamesPage);
23    this.gamesLoading$ = store.select(fromRoot.getGamesLoadingState);
24  }
25  /*
26   When the component initializes, render the first page ofresults
27  */
28  ngOnInit() {
29    this.store.dispatch(new games.LoadGamesAction(1));
30  }
31
32  //...
33  onGamesPageChanged(page: number) {
34    this.store.dispatch(new games.LoadGamesAction(page));
35  }
36}
ts

Lastly, add GamesListComponent's selector to AppComponent's template: app.component.html

1<div id="main-content">
2  <!-- ... -->
3  <games-list [games]="games$ | async" [count]="gamesCount$ | async" [page]="gamesPage$ | async" [loading]="gamesLoading$ | async" (onPageChanged)="onGamesPageChanged($event)"></games-list>
4</div>
html

The async pipe uses the latest value of the observables, watches for state changes, and passes them as inputs.

Here is how pagination works in action:

pagination

Conclusion

These examples represent many of the use cases that you might encounter when building an Angular 2 application using Redux. In a larger sense, they provide a boilerplate for more specific use cases and hopefully give new ideas for implementing other use cases.

Does Redux do a good job in controlling the UI layout? In my opinion, it absolutely does. It may require a little bit more code to be written at times, but the benefits truly start to shine as the application's codebase grows and logic gets reused.