Angular Input和Output

  • 2019 年 11 月 6 日
  • 筆記

Input 是屬性裝飾器,用來定義組件內的輸入屬性。在實際應用場合,我們主要用來實現父組件向子組件傳遞數據。Angular 應用是由各式各樣的組件組成,當應用啟動時,Angular 會從根組件開始啟動,並解析整棵組件樹,數據由上而下流下下一級子組件。

@Input()

counter.component.ts

import { Component, Input } from '@angular/core';    @Component({      selector: 'exe-counter',      template: `        <p>當前值: {{ count }}</p>        <button (click)="increment()"> + </button>        <button (click)="decrement()"> - </button>      `  })  export class CounterComponent {      @Input() count: number = 0;        increment() {          this.count++;      }        decrement() {          this.count--;      }  }

app.component.ts

import { Component } from '@angular/core';    @Component({    selector: 'exe-app',    template: `     <exe-counter [count]="initialCount"></exe-counter>    `  })  export class AppComponent {    initialCount: number = 5;  }

@Input(『bindingPropertyName』)

Input 裝飾器支援一個可選的參數,用來指定組件綁定屬性的名稱。如果沒有指定,則默認使用 @Input 裝飾器,裝飾的屬性名。具體示例如下:

counter.component.ts

import { Component, Input } from '@angular/core';    @Component({      selector: 'exe-counter',      template: `        <p>當前值: {{ count }}</p>        <button (click)="increment()"> + </button>        <button (click)="decrement()"> - </button>      `  })  export class CounterComponent {      @Input('value') count: number = 0;  	... // 其餘程式碼未改變  }

app.component.ts

import { Component } from '@angular/core';    @Component({    selector: 'exe-app',    template: `     <exe-counter [value]="initialCount"></exe-counter>    `  })  export class AppComponent {    initialCount: number = 5;  }

setter & getter

setter 和 getter 是用來約束屬性的設置和獲取,它們提供了一些屬性讀寫的封裝,可以讓程式碼更便捷,更具可擴展性。通過 setter 和 getter 方式,我們對類中的私有屬性進行了封裝,能避免外界操作影響到該私有屬性。此外通過 setter 我們還可以封裝一些業務邏輯,具體示例如下:

counter.component.ts

import { Component, Input } from '@angular/core';    @Component({      selector: 'exe-counter',      template: `        <p>當前值: {{ count }} </p>        <button (click)="increment()"> + </button>        <button (click)="decrement()"> - </button>      `  })  export class CounterComponent {      _count: number = 0; // 默認私有屬性以下劃線開頭,不是必須也可以使用$count      biggerThanTen: boolean = false;        @Input()      set count (num: number) {          this.biggerThanTen = num > 10;          this._count = num;      }        get count(): number {          return this._count;      }        increment() {          this.count++;      }        decrement() {          this.count--;      }  }

ngOnChanges

當數據綁定輸入屬性的值發生變化的時候,Angular 將會主動調用 ngOnChanges 方法。它會獲得一個 SimpleChanges 對象,包含綁定屬性的新值和舊值,它主要用於監測組件輸入屬性的變化。具體示例如下:

import { Component, Input, SimpleChanges, OnChanges } from '@angular/core';    @Component({      selector: 'exe-counter',      template: `        <p>當前值: {{ count }}</p>        <button (click)="increment()"> + </button>        <button (click)="decrement()"> - </button>      `  })  export class CounterComponent implements OnChanges{      @Input() count: number = 0;        ngOnChanges(changes: SimpleChanges) {          console.dir(changes['count']);      }        increment() {          this.count++;      }        decrement() {          this.count--;      }  }

上面例子中需要注意的是,當手動改變輸入屬性的值,是不會觸發 ngOnChanges 鉤子的。


Output 是屬性裝飾器,用來定義組件內的輸出屬性。前面我們介紹了 Input 裝飾器的作用,也了解了當應用啟動時,Angular 會從根組件開始啟動,並解析整棵組件樹,數據由上而下流下下一級子組件。而我們今天介紹的 Output 裝飾器,是用來實現子組件將資訊通過事件的形式通知到父級組件。

在介紹 Output 屬性裝飾器前,我們先來介紹一下 EventEmitter 這個幕後英雄。它用來觸發自定義事件,具體使用示例如下:

let numberEmitter: EventEmitter<number> = new EventEmitter<number>();  numberEmitter.subscribe((value: number) => console.log(value));  numberEmitter.emit(10);

在 Angular 中的 EventEmitter 應用場景是:

子指令創建一個 EventEmitter 實例,並將其作為輸出屬性導出。子指令調用已創建的 EventEmitter 實例中的 emit(payload) 方法來觸發一個事件,父指令通過事件綁定 (eventName) 的方式監聽該事件,並通過 $event 對象來獲取 payload 對象。是不是感覺有點抽象,我們馬上實戰一下。

@Output()

counter.component.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';    @Component({      selector: 'exe-counter',      template: `        <p>當前值: {{ count }}</p>        <button (click)="increment()"> + </button>        <button (click)="decrement()"> - </button>      `  })  export class CounterComponent {      @Input() count: number = 0;        @Output() change: EventEmitter<number> = new EventEmitter<number>();        increment() {          this.count++;          this.change.emit(this.count);      }        decrement() {          this.count--;          this.change.emit(this.count);      }  }

app.component.ts

import { Component } from '@angular/core';    @Component({    selector: 'exe-app',    template: `     <p>{{changeMsg}}</p>     <exe-counter [count]="initialCount"      (change)="countChange($event)"></exe-counter>    `  })  export class AppComponent {    initialCount: number = 5;      changeMsg: string;      countChange(event: number) {      this.changeMsg = `子組件change事件已觸發,當前值是: ${event}`;    }  }

@Output(『bindingPropertyName』)

Output 裝飾器支援一個可選的參數,用來指定組件綁定屬性的名稱。如果沒有指定,則默認使用 @Output 裝飾器,裝飾的屬性名。具體示例如下:

counter.component.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';    @Component({      selector: 'exe-counter',      template: `        <p>當前值: {{ count }}</p>        <button (click)="increment()"> + </button>        <button (click)="decrement()"> - </button>      `  })  export class CounterComponent {      @Input() count: number = 0;        @Output('countChange') change: EventEmitter<number> = new EventEmitter<number>();  	... // 其餘程式碼未改變  }

app.component.ts

import { Component } from '@angular/core';    @Component({    selector: 'exe-app',    template: `     <p>{{changeMsg}}</p>     <exe-counter [count]="initialCount"      (countChange)="countChange($event)"></exe-counter>    `  })  export class AppComponent {    initialCount: number = 5;      changeMsg: string;      countChange(event: number) {      this.changeMsg = `子組件change事件已觸發,當前值是: ${event}`;    }  }

雙向綁定

在介紹雙向綁定之前,我們先來說個需求:即在 CounterComponent 子組件 count 值發生變化的時候,需同步更新 AppComponent 父組件中的 initialCount 的值。通過上面的實例,我們知道我們可以在 AppComponent 父組件中監聽 CounterComponent 子組件的 change 事件,然後在 change 事件中更新 initialCount 的值。具體示例如下:

counter.component.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';    @Component({      selector: 'exe-counter',      template: `        <p>子組件當前值: {{ count }}</p>        <button (click)="increment()"> + </button>        <button (click)="decrement()"> - </button>      `  })  export class CounterComponent {      @Input() count: number = 0;        @Output() change: EventEmitter<number> = new EventEmitter<number>();        increment() {          this.count++;          this.change.emit(this.count);      }        decrement() {          this.count--;          this.change.emit(this.count);      }  }

app.component.ts

import { Component } from '@angular/core';    @Component({    selector: 'exe-app',    template: `     <p>父組件當前值:{{ initialCount }}</p>     <exe-counter [count]="initialCount"      (change)="initialCount = $event"></exe-counter>    `  })  export class AppComponent {    initialCount: number = 5;  }

其實雙向綁定是由兩個單向綁定組成:

  • 模型 -> 視圖數據綁定
  • 視圖 -> 模型事件綁定

Angular 中 [] 實現了模型到視圖的數據綁定,() 實現了視圖到模型的事件綁定。把它們兩個結合在一起 [()] 就實現了雙向綁定。也被稱為 banana in the box 語法。

[()] 語法示例

counter.component.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';    @Component({      selector: 'exe-counter',      template: `        <p>子組件當前值: {{ count }}</p>        <button (click)="increment()"> + </button>        <button (click)="decrement()"> - </button>      `  })  export class CounterComponent {      @Input() count: number = 0;        // 輸出屬性名稱變更: change -> countChange      @Output() countChange: EventEmitter<number> = new EventEmitter<number>();      ... // 其餘程式碼未改變  }

app.component.ts

import { Component } from '@angular/core';    @Component({    selector: 'exe-app',    template: `     <p>父組件當前值:{{ initialCount }}</p>     <exe-counter [(count)]="initialCount"></exe-counter>    `  })  export class AppComponent {    initialCount: number = 5;  }

從上面可以看出,[(modelName)] 可以拆分成兩部分 modelNamemodelNameChange[modelName] 用於綁定輸入屬性,(modelNameChange) 用於綁定輸出屬性。當 Angular 在解析模板時,遇到 [(modelName)] 形式的綁定語法,它會期待這個指令中會存在一個名為 modelName 的輸入屬性和一個名為 modelNameChange 的輸出屬性。

ngModel

使用過 Angular 1.x 的讀者,應該很熟悉 ng-model 這個指令,我們通過它來實現數據的雙向綁定。那麼在 Angular 中有對應的指令么 ?答案是有滴,它就是 ngModel 指令。

ngModel雙向綁定示例

import { Component } from '@angular/core';    @Component({    selector: 'exe-app',    template: `     <p>你輸入的用戶名是:{{ username }}</p>     <input type="text" [(ngModel)]="username" />     `  })  export class AppComponent {    username: string = '';  }

ngModel表單驗證示例

import { Component } from '@angular/core';    @Component({    selector: 'exe-app',    styles:[      `.error { border: 1px solid red;}`    ],    template: `     <p>你輸入的用戶名是:{{ username }}</p>     <input type="text"        [(ngModel)]="username"        #nameModel="ngModel"        [ngClass]="{error: nameModel.invalid}"        required/>     {{nameModel.errors | json}}     `  })  export class AppComponent {    username: string = '';  }

以上示例利用 @Directive 指令 metadata 資訊中的 exportAs 屬性,獲取 ngModel 實例,進行獲取控制項的狀態,控制項狀態分類如下:

  • valid – 表單值有效
  • pristine – 表單值未改變
  • dirty – 表單值已改變
  • touched – 表單已被訪問過
  • untouched – 表單未被訪問過