Angular Input和Output
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)]
可以拆分成兩部分modelName
和modelNameChange
,[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 - 表單未被訪問過