Angular

Resource

Main reference: Angular 官方網站
Main reference: Angular 大師之路
Main Reference: Book - 圖像 Angular 開發入門

Related Reference: Book - 打通 Rxjs 任督二脈

Time: 10/12 ~ 10/20

實際進度

  • 10/13 報告排程
    • 10/12 : Angular 快速上手 ~ 英雄之旅 建立特性元件
    • 10/13 : 英雄之旅建立特性元件 ~ 新增服務 + 圖像 Angular 開發入門
  • 10/18 報告排程:
    • 10/14 : 英雄之旅新增導航 ~ 建構表單前 + Angular 大師之路理論與實作
    • 10/17 : 建構表單 & 圖像 Angular 開發入門
    • 10/18 : 圖像 Angular 開發入門
  • 10/20 報告排程:
    • 10/19 : NgModel & Change detection study
    • 10/20 : 整理設計原則、Change detection、NgModel 並撰寫範例

介紹規劃


Rxjs 回顧

retry and retryWhen

  • retry
    重新嘗試,錯誤就讓他進到錯誤

    範例:

    ​​​​import { interval, of, switchMap, iif, throwError, retry } from 'rxjs'; ​​​​// 先嘗試一次, retry 3 次, 如果還是錯誤就讓他進到 error ​​​​interval(1000) ​​​​ .pipe( ​​​​ switchMap((data) => ​​​​ iif( ​​​​ () => data % 2 === 0, ​​​​ of(data), ​​​​ throwError(() => new Error('發生錯誤')) ​​​​ ) ​​​​ ), ​​​​ retry(3) ​​​​ ) ​​​​ // Open the console in the bottom right to see results. ​​​​ .subscribe({ ​​​​ next: (data) => { ​​​​ console.log(`retry sample 1 : ${data}`); ​​​​ }, ​​​​ error: (error) => { ​​​​ console.log(`retry sample 1: error - ${error}`); ​​​​ }, ​​​​ complete: () => { ​​​​ console.log(`retry sample 1: complete`); ​​​​ }, ​​​​ });

    (參考程式碼: https://stackblitz.com/edit/rxjs-5q2cxq?file=index.ts)

  • retryWhen
    retryWhen 內部需要設計一個 notifier callback function,retryWhen 會將錯誤傳到 notifier callback function,同時需要回傳一個 Observable 物件,retryWhen 會去訂閱他

    白話文: 回傳的 Observable 有訂閱發生,重新嘗試,直到 Observable 訂閱結束,才會停止重新嘗試。

    需要注意的是:
    retryWhen 內傳入一個 callback,這 callback 有一個參數會傳入一個 observable,這個 observable 不是原本的 observable(example) 而是"例外事件送出的錯誤所組成的一個 observable",等到這次的處理完成後就會重新訂閱我們原本的 observable。

    假設我們一遇到 error 就會 delay 再嘗試

    圖解會是以下這樣的:

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    • return complete 情況

      範例:

      ​​​​​ import { ​​​​​ interval, ​​​​​ of, ​​​​​ switchMap, ​​​​​ iif, ​​​​​ throwError, ​​​​​ retryWhen, ​​​​​ take, ​​​​​ } from 'rxjs'; ​​​​​ // 先嘗試一次, retry 3 次, 如果還是錯誤就讓他進到 error -> retryWhen 抓到 error 會傳進 notifier callback function (這裡是 interval(3000) 進行三次), retryWhen 會去訂閱他,有訂閱就重新嘗試,直到整體聽月都結束才會回到 complete ​​​​​ interval(1000) ​​​​​ .pipe( ​​​​​ switchMap((data) => ​​​​​ iif( ​​​​​ () => data % 2 === 0, ​​​​​ of(data), ​​​​​ throwError(() => new Error('發生錯誤')) ​​​​​ ) ​​​​​ ), ​​​​​ retryWhen((error) => interval(3000).pipe(take(3))) ​​​​​ ) ​​​​.subscribe({ ​​​​​ next: (data) => { ​​​​​ console.log(`retry sample 1 : ${data}`); ​​​​​ }, ​​​​​ error: (error) => { ​​​​​ console.log(`retry sample 1: error - ${error}`); ​​​​​ }, ​​​​​ complete: () => { ​​​​​ console.log(`retry sample 1: complete`); ​​​​​ }, ​​​​});

      (參考程式碼: https://stackblitz.com/edit/rxjs-q1oapr?file=index.ts)

    • return error 情況

      範例:

      ​​​​import { ​​​​ interval, ​​​​ of, ​​​​ switchMap, ​​​​ iif, ​​​​ throwError, ​​​​ retryWhen, ​​​​ take, ​​​​} from 'rxjs'; ​​​​// 概述: ​​​​// 先嘗試一次, retry 3 次,如果還是錯誤就讓他進到 error ​​​​ ​​​​// 實際行為: ​​​​// retryWhen 抓到 error 會傳進 notifier callback function (這裡是 interval(3000) 進行三次) ​​​​ ​​​​// 備註: ​​​​// retryWhen 會去訂閱他,有訂閱就重新嘗試,直到整體訂閱都結束才會回到 complete (因為是 return 嘗試的 Observable,重新嘗試的 Observable 完成,會回到完成) ​​​​interval(1000) ​​​​.pipe( ​​​​ switchMap((data) => ​​​​ iif( ​​​​ () => data % 2 === 0, ​​​​ of(data), ​​​​ throwError(() => new Error('發生錯誤')) ​​​​ ) ​​​​ ), ​​​​ retryWhen((error) => throwError(() => new Error('發生錯誤'))) ​​​​) ​​​​.subscribe({ ​​​​ next: (data) => { ​​​​ console.log(`retry sample 1 : ${data}`); ​​​​ }, ​​​​ error: (error) => { ​​​​ console.log(`retry sample 1: error - ${error}`); ​​​​ }, ​​​​ complete: () => { ​​​​ console.log(`retry sample 1: complete`); ​​​​ }, ​​​​});

      但說實話這樣有一點多此一舉,因為例外處理通常是使用 catch 而不是 retryWhen, retryWhen 通常是用來蒐集 error 並通知有 error,如下:

      ​​​​import { interval, of, switchMap, iif, throwError, retryWhen } from 'rxjs'; ​​​​const retryTimesAndThrowError = (duration, times) => ​​​​ interval(duration).pipe( ​​​​ switchMap((value, index) => ​​​​ iif( ​​​​ () => index === times, ​​​​ throwError(() => new Error('重試後發生錯誤')), ​​​​ of(value) ​​​​ ) ​​​​ ) ​​​​ ); ​​​​// 先嘗試一次, retry 3 次, 如果還是錯誤就讓他進到 error ​​​​interval(1000) ​​​​ .pipe( ​​​​ switchMap((data) => ​​​​ iif( ​​​​ () => data % 2 === 0, ​​​​ of(data), ​​​​ throwError(() => new Error('發生錯誤')) ​​​​ ) ​​​​ ), ​​​​ // 若只是回傳 throwError 的話好像就有點多此一舉,此處我們 3 秒重新測試一次 Observable ​​​​ retryWhen((errors) => retryTimesAndThrowError(3000, 3)) ​​​​ ) ​​​​ // Open the console in the bottom right to see results. ​​​​ .subscribe({ ​​​​ next: (data) => { ​​​​ console.log(`retry sample 1 : ${data}`); ​​​​ }, ​​​​ error: (error) => { ​​​​ console.log(`retry sample 1: error - ${error}`); ​​​​ }, ​​​​ complete: () => { ​​​​ console.log(`retry sample 1: complete`); ​​​​ }, ​​​​ });

      (參考程式碼: https://stackblitz.com/edit/rxjs-sojppc?file=index.ts)


Binding 繫結

Reference

Main reference: Angular 官方網站
Main reference: Angular 大師之路
Main Reference: Book - 圖像 Angular 開發入門

Binding 分類

分類介紹:

  • 輸出資料:

    • 文字插值 text interpolation
    • 屬性繫結 Property / Arribute binding
    • 樣式繫結 Style binding
    • 類別繫結 class binding
  • 回應事件:

    • 事件繫結 Event Binding
    • 雙向繫結 Two-way-binding

文字插值 text interpolation

  • 說明:
    文字插值 {{}} 的方式可以讓我們將程式內的屬性值繫結在頁面上。

  • 範例:

    在 html 中這樣寫:

    ​​​​<h1>Hello Wolrd, {{name}}</h1>

    在 ts 中這樣寫:

    ​​​​name = 'Clemmy'

    (參考程式碼: https://stackblitz.com/edit/angular-ivy-npvs8c?file=src/app/app.component.ts)

  • 事件繫結 Event Binding

    用途:

    想要監控使用者動作,讓係寵在觸發特定動作時,可以執行指定的元件方法

    在 html 中這樣寫:

    ​​​​<any-tag-name (eventName)="testMethod()"></any-tag-name>

    在 ts 中這樣寫:

    ​​​​testMethod(){ ​​​​ // 定義綁定的 Method ... ​​​​}
  • 屬性繫結

    • Attribute & Property 的差別
      Attribute 是 Html 中所定義的,Property 是 DOM 的節點屬性。

    在 html 中這樣寫:

    ​​​​<any-tag-name [property_name]="value"></any-tag-name>
  • 樣式繫結 style binding

    在 html 中這樣寫:

    ​​<any-tag-name [style.fontSize]="fontSize + 'pt'" ></any-tag-name>

    需求:

    想要根據屬性數值改變樣式,根據 fontSize 改變實際的 fontSize 樣式 和 color

    ​​​​<p [style]="{fontSize: fontSize+ ' pt'}">目前文字尺寸: {{ fontSize }} pt</p>

    如果想要針對特定的樣式做 binding,例如針對 pt 值:

    ​​​​<div [style.fontSize.pt]="fontSize">{{fontSize}}</div>

    (參考程式碼: https://stackblitz.com/edit/ng-book-single-style-binding-moqmdg?file=src/app/app.component.html)

  • 類別繫結 class binding

    需求:

    我們元素的樣式變得很複雜,需要用到 class 對應 style,希望可以透過實際切換 class name 的方式來讓樣式變換

    以下例子我們想要切換文字的大小,我們有小中大三種可以選擇:
    (參考程式碼: https://stackblitz.com/edit/ng-book-class-binding-7ddjsd?file=src/app/app.component.ts)

  • 雙向 binding

    • 盒子裡的香蕉:
      在 template 實現雙向綁定的語法為 [()] ,又稱為banana-in-box語法(盒子裡的香蕉)。

    • NgModel 的意涵:
      [ (NgModel) ] = ngModel 的 property binding + ngModelChange 的 event binding

    • 使用注意事項:
      記得在 app.module.ts imports FormsModule,再使用 NgModel

      ​​​​​​​​@NgModule({ ​​​​​​​​ declarations: [ ​​​​​​​​ AppComponent, ​​​​​​​​ HeroesComponent ​​​​​​​​ ], ​​​​​​​​ imports: [ ​​​​​​​​ FormsModule, ​​​​​​​​ BrowserModule, ​​​​​​​​ AppRoutingModule ​​​​​​​​ ], ​​​​​​​​ providers: [], ​​​​​​​​ bootstrap: [AppComponent] ​​​​​​​​}) ​​​​​​​​export class AppModule { }
    • 範例: 雙向綁定的實作方式

      採用 [(NgModel)]:

      ​​​​​​<input [(ngModel)]="username" /> ​​​​​​<p>two-way binding: {{ username }}</p>

      採用分開的 NgModel & NgModelChange

      ​​​​​​<input [ngModel]="username2" (ngModelChange)="username2 = $event" /> ​​​​​​<p>seperate binding: {{ username2 }}</p>

      不採用 NgModel

      ​​​​​​<input [value]="username3" (input)="username3 = $any($event.target).value" /> ​​​​​​<p>without ng model: {{ username3 }}</p>

      程式碼備註:
      $any 說明:

      $any() can be used in binding expressions to disable type checking of this expression

      (參考程式碼: https://stackblitz.com/edit/angular-ivy-3fwwkp?file=src/app/app.component.html)


巢狀互動 - 父傳子,子傳父的方法

懶人救星 - template 和 content

問題:

面對重複使用的程式碼,如果我們使用一直複製貼上的方式會不太好,假設我們想要改一個地方的程式碼而其他地方沒改到豈不是GG?

封裝範本 solution:

  • <ng-template></ng-template>
    ​​​​<div *ngTemplateOutlet="sample"></div> ​​​​<ng-template #sample> ​​​​ <div>哇系 reused sample code!</div> ​​​​</ng-template>
    但是,如果我們的 content 只是部分需要變動怎麼辦?
    我們可以使用 ng-template 帶入參數的方式
    ​​​​<div *ngTemplateOutlet="sample; context: {test_text: 'first'}"> ​​​​</div> ​​​​<div *ngTemplateOutlet="sample; context: {test_text: 'second'}"> ​​​​</div> ​​​​<ng-template #sample let-test_text="test_text"> ​​​​ <div>哇系 reused sample code! {{test_text}}</div> ​​​​</ng-template>
    也可以使用 $implicit 屬性來傳入預設資料,這樣就不用重複設定參數惹
    ​​​​<div *ngTemplateOutlet="sample; context: {$implicit: 'first'}"> ​​​​</div> ​​​​<div *ngTemplateOutlet="sample; context: {$implicit: 'second'}"> ​​​​</div> ​​​​<ng-template #sample let-test_text> ​​​​ <div>哇系 reused sample code! {{test_text}}</div> ​​​​</ng-template>

外部元件使用時決定元件使用內容 solution (動態投影):

  • <ng-content></ng-content>
    ​​​​// 父元件 template ​​​​<app-child> ​​​​ <h1>我爽指定就指定</h1> ​​​​ <p>你管我用什麼</p> ​​​​</app-child>
    ​​​​// 子元件 ​​​​<ng-content></ng-content>
    !!如果想要掌控父元件丟啥進去可以這樣寫 - 使用 select
    ​​​​// 父元件 template ​​​​<app-child #childComponent> ​​​​​ <h3>我只能指定 h3</h3> ​​​​​ <p class="test">我只能用這個 class</p> ​​​​​ <h1 test_attr>還不是只能用這個屬性 test</h1> ​​​​</app-child>
    ​​​​// 子元件 ​​​​<ng-content select="h3"></ng-content> ​​​​<ng-content select=".test"></ng-content> ​​​​<ng-content select=[test_attr]></ng-content>
    成果示意圖:

動態內容投影

  • 在使用 ng-content 的元件內,取得特定外部由外部傳來的範本參考變數或子元件的實體時,可以用 "@ContentChild" 裝飾器來完成:

  • 白話文:
    要從子元件的 ts 裡,抓到父元件 html 裡的子元件裡的 content
    也就是說假設有一個父元件如下:

    ​​<demo [data]="data"> ​​ <!-- content 又稱 projection (用ViewChild是抓不到的)--> ​​ <div class="someClass" #carFooter> ​​ <h1>有someClass的</h1> ​​ </div> ​​ <div> ​​ <h1>沒有someClass的</h1> ​​ </div> ​​ ​​ <ng-container ngProjectAs="some"> 會把內容轉為some的select ​​ <h1>如果content內容很多,可以用ng-container包起來</h1> ​​ </ng-container> ​​ ​​ ngProjectAs="some"等同於「some的element」 ​​ <some></some> ​​ ​​</demo>

    子元件如下:

    ​​<ng-content select=".someClass"></ng-content> ​​如果沒這行,則app.component.html的<demo>content的部分不會顯示出來</demo> ​​只顯示class="someClass"的content (<div class="someClass" ...) ​​<ng-content select="some"></ng-content> ​​顯示app.component.html裡的<demo>裡的<ng-container ngProjectAs="some"> </demo> ​​<ng-content></ng-content> ​​沒有someClass的會被放在下面

    ts 如下:

    ​​import { Component, AfterContentInit } from '@angular/core'; ​​@Component({ ​​ selector: 'demo',... ​​ }) ​​ ​​export class DemoComponent { ​​ @Input() data; ​​ @ContentChild('.someClass') content; // 用className取不到 ​​ @ContentChild('cardFooter') content; // 用templateRef才取得到 ​​ constructor(){} ​​ ngAfterContentInit(){ ​​ console.log(this.content); ​​ // ElementRef ​​ // nativeElement: HTMLDivElement ​​ } ​​}
    • 取得外部元件實體的方法(單個):

      ​​​​​​​​@ContentChild('title') titleElement!: ElementRef;

      加入 ContentChild directive 屬性,會在 ngAfterContentInit() 方法中依條件取得元素實體,此方法只會在 onDoCheck() 後被觸發,且只會觸發一次。

      備註: ContentChildren 取得多元素的實體,會得到一個包含全部實體的 QueryList 泛型屬性

      ngAfterContentInit() -> ngAfterContentChecked()

      變更檢測 -> ngDoCheck 後觸發 ngAfterContentChecked()

    • 取得外部元件實體的方法(多個)
      此處的 descendants true 來決定搜尋子元素以及後代所有元素

      ​​​​​​​​@ContentChildren('button' , { descendants : true }) ​​​​​​​​buttonElements!: QueryList<ElementRef>;

ViewChild - 在父元件中使用子元件的屬性和方法

  • ViewChild 使用方式:
    在父元件中:

    ​​​​@ViewChild('fontSize') fontSize!: FontSizeComponent; ​​​​// or 指定元件名稱 ​​​​@ViewChild(FontSizeComponent) fomtSize!: FontSizeComponent; ​​​​// 想要取得 html 元素實體時,則會指定 ElementRef ​​​​@ViewChild('firstDev') divElement: ElementRef;
  • 指定第二個選擇性參數內的數值,決定我們什麼時候可以在特定鉤子方法中取得實體

    此處通知 ngOnInit() 取得所需要的頁面元素實體

    ​​@ViewChild(FontSizeComponent, {static: true}) ​​size!: FontSizeComponent;

Life cycle

  • 圖解:

  • ngOnChanges - 呼叫時機點: ngOnInit 之前被呼叫、元件輸入性值 input 發生變化時呼叫

    ​​​​// ts 在嚴格模式下,宣告一個不允許為 null or undefined 的變數時,若未指定初始數值會出現警告訊息,如果要確定此變數一定有數值的話,我們可以透過使用 " ! " 運算子來排除 null or undefined ​​​​@Input size!: number; ​​​​@Output() sizeChange = new EventEmitter<string>(); ​​​​ngOnChanges(changes: SimpleChanges): void { ​​​​// 會帶入 SimpleChanges , 我們可以透過這些獲取 前一個或目前的屬性數以及是否回屬性變更的資訊 ​​​​ if(changes.size) { ​​​​ this.sizeChange.emit(`${this.size}pt`); ​​​​ } ​​​​}

    注意! 若輸入值是 Object (紀錄reference 位置),我們又只有改變物件的屬性的值時,並不會改到 Component 所記錄的參考位置 -> 可以使用 ngDoCheck() 檢查

    (參考程式碼: https://stackblitz.com/edit/ng-book-input-property-rgfmhr?file=src/app/app.component.ts)

  • ngOnInit - 呼叫時機點: ngOnChanges 被呼叫後 -> ngOnInit 被呼叫一次,在總體的 LifeCycle 只會被呼叫一次

  • ngDoCheck - 呼叫時機點: 變更檢測週期其實都會呼叫 ngDoCheck
    更詳細的內容可以參考李梅的文章:https://limeii.github.io/2019/06/angular-ngdocheck-onpush-strategy/

  • ngAfterContentInit - 呼叫時機點: 把外部內容投影至元件/文件的內容之後呼叫

    • 通常會是在使用 <ng-content></ng-content> 或是 @ContentChild 時,在 ngDoCheck() 後被觸發
  • ngAfterContentChecked - 呼叫時機點:觸發 ngAfterContentInit() 後觸發,除此之外也會在 ngDoCheck() 方法之後被觸發

  • ngAfterViewInit - 呼叫時機點:加入 @ViewChild 裝飾器屬性,當頁面載入時會在 ngAfterViewInit() 鉤子方法中一條件取元素實體,鉤子方法會在 onAfterContentChecked() 方法後觸發,且只會觸發一次
    備註: 若要在 ngInit() 就先取得所需要的頁面實體,我們可以在 ViewChild 上設定第二個參數

    ​​​​@ViewChild(FontSizeComponent, { static: true })
    ​​​​size!: FontSizeComponent;
    ​​​​// 但要記得此處也會觸發 ngAfterViewInit
    

    !!注意:但在針對含有 *ngFor、*ngIf 等指令的情況,無法在ngOnInit() 鉤子方法時取得實體

  • ngAfterViewChecked - 呼叫時機點:會在 ngAfterViewInit() 後被觸發,在變更監測時則會在 ngDoCheck() 時被觸發

  • ngOnDestory - 呼叫時機點: 使用者離開頁面,Angular 會銷毀頁面內的元件與指令實體,而在銷毀之前會觸發 ngOnDestroy()
    實務上釋放的資料大多包含:

    • DOM 事件或可監控物件的 unsubscribe()
    • stop Interval
    • 取消註冊此指令所註冊的 callback

(10/18 報告範圍)

元件樣式

特殊選擇器

  • 需求:

    為元件 (selector 指定的 tag) 加上樣式

    例如: 我們想要為 "<app-demo></app-demo>" 加上樣式

  • host:
    針對該元件對象進行樣式設定

    ​​​​:host { ​​​​ border: solid 2px black; ​​​​} ​​​​// 預設狀態下,會以行內 inline 的顯示模式顯示,若要元件以區塊元素 block 方式顯示, ​​​​:host { ​​​​ border: solid 2px black; ​​​​ display: block; ​​​​}
  • :host(.target_style)
    指定使用 target_style 為 className 的才會套用

    ​​​​:host(.target_class) { ​​​​ border: solid 2px blue; ​​​​} ​​​​// 在指定 tag 為 target_class 的情況下才會套用到此 css style
  • :host-context(.target_class / empty)
    會一直向上層尋找

    ​​​​// 設定到更外層的 css style 時可以使用此方法 ​​​​// 此方法會向上層頁面搜尋,直到根節點 ​​​​:host-context(.target_class) { ​​​​ // 略 ​​​​}

    (參考程式碼: https://stackblitz.com/edit/ng-book-host-selector-4jknfk?file=src/app/demo/demo.component.css)

範本參考變數

  • 需求
    想要父元件透過資料繫結去設定或是接受屬性資料,會使用到範本參考變數。

    可以在 html 那邊直接取得並使用他

  • 使用方式範本:

    父元件

    ​​​​<app-child #child></app-child> ​​​​<span>Parent call {{ child.propertyA }}</span> ​​​​<button type="button" (click)="child.doSomething()">Do Something</button>

    子元件

    ​​​​@Component({ ​​​​ selector: 'app-child' ​​​​}) ​​​​propertyA = 'Child property'; ​​​​ ​​​​doSomething(): void { ​​​​ // do something ​​​​ alert('parent call child method'); ​​​​} ​​​​

    (參考程式碼連結: https://stackblitz.com/edit/angular-ivy-xbtqke?file=src/app/app.component.html)

    備註說明:

    1. ref-name 等同於 #name,差別在於 # 為語法糖
    2. 預設在 Component 是不能使用的,範本參考變數只能在範本 ( Template ) 內使用
  • 使用說明

    • 範本內會建立該區域變數
    • 只能用於目前元件範本中
    • 儲存於該標籤的 DOM 物件

    範例:
    #tinputValue 的值會是這個 input 的 DOM 物件,我們可以直接操作 input DOM 物件

    ​​​​// 取用到裡面定義的方法 ​​​​<div class="widget-content"> ​​​​ <div id="searchbox"> ​​​​ <input type="text" placeholder="請輸入搜尋關鍵字" accesskey="s" ​​​​ #tinputValue ​​​​ [(ngModel)]="inputValue"> ​​​​ </div> ​​​​ 輸入文字:{{inputValue}} 目前字數 <span>{{tinputValue.value.length}}</span> ​​​​</div>

    (參考程式碼: https://stackblitz.com/edit/angular-ivy-xbtqke?file=src/app/app.component.ts)

Service

Reference

Related reference:
https://blog.typeart.cc/dive-deeper-service-in-angular/

Related reference:
https://www.gss.com.tw/blog/angular-26-深入-service-3

注入 Service 的方法

  • 依賴注入 DI - Injectable & providedIn

    需求:

    希望整個應用程式共用該 service

    範例:

    ​​​​@Injectable({ providedIn: 'root'}) ​​​​export class TaskService {}

    上述寫法意味著:

    為應用程式注入級別 (root) 的 Service,也就是整個應用程式都可以注入,是在整個 Angular 應用中提供單例 (singleton) 服務,避免產生多個實例
    (備註: singleton 意味著 "在注入器所屬模組內,相同的服務最多只會有一個實體,Angular 會透過裝飾器的定義,將未使用的依賴對象進行搖樹優化而排除。")

    上述 providedIn: 'root' 的寫法同在 app.module.ts 中的 provider 後面填上: 'xxxService' (範例為 TaskService)

    同等程式碼如下:

    ​​​​@NgModule({ ​​​​ imports: [ ​​​​ BrowserModule, ​​​​ CommonModule, ​​​​ FormsModule ​​​​ ], ​​​​ declarations: [ ​​​​ AppComponent, ​​​​ HeroFormComponent ​​​​ ], ​​​​ providers: [ TaskService ], ​​​​ bootstrap: [ AppComponent ] ​​​​}) ​​​​ ​​​​export class AppModule { }
  • Injectable single module

    需求:

    不想讓服務給整個應用程式共用,想在單獨的模組中使用或是在單獨的 Component 中使用

    • 模組注入器,使用 Injectable
      • @Injectable
        ​​​​​​​​// 在 service 中設定 providedIn ​​​​​​​​@Injectable({ providedIn: xxModule }) ​​​​​​​​export class TaskService {} ​​​​​​​​
      • @NgModule 的 providers 屬性陣列配置
        ​​​​​​​​// 在 module 中設定 providers ​​​​​​​​@NgModule({ ​​​​​​​​ // ... ​​​​​​​​ providers: [ ​​​​​​​​ xxxService ​​​​​​​​ ], ​​​​​​​​ // ... ​​​​​​​​})
    • 元素注入器,使用 @Component 的 providers
      ​​​​@Component({ ​​​​ /* . . . */ ​​​​ providers: [xxxService] ​​​​})

providers 中的 service 切換 - SOLID 開放封閉原則

核心概念:

遇到需求變更時,優先考慮加入新的程式,而非改動舊的程式

服務抽換的方式:

  • useClass

    用途:

    轉換 Service provider 的一種方式

    [情境題] 假設今天的 service 原本是原價,變成全面九折,我們可以使用 改變 NgModule provider 的方式 將原本的 service provider 與 另外一個新的 service 做 useClass 替換

    ​​​​@NgModule({ ​​​​ providers:[{provide: OrderService, useClass: OrderDiscountService }] ​​​​})

    (參考程式碼: https://stackblitz.com/edit/ng-book-provider-class-gkzpxu?file=src/app/app.component.ts)

  • useExisting

    用途:

    抽換服務,與 useClass 的不同在於,此使用的方式是用原本的實體,而不會重新再建立實體,若不存在任何實體就會拋出例外

    以下為不會拋出例外的寫法:

    ​​​​providers: [{ ​​​​ provide: OrderService, ​​​​ useExisting: OrderDiscountService ​​​​ }, ​​​​ OrderDiscountService ​​​​]

    以下為會拋出例外的寫法:

    ​​​​providers: [{ ​​​​ provide: OrderService, ​​​​ useExisting: OrderDiscountService ​​​​ } ​​​​]

    (參考程式碼: https://stackblitz.com/edit/ng-book-provider-class-gkzpxu?file=src/app/app.module.ts)

  • useValue

    需求:

    如果替換掉的邏輯不混複雜,也可以使用 useValue 抽象服務,透過 useClass 來替換類別

    ​​{ ​​ provide: OrderService, ​​ useValue: { computeTotal: () => 100 } ​​} ​​

    (參考程式碼: https://stackblitz.com/edit/ng-book-provider-class-gkzpxu?file=src/app/app.component.ts)

  • useFactory

    需求:

    當我們需要在不同情況下 return 不同 service 時,我們可以使用到 useFactory

    ​​​​@NgModule({ ​​​​ provide: OrderService, ​​​​ useFactory: () => { ​​​​ return (new Date()).getMonth() === 9 ? new OrderAnnisersaryService() : new OrderService(); ​​​​ } ​​​​})

    若我們需要將外部服務來做判斷或是注入到訂單服務內,我們可以使用useFactory 將 deps 中指定的外部服務傳入 useFactory

    ​​​​{ ​​​​ provide: OrderService, ​​​​ useFactory: (httpClient: HttpClient) => { ​​​​ ... ​​​​ }, ​​​​ deps: [HttpClient] ​​​​}

    (參考程式碼: https://stackblitz.com/edit/ng-book-provider-factory-cfyysv?file=src/app/app.module.ts)


指令

指令是透過 @Directive 來定義

ts:

// directive selector 以駝峰式命名 @Directive({ selector: "[testDir]" // selector: "div[testDir]" 則是限制在 div 裡免使用 testDir })

html:

// template 指令以屬性方式使用 <div testDir></div>

指令有分為管理樣式的屬性指令和變更DOM佈局的結構指令:

指令 (Directive) 包含了屬性型指令 (Attribute Directive) 與結構型指令 (Structural Directive) 兩種,前者用於改變 DOM 元素的外觀與行為,後者則是操控 DOM 樹,透過新增、移除或替換 DOM 元素來修改頁面結構。

Angular 內建的結構指令:

使用內建指令前需要先引入 CommonModule 模組

  • *ngFor

    • 介紹:
      *ngFor 為 Angular 提供的語法糖

      範例為:

      ​​​​​​​​<app-task *ngFor="let task of tasks" [task] = "task"></app-task>

      實際上 *ngFor 會轉換成:

      而他轉換的方式就是將 *ngFor 轉換成 ng-template 並且把宿主元素放置其中,也就是會轉換成如下:

      ​​​​​​​​<ng-template ngFor let-task [ngForOf]="tasks"> ​​​​​​​​ <app-task [task]="task"></app-task> ​​​​​​​​</ng-template>

      https://stackblitz.com/edit/ng-book-ngfor-directive

    • ngFor 提供的變數:
      index , first , last , odd , even 等變數

      透過這些變數,我們可以針對這些變數做一些 class binding 來做樣式設定

      範例:

      可以針對奇數的 task 加上特定的樣式

      ​​​​​​​​<app-task *ngFor="let task of tasks; let odd = odd;" [class.odd] = "odd"></app-task>
    • ngFor 的使用注意事項:

      若使用 *ngFor 而為進行其他設定,當來源被重設時就會 DOM 會被重新渲染,聽起來效能很不好對吧?

      解決方式:

      可以設定 trackBy 來追蹤已經更改的項目,讓 Angular 可以只重新渲染他們

      使用方式:

      ​​​​​​​​trackByItems(index: number,task: Task(特定 type)) : string { ​​​​​​​​ return task.TaskSn; ​​​​​​​​}
      ​​​​​​​​// template ​​​​​​​​<app-task *ngFor="let task of tasks; trackBy: trackByItems" [task] = "task"></app-task>

      (參考程式碼: https://stackblitz.com/edit/ng-book-ngfor-trackby-directive-4jd2am?file=src/app/app.component.html)
      (參考網站: https://ng-book-ngfor-trackby-directive-4jd2am.stackblitz.io/ 開啟開發者工具)

  • *ngIf

    ​​​​<div *ngIf="tasks.length>=1; then list; else empty"> ​​​​<ng-template #list> ​​​​ <app-task *ngFor="let task of tasks; let odd = odd;" [class.odd] = "odd"></app-task> ​​​​</ng-template> ​​​​<ng-template #empty> ​​​​ <div>查無資料</div> ​​​​</ng-template>

Angular 內建的屬性指令:

  • *ngSwitch - [ngSwitch]

    ​​​​<div [ngSwitch]="task.state"> ​​​​ <span *ngSwitchCase="'Doing'">進行中</span> ​​​​ <span *ngSwitchCase="'Finish'">完成</span> ​​​​ <span *ngSwitchDefault>未安排</span> ​​​​</div>

相關 tag: ng-container 的使用方式 - 避免添加的 tag 又跑版

  • 需求:
    一個元素只能使用一個結構指令,不能在同個元素內同時使用 *ngIf 又使用 *ngFor,但若不想為了建立一個結構指令而多了 DOM,可以使用 ng-container

    ​​​​<ng-container> ​​​​ <div>內容</div> ​​​​</ng-container>

ngComponentOutlet

  • 使用情境:
    針對動態元件的部分,有時候會需要比較複雜的載入,例如在不同條件下,我們想要顯示成一般的文字、圖片或影片,這樣會增加範本的程式複雜度,這時侯可以使用 *ngComponentOutlet 來依照條件動態載入特定元件。

    使用方式就參考:
    https://stackblitz.com/edit/ng-book-componet-outlet?file=src%2Fapp%2Fapp.component.html

    範例:

    ​​component!: Type<any>
    ​​<div *ngComponentOutlet="component"></div>

    可以在 ts 裡面針對 component 去做切換

Pipe

限制長度使用的 pipe

<div>{{'1223' | slice: 0:2}}</div>

方便的地方在於,這也適用於 *ngFor,可以將 let task of tasks 後面加上 | slice start_number : end_number

KeyValuePipe


Router

Reference

Related refernce: https://ithelp.ithome.com.tw/articles/10209035

使用方法:

  • 建立 Component

  • 設定指定頁面以及路由

    ​​​​<h1>Angular Router</h1> ​​​​<hr /> ​​​​<ul> ​​​​ <li><a [routerLink]="['/home']">home</a></li> ​​​​ <li><a [routerLink]="[ '/second']">second</a></li> ​​​​</ul> ​​​​<!-- 記得加上以下 router-outlet--> ​​​​<router-outlet></router-outlet>

    (備註: router-outlet 功用: 用來存放被 route 的 Component)

  • 建立路由

    ​​​​const routes: Routes = [ ​​​​ { ​​​​ path: "", ​​​​ redirectTo: "/home", ​​​​ pathMatch: "full", // 當路徑是空的時候轉址到 home ​​​​ }, ​​​​ { ​​​​ path: "home", ​​​​ component: HomeComponent, ​​​​ // children: 子頁面 /home/home1 ​​​​ children: [ ​​​​ { ​​​​ path: '', // 一定要加這行 ​​​​ }, ​​​​ { ​​​​ path: 'home1', ​​​​ component: Home1Component, ​​​​ }, ​​​​ { ​​​​ path: 'home2', ​​​​ component: Home2Component, ​​​​ }, ​​​​ ], ​​​​ }, ​​​​ { ​​​​ path: "second", ​​​​ component: SecondComponent, ​​​​ }, ​​​​ { ​​​​ path: "**", ​​​​ component: HomeComponent, // 萬用路徑,路由沒有比對到,永遠會執行 ​​​​ }, ​​​​];

    切換路由頁面可以透過 Router 方法:

    ​​​​this.router.navigate(['parent','child'])

    or

    ​​​​this.router.navigateByUrl('parent/child')

    若需要路由參數,需要透過 ActivatedRoute 內的 snapshot 屬性,取得路由變數

    範例:

    ​​​​constructor(private route: ActivatedRoute){ ​​​​ ​​​​}
    ​​​​ngOnInit(){ ​​​​ const taskSn = this.route.snapshot.paramMap.get('sn'); ​​​​ // 上述取得 taks/form/:sn 中的 sn ​​​​ // 取得查詢字串參數: snapshot.queryParamMap.get('name'); ​​​​ // 上述取得 task/form?name=1 的 name ​​​​}

    (參考程式碼: https://stackblitz.com/edit/ng-book-router-param-snapshot-nvhnk5?file=src/app/task-form-page/task-form-page.component.ts)

參數說明:

  • RouterLink 是一種 Directive:

    當應用於範本中的元素時,使該元素成為開始導航到某個路由的連結。導航會在頁面上的 <router-outlet> 位置上開啟一個或多個路由元件。

  • snapshot

    路由引數與你在此路由中定義的路徑變數相對應。要訪問路由引數,我們使用 route.snapshot,它是一個 ActivatedRouteSnapshot,其中包含有關該特定時刻的活動路由資訊。


Form

  • Reference:

    Template-Driven Form 與 Reactive Form 的差異:
    https://ithelp.ithome.com.tw/articles/10195280

  • 分類 - Template-Driven Form & Reactive Form:

    Form 功能 \ Form 種類 Template Driven Form Reactive Form
    組件驗證控制寫法 寫在<input> or <select> 標籤內,並利用 ngModel 來確認是否輸入了能驗證通過的內容 通常使用 Controller 來做驗證 (寫在 ts)
    同步 or 非同步 非同步 同步
    同步 or 非同步原因 個表單元件透過 directive 委派檢查的功能 任何一個節點可以取得表單資料,且資料是同步被更新的
    Module FormsModule ReactiveFormsModule
    Control 實體 透過 NgModel、NgModelGroup 建立 FormControl、FormGroup 表單型別實體 開發者自行宣告與建立表單類型實體
  • 建立 Form 的步驟:

    • 確認表單邏輯是否複雜 ? 若複雜又需要以方便的方式處理同步資訊的問題 (跨欄位更新之類的),可以使用 Reactive Form,反之,則可以使用 Template Driven Form

    • Template Driven Form

      • 使用基礎類別

        • Angular 依據 NgModel & NgModelGroup 指令建立 FormControl、 FormGroup 實體
      • 把表單欄位的 Type 定義好 - 建立 interface

        範例:

        ​​​​​​​​​​export class Hero { ​​​​​​​​​​ constructor( ​​​​​​​​​​ public id: number, ​​​​​​​​​​ public name: string, ​​​​​​​​​​ public power: string, ​​​​​​​​​​ public alterEgo?: string ​​​​​​​​​​ ) { } ​​​​​​​​​​}
      • 建立 input value 與 model 屬性的關係 - ngModel 資料繫結
        把範本驅動表單中的控制元件繫結到資料模型中的屬性,如果在元素上使用 ngModel 時,必須為該元素定義一個 name 屬性。Angular 會用這個指定的名字來把這個元素註冊到父 <form> 元素上的 NgForm 指令中。

        範例:

        ​​​​​​​​​​<input type="text" class="form-control" id="name" ​​​​​​​​​​ required ​​​​​​​​​​ [(ngModel)]="model.name" name="name">
      • 追蹤整個 Form 的資料驗證狀態 -
        ngForm 追蹤整個表單資料的狀態 (設立範本樣式變數以讓其他 view 元素可以追蹤到整體表單的狀態,例如: form 是否 invalid):

        只要 import FormModule,就會建立一個最上層的 FormGroup 實例,並把它繫結到 <form> 元素上,以追蹤它所聚合的那些表單值並驗證狀態。

        範例:

        ​​​​​​​​​​​​<div class="container"> ​​​​​​​​​​​​ <div [hidden]="submitted"> ​​​​​​​​​​​​ <h1>Hero Form</h1> ​​​​​​​​​​​​ <!-- ngSubmit 為事件屬性,此處把 ngSubmit這個事件屬性繫結到onSubmit()方法中 --> ​​​​​​​​​​​​ <!-- #heroForm 為範本引數變數,ngForm 為 import FormModule 後會自動被 Angular 建立的 directive,並且把它 bind 到 form 上 --> ​​​​​​​​​​​​ <form (ngSubmit)="onSubmit()" #heroForm="ngForm"> ​​​​​​​​​​​​ <div class="form-group"> ​​​​​​​​​​​​ <label for="name">Name</label> ​​​​​​​​​​​​ <input ​​​​​​​​​​​​ type="text" ​​​​​​​​​​​​ class="form-control" ​​​​​​​​​​​​ id="name" ​​​​​​​​​​​​ required ​​​​​​​​​​​​ [(ngModel)]="model.name" ​​​​​​​​​​​​ name="name" ​​​​​​​​​​​​ #name="ngModel" ​​​​​​​​​​​​ /> ​​​​​​​​​​​​ <div [hidden]="name.valid || name.pristine" class="alert alert-danger"> ​​​​​​​​​​​​ Name is required ​​​​​​​​​​​​ </div> ​​​​​​​​​​​​ </div> ​​​​​​​​​​​​ <!-- ... 略 --> ​​​​​​​​​​​​ </div> ​​​​​​​​​​​​</div>
      • 驗證表單 style

        ​​​​​​​​​​ .ng-valid[required], .ng-valid.required { ​​​​​​​​​​ border-left: 5px solid #42A948; /* green */ ​​​​​​​​​​ } ​​​​​​​​​​ .ng-invalid:not(form) { ​​​​​​​​​​ border-left: 5px solid #a94442; /* red */ ​​​​​​​​​​ }
      • 驗證表單操作
        submit 數值後的操作
        (UI 顯示: 點選 Submit 按鈕後,submitted 標誌就變為 true,表單就會消失。接著會顯示已經 submitted 的資料。)

        範例:

        ​​​​​​​​​​submitted = false; ​​​​​​​​​​onSubmit() { this.submitted = true; }

      (參考程式碼: https://stackblitz.com/edit/angular-rujzjq?file=src/app/hero-form/hero-form.component.html)

    • Reactive Form

      • 使用前先在 app module import ReactiveFormsModule
      • 使用基礎類別
        • FormControl
          在單元表單元件中檢查值並且驗證狀態 ( input、select 等等)

        • FormArray
          以索引方式追蹤表單驗證狀態

        • FormGroup
          一組值與驗證狀態 (FormControl),一個 form 表單就是一個 FormGroup。

          範例:
          html:
          需要屬性綁定 formGroup 與 要關聯的 form name (範例為 form),在 input 等元件需要設定 formControl 的 name (formControlName) 以方便未來追蹤元件狀態。

          ​​​​​​​​​​​​​​<div [formGroup]="form"> ​​​​​​​​​​​​​​ <label> ​​​​​​​​​​​​​​ <span>Email</span> ​​​​​​​​​​​​​​ <!-- need to set formControlName --> ​​​​​​​​​​​​​​ <!-- for styles, we set ngClass --> ​​​​​​​​​​​​​​ <input ​​​​​​​​​​​​​​ type="email" ​​​​​​​​​​​​​​ class="formControl" ​​​​​​​​​​​​​​ formControlName="email" ​​​​​​​​​​​​​​ [ngClass]="{ errors: f.email.errors }" ​​​​​​​​​​​​​​ placeholder="Account" ​​​​​​​​​​​​​​ /> ​​​​​​​​​​​​​​ </label> ​​​​​​​​​​​​​​<div>

          ts:

          ​​​​​​​​​​​​​​email = true; ​​​​​​​​​​​​​​password = true; ​​​​​​​​​​​​​​form: FormGroup; ​​​​​​​​​​​​​​// with FormBuilder ​​​​​​​​​​​​​​constructor(public formBuilder: FormBuilder) {} ​​​​​​​​​​​​​​// without FormBuilder ​​​​​​​​​​​​​​// constructor() {} ​​​​​​​​​​​​​​ngOnInit(): void { ​​​​​​​​​​​​​​ // 表單驗證 ​​​​​​​​​​​​​​ // formBuilder 有 group(), control(), array() method,可以用來簡寫 new ///// FormGroup({ email: new FormControl('', ​​​​​​​​​​​​​​ // [Validators.required, Validators.email]), ​​​​​​​​​​​​​​ // password: new FormControl('',[Validators.required, Validators.minLength(6)]) ​​​​​​​​​​​​​​ // }), ​​​​​​​​​​​​​​ // with formbuilder ​​​​​​​​​​​​​​ this.form = this.formBuilder.group({ ​​​​​​​​​​​​​​ email: ['', [Validators.required, Validators.email]], ​​​​​​​​​​​​​​ password: ['', [Validators.required, Validators.minLength(6)]], ​​​​​​​​​​​​​​ }); ​​​​​​​​​​​​​​ // without formBuilder ​​​​​​​​​​​​​​ // this.form = new FormGroup({ ​​​​​​​​​​​​​​ // email: new FormControl('', [Validators.required, Validators.email]), ​​​​​​​​​​​​​​ // password: new FormControl('', [ ​​​​​​​​​​​​​​ // Validators.required, ​​​​​​​​​​​​​​ // Validators.minLength(6), ​​​​​​​​​​​​​​ // ]), ​​​​​​​​​​​​​​ // }); ​​​​​​​​​​​​​​} ​​​​​​​​​​​​​​get f() { ​​​​​​​​​​​​​​ return this.form.controls; ​​​​​​​​​​​​​​}

          (上述範例參考程式碼: https://stackblitz.com/edit/angular-ivy-bk3bkq?file=src/app/app.component.ts)


(10/20 sharing 範圍)

About previous sharing: "依賴反轉 DI" 與 "開放封閉原則 OCP"

  • Reference:
    依賴反轉原則 - Dependency Inversion Principle
    Angular DI
    開放封閉原則 - Open-Closed Principle

  • SOLID 設計原則:

    • S = Single-responsibility principle (SRP) = 單一職責原則
    • O = Open–closed principle (OCP) = 開放封閉原則
    • L =Liskov substitution principle (LSP) = 里氏替換原則
    • I = Interface segregation principle (ISP) = 介面隔離原則
    • D = Dependency inversion principle (DIP) = 依賴反向原則
  • 依賴反轉原則 DIP

    • 核心: 高層模組不應該依賴低層模組,兩者應該依賴抽象。
    • 核心解說:
      若高層模組依賴低層模組,也就是說低層模組的變動讓高層模組受到影響,所以必須將依賴反轉,讓高層不再依賴低階模組,而是透過抽象的方式來達到依賴反轉。
    • 範例:
      ​​​​​​import { Component, Input, OnInit } from "@angular/core"; ​​​​​​import { Task } from "../../model/task"; ​​​​​​import { TaskLocalService } from "../services/task-local.service"; ​​​​​​@Component({ ​​​​​​ selector: "app-task-list", ​​​​​​ templateUrl: "./task-list.component.html", ​​​​​​ styleUrls: ["./task-list.component.css"], ​​​​​​}) ​​​​​​export class TaskListComponent implements OnInit { ​​​​​​ ​​​​​​ tasks: Task[]; ​​​​​​ constructor(private taskService: TaskLocalService) {} ​​​​​​ ngOnInit(): void { ​​​​​​ this.tasks = this.taskService.getData(); ​​​​​​ } ​​​​​​}
  • 開放封閉原則 OCP : 利用依賴的提供者抽換服務

    • 核心: 一個軟體製品應該對於擴展是開放的,但對於修改是封閉的。

    • 核心解說:
      軟體在面對擴展時,應該是開放的且擴充時不應該修改到原有的程式。

    • 範例:

      ​​​​​​​​@NgModule({ ​​​​​​​​ providers: [ ​​​​​​​​ TaskLocalService, ​​​​​​​​ { provide: TaskRemoteService, useExisting: TaskLocalService }, ​​​​​​​​ ], ​​​​​​​​}) ​​​​​​​​export class AppModule {}
  • 總結 Provider 相關的命題:

    • @NgModule 裝飾器的提供者 (provider) 陣列內,在此定義的依賴對象則皆會被封裝至模型內。 這個行為是在做配置可注入的服務,是屬於 DI 範圍。
    • Provider 原則上是遵從依賴反轉原則,但若是著重在不同 service 彼此之間的抽換擴充,這種設計模式是屬於 "開放封閉原則"
    • 誤區:
      不應該將 Provider 與 SOLID 的 OCP 一起做解說,書中想要強調的應該是利用"依賴"的提供者抽換服務,如果變更服務時直接修改服務內容而不是新增服務來做額外擴充,就會是違反 OCP 原則的核心概念。

Change Detection

  • Reference:

    Main reference:
    https://indepth.dev/posts/1305/the-last-guide-for-angular-change-detection-youll-ever-need
    https://hackmd.io/@sherman/SJxW6tuT-w

    Related reference (Zone.js):
    https://ithelp.ithome.com.tw/articles/10208831
    https://www.php.cn/js-tutorial-488503.html

    Related refernce (Inline Caching):
    https://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html

    Related reference: (change detection)
    https://ithelp.ithome.com.tw/articles/10209026

    changeDetection: ChangeDetectionStrategy.OnPush

    (參考程式碼: https://stackblitz.com/edit/ironman2019-change-detector-ref-gi67zw?file=src/app/app.component.ts)

  • 什麼是變更偵測?
    框架藉由組合狀態 Model & 範本,複製 app 狀態到 UI 上,若狀態變更,也必須要更新 DOM ,這種將 HTML 與資料同步更新的機制稱作 "變更偵測 Change Detection"

    白話文: 資料改變更新 DOM 的過程

  • 變更偵測的運作過程?

    • 開發者更新 data Model (Ex. 更新元件的綁定)
    • Angular 偵測變更
    • 變更偵測對元件樹的每一個元件由上到下,檢查對應的 Model 是否有改變
    • 若有更新資料(資料有新的值),就更新元件的 View (DOM)

    GIF 圖解:

  • Zone.js

    • 用途:

      Angular 的變更偵測方式是採取 "當非同步事件發生後,進行變更偵測" (這些非同步的例子如 Http Request、setTimeout 等),而 Angular 包裝 zone.js 程式來讓我們可以透過他得知所有事件的發生,並且觸發變更偵測,這個工具也就是 " NgZone " 服務

      要注意的一點是: 一個 Angular 應用中只有存在一個 Angular Zone,每個 task 都會在 Angulra Zone 中執行。

      白話文: zone 可以保持追蹤並攔截任何非同步的 task

    • zone 通常有這些階段

      • 一開始通常是 stable 穩定 狀態
      • task 在 zone 中執行 -> unstable 不穩定
      • task 完成 -> stable 穩定
    • 會觸發 Change Detection 的事件:

      • Events (ex. click、change、input、submit)
      • XMLHttpRequests - 網路請求
      • Timers - setTimeout() and setInterval() API
    • 範例: (onUnstable & onStable 的範例)

      ​​​​​​​​import { Component, NgZone } from '@angular/core'; ​​​​​​​​@Component({ ​​​​​​​​ selector: 'my-app', ​​​​​​​​ templateUrl: './app.component.html', ​​​​​​​​ styleUrls: ['./app.component.css'] ​​​​​​​​}) ​​​​​​​​export class AppComponent { ​​​​​​​​ constructor(private zone: NgZone) { } ​​​​​​​​ ngOnInit() { ​​​​​​​​ this.zone.onUnstable.subscribe(() => { console.log('有事件發生了') }); ​​​​​​​​ this.zone.onStable.subscribe(() => { console.log('事件結束了') }); ​​​​​​​​ } ​​​​​​​​}

      (參考程式碼: https://stackblitz.com/edit/angular-ivy-dvghpm?file=src/app/app.component.ts)

    • runOutsideAngular()

      當我們在會觸發變更偵測的狀態下想要執行一些跟畫面無關的程式時(例如某個數字加一或是呼叫一個 API 等等,但不會影響畫面),可以把程式放在 runOutsideAngular(),來避免發生變更偵測造成的效能耗損:

      注意:
      runOutsideAngular 內所執行的 function 是不會觸發任何 change detection 的 (此處以一個延遲執行的 function 來進行測試)

      ​​​​​​ this.zone.runOutsideAngular(() => { ​​​​​​ // 進行跟 UI 無關的複雜運算 ​​​​​​ });

      (參考程式碼: https://stackblitz.com/edit/angular-ivy-qzk9ki?file=src/app/app.component.ts)

    • run()
      跟 runOutsideAngular 相反,如果在程式中「不小心」脫離了 Angular 變更偵測的範圍,像是使用 jQuery 或其他第三方與 DOM 操作有關的套件時,很容易不小心就脫離變更偵測了,這時候可以用 run() 方法來讓程式回到 Angular 變更偵測內。

      ​​​​​​ this.zone.run(() => { ​​​​​​ this.i++; ​​​​​​ }); ​​​​​​
  • 變更偵測策略

    • Default

      • Default 介紹

        設定:

        Angular 預設使用 ChangeDetectionStrategy.Default 的變更偵測策略。

        設定範例:

        ​​​​​​​​​​@Component({ ​​​​​​​​​​ selector: 'tooltip', ​​​​​​​​​​ template: ` ​​​​​​​​​​ <h1>{{config.position}}</h1> ​​​​​​​​​​ {{runChangeDetection}} ​​​​​​​​​​ `, ​​​​​​​​​​ changeDetection: ChangeDetectionStrategy.Default, ​​​​​​​​​​ })

        變更偵測過程:

        當每一次事件觸發變更偵測時(例如使用者事件、timer、XHR、Promise),會由上到下檢查元件樹中的每一個元件。(dirty check)。

        存在的問題:

        在較大的 app 中可能會降低效能,因為含有太多元件。

    • OnPush

      • OnPush 介紹

        設定:

        我們可以用在元件裝飾器的 metadata 中增加 changeDetection 屬性,並設定為 ChangeDetectionStrategy.OnPush。

        變更偵測過程:

        這個變更偵測策略可以對這個元件和子元件忽略非必要的檢查。

      • 變更偵測的時機:

        以下為 async pipe update 的原始碼

        ​​​​​​​​​​​​private _updateLatestValue(async: any, value: Object): void { ​​​​​​​​​​​​if (async === this._obj) { ​​​​​​​​​​​​ this._latestValue = value; ​​​​​​​​​​​​ // Note: `this._ref` is only cleared in `ngOnDestroy` so is known to be available when a ​​​​​​​​​​​​ // value is being updated. ​​​​​​​​​​​​ this._ref!.markForCheck(); ​​​​​​​​​​​​} ​​​​​​​​​​}

        由上面的 code 我們可以看到,Angular為我們呼叫 markForCheck(),所以我們能看到檢視更新了即使input的參照沒有發生改變。

        • 手動變更偵測的工具 ChangeDetectorRef
          (詳細說明如下述)
      • 手動變更偵測的工具

        • ChangeDetectorRef.detectChanges() :
          檢查這個元件跟子元件,可以搭配 detach() 實作本地變更偵測檢查

          (未搭配 detach 參考程式碼: https://stackblitz.com/edit/angular-ivy-ekbjro?file=src/app/counter/counter.component.ts)

          他只會在當前的 view 和子 view 進行一次變更檢測,也就是說無論當前組件的狀態是什麼,他只會進行一次檢測,如果前面的元件有 trigger detach 把狀態設置違禁用狀態,就有可能不會被檢查。

          圖解 ChangeDetectorRef 相關的 Method:

          • reattach():
            將先前分離的視圖重新附加到更改檢測樹。默認情況下,視圖附加到樹。
            -> 白話文: 會保留原本被設定成的模式 onPush or Default,在把它重新放到 CD 樹中

          • detach():
            將此視圖與更改檢測樹分離。在重新附加之前,不會檢查分離的視圖。與 detectChanges() 結合使用以實現本地更改檢測檢查。

            (搭配 detach 參考程式碼: https://stackblitz.com/edit/ironman2019-change-detector-ref-oupbtv?file=src/app/app.component.ts)

        • ChangeDetectorRef.markForCheck(): (影響範圍: 自己跟自己之上的元素)

          不觸發變更偵測,但將所有 OnPush 標記為檢查一次,不管是在當前或是下一個變更偵測循環中。會對標記的元件執行變更偵測"檢查",本身不觸變更發偵測。

          影響範圍:

          將自己及 parent Component 等都標示為要被檢查,所以影響範圍是本身以上。

          解決 reattach 在沒有啟動檢查的 Parent Component 的 Child Component 沒有作用的問題。

          ​​​​​​​​​​​​import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit} from '@angular/core'; ​​​​​​​​​​​​@Component({ ​​​​​​​​​​​​ selector: 'my-app', ​​​​​​​​​​​​ templateUrl: './app.component.html', ​​​​​​​​​​​​ styleUrls: [ './app.component.css' ], ​​​​​​​​​​​​ changeDetection: ChangeDetectionStrategy.OnPush ​​​​​​​​​​​​}) ​​​​​​​​​​​​export class AppComponent { ​​​​​​​​​​​​ vcard = [{name: 'Kevin'}]; ​​​​​​​​​​​​ constructor(public cd: ChangeDetectorRef) {} ​​​​​​​​​​​​ ngOnInit() { ​​​​​​​​​​​​ setTimeout(() => { ​​​​​​​​​​​​ this.vcard.push({name: 'Jeff'}); ​​​​​​​​​​​​ // markForCheck 標示需要被檢查 ​​​​​​​​​​​​ this.cd.markForCheck(); ​​​​​​​​​​​​ }, 2000); ​​​​​​​​​​​​ } ​​​​​​​​​​​​}

          (參考程式碼: https://stackblitz.com/edit/angular-ivy-gyffx7?file=src/app/app.component.ts)
          上述標記雖然不是直接觸發變更檢測,但因為標記了需要被檢查,所以當 resource 改變時,一併觸發了變更檢測,然後檢測到需要被檢測的值,就進行了檢測,所以刷新 View。

NgModel 與 Form

  • Reference:

  • NgModel 的作用為何 ?

    Creates a FormControl instance from a domain model and binds it to a form control element.
    (引用自: https://angular.io/api/forms/NgModel#see-also)

    白話文:
    NgModel 創建 FormControl 實例,並且把他綁定到表單控管元件 (form control element)

  • 不同 ngModel 相關的用法

    • 單獨 ngModel + form 的用法

      用途:

      單獨使用 ngModel 的作用是指 "要向該組件加入一個 property,其 key 會是組件的 name 屬性值,value 為空的字串",這也就是為什麼需要存在 name 的原因。(反之,如果 ngModel 沒有賦值的話,必須要存在 name 的屬性)

      使用情境:

      提交時,想要讓 input 有初始值,最終提交是使用父表單的值。 (沒有需要針對 Model Value 做操作的情況下使用)

      範例程式碼:

      ​​​​​​<form #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate> ​​​​​​ <input name="first" ngModel required #first="ngModel"> ​​​​​​ <br> ​​​​​​ <!-- 如下單獨使用 ngModel --> ​​​​​​ <input name="last" ngModel> ​​​​​​ <br> ​​​​​​ <button>Submit</button> ​​​​​​</form>

      (參考程式碼: https://stackblitz.com/edit/angular-ivy-cxz5zk?file=src/app/app.component.ts)

    • 雙向綁定 NgModel + form

      使用情境:
      提交時,想要將模型 data Model 的值做提交,我們可以使用 NgModel 雙向綁定。

      (參考程式碼: https://stackblitz.com/edit/angular-ivy-cxz5zk?file=src/app/app.component.ts)

  • ngmodel v.s. ng-model 的差別

    Q:
    https://stackoverflow.com/questions/44299126/is-there-a-difference-between-ng-model-and-ngmodel-in-angular-2

    Stackoverflow 解答:

    No, there is no difference, but the one in Angular gives you more flexibility than the one in AngularJS.

    [( in Angular is signalling a two-way data binding. Theoretically you could only bind to an event ((ngModel)) or to a value ([ngModel]). This gives you the ability to handle changes going down in a different way than changes coming up. With AngularJS you do not have that flexibility.


(自己查閱的筆記)

自訂指令

屬性指令

  • 改變元素樣式
    自訂變更樣式的屬性指令:
@Directive({
    selector: "button[appCustomButton]"
})

// 注入 ElementRef 來取得此指令的宿主元素
// 透過注入 Renderer2 針對 ElementRef 的 nativeElement 屬性設定所需要的樣式內容

export class CustomButtonDirective {
    constructor(
        private elRef: ElementRef,
        private renderer: Renderer2
    ) {}
    ngOnInit(): void {
        this.renderer.setStyle(elRef.nativeElement, 'padding', '10px');
    }
}
// template
<button appCustomButton>這是 custom button</button>

如果需要取得自訂元素的指令實體的話,需要記得定義 @ViewChild 以及exportAs 並且設定範本樣式變數才能取得

// component 記得設定參考實體
@ViewChild('button', { static: true }) button!: any;
@Directive({
    selector: '略',
    exportAs: 'customButton'
})
// template
<button type="button" #button="customButton" appCustomButton>
    自訂 Button
</button>
  • 改變元素行為
    若要改變元素行為,需要設定避免預設行為以及指令內的 binding (HostListener)
    針對 directive 做 input 設定以及 output 設定,可以先攔截預設行爲之後再執行相要進行的操作
@Input('appButtonConfirm') message!: string;
@Output() confirm = new EventEmitter<void>();

@HostListener('click', ['$event'])
clickEvent(event: Event) {
    event.preventDefault();
    event.stopPropagation();
    if (confirm(this.message)) {
        this.confirm.emit();
    }
}

review(output): https://hsuchihting.github.io/angular/20210304/1004423002/

結構指令

在使用結構指令前,需要先介紹 ViewContainer。
在 Angular 的世界中,View 是一個應用程式 UI 的基本組成。他是最小的 element 組成單位,在同一個 view 中的 element 會同時被新增或同時被摧毀 (destroyed)。Angular 建議開發者把 UI 視為 Views 的組合,而不是 HTML Tag 樹狀結構的一部分。Angular 支援兩種不同類型的 View:

ViewContainerRef 表示 Container 可以 attach 一到多個 view,而任何 DOM 都可以被當作 View Container

  • 權限顯示
@Directive({
    selector: ''
})

HostBinding

  • 可以用來繫結宿主元素的屬性
  • 作用:
    期望透過外部來決定改變的文字顏色,針對特殊的動作做 HostListener 針對特定 class 用 HostBinding 來做綁定
  • 範例:
    ​​// 此處透過 class.overing 與 isOvering 做綁定 ​​@HostBinding('class.overing') ​​isOvering = false; ​​@HostListener('mouseover') ​​onMouseOver() { ​​ this.isOvering = true; ​​} ​​@HostListener('mouseout') ​​onMouseOut() { ​​ this.isOvering = false; ​​}

View 封裝

回顧到 @Component 需要設定的 attribute 上

@Component({ selector: 'my-tag-name', templateUrl: './my-tag-name.component.html', encapsulation: ViewEncapsulation.Emulated, styleUrls: './my-tag-name.component.css' })

可以看到上述的 component 是一個 template 對應到一個 css
而 encapsulation 對應到的預設值是 Emulated

參數說明:

  • encapsulation: ViewEncapsulation.Emulated

    此處的 Emulated 正意味著 Angular 會透過 shadow css 的方式來讓本身組件內容的 css 不會影響到其他樣式,但全域樣式可以影響到現在的樣式

  • encapsulation: ViewEncapsulation.None
    不針對頁面樣式進行封裝,而是將元件樣式放在全域樣式中

  • encapsulation: ViewEncapsulation.ShadowDom
    使用瀏覽器原生的 ShadowDom 機制,元件的檢視與樣式會被放在 Shadow Dom,讓元件樣式不會影響到其他元件,也讓全域性的樣式設定無法影響到此元件樣式


Bad practice you should avoid with Angular