Wednesday, July 11, 2018

Angular Ngrx Store Tutorial - Learn Angular State Management

Copyright from: https://coursetro.com/posts/code/

Starting the Project

I'm going to use the Angular CLI to start a new Angular 5 project:
> ng new ngrx-tutorial
> cd ngrx-tutorial
Once inside, we'll need to install Ngrx Store:
> yarn add @ngrx/store
Then, if you're using Visual Studio Code as an editor, run code . followed by:
> ng serve -o
This will open a new browser window at http://localhost:4200.

Creating the Model

For our fictional little project, we're going to allow users to submit both a name and url of a tutorial using a simple form. 
We won't be persisting this data in a database, because that would extend the scope of this tutorial too much. We only want to focus on Ngrx/store.
Being that we want to store 2 pieces of data (in the flavor of an array of objects), we will first define a model.
Create the following folder and file: /src/app/models/tutorial.model.ts and place inside of it the following code:
export interface Tutorial {
    name: string;
    url: string;
}
Now that we've defined our model, we'll move onto creating an action.

Creating an Action

An action in Ngrx/store is two things:
  1. type in the form of a string. It describes what's happening.
  2. It contains an optional payload of data.
So, think of an action as your mailman who delivers you a message "Hey, add this package to your shelf." It's up to you to determine where and how you're going to add that package, which is called a reducer, (we'll get to that shortly).
Create the following folder and file: /src/app/actions/tutorial.actions.ts with the following contents:
// Section 1
import { Injectable } from '@angular/core'
import { Action } from '@ngrx/store'
import { Tutorial } from './../models/tutorial.model'

// Section 2
export const ADD_TUTORIAL       = '[TUTORIAL] Add'
export const REMOVE_TUTORIAL    = '[TUTORIAL] Remove'

// Section 3
export class AddTutorial implements Action {
    readonly type = ADD_TUTORIAL

    constructor(public payload: Tutorial) {}
}

export class RemoveTutorial implements Action {
    readonly type = REMOVE_TUTORIAL

    constructor(public payload: number) {}
}

// Section 4
export type Actions = AddTutorial | RemoveTutorial
I'm going to dissect the above 4 sections so that you can understand what's happening:
  • Seciton 1
    Here, we're simply importing our Tutorial model and Action from ngrx/store. This makse sense, being that we're working with actions.
  • Section 2
    We're defining the type of action, which is in the form of a string constant.
  • Section 3
    We're creating a class for each action with a constructor that allows us to pass in the payload. This isn't a required step, but it does provide you with strong typing.
  • Section 4
    We're exporting all of our action classes for use within our upcoming reducer.

Creating a Reducer

Now that we have a model and our actions, we need to create a reducer. A reducer is what takes the incoming action and decides what to do with it. It takes the previous state and returns a new state based on the given action. 
Create the following folder and file: /src/app/reducers/tutorial.reducer.ts with the following contents:
import { Action } from '@ngrx/store'
import { Tutorial } from './../models/tutorial.model'
import * as TutorialActions from './../actions/tutorial.actions'

// Section 1
const initialState: Tutorial = {
    name: 'Initial Tutorial',
    url: 'http://google.com'
}

// Section 2
export function reducer(state: Tutorial[] = [initialState], action: TutorialActions.Actions) {

    // Section 3
    switch(action.type) {
        case TutorialActions.ADD_TUTORIAL:
            return [...state, action.payload];
        default:
            return state;
    }
}
Here's what's happening:
  • Section 1
    Here, we're defining an initial or default state. This isn't required if you don't want to define a state right out of the box.
  • Section 2
    This is our actual reducer. It takes in a state, which we're defining as a Tutorial type and we've optionally bound it to initialState. It also takes in the action from our /actions/tutorial.actions file.
  • Section 3
    First, we use a switch to determine the type of action. In the case of adding a tutorial, we return the new state with the help of our newState() function. We're simply passing in the previous state in the first parameter, and then our action in the second.

    In the event that the action.type does not match any cases, it will simply return the state, as provided in the first parameter of our reducer.
 Whew!

Creating an App State

Don't worry, we're almost done with the setup work here. One last thing we need to do is to define an app state.
Create the following file: /src/app/app.state.ts:
import { Tutorial } from './models/tutorial.model';

export interface AppState {
  readonly tutorial: Tutorial[];
}
We will import this file within the components that we wish to access ngrx.

Updates to App.Module

We need to import @ngrx/store and our reducer. 
Open up /src/app/app.module.ts and update the following:
// Other imports removed for brevity

import { StoreModule } from '@ngrx/store';
import { reducer } from './reducers/tutorial.reducer';

@NgModule({
  // Other code removed for brevity
  imports: [
    BrowserModule,
    StoreModule.forRoot({
      tutorial: reducer
    })
  ],
  // Other code removed for brevity

Generating Components

Let's go ahead and generate two components for handling reading from ngrx/store, and writing to it. 
In your console within the project, type:
> ng g c read
> ng g c create

Reading from Ngrx Store

Open up /src/app/read/read.component.ts and import the following:
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import { Tutorial } from './../models/tutorial.model';
import { AppState } from './../app.state';
Next, in the class:
export class ReadComponent implements OnInit {

  // Section 1
  tutorials: Observable<Tutorial[]>;

  // Section 2
  constructor(private store: Store<AppState>) { 
    this.tutorials = store.select('tutorial');
  }

  ngOnInit() {}

}
  • Section 1 
    We're defining an observable named tutorials which we will later display in the template.
  • Section 2 
    We're accessing the store from ngrx within the constructor, and then selecting tutorial which is defined as a the property from app.module.ts in StoreModule.forRoot({}). This calls the tutorial reducer and returns the tutorial state.
Next, visit /src/app/read/read.component.html and paste the following HTML:
<div class="right" *ngIf="tutorials">

  <h3>Tutorials</h3>
  <ul>
    <li *ngFor="let tutorial of tutorials | async">
      <a [href]="tutorial.url" target="_blank">{{ tutorial.name }}</a>
    </li>
  </ul>

</div>
Then, visit /src/app/app.component.html and paste:
<app-create></app-create>
<app-read></app-read>
And while we're dealing with non-ngrx stuff at the moment, visit /src/styles.css and paste the following CSS rulesets:
body, html {
    margin: 0;
    padding: 0;
    font-family: 'Arial';
}

.left, .right {
    float:left;
    width: calc(50% - 6em);
    padding: 3em;
}

input[type="text"] {
    width: 100%;
    padding: 5px;
    margin-bottom: 10px;
}
At this point, if you save the project and view the browser, you should see some ugliness like this:
Congrats! You've just read from ngrx/store!

Writing to Ngrx Store

Now that we know how to read from Ngrx, let's write to it and actually call our ADD_TUTORIAL action.
Visit /src/app/create/create.component.ts and import the following:
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from './../app.state';
import { Tutorial } from './../models/tutorial.model'
import * as TutorialActions from './../actions/tutorial.actions';
import { Observable } from 'rxjs/Observable';
In the class, specify:
export class CreateComponent implements OnInit {

  constructor(private store: Store<AppState>) {}

  addTutorial(name, url) {
    this.store.dispatch(new TutorialActions.AddTutorial({name: name, url: url}) )
  }

  ngOnInit() {
  }

}
The main area of focus is store.dispatch which takes in the object containing a name and url property.
Open /src/app/create/create.component.html and paste the following:
<div class="left">

  <input type="text" placeholder="name" #name>
  <input type="text" placeholder="url" #url>

  <button (click)="addTutorial(name.value,url.value)">Add a Tutorial</button>
</div>
Normally, we would use Reactive Forms for this, but that would extend beyond the scope of this tutorial.
Save the project and you will see the following:
Try adding a tutorial:
Awesome. As you can see, our observale on the right is updated with data we submitted to Ngrx store.
Let's try dispatching another action that will allow us to remove a tutorial.

Removing from Ngrx Store

To give us some more muscle memory, let's try removing tutorials on click.
Visit /src/app/read/read.component.html and modify the li line to:
<li (click)="delTutorial(i)" *ngFor="let tutorial of tutorials | async; let i = index">
This will pass the index of the currently clicked list item to a function called delTutorial().
Open read.component.ts and make the following adjustments:
// Import our actions at the top
import * as TutorialActions from './../actions/tutorial.actions';

// In the class, add:
  delTutorial(index) {
    this.store.dispatch(new TutorialActions.RemoveTutorial(index) )
  }
This will call our RemoveTutorial action and pass in the index.
Now, let's visit /src/app/reducers/tutorial.reducer.ts and add another case:
export function reducer(state: Tutorial[] = [initialState], action: TutorialActions.Actions) {
    switch(action.type) {
        case TutorialActions.ADD_TUTORIAL:
            return [...state, action.payload];
        
        // Add this case:
        case TutorialActions.REMOVE_TUTORIAL:
            state.splice(action.payload, 1)
            return state;
            
        default:
            return state;
    }
}
We're simply using .splice to modify the state and then we return it.
Now, try it out in the browser!  Try clicking on (or to the right) of one of the list items, and it will be removed.

Conclusion

That's Ngrx Store in a nutshell. Of course, there's a lot more to it, but you should have a basic understanding. Shortly, I will create a new tutorial that will take a look at using Ngrx Store for making actual API calls.

No comments: