介紹 3 種實務上常用的方法

下拉選單為常用的使用者介面,該如何優雅地將資料綁定在元件上,並且優雅地取得使用者的選擇資料呢?

Version


Angular CLI 1.1.2
Angular 4.2.3

Introudction


select000

將實作出一下拉選單,其顯示資料來自於資料綁定,當使用者有不同的選擇,會將其值顯示在 select 下方。

將示範 3 種實作方式:

  • 使用 DOM event
  • 使用 Template Reference Variable
  • 使用 Two-Way Binding

使用 DOM event 物件


src/app/app.component.html1 1GitHub Commit : app.component.html

1
2
3
4
5
<select (change)="onChange($event)">
<option *ngFor="let cloud of clouds" [value]="cloud.id">{{ cloud.name }}</option>
</select>
<p></p>
{{ selectedId }}

第 2 行

1
<option *ngFor="let cloud of clouds" [value]="cloud.id">{{ cloud.name }}</option>

使用 *ngFor 這個 structure directive 重複顯示 <option>,其中 clouds 型別為 Cloud[],每一筆資料 cloudCloud ViewModel,有 nameid 兩個欄位,稍後會看到 Cloud ViewModel 的定義。

第 1 行

1
<select (change)="onChange($event)">

(change) 為 event binding,當 change event 被觸發時,執行 onChange() event handler。

$event 為 event object,若 event 為原生的 DOM event,則 $event 為 DOM event object,擁有 targettarget.value 等 property。

$event 以參數傳進 onChange()

在原生 JavaScript 中,event 物件可直接使用,不需要前面加上 $,但在 Angular 的 HTML template 中,若要使用 event object,Angular 規定要從 event 改成 $event,Angular 在底層另有處理,暫時只能當語法背起來。

第 5 行

1
{{ selectedId }}

顯示 select 所選擇的 value,即 cloud.id

src/app/app.component.ts2 2GitHub Commit : app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Component } from '@angular/core';
import { Cloud } from './cloud';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
clouds: Cloud[] = [
{id: 0, name: 'AWS'},
{id: 1, name: 'Azure'},
{id: 2, name: 'Aliyun'},
];

selectedId: number;

onChange(event: Event) {
this.selectedId = +(<HTMLSelectElement>event.target).value;
}
}

第 10 行

1
2
3
4
5
clouds: Cloud[] = [
{id: 0, name: 'AWS'},
{id: 1, name: 'Azure'},
{id: 2, name: 'Aliyun'},
];

clouds 為 select 欲作 data binding 的資料,實務上此資料會透過 API 取得,在此為了簡化起見,先直接 hardcode 一個陣列。

16 行

1
selectedId: number;

宣告 selectedCloudIdnumber 型別,雖然也可以宣告為 string,但因為 idcloud 宣告為 number 型別,所以 selectedId 也宣告為 number 型別較合適。

18 行

1
2
3
onChange(event: Event) {
this.selectCloudId = +(<HTMLSelectElement>event.target).value;
}

onChange 為 select change event 的 event handler,其中 event 為 HTML template 傳進來的 $event,型別為 Event

event.targetlib.es6.d.ts 定義的型別為 EventTarget,但我們知道其本質型別為 HTMLSelectElement,因此使用 type assertion 加上 <HTMLSelectElement>event target 轉型成 HTMLSelectElement,則 intellisense 就會有 value 可選,不過 value 的型別為 string,因此要再加上 +string 轉成 number

src/app/cloud.ts3 3GitHub Commit : cloud.ts

1
2
3
4
export interface Cloud {
id: number,
name: string
}

宣告 Cloud 的 ViewModel,idnumbernamestring

使用 Template Reference Variable


src/app/app.component.html4 4GitHub Commit : app.component.html

1
2
3
4
5
<select (change)="onChange(mySelect)" #mySelect>
<option *ngFor="let cloud of clouds" [value]="cloud.id">{{ cloud.name }}</option>
</select>
<p></p>
{{ selectedId }}

第 1 行

1
<select (change)="onChange(mySelect)" #mySelect>

原本 onChange() 是傳進 $event,這裡改傳 mySelect

# 為 template reference variable,我們可以在 HTML template 內,對任意 HTML element 加上 # 與變數名稱,Angular 會自動幫我們對該 element 建立物件。

src/app/app.component.ts4 4GitHub Commit : app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Component } from '@angular/core';
import { Cloud } from './cloud';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
clouds: Cloud[] = [
{id: 0, name: 'AWS'},
{id: 1, name: 'Azure'},
{id: 2, name: 'Aliyun'},
];

selectedId: number;

onChange(element: HTMLSelectElement) {
this.selectedId = +element.value;
}
}

18 行

1
2
3
onChange(element: HTMLSelectElement) {
this.selectedId = +element.value;
}

onChange() 改接收 template reference variable 後,因為我們確定 onChange() 為 select 的 event handler,所以傳進的 element 型別必為 HTMLSelectElement

由於 element.value 型別為 string,必須加上 + 轉型為 number

使用 Two-Way Binding


src/app/app.module.ts5 5GitHub Commit : app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { FormsModule } from "@angular/forms";

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

11 行

1
2
3
4
imports: [
BrowserModule,
FormsModule
],

要使用 two-way binding,必須在 AppModule 手動 import FormsModule

src/app/app.component.html5 5GitHub Commit : app.component.html

1
2
3
4
5
<select [(ngModel)]="selectedId">
<option *ngFor="let cloud of clouds" [value]="cloud.id">{{ cloud.name }}</option>
</select>
<p></p>
{{ selectedId }}

使用 [(ngModel)] 直接 two-way binding 到 selectedId,其他都可以拿掉。

src/app/app.component.ts6 6GitHub Commit : app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {Component} from '@angular/core';
import {Cloud} from './cloud';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
clouds: Cloud[] = [
{id: 0, name: 'AWS'},
{id: 1, name: 'Azure'},
{id: 2, name: 'Aliyun'},
];

selectedId: number;
}

由於使用了 two-way binding,所有的 event handler 也可以拿掉,當 select 選擇改變時,自動會改變 selectedId

實務上該使用哪種寫法?

雖然表面上 two-way binding 的寫法最精簡,若以物件導向強型別觀點,template reference variable 的寫法語意較佳

  • 明確將物件傳入 event handler 當中。
  • Event handler 的參數可明確宣告物件型別加以檢查。
  • 取得物件的值較直觀,不必搭配 type assertion。

Conclusion


  • 仍然可以在 HTML template 使用 DOM 的 event 物件,但必須加上event 前面加上 $ 變成 $event
  • Template reference variable 技巧在實務上常常使用,可隨時在 HTML template 中將 HTML element 宣告成變數傳入 event handler。
  • Two-way binding 實際上是個 syntax sugur,Angular 會展開實作 (ngModelChange) event handler。

Sample Code


完整的範例可以在我的 GitHub 上找到。

2017-07-02