Angular 2 Output
Output 是屬性裝飾器,用來定義元件内的輸出屬性。在 Angular 2 Input 文章中,我們介紹了 Input 裝飾器的作用,也了解了當應用啟動時,Angular 會從根元件開始啟動,并解析整棵元件樹,資料由上而下流下下一級子元件。而我們今天介紹的 Output 裝飾器,是用來實作子元件将資訊通過事件的形式通知到父級元件。具體如下圖所示:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI4VGbjlGdyF2XkZTN5YDZmV2NyQGO10SNzgTOyAjN2YTMvwlMwYzLcZjNx8CXt92YuQHb1FmZ05WZtdWZz5yYpRXY0NXLldWYtl2Lc9CX6MHc0RHaiojIsJye.jpg)
在介紹 Output 屬性裝飾器前,我們先來介紹一下
EventEmitter
這個幕後英雄:
EventEmitter 用來觸發自定義事件,具體使用示例如下:
let numberEmitter: EventEmitter<number> = new EventEmitter<number>();
numberEmitter.subscribe((value: number) => console.log(value));
numberEmitter.emit(10);
在 Angular 2 中的 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}`;
}
}
@Component() - outputs
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>
`,
outputs: ['change:countChange']
})
export class CounterComponent {
@Input() count: number = 0;
change: EventEmitter<number> = new EventEmitter<number>();
increment() {
this.count++;
this.change.emit(this.count);
}
decrement() {
this.count--;
this.change.emit(this.count);
}
}
Two-Way Data Binding
在介紹雙向綁定之前,我們先來說個需求:即在 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 2 中
[]
實作了模型到視圖的資料綁定,
()
實作了視圖到模型的事件綁定。兩個結合在一起
[()]
就實作了雙向綁定。也被稱為
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 2 中有對應的指令麼 ?答案是有滴,它就是 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 - 表單未被通路過
我有話說
1.ngModel指令定義
@Directive({
selector: '[ngModel]:not([formControlName]):not([formControl])',
providers: [formControlBinding],
exportAs: 'ngModel'
})
export class NgModel extends NgControl implements OnChanges,
OnDestroy { ... }
2.不能同時使用 @Output 裝飾器 或在 @Directive、@Component outputs 字段中定義同一個輸入屬性,具體示例如下:
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'exe-counter',
outputs: ['change:countChange']
})
export class CounterComponent {
@Output('countChange') change: EventEmitter<number> = new EventEmitter<number>();
}
[email protected] vs outputs
相同點:
- 它們都是用來定義輸出屬性
異同點:
- outputs 定義在指令的 metadata 資訊中,開發者對指令的輸入屬性一目了然。此外對于未選用 TypeScript 作為開發語言的開發者,也隻能在 metadata 中定義指令的輸入屬性。
- @Output 屬于屬性裝飾器,通過它我們可以一起定義屬性的通路描述符 (public、private、protected):
@Output('countChange') change: EventEmitter<number> = new EventEmitter<number>();
4.輸入、輸出屬性風格指南
4.1 堅持使用 @Input 和 @Output ,而非 @Directive 和 @Component 裝飾器的 inputs 和 outputs 屬性:
- 易于在類裡面識别哪些屬性是輸入屬性或輸出屬性。
4.2 堅持把 @Input 或者 @Output 放到所裝飾的屬性的同一行:
- 如果需要重命名與
或者@Input
關聯的屬性或事件名,你可以在一個位置修改。@Output
4.3 避免為輸入和輸出屬性指定别名
- 同一個屬性有兩個名字(一個對内一個對外)很容易導緻混淆。
詳細資訊請參考 - Angular 2 風格指南 - STYLE 05-12
5.父元件什麼時候訂閱子元件
EventEmitter
類型的輸出屬性 ?
AppComponent/component.ngfactory.js
CounterComponent/wrapper.ngfactory.js