2017年1月10日 星期二

[Angular] Redux with ngrx/store – Shopping cart sample (1)

 Angular     ngrx      ngrx/store     Redux  




Introduction




Environment


Angular 5.2.0
@ngrx/store  5.2.0


Implement


Before we implement the shopping cart, the application currently is looked like this,







Install packages



Booking component

Let’s create a component for booking a single product like this,



ShopItem.ts

export class ShopItem {
    count: number; //Counter
    price: number; //Cash summary
    public constructor(
        fields?: {
            count?: number,
            price?: number
        }) {
        if (fields) {
            Object.assign(this, fields);
        } else {
            this.count = 0;
            this.price = 0;
        }
    }
}



product-booking.component.ts

@Component({
    selector: 'product-booking',
    template: `
               <table>
                 <tr>
                   <td (click)="decrement()"><label style="min-width:25px"><i class="fa fa-minus"></i></label></td>
                   <td><input style="max-width:50px" readonly="readonly" type="text" value="{{shopItem.count}}"/></td>
                   <td (click)="increment()"><label style="min-width:25px"><i class="fa fa-plus"></i></label></td>
                 </tr>
               </table>
              `
})

export class ProdBookingComponent implements OnInit, OnChanges {

    @Input('product') product: Product;
    @Input('default-number') defaultNumber: number;

    private counter: number = 0;
    private shopItem: ShopItem = null;
    private shopcart$: Observable<IShopCart>;

    constructor(
        private router: Router
    ) {
        //Create ShopItem
        this.shopItem = new ShopItem();
    }
    public ngOnChanges() {
        this.shopItem.id = this.product.id;
        this.shopItem.title = this.product.title;
        this.shopItem.count = this.defaultNumber ? this.defaultNumber : 0;
        this.shopItem.price = this.product.price;
    }

    private increment() {
        this.shopItem.count += 1;
    }

    private decrement() {
        this.shopItem.count -= 1;
    }

    private reset() {
        this.shopItem.count = 0;
    }
}

Then put the ProductBookingComponent into all three product components’ HTML:
1.Books
2.Toys
3.Music

Take ProductBooksComponent for example,


product-books.component.html

<div>
    <table class="table table-bordered table-hover">
        <thead>
            <tr>
                <th  class="text-center">ID</th>
                <th  class="text-center">Title</th>
                <th class="text-center">Price</th>
                <th>Shopping Cart</th>
                <th></th>
            </tr>
        </thead>
        <tr *ngFor="let prod of books">
            <td>{{prod.id}}</td>
            <td>{{prod.title}}</td>
            <td>{{prod.price}}</td>
            <td><product-booking [product]="prod" [default-number]="itemNumbers[prod.id]" (emit-events)="setShopCart($event)"></product-booking></td>
            <td>
                <input type="button" value="Edit" class="btn btn-info" (click)="edit(prod.id)" />
                &nbsp;
                <input type="button" value="Remove" class="btn btn-danger" (click)="remove(prod.id)" />
            </td>
        </tr>
    </table>
</div>






Result:




Shopping Cart : State

We would like to have a shopping cart, which can stores how many items we orders and how much totally cost.

Yes! We can use Redux pattern here to stores the shopping cart state.
Furthermore,  while the state (shopping cart) changes by the ProductBookingComponent, emit the current state (shopping cart) to parent components.


First, create a State interface/class.

IShopCart.ts

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

export interface IShopCart {
    cnt: number; //Counter
    sum: number; //Cash summary
}


ShopCart.ts

import { IShopCart } from '../interface/IShopCart';

export class ShopCart implements IShopCart {
    cnt: number;
    sum: number;

    constructor() {
        this.items = [];
        this.cnt = 0;
        this.sum = 0;
    }
}



Shopping Cart : Reducer

Notice that the interface Action removes payload property in latest ngrx.
See migration guide here and issue #513.

You can use Module Augmentation (See Declaration Merging) to patch the existing declaration like following.

declare module '@ngrx/store' {
  interface Action {
    type: string;
    payload?: any;
  }
}


However, we would like to declare a more strong-typing ”payload” property. So let’s create a new class and implements interface “Action” like following.

ShopcartAction.ts


export class ShopcartAction implements Action {
    constructor(
        public type: string,
        public payload: ShopItem) {
    }
}



shopcart.action.ts

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

export const PUSH = 'PUSH';
export const PULL = 'PULL';
export const CLEAR = 'CLEAR';

export function shopcartReducer(state: ShopCart = new ShopCart(), action: ShopcartAction) {
    switch (action.type) {
        case PUSH:
            return pushToCart(state, action.payload);

        case PULL:
            return pullFromCart(state, action.payload);

        case CLEAR:
            state.cnt = 0;
            state.sum = 0;
            return state;

        default:
            return state;
    }
}

function pushToCart(shopcart: ShopCart, payload: ShopItem) {
    shopcart.cnt += 1;
    shopcart.sum += payload.price * 1;
    return shopcart;
}

function pullFromCart(shopcart: ShopCart, payload: ShopItem) {
    shopcart.cnt -= 1;
    shopcart.sum -= payload.price * 1;
    return shopcart;
}


Notice that,
1.  The state is a ShopCart object, that means State == Shopping cart.
2.  The reducer needs to know who/what is updating the state thru the information in action payloads.


Import the reducer to injector

Use StoreModule.forRoot(…)

imports: [
  StoreModule.forRoot({shopcart: shopcartReducer}),
]


or if you have multiple reducers, import them like this,

app.module.ts

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

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



Furthermore, we created an interface, IStore
for strong-typing usage later.

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

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



Update ProductBookingComponent

product-booking.component.ts

export class ProdBookingComponent implements OnInit, OnChanges {

    @Input('product') product: Product;
    @Input('default-number') defaultNumber: number;
    @Output('emit-events') emitEvents = new EventEmitter<ShopCart>(true); //Must set the EventEmitter to async
   
private shopItem: ShopItem = null;
    private shopcart$: Observable<IShopCart>;

    constructor(
        private router: Router,
        private productService: ProductService,
        private store: Store<IStore>
    ) {
        this.productService = productService;

        //Create ShopItem
        this.shopItem = new ShopItem();
        //Get the reducer
        this.shopcart$ = store.select<IShopCart>(x=>x.shopcart);       
    }
    public ngOnChanges() {
        this.shopItem.id = this.product.id;
        this.shopItem.title = this.product.title;
        this.shopItem.count = this.defaultNumber ? this.defaultNumber : 0;
        this.shopItem.price = this.product.price;
    }

    private increment() {
        this.shopItem.count += 1;
        let action = new ShopcartAction(PUSH, this.shopItem);
        this.store.dispatch(action);
    }

    private decrement() {
        this.shopItem.count -= 1;
        this.store.dispatch({ type: PULL, payload: this.shopItem })
    }

    private reset() {
        this.shopItem.count = 0;
        this.store.dispatch({ type: CLEAR });
    }
}




Demo





Whaz next?

You may find that although we store the total counts and prices in the state (shopping cart), we does not have the information of individual product in the state.

We will fix it and complete the sample in the next day J


Sample codes






Reference






沒有留言:

張貼留言