https://www.pexels.com/photo/arrows-close-up-dark-energy-394377/

มาลองทำรถเข็นใส่สินค้า (Part I)

Arkhom Khamthip
CheHipster
Published in
6 min readSep 7, 2017

--

หลังจากที่เรามีสินค้าแสดงในหน้าเวปกันมาบ้างแล้ว และในโพสที่แล้ว เราทำหน้า “สินค้าที่สั่งซื้อ” แบบว่างๆ ไว้ โพสต่อไปเราจะลองเอาสินค้าใส่เข้าไปในรถเข็นกัน โดยที่มันสามารถเพิ่มลดจำนวนสินค้าได้ด้วย.

ออกตัวไว้ก่อนว่า อันนี้ลองคิดเองทำเอง ผสมผสานความรู้จากเวปโน้นเวปนี้มา ซึ่งระบบจริงๆ คงจะต้องทำได้ดีกว่านี้

  • order.component.ts

เราจะสร้าง OrderComponent เอาไว้สำหรับสั่งสินค้า โดยที่จะถูกเรียกมาจากการ คลิ๊กปุ่ม “ใส่รถเข็น” จากหน้าแสดงสินค้า ดังนั้นเราจะต้องเพิ่ม route ขึ้นมาอีก 1 route. แต่เราจะสร้าง order.component.ts แบบง่ายๆ แบบนี้ไปก่อน

import { Component} from '@angular/core';
import { Item } from './item.model';
@Component({
selector: 'order-item'
templateUrl: './src/order.component.html',
})
export class OrderComponent {
item: Item;
}

เราจะเพิ่ม route เพื่อให้มาเปิด OrderComponent นี้ แต่คราวนี้เราจะส่ง id เข้าไปเป็น parameter ใน URL ด้วย เพื่อให้ OrderComponent แสดงผลเฉพาะสินค้าที่เราคลิ๊กที่ปุ่ม “ใส่รถเข็น”. อ่านเพิ่มเติมสำหรับ routing ได้อีกเยอะจากที่นี่

import { NgModule }             from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ItemComponent } from './item.component';
import { CartComponent } from './cart.component';
import { OrderComponent } from './order.component';
const routes: Routes = [
{ path: '', redirectTo '/item', pathMatch: 'full' },
{ path: 'item', component: ItemComponent },
{ path: 'cart', component: CartComponent },
{ path: 'order/:id', component: OrderComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes, {useHash: true}) ],
exports: [ RouterModule ]
})
export class AppRouteModule {}
  • Refactor ItemComponent

ก่อนหน้านี้ ItemComponent แสดงสินค้า โดยที่เราสร้างตัวสินค้ามาจากในตัว ItemComponent เอง ซึ่งในระบบการทำงานจริงเราจะได้สินค้ามาจาก back end server. ปกติแล้วเราจะสร้าง service สำหรับ item เพื่อทำหน้าที่ไปเอาข้อมูล มาให้ ItemComponenent แสดงผล. ดังนั้นเราจะสร้าง item.service.ts เพื่อทำหน้าที่นี้ แต่ยังไม่ต้องไปเอาข้อมูลจาก back end server.

  • item.service.ts

ItemService นี้ทำหน้าที่สำหรับไปเอาข้อมูลสินค้า ซึ่งสามารถถูกเรียกใช้จาก component ใดๆ ก็ได้ในระบบ การที่จะทำให้ component ในระบบใช้งาน ItemService นี้ได้เราจะต้องใช้ decorator @Injectable สำหรับการเรียกใช้งานกับคลาสที่ใช้ @Injectableนั้นเราไม่จำเป็นต้อง new() เพื่อสร้าง instance แค่เราเอาไปประกาศใน provider ใน app.module เราก็สามารถ inject service นั้นเข้าไปใช้ใน component ใดๆ ก็ได้เลย.

import { Component, Injectable }  from '@angular/core';
import { Item } from './item.model';
@Injectable()
export class ItemService {
private items: Array<Item>;

constructor() {
this.items = [
new Item(1,'ไม้จิ้มฟัน',20),
new Item(3,'สากกระเบือ',300),
new Item(2,'เรือรบ', 150) ,
new Item(4,'Kinder egg', 45)
];
}

getItems(): Array<Item> {
return this.items;
}

getItem(id: number): Item {
return this.items.find(item => item.id === id);
}
}

ใน ItemService จะมี

  • getItems() ซึ่งจะทำหน้าที่ return สินค้าทั้งหมด.
  • getItem(id: number) จะทำหน้าที่ return สินค้าตาม id ที่ส่งมา.
  • goBack() ก็เพียงกลับไปเพจก่อนหน้า.

เราจะลอง inject ItemService เข้าไปใน ItemComponent ผ่านทาง constructor แล้วเอามาใช้งานในแบบนี้

import { Component, OnInit } from '@angular/core';
import { Item } from './item.model';
import { ItemService } from './item.service';
@Component({
selector: 'my-item',
templateUrl: './src/item.component.html',
})
export class ItemComponent implements OnInit {
items: Array<Item>;
constructor(private itemService: ItemService) {}

ngOnInit(): void {
this.loadItems();
}

private loadItems(): void {
this.items = this.itemService.getItems();
}
}

เรายังได้เพิ่ม ngOnInit ซึ่งเป็น Component Lifecycle Hooks ของ Angular สำหรับทำงานในตอนเริ่มต้น อ่านเพิ่มได้ที่นี่ เพื่อไปเอาสินค้าทั้งหมดจาก ItemService ด้วย itemService.getItems(). ซึ่งทดลองรันโปรแกรมดู เราจะได้ผลลัพธ์เหมือนเดิม.

  • OrderComponent

ตอนนี้เราจะ inject ItemService เข้าไปใน OrderComponent บ้างเพื่อเตรียมการใส่เข้าไปในรถเข็น. ก่อนอื่นเราจะต้องทำการส่ง id ของสินค้าจากหน้าแสดงสินค้า ซึ่งต้องเพิ่มโค๊ดใน item.component.html แบบนี้

<div class="card card-block" *ngFor="let item of items">
<h5 class="card-title">สินค้า: {{item.description}}</h5>
<p class="card-text">ราคา: {{item.price}} บาท</p>

<button type="button"
class="btn"
[routerLink] = "['/order', item.id]">ใส่รถเข็น
<i class="fa fa-cart-plus fa-sm" aria-hidden="true"></i>
</button>
</div>

เราใช้ [routeLink] อีกครั้งและครั้งนี้เพิ่ม item.id เข้าไปใน URL ด้วย เพื่อให้ item.id เป็น parameter ให้กับ OrderComponent ในการเอาของไปไว้ในรถเข็น

  • OrderComponent

เราใช้ ActivatedRoute สำหรับแสดง router ตัวที่กำลังทำงานอยู่ ส่วน ParamMap เราใช้เพื่อสำหรับอ่านค่า parameter จาก URL. สำหรับ subscibe() จะขอโพสในครั้งหน้าในเรื่องของ RxJs

สำหรับเครื่องหมาย => หมายถึงการทำ function ส่วนตัวที่อยู่ในวงเล็บ จะให้เป็น parameter ในการส่งเข้าไปใน function และสุดท้ายคือการ exeute function. เรายังได้เพิ่มเครื่องหมายบวก “+” เพื่อการ convert จาก string ไปเป็น number.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Location } from '@angular/common';
import { Item } from './item.model';
@Component({
selector: 'order-item'
templateUrl: './src/order.component.html',
})
export class OrderComponent implements OnInit {
item: Item;
constructor(
private itemService: ItemService,
private route: ActivatedRoute,
private location: Location

) {}

ngOnInit(): void {
this.route.paramMap.subscribe((p) => {
this.loadItem(+p.params.id);
});

}

private loadItem(id: number): void {
this.item = this.itemService.getItem(id);
}

goBack(): void {
this.location.back();
}
}
  • order.component.html

สำหรับไฟล์นี้ นอกจากการแสดงรายละเอียดสินค้า เราจะเพิ่มหน้าที่สำหรับเพิ่ม/ลดจำนวนสินค้าในรถเข็น ซึ่งตอนนี้เราใส่ปุ่ม +/- เพื่อเรียก method addToCart ()/removeFromCart()

<div class="card card-block">
<h5 class="card-title">สั่งซื้อสินค้า: {{item.description}}</h5>
<p class="card-text">ราคา: {{item.price}} บาท</p>
<table>
<tr>
<td>
<button type="button" class="btn" >
<i class="fa fa-plus" aria-hidden="true"></i>
</button>
</td>
<td>
<button type="button" class="btn">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</td>
<td></td>
<td></td>
</tr>
</table>
</div>
<button type="button" class="btn" (click)="goBack()">
<i class="fa fa-arrow-left" aria-hidden="true">Back</i>
</button>
  • Cart domain model

เริ่มด้วยการออกแบบว่าเราจะมี domain model ของรถเข็นจะเป็นอย่างไร ซึ่งหากจะคิดแบบง่ายๆ ก็คงจะได้แบบนี้คือ เก็บ ID ซึ่งเป็น key ของ item, item, quantity, และ total.

//cart.model.ts
export class Cart{
constructor(
public id: number,
public item?: Item,
public quantity?: number,
public total?: number
) {}
}
  • CartService

เราจะใช้ Map มาช่วยในการสั่งซื้อสินค้า โดยให้ item.id เป็น key ของ map และ addToCart(item: Item)/removeFromCart(item: Item)จะทำหน้าที่สำหรับการใส่ของเข้า/ออกในรถเข็น. เรายังได้ใช้ console.log เพื่อพ่น log ตามที่เราต้องการดูการทำงานของโปรแกรมเราที่ console ของ browser. กดปุ่ม F12 ที่ Chrome เพื่อเปิดหน้า console. getCarts() จะ return สินค้าทั้งหมดในรถเข็นออกไปในรูปแบบของ array.

import { Injectable, Component } from '@angular/core';
import { Cart } from './cart.model';
import { Item } from './item.model';
@Injectable()
export class CartService {
private cache: Map<number, Cart> = new Map<string, Cart>();

addToCart(item: Item): void {
if (this.cache.has(item.id)) {
console.log(`%cUp quantity and total for itemId: ${item.id}`,
'color: green');
const cart = this.cache.get(item.id);
cart.quantity += 1;
cart.total = cart.quantity * cart.item.price;
this.cache.set(item.id, cart);
} else {
console.log(`%cCache set for item id: ${item.id}`, 'color: blue');
const cart = new Cart();
cart.item = item;
cart.quantity = 1;
cart.total = cart.quantity * cart.item.price;
this.cache.set(item.id, cart);
}
}

removeFromCart(item: Item): void {
if (this.cache.has(item.id)) {
const cart = this.cache.get(item.id);
cart.quantity -= 1;
if (cart.quantity > 0) {
console.log(`%cDown quantity and total for itemId: ${item.id}`,
'color: purple');
cart.total = cart.quantity * cart.item.price;
this.cache.set(item.id, cart);

} else {
console.log(`%cItem removed for itemId: ${item.id}`,'color: red');
this.cache.delete(item.id);
}
} else {
console.log(`%cNo item found in cart: ${item.id}`,'color: blue');
}
}
getCarts(): Array<Cart> {
const arr = Array.from(this.cache, ([key, cart]) => {
return {cart};
});
return arr;
}
}
  • OrderComponent

ทำหน้าที่เชื่อมการสั่งซื้อสินค้าระหว่างผู้ใช้กับ CartService โดยเราจะเพิ่ม method addToCart()/removeFromCart() แบบนี้

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Location } from '@angular/common';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import { Item } from './item.model';
import { CartService } from './cart.service'
import { ItemService } from './item.service';
@Component({
selector: 'order-item'
templateUrl: './src/order.component.html',
})
export class OrderComponent implements OnInit {
item: Item;
constructor(
private cartService: CartService,
private itemService: ItemService,
private route: ActivatedRoute,
private location: Location
) {}

ngOnInit(): void {
this.route.paramMap.subscribe((p) => {
this.loadItem(+p.params.id);
});
}

private loadItem(id: number): void {
this.item = this.itemService.getItem(id);
}

addToCart(): void {
this.cartService.addToCart(this.item);
}


removeFromCart(): void {
this.cartService.removeFromCart(this.item);
}

goBack(): void {
this.location.back();
}
}
  • order.component.html

เราได้เพิ่มปุ่ม +/- และทำการ binding ไปหา function addToCart()/removeFromCart() ใน OrderComponent

<div class="card card-block">
<h5 class="card-title">สั่งซื้อสินค้า: {{item.description}}</h5>
<p class="card-text">ราคา: {{item.price}} บาท</p>
<table>
<tr>
<td>
<button type="button" class="btn" (click)="addToCart(item)" >
<i class="fa fa-plus" aria-hidden="true"></i>
</button>
</td>
<td>
<button type="button" class="btn" (click)="removeFromCart(item)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</td>
<td></td>
<td></td>
</tr>
</table>
</div>
<button type="button" class="btn" (click)="goBack()">
<i class="fa fa-arrow-left" aria-hidden="true">Back</i>
</button>
  • CartComponent

เพิ่ม function ของการแสดงสินค้าทั้งหมดในรถเข็นด้วยการเรียกผ่าน CartService ใน method getCarts(). อย่าลืม inject CartSerivce เข้ามาด้วยนะคับ

import { Component, OnInit }  from '@angular/core';
import { Location } from '@angular/common';
import { CartService } from './cart.service';
@Component({
selector: 'my-cart',
templateUrl: './src/cart.component.html',
})
export class CartComponent implements OnInit{
carts: Array<Cart>;
constructor(
private location: Location,
private cartService: CartService) {}

ngOnInit(): void {
this.carts = this.cartService.getCarts();
}

goBack() : void {
this.location.back();
}
}
  • cart.cartcomponent.html

ทำหน้าที่แสดงสินค้าทั้งหมดในรถเข็น

<div class="card card-block">
<h4 class="card-title">รายการที่ซื้อ</h4>
<table width="100%">
<tr>
<td>รายการ</td>
<td>ราคา</td>
<td>จำนวน</td>
<td>รวม</td>
</tr>
<tr *ngFor="let cart of carts">
<td>{{cart.cart.item.description}}</td>
<td>{{cart.cart.item.price}}</td>
<td>{{cart.cart.quantity}}</td>
<td>{{cart.cart.total}}</td>
</tr>
</table>
</div>
<p></p>
<button type="button" class="btn" (click)="goBack()">
<i class="fa fa-arrow-left" aria-hidden="true">Back</i>
</button>

ทดลองรันโปรแกรม จะได้ผลแบบนี้

โค๊ดดูได้จาก https://plnkr.co/edit/zq7xMUz89dUMdzsrLpvL

โครงสร้างของโปรเจคจะออกมาในแบบนี้

ดพ

โพสนี้จะเห็นได้ว่า เรายังไม่ได้แสดงจำนวนสินค้าทั้งหมดที่ปุ่มรถเข็น และยังไม่ได้แสดงจำนวนสินค้าและราคารวมเวลากดปุ่ม +/- สำหรับโพสหน้า เราจะมาลองใช้ความสามารถของ RxJs ในการทำงานแบบ reactive programming ซึ่งเราจะทำการแสดง จำนวนสินค้าและราคารวมไปพร้อมๆ กัน

References:
https://codecraft.tv/courses/angular/quickstart/overview/ https://angular.io/

--

--