Angular ngrx ngrx/store Redux
▌Introduction
▋Source code
▋Related articles
[Angular] Redux with ngrx/store –
Shopping cart sample (2)
[Angular] Redux with ngrx/effects – Shopping cart sample (3)
[Angular] Redux with ngrx/effects – Shopping cart sample (3)
▌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() {
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
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)" />
<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.
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
▌Reference
沒有留言:
張貼留言