2017年3月30日 星期四

[Angular] Redux with ngrx/effects – Shopping cart sample (3)

 Angular     ngrx      ngrx/effects     Redux  




Introduction

What is ngrx/effects?

ngrx/effects is a side effect model for ngrx/store.
ngrx/store is used for the state management with reducer, which is supposed to be pure function. So how can we do async or logical calls with side effect in ngrx?

Yes, ngrx/effects will handle the calls with side effect for us.


Why ngrx/effects?

There are several benefits of it.

1.  Decoupling between state management and component.
2.  Keep ngrx/store reducer pure and clean.
3.  Make state changing easy with State Pattern.


In the following sample, we will keep writing the Shopping cart application. So far we can put products into the shopping cart, and we are going to create an order and save it to firebase.



Source code



Related articles








Environment

Angular 5.2.0
@ngrx/store  5.2.0
@ngrx/effects  5.2.0



Implement


Current output





Our goal is to implement the “Send Order” event.




Create Reducer (with ngrx/store)

IOrder.ts

import { ShopItem } from '../class/ShopItem';

export interface IOrder {
    id: string;
    customer: string;
    status: string;
    date: string;
    items: ShopItem[];
}



Order.ts

import { IOrder } from '../interface/IOrder';
import { ShopItem } from '../class/ShopItem';

export class Order implements IOrder{
    id: string;
    customer: string;
    status: string;
    date: string;
    items: ShopItem[];

    constructor() {
        this.date = new Date().toLocaleDateString();
        this.items = [];
    }
}


OrderAction.ts

import { Action } from "@ngrx/store";
import { Order } from "./Order";

export class OrderAction implements Action {

    constructor(
        public type: string,
        public payload: Order) {
    }
}



order.action.ts

An order has the status such as “save”, “saved”, “cancel” …
Our goal in this sample is letting the user save their order with items in the shopping cart. That means we will only use “save” and “saved” for example.
export const SAVE = 'SAVE';
export const SAVED = 'SAVED';
export const CANCEL = 'CANCEL';
export const CANCELLED = 'CANCELLED';
export const COMPLETE = 'COMPLETE';

export function orderReducer(state: Order = new Order(), action: OrderAction){
    switch (action.type) {
        case SAVE:
            state = action.payload;
            console.log("Order's state : " + state.status);
            return state;

        case SAVED:
            state = action.payload;
            console.log("Order's state : " + state.status);
            return state;

        case CANCEL:
            return state;

        case CANCELLED:
            return state;

        case COMPLETE:
            state = action.payload;
            return state;

        default:
            return state;
    }
}



Don’t forget to add the new reducer into StoreModule.

app.module.ts

let reducers: IStore = {
    shopcart: shopcartReducer,
    order: orderReducer
}

@NgModule({
  imports: [
    StoreModule.forRoot(reducers),
  ],
})
export class AppModule { }



Component

shopcart.component.html

<button class='btn btn-success'  (click)="sendOrder()">


shopcart.component.ts

export class ShopcartComponent implements OnInit {

    private shopcart$: Observable<IShopCart>;
    private order$: Observable<IOrder>;
    private states: string[] = [];
   
    constructor(
        private store: Store<IStore>,
   ) {
        //Get the reducer
        this.shopcart$ = this.store.select<IShopCart>(x => x.shopcart);
        this.order$ = this.store.select<IOrder>(x => x.order);
    }
    private sendOrder() {

        this.shopcart$.subscribe(data => {
            let date = new Date();
            let orderItem: Order = {
                id: AppUtility.generateUUID(),
                customer: this.loginUser.email,
                status: SAVE,
                date: date.toLocaleDateString().concat(' ', date.toLocaleTimeString()),
                items: data.items
            };

            this.store.dispatch({ type: SAVE, payload: orderItem });

            //Output states
            this.order$.subscribe(ord => {
                this.states.push(ord.status);
            });
        });
    }
}




As we click the “Send Order” button, we will get the following result now.








The state is “Saving” so far. We will use ngrx/effects to save the order and update the state to “Saved”.


Create async calls


First we have to create a service which includes async calls.


order.service.ts

@Injectable()
export class OrderService {
//Create new product
    public create(ord: Order) :Promise<void> {
        //Save an order
    }
}



Side effect by using ngrx/effects

order.effects.ts

import { Effect, Actions } from "@ngrx/effects";
import { SAVE, SAVED, CANCEL, CANCELLED, COMPLETE } from './order.action';
import { OrderService } from '../service/order.service';

@Injectable()
export class orderEffects {

    constructor(
        private action$: Actions,
        private orderService: OrderService
    ) { }
    @Effect() save$ = this.action$
        .ofType(SAVE)
        .switchMap((action) => {

            let oa = <OrderAction>action;
            oa.payload.id = AppUtility.generateUUID();
            oa.payload.status = SAVED;

            //Save the order to backend, database ...etc Or get something
            let create$ = Observable.fromPromise(this.orderService.create(payload));
            return create$.delay(1000).switchMap(() => {
                return Observable.of({ 'type': SAVED, 'payload': oa.payload });
            });

        });

    @Effect() saved$ = this.action$
        .ofType(SAVED).delay(1000)
        .switchMap((action) => {
            let oa = <OrderAction>action;
            oa.payload.status = COMPLETE;
            return Observable.of({ 'type': COMPLETE, 'payload': oa.payload });
        });
}


    


Here some key points,

n   @Effect() save$ = this.action$.ofType(SAVE).switchMap(…)

This effect will be triggered and translate the dispatched event with type: SAVE, into an observable.

PS. If you are not familiar with
rxjs: switchMap, have a look at this article.

n   What should be returned in effects?

The next state (as Observable)!
DO NOT return the current state, or the returned observable will trigger the same effect again and result in infinite loop!

When we return the next state, it will trigger the mapping effect.



At last, import it to Effects Module.

app.module.ts

import { orderEffects } from './ngrx/order.effects';
import { EffectsModule } from '@ngrx/effects';
@NgModule({
  imports: [
    EffectsModule.forRoot([orderEffects]),
  ],
})
export class AppModule { }



Let’s see the power of ngrx/effects.
Remember that we logged the latest state in reducer?


 

And this is the result after sending an order:




We called the service in Effects: save$, to create an order into the database, here is the data saved to RTDB in Firebase.








Demo




Sample codes


Reference