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