Angular WorkShop - 4
===
第一堂:https://hackmd.io/@lala-lee-md/angular-workshop-1
* Angular環境
* TypeScript&ES6
* Component
* Data Binding
第二堂:https://hackmd.io/@lala-lee-md/angular-workshop-2
* Component Interaction
* Directives
* Pipes、NgModule
第三堂:https://hackmd.io/@lala-lee-md/angular-workshop-3
* Service
* Routing
## RxJS 處理非同步
### observable (觀察者)
* 觀察者模式(Observer Pattern)
* 觀察者模式有二個角色:觀察者(Observer)、被觀察者(Observable)
* Observer -> 訂閱(Subscribe) -> Observable
* Observable 有事件發生 -> 通知(Notify) -> Observer
![](https://i.imgur.com/FZvUzgO.png)
ex. 粉絲專頁被粉絲訂閱後,當粉絲專頁有新的PO文,粉絲會收到通知得到最新消息
### stream (資料流)
* 每一個 Observable 就是一個資料流
* 什麼是資料流?可以想像成是會一直增加元素的陣列,當有新的事件發生就 push 進去。
* 專業說法:「時間序列上的一連串資料事件」
### subscribe (訂閱)
```typescript=
observable.subscribe(observer);
// [...].subscribe({...});
// [資料流].sbscribe({處理資料流的程式碼});
```
![](https://thumbs.gfycat.com/MelodicCalculatingBluejay-small.gif)
observer透過訂閱的動作收到消息,會有三種回應的方式
```typescript=
observable.subscribe(
(data) => {
// 成功的時候做什麼
},
(error) => {
// 失敗的時候做什麼
},
() => {
// 不論成功或失敗,都要做什麼
}
);
```
### Angular 使用 RxJS 來幫忙處理非同步事件
* Reactive programming (響應式程式設計) 是一種「面向資料流」和「變更傳播」的非同步處理的程式設計模式,RxJS 是實現成JavaScript的版本。
* Observable是資料流,觀察者收到資料之前,中間可以對資料流做一連串的轉換及處理,變成另一個資料流
* 中間轉換的過程,在RxJS的世界中是使用```operator```來完成。
>從動圖上來看資料流(生產線上的月餅半成品)透過不同的operator(操作,ex.壓扁/加料/包裝/塑型)來轉化成不同樣子的資料流,用以面向各式需求的處理。
![](https://thumbs.gfycat.com/MelodicCalculatingBluejay-small.gif)
### Angular 中常見的 Observable
* HTTP模組使用 Observable 來處理 AJAX 請求和響應。
```typescript=
this.httpClient.get(url).subscribe(
(data) => {...},
(error) => {...},
() => {...}
);
```
* 路由資訊 ActivatedRoute
```typescript=
this.activatedRoute.params.subscribe(
(params) => {
console.log('params', params);
}
);
```
* 響應式表單 (reactive forms)
後面會提到,先記得觀念就好
```typescript=
this.reactiveForm.valueChanges.subscribe(
(formValue) => {
console.log('form value changed');
console.log('formValue', formValue);
}
);
```
### operator 使用 pipe 來包裝訂閱前的一系列操作
```typescript=
this.xxxService.getItems()
.pipe(
toUseOperator1(...)
).subscribe(
(data) => { ....}
);
this.xxxService.getItems()
.pipe(
toUseOperator1(...),
toUseOperator2(...),
...,
toUseOperatorN(...)
).subscribe(
(data) => { ....}
);
```
### 常用的 Operator
https://ithelp.ithome.com.tw/articles/10209779
透過實作來熟悉幾個常用的operator
#### map
map() 轉換資料流格式
* 只顯示運費<10的資料 https://git.io/Jv0vO
```
map((results: any[]) => {
return results.filter(r => r.price < 10);
})
```
```typescript=
this.shippingService.getItems()
.pipe(
map((results: any[]) => {
return results.filter(r => r.price < 10);
})
).subscribe(
(data) => {
console.log(data);
this.shippingCosts = data;
}
);
```
* 增加清單的預設值 https://git.io/Jv0vZ
```
map(results => [{type: 'Please Select'}, ...results])
```
```typescript=
this.shippingService.getItems()
.pipe(
map((results: any[]) => results.filter(r => r.price < 10)),
map((results: any[]) => [{type: 'Please Select'}, ...results])
).subscribe(
(data) => {
console.log(data);
this.shippingCosts = data;
}
);
```
#### switchMap 切換成不同 observable
switchMap() 可以在收到 observable 資料時,轉換成另外一個 observable
* 取得路由資訊的id,再用id去跟service要資料 https://git.io/Jv0vD
```
// 多行表示式
switchMap(params => {
return this.productService.getItem(params.id);
}
// 單行表示式,結果等同於上面的程式碼
switchMap(params => this.productService.getItem(params.id))
```
```typescript=
this.route.params.pipe(
switchMap(params => {
return this.productService.getItem(params.id);
})
).subscribe(data => {
this.product = data;
});
```
#### filter 過濾出符合條件值的資料流
* 在根元件監聽 RouterEvent 的變化
```typescript=
constructor(
private router: Router
) {
this.router.events.subscribe(event => {
console.log(event);
});
}
```
* 利用 ```filter``` 只印出 NavigationEnd Event 的值 https://git.io/Jv0fY
```typescript=
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe(event => {
console.log(event);
});
```
#### forkJoin
所有 observable 都完成(complete)後,才會取得最終的結果
* 在商品清單元件,等商品及運費資訊全部完成後,顯示出來 https://git.io/JvEmG
```typescript=
forkJoin([
this.productService.getItems(),
this.shippingService.getItems()
]).subscribe(data => {
console.log('data', data);
});
```
![](https://i.imgur.com/twDVnRR.png)
* 利用 map 整理資料流 https://git.io/JvEmc
```typescript=
forkJoin([
this.productService.getItems(),
this.shippingService.getItems()
]).pipe(
map(([products, shipping]) => ({products, shipping}))
).subscribe(data => {
console.log('data', data);
});
```
![](https://i.imgur.com/j1HIe3i.png)
```typescript=
forkJoin([
this.productService.getItems(),
this.shippingService.getItems()
]).pipe(
map(([products, shipping]) => ({products, shipping}))
).subscribe(data => {
console.log('data', data);
this.products = data.products;
this.shipping = data.shipping;
});
```
```html
<div>
<p>Price: {{ product.price }}</p>
<ng-container *ngFor="let s of shipping">
<p>Plus Shipping - {{s.type}}: {{product.price + s.price}} </p>
</ng-container>
</div>
```
![](https://i.imgur.com/qDPvejU.png)
## Form - Angular 表單開發模型
Angular 提供了兩種不同的方法來透過表單處理使用者輸入
* Template-Driven Form 以範本為主的表單開發模型
* Model-Driven Form 以資料模型為主的表單開發模型
### 二種表單開發模型的差異
| Template-Driven | Model-Driven |
| -------- | -------- |
| 以 **範本** 為主 | 以 **模型** 為主 |
| 匯入 FormsModule | 匯入 ReactiveFormsModule |
| 在模版上用 **宣告** 的方式 建立表單 | 用 **程式碼** 建立表單 |
| 使用 ngModel 指令 (Directive) | 使用 formControlName 屬性 |
| 驗證規則寫在 **元件HTML模版** | 驗證規則寫在 **元件類別程式碼** |
| 只能對表單做 E2E 測試 | 可對表單做單元測試及E2E測試 |
### 共通基礎
雖然寫法不同,但構成要素的概念是一樣的。
* FormControl 表單控制項
用來追蹤「某一個」表單控制項(input、select)的欄位值與驗證狀態
* FormGroup
* 用來追蹤「某一個表單群組」內的表單控制項的欄位值與驗證狀態
* 所有控制項的值使用「Object」的方式記錄
* 欄位的索引值是字串,沒有順序性
* FormArray
* 用來追蹤「一群表單控制項」的欄位值與驗證狀態
* 所有控制項的值使用「Array」的方式記錄
* 欄位的索引值是數字,含有順序性
* AbstractControl
是FormControl、FormGroup、FormArray 的抽象類別,提供了共通的屬性及行為。
![](https://i.imgur.com/uzABwCr.png)
> 通常會用 <form> tag,做為最上層的 FormGroup
> 有使用 <form> tag 的話,表單輸入控制項要加上 name 的屬性
### Template-Driven
#### 匯入 FormsModule https://git.io/JvEml
```
import { FormsModule } from '@angular/forms';
```
> 因為有獨立出可重複使用的 Shared Module
> 要在 shared.module.ts 加入 imports 跟 exports
#### 透過 NgModel 建立一個 FormControl 的實體 https://git.io/JvEm4
```html
<div>
<input type="number" [(ngModel)]="quantity"/>
</div>
```
```typescript=
export class ProductDetailComponent implements OnInit {
quantity = 1;
...
}
```
![](https://i.imgur.com/g5Xyqyo.png)
#### 表單欄位名稱的必要性 https://git.io/JvEm9
```html
<form>
<input type="number" [(ngModel)]="quantity"/>
</form>
```
![](https://i.imgur.com/yc0FqKp.png)
* 放在 <form> 裡面的輸入欄位必須要有 name 屬性
```html
<form>
<input type="number" [(ngModel)]="quantity" name="quantity"/>
</form>
```
#### 使用範本參數變數取得表單控制項實體
* 範本參考變數:可以用在 Template 上的一種設定變數的方式。
* 語法: 在任意的HTML標籤裡面使用一個 # 字號加上一個變數名稱,ex.```#name```
* 把 DOM物件 匯出成範本參考變數 https://git.io/JvEGT
```html
<input type="text" #tInput (keyup)="printInput(tInput)">
```
```typescript=
printInput(tInput) {
console.log('tInput', tInput);
console.log('tInputValue', tInput.value);
}
```
* 把 ngModel 匯出成範本參考變數 https://git.io/JvEGk
```html
// bad syntax
<input type="number" name="quantity"
[(ngModel)]="quantity"
#quantity="ngModel">
```
![](https://i.imgur.com/iZhK4Xy.png)
> 不要與元件的property同名,編譯器會報錯
```html
// good syntax
<input type="number" name="quantity"
[(ngModel)]="quantity"
#mQuantity="ngModel">
```
#### 加上驗證 Validation
* Native HTML form validation
| 驗證屬性 | 支援的 Input Type | 描述 |
| -------- | -------- | -------- |
| required | 所有input, select, texarea | 必填 |
| pattern | text, url, tel, email, password | 要符合正則式指定的格式 |
| min | range, number,date | 輸入值需大於等於設定的值 |
| max | range, number,date | 輸入值需小於等於設定的值 |
| minlength | text, url, tel, email, password, textarea | 輸入字元數需大於等於設定的值 |
| maxlength | text, url, tel, email, password, textarea | 輸入字元數需小於等於設定的值 |
* 使用方式:在輸入欄位上套用「驗證屬性」https://git.io/JvEX9
![](https://i.imgur.com/zR330je.png)
```html
<input type="number" name="quantity"
[(ngModel)]="quantity"
#mQuantity="ngModel"
required />
<!-- 使用JSON pipe觀察NgModel物件的errors屬性 -->
<pre>{{mQuantity.errors | json}}</pre>
```
* 如果欄位驗證成功 - NgModel物件的errors屬性會回傳 null
![](https://i.imgur.com/P3YclTG.png)
* 如果欄位驗證失敗 - NgModel物件的errors屬性會有所有驗證狀態
![](https://i.imgur.com/7gudWoa.png)
* 從驗證狀態來顯示是否出現錯誤提示
在範本裡```?```使用在可能為空值的屬性上 https://git.io/JvEX7
```html
<p *ngIf="mQuantity.errors?.required">必填</p>
```
#### 取得NgModel實體的常用屬性
| 屬性名稱 | 型別 | 用途說明 |
| -------- | -------- | -------- |
| value | any | 欄位值 |
| valid、invalid | boolean | 有效、無效 (欄位驗證結果) |
| errors | {[key: value]} | 當出現無效欄位時,會出現的錯誤狀態 |
| dirty | boolean | 欄位值是否更動過 |
| pristine | boolean | 欄位值是否為原始值 |
| touched、untouched | boolean | 欄位 曾經/從未 經歷過 focus 事件 |
| disabled | boolean | 欄位設定為 disabled 狀態 |
| valueChanges | EventEmitter | 用來訂閱欄位**值變更**的事件 |
* 利用ngModel實體取得的屬性,增加判斷「尚未輸入該控制項或者輸入值是合法的,則不顯示錯誤訊息」。https://git.io/JvE7d
```html
<p [hidden]="mQuantity.pristine || mQuantity.valid">必填</p>
```
#### 利用 ```NgForm``` 取得<form>最上層的表單群組(FormGroup)實體
從FormGroup實體取得所有FormControl的欄位值與驗證狀態,語法如下
```html
<form name="buyForm" #buyForm="ngForm">
...
</form>
```
* 控制如果 buyForm 是 ```invalid``` 則按鈕不可按 https://git.io/JvE5g
```html
<form name="buyForm" #buyForm="ngForm">
...
</form>
<button (click)="addToCart(product)" [disabled]="buyForm.invalid">Buy</button>
```
![](https://i.imgur.com/vc8Rqmr.png)
* 使用 NgForm實體 取得表單所有欄位值 https://git.io/JvE5X
```html
<pre>{{buyForm.value | json}}</pre>
<button (click)="addToCart(product, buyForm)" [disabled]="buyForm.invalid">Buy</button>
```
```typescript=
addToCart(product, buyForm) {
console.log('product', product);
console.log('buyForm', buyForm.value);
// window.alert('Your product has been added to the cart!');
// this.cartService.addToCart(product);
}
```
* 把 購買數量 也加入購車且顯示出來 https://git.io/JvE59
in product-detail.component.ts
```typescript
addToCart(product, buyForm) {
const formValue = buyForm.value;
const carItem = Object.assign({}, product, {quantity: formValue.quantity });
console.log('carItem', carItem);
this.cartService.addToCart(carItem);
}
```
in cart.component.html
```html
<div class="cart-item" *ngFor="let item of carts">
<span>{{ item.name }}</span>
<span>{{ item.price | currency }}</span>
<span>{{ item.quantity }}</span>
</div>
```
### Model-Driven Forms (Reactive Forms)
* Model-driven Forms 又稱 Reactive Forms
* 在同一個 Angular 應用程式中可以混合使用 Model-driven 及 Template-driven
* 在程式碼中定義一個資料模型(Model),集中管理/設定所有表單欄位
#### 匯入 ReactiveFormsModule
```
import { ReactiveFormsModule } from '@angular/forms';
```
#### 使用```FormBuilder```建立表單模型
* 宣告表單模型與注入 FormBuilder 服務元件 https://git.io/Jvule
```typescript=
form: FormGroup;
constructor(private fb: FormBuilder)() {
}
```
* 建立表單模型物件 ( 頂層表單群組 + 子表單控制項 ) https://git.io/JvulJ
```typescript=
ngOnInit(): void {
this.form = this.fb.group({
quantity: 1
});
}
```
#### 在範本中關連表單模型物件
使用 [formGroup] 繫結最上層的表單群組物件
使用 formControlName 繫結 子表單控制項
https://git.io/JvulC
```html
<form name="buyForm" [formGroup]="form">
<input type="number" name="quantity"
formControlName="quantity" />
</form>
<pre>{{form.value | json}}</pre>
```
#### 套用欄位驗證
* 匯入驗證器 (Validators) https://git.io/JvulR
```typescript=
import { FormGroup, FormBuilder , Validators} from '@angular/forms';
```
* 套用預設值與驗證器 https://git.io/Jvulh
```typescript=
this.form = this.fb.group({
quantity: [1, [
Validators.required,
Validators.max(10)
]]
});
```
* 使用 form.get('xxx') 取得控制項 https://git.io/Jvulj
```html
<pre>{{form.get('quantity').errors | json}}</pre>
```
* 改用 getter method 語法 簡短宣告控制項 https://git.io/Jvu8s
```typescript=
get quantity(): FormControl {
return this.form.get('quantity') as FormControl;
}
```
```html
<pre>{{quantity.errors | json}}</pre>
```
* 顯示錯誤提醒 https://git.io/Jvu88
```html
<div [hidden]="quantity.pristine || quantity.valid">
<p *ngIf="quantity.errors?.required">必填</p>
<p *ngIf="quantity.errors?.max">超過最大可購買數量</p>
</div>
```
* 完善加入購物車的程式碼 https://git.io/Jvu8V
```typescript=
addToCart(product) {
const cartItem = Object.assign({}, product, this.form.value);
console.log('cartItem', cartItem);
this.cartService.addToCart(cartItem);
}
```
```html
<button (click)="addToCart(product)"
[disabled]="form.invalid">Buy</button>
```
> 挑戰完成:https://angular.tw/start/forms
## Angular Lifecycle Hooks
https://angular.tw/guide/lifecycle-hooks
![](https://i.imgur.com/YfWeCkQ.png)
![](https://i.imgur.com/bOUrfYX.png)
![](https://i.imgur.com/jH0TthN.png)
## Q&A