當咱們打算自定義表單控件前,咱們應該先考慮一下如下問題:javascript
是否已經有相同語義的 native (本機) 元素?如:<input type="number">
html
若是有,咱們就應該考慮可否依賴該元素,僅使用 CSS 或漸進加強的方式來改變其外觀/行爲就能知足咱們的需求?html5
若是沒有,自定義控件會是什麼樣的?java
咱們如何讓它能夠訪問 (accessible)?react
在不一樣平臺上自定義控件的行爲是否有所不一樣?typescript
自定義控件如何實現數據驗證功能?shell
可能還有不少事情須要考慮,但若是咱們決定使用 Angular 建立自定義控件,就須要考慮如下問題:json
如何實現 model -> view 的數據綁定?bootstrap
如何實現 view -> model 的數據同步?segmentfault
若須要自定義驗證,應該如何實現?
如何向DOM元素添加有效性狀態,便於設置不一樣樣式?
如何讓控件能夠訪問 (accessible)?
該控件能應用於 template-driven 表單?
該控件能應用於 model-driven 表單?
(備註:主要瀏覽器上 HTML 5 當前輔助功能支持狀態,能夠參看 - HTML5 Accessibility)
如今咱們從最簡單的 Counter 組件開始,具體代碼以下:
counter.component.ts
import { Component, Input } from '@angular/core'; @Component({ selector: 'exe-counter', template: ` <div> <p>當前值: {{ count }}</p> <button (click)="increment()"> + </button> <button (click)="decrement()"> - </button> </div> ` }) export class CounterComponent { @Input() count: number = 0; increment() { this.count++; } decrement() { this.count--; } }
app.component.ts
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'exe-app', template: ` <exe-counter></exe-counter> `, }) export class AppComponent { }
app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { CounterComponent } from './couter.component'; import { AppComponent } from './app.component'; @NgModule({ imports: [BrowserModule], declarations: [AppComponent, CounterComponent], bootstrap: [AppComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class AppModule { }
很好,CounterComponent 組件很快就實現了。但如今咱們想在 Template-Driven
或 Reactive
表單中使用該組件,具體以下:
<!-- this doesn't work YET --> <form #form="ngForm"> <exe-counter name="counter" ngModel></exe-counter> <button type="submit">Submit</button> </form>
如今咱們還不能直接這麼使用,要實現該功能。咱們要先搞清楚 ControlValueAccessor
,由於它是表單模型和DOM 元素之間的橋樑。
當咱們運行上面示例時,瀏覽器控制檯中將輸出如下異常信息:
Uncaught (in promise): Error: No value accessor for form control with name: 'counter'
那麼,ControlValueAccessor
是什麼?那麼大家還記得咱們以前提到的實現自定義控件須要確認的事情麼?其中一個要確認的事情就是,要實現 Model -> View,View -> Model 之間的數據綁定,而這就是咱們 ControlValueAccessor 要處理的問題。
ControlValueAccessor 是一個接口,它的做用是:
把 form 模型中值映射到視圖中
當視圖發生變化時,通知 form directives 或 form controls
Angular 引入這個接口的緣由是,不一樣的輸入控件數據更新方式是不同的。例如,對於咱們經常使用的文本輸入框來講,咱們是設置它的 value
值,而對於複選框 (checkbox) 咱們是設置它的 checked
屬性。實際上,不一樣類型的輸入控件都有一個 ControlValueAccessor
,用來更新視圖。
Angular 中常見的 ControlValueAccessor 有:
DefaultValueAccessor - 用於 text
和 textarea
類型的輸入控件
SelectControlValueAccessor - 用於 select
選擇控件
CheckboxControlValueAccessor - 用於 checkbox
複選控件
接下來咱們的 CounterComponent 組件須要實現 ControlValueAccessor
接口,這樣咱們才能更新組件中 count 的值,並通知外界該值已發生改變。
首先咱們先看一下 ControlValueAccessor
接口,具體以下:
// angular2/packages/forms/src/directives/control_value_accessor.ts export interface ControlValueAccessor { writeValue(obj: any): void; registerOnChange(fn: any): void; registerOnTouched(fn: any): void; setDisabledState?(isDisabled: boolean): void; }
writeValue(obj: any):該方法用於將模型中的新值寫入視圖或 DOM 屬性中。
registerOnChange(fn: any):設置當控件接收到 change 事件後,調用的函數
registerOnTouched(fn: any):設置當控件接收到 touched 事件後,調用的函數
setDisabledState?(isDisabled: boolean):當控件狀態變成 DISABLED
或從 DISABLED
狀態變化成 ENABLE
狀態時,會調用該函數。該函數會根據參數值,啓用或禁用指定的 DOM 元素。
接下來咱們先來實現 writeValue()
方法:
@Component(...) class CounterComponent implements ControlValueAccessor { ... writeValue(value: any) { this.counterValue = value; } }
當表單初始化的時候,將會使用表單模型中對應的初始值做爲參數,調用 writeValue()
方法。這意味着,它會覆蓋默認值0,一切看來都沒問題。但咱們回想一下在表單中 CounterComponent 組件預期的使用方式:
<form #form="ngForm"> <exe-counter name="counter" ngModel></exe-counter> <button type="submit">Submit</button> </form>
你會發現,咱們沒有爲 CounterComponent 組件設置初始值,所以咱們要調整一下 writeValue() 中的代碼,具體以下:
writeValue(value: any) { if (value) { this.count = value; } }
如今,只有當合法值 (非 undefined、null、"") 寫入控件時,它纔會覆蓋默認值。接下來,咱們來實現 registerOnChange()
和 registerOnTouched()
方法。registerOnChange() 能夠用來通知外部,組件已經發生變化。registerOnChange() 方法接收一個 fn
參數,用於設置當控件接收到 change 事件後,調用的函數。而對於 registerOnTouched() 方法,它也支持一個 fn
參數,用於設置當控件接收到 touched 事件後,調用的函數。示例中咱們不打算處理 touched
事件,所以 registerOnTouched() 咱們設置爲一個空函數。具體以下:
@Component(...) class CounterComponent implements ControlValueAccessor { ... propagateChange = (_: any) => {}; registerOnChange(fn: any) { this.propagateChange = fn; } registerOnTouched(fn: any) {} }
很好,咱們的 CounterComponent 組件已經實現了ControlValueAccessor 接口。接下來咱們須要作的是在每次count 的值改變時,須要調用 propagateChange() 方法。換句話說,當用戶點擊了 +
或 -
按鈕時,咱們但願將新值傳遞到外部。
@Component(...) export class CounterComponent implements ControlValueAccessor { ... increment() { this.count++; this.propagateChange(this.count); } decrement() { this.count--; this.propagateChange(this.count); } }
是否是感受上面代碼有點冗餘,接下來咱們來利用屬性修改器,重構一下以上代碼,具體以下:
counter.component.ts
import { Component, Input } from '@angular/core'; import { ControlValueAccessor } from '@angular/forms'; @Component({ selector: 'exe-counter', template: ` <p>當前值: {{ count }}</p> <button (click)="increment()"> + </button> <button (click)="decrement()"> - </button> ` }) export class CounterComponent implements ControlValueAccessor { @Input() _count: number = 0; get count() { return this._count; } set count(value: number) { this._count = value; this.propagateChange(this._count); } propagateChange = (_: any) => { }; writeValue(value: any) { if (value !== undefined) { this.count = value; } } registerOnChange(fn: any) { this.propagateChange = fn; } registerOnTouched(fn: any) { } increment() { this.count++; } decrement() { this.count--; } }
CounterComponent 組件已經基本開發好了,但要能正常使用的話,還須要執行註冊操做。
對於咱們開發的 CounterComponent 組件來講,實現 ControlValueAccessor 接口只完成了一半工做。要讓 Angular 可以正常識別咱們自定義的 ControlValueAccessor
,咱們還須要執行註冊操做。具體方式以下:
步驟一:建立 EXE_COUNTER_VALUE_ACCESSOR
import { Component, Input, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; export const EXE_COUNTER_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CounterComponent), multi: true };
友情提示:想了解 forwardRef 和 multi 的詳細信息,請參考 Angular 2 Forward Reference 和 Angular 2 Multi Providers 這兩篇文章。
步驟二:設置組件的 providers 信息
@Component({ selector: 'exe-counter', ... providers: [EXE_COUNTER_VALUE_ACCESSOR] })
萬事俱備只欠東風,咱們立刻進入實戰環節,實際檢驗一下咱們開發的 CounterComponent
組件。完整代碼以下:
counter.component.ts
import { Component, Input, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; export const EXE_COUNTER_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CounterComponent), multi: true }; @Component({ selector: 'exe-counter', template: ` <div> <p>當前值: {{ count }}</p> <button (click)="increment()"> + </button> <button (click)="decrement()"> - </button> </div> `, providers: [EXE_COUNTER_VALUE_ACCESSOR] }) export class CounterComponent implements ControlValueAccessor { @Input() _count: number = 0; get count() { return this._count; } set count(value: number) { this._count = value; this.propagateChange(this._count); } propagateChange = (_: any) => { }; writeValue(value: any) { if (value) { this.count = value; } } registerOnChange(fn: any) { this.propagateChange = fn; } registerOnTouched(fn: any) { } increment() { this.count++; } decrement() { this.count--; } }
Angular 4.x 中有兩種表單:
Template-Driven Forms - 模板驅動式表單 (相似於 Angular 1.x 中的表單 )
Reactive Forms - 響應式表單
瞭解 Angular 4.x Template-Driven Forms 詳細信息,請參考 - Angular 4.x Template-Driven Forms。接下來咱們來看一下具體如何使用:
app.module.ts
import { FormsModule } from '@angular/forms'; @NgModule({ imports: [BrowserModule, FormsModule], ... }) export class AppModule { }
app.component.ts
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'exe-app', template: ` <form #form="ngForm"> <exe-counter name="counter" ngModel></exe-counter> </form> <pre>{{ form.value | json }}</pre> `, }) export class AppComponent { }
友情提示:上面示例代碼中,form.value 用於獲取表單中的值,json 是 Angular 內置管道,用於執行對象序列化操做 (內部實現 - JSON.stringify(value, null, 2))。若想了解 Angular 管道詳細信息,請參考 - Angular 2 Pipe。
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'exe-app', template: ` <form #form="ngForm"> <exe-counter name="counter" [ngModel]="outerCounterValue"></exe-counter> </form> <pre>{{ form.value | json }}</pre> `, }) export class AppComponent { outerCounterValue: number = 5; }
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'exe-app', template: ` <form #form="ngForm"> <p>outerCounterValue value: {{outerCounterValue}}</p> <exe-counter name="counter" [(ngModel)]="outerCounterValue"></exe-counter> </form> <pre>{{ form.value | json }}</pre> `, }) export class AppComponent { outerCounterValue: number = 5; }
瞭解 Angular 4.x Reactive (Model-Driven) Forms 詳細信息,請參考 - Angular 4.x Reactive Forms。接下來咱們來看一下具體如何使用:
app.module.ts
import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [BrowserModule, ReactiveFormsModule], ... }) export class AppModule { }
import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; @Component({ selector: 'exe-app', template: ` <form [formGroup]="form"> <exe-counter formControlName="counter"></exe-counter> </form> <pre>{{ form.value | json }}</pre> `, }) export class AppComponent { form: FormGroup; constructor(private fb: FormBuilder) { } ngOnInit() { this.form = this.fb.group({ counter: 5 // 設置初始值 }); } }
友情提示:上面代碼中咱們移除了 Template-Driven 表單中的 ngModel 和 name 屬性,取而代之是使用 formControlName 屬性。此外咱們經過 FormBuilder 對象提供的
group()
方法,建立 FromGroup 對象,而後在模板中經過[formGroup]="form"
的方式實現模型與 DOM 元素的綁定。關於 Reactive Forms 的詳細信息,請參考 Angular 4.x Reactive Forms 。
最後咱們在來看一下,如何爲咱們的自定義控件,添加驗證規則。
在 Angular 4.x 基於AbstractControl自定義表單驗證 這篇文章中,咱們介紹瞭如何自定義表單驗證。而對於咱們自定義控件來講,添加自定義驗證功能 (限制控件值的有效範圍:0 <= value <=10),也很方便。具體示例以下:
export const validateCounterRange: ValidatorFn = (control: AbstractControl): ValidationErrors => { return (control.value > 10 || control.value < 0) ? { 'rangeError': { current: control.value, max: 10, min: 0 } } : null; };
export const EXE_COUNTER_VALIDATOR = { provide: NG_VALIDATORS, useValue: validateCounterRange, multi: true };
接下來咱們更新一下 AppComponent 組件,在組件模板中顯示異常信息:
@Component({ selector: 'exe-app', template: ` <form [formGroup]="form"> <exe-counter formControlName="counter"></exe-counter> </form> <p *ngIf="!form.valid">Counter is invalid!</p> <pre>{{ form.get('counter').errors | json }}</pre> `, })
CounterComponent 組件的完整代碼以下:
counter.component.ts
import { Component, Input, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, AbstractControl, ValidatorFn, ValidationErrors, FormControl } from '@angular/forms'; export const EXE_COUNTER_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CounterComponent), multi: true }; export const validateCounterRange: ValidatorFn = (control: AbstractControl): ValidationErrors => { return (control.value > 10 || control.value < 0) ? { 'rangeError': { current: control.value, max: 10, min: 0 } } : null; }; export const EXE_COUNTER_VALIDATOR = { provide: NG_VALIDATORS, useValue: validateCounterRange, multi: true }; @Component({ selector: 'exe-counter', template: ` <div> <p>當前值: {{ count }}</p> <button (click)="increment()"> + </button> <button (click)="decrement()"> - </button> </div> `, providers: [EXE_COUNTER_VALUE_ACCESSOR, EXE_COUNTER_VALIDATOR] }) export class CounterComponent implements ControlValueAccessor { @Input() _count: number = 0; get count() { return this._count; } set count(value: number) { this._count = value; this.propagateChange(this._count); } propagateChange = (_: any) => { }; writeValue(value: any) { if (value) { this.count = value; } } registerOnChange(fn: any) { this.propagateChange = fn; } registerOnTouched(fn: any) { } increment() { this.count++; } decrement() { this.count--; } }
除了在 CounterComponent 組件的 Metadata 配置自定義驗證器以外,咱們也能夠在建立 FormGroup
對象時,設置每一個控件 (FormControl) 對象的驗證規則。需調整的代碼以下:
counter.component.ts
@Component({ selector: 'exe-counter', ..., providers: [EXE_COUNTER_VALUE_ACCESSOR] // 移除自定義EXE_COUNTER_VALIDATOR })
app.component.ts
import { validateCounterRange } from './couter.component'; ... export class AppComponent { ... ngOnInit() { this.form = this.fb.group({ counter: [5, validateCounterRange] // 設置validateCounterRange驗證器 }); } }
自定義驗證功能咱們已經實現了,但驗證規則即數據的有效範圍是固定 (0 <= value <=10),實際上更好的方式是讓用戶可以靈活地配置數據的有效範圍。接下來咱們就來優化一下現有的功能,使得咱們開發的組件更爲靈活。
咱們自定義 CounterComponent 組件的預期使用方式以下:
<exe-counter formControlName="counter" counterRangeMax="10" counterRangeMin="0"> </exe-counter>
首先咱們須要更新一下 CounterComponent 組件,增量 counterRangeMax 和 counterRangeMin 輸入屬性:
@Component(...) class CounterInputComponent implements ControlValueAccessor { ... @Input() counterRangeMin: number; @Input() counterRangeMax: number; ... }
接着咱們須要新增一個 createCounterRangeValidator()
工廠函數,用於根據設置的最大值 (maxValue) 和最小值 (minValue) 動態的建立 validateCounterRange()
函數。具體示例以下:
export function createCounterRangeValidator(maxValue: number, minValue: number) { return (control: AbstractControl): ValidationErrors => { return (control.value > +maxValue || control.value < +minValue) ? { 'rangeError': { current: control.value, max: maxValue, min: minValue }} : null; } }
在 Angular 4.x 自定義驗證指令 文章中,咱們介紹瞭如何自定義驗證指令。要實現指令的自定義驗證功能,咱們須要實現 Validator
接口:
export interface Validator { validate(c: AbstractControl): ValidationErrors|null; registerOnValidatorChange?(fn: () => void): void; }
另外咱們應該在檢測到 counterRangeMin
和 counterRangeMax
輸入屬性時,就須要調用 createCounterRangeValidator()
方法,動態建立 validateCounterRange()
函數,而後在 validate()
方法中調用驗證函數,並返回函數調用後的返回值。是否是有點繞,咱們立刻看一下具體代碼:
import { Component, Input, OnChanges, SimpleChanges, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, Validator, AbstractControl, ValidatorFn, ValidationErrors, FormControl } from '@angular/forms'; ... export const EXE_COUNTER_VALIDATOR = { provide: NG_VALIDATORS, useExisting: forwardRef(() => CounterComponent), multi: true }; export function createCounterRangeValidator(maxValue: number, minValue: number) { return (control: AbstractControl): ValidationErrors => { return (control.value > +maxValue || control.value < +minValue) ? { 'rangeError': { current: control.value, max: maxValue, min: minValue } } : null; } } @Component({ selector: 'exe-counter', template: ` <div> <p>當前值: {{ count }}</p> <button (click)="increment()"> + </button> <button (click)="decrement()"> - </button> </div> `, providers: [EXE_COUNTER_VALUE_ACCESSOR, EXE_COUNTER_VALIDATOR] }) export class CounterComponent implements ControlValueAccessor, Validator, OnChanges { ... private _validator: ValidatorFn; private _onChange: () => void; @Input() counterRangeMin: number; // 設置數據有效範圍的最大值 @Input() counterRangeMax: number; // 設置數據有效範圍的最小值 // 監聽輸入屬性變化,調用內部的_createValidator()方法,建立RangeValidator ngOnChanges(changes: SimpleChanges): void { if ('counterRangeMin' in changes || 'counterRangeMax' in changes) { this._createValidator(); } } // 動態建立RangeValidator private _createValidator(): void { this._validator = createCounterRangeValidator(this.counterRangeMax, this.counterRangeMin); } // 執行控件驗證 validate(c: AbstractControl): ValidationErrors | null { return this.counterRangeMin == null || this.counterRangeMax == null ? null : this._validator(c); } ... }
上面的代碼很長,咱們來分解一下:
export const EXE_COUNTER_VALIDATOR = { provide: NG_VALIDATORS, useExisting: forwardRef(() => CounterComponent), multi: true }; @Component({ selector: 'exe-counter', ..., providers: [EXE_COUNTER_VALUE_ACCESSOR, EXE_COUNTER_VALIDATOR] })
export function createCounterRangeValidator(maxValue: number, minValue: number) { return (control: AbstractControl): ValidationErrors => { return (control.value > +maxValue || control.value < +minValue) ? { 'rangeError': { current: control.value, max: maxValue, min: minValue } } : null; } }
export class CounterComponent implements ControlValueAccessor, Validator, OnChanges { ... @Input() counterRangeMin: number; // 設置數據有效範圍的最大值 @Input() counterRangeMax: number; // 設置數據有效範圍的最小值 // 監聽輸入屬性變化,調用內部的_createValidator()方法,建立RangeValidator ngOnChanges(changes: SimpleChanges): void { if ('counterRangeMin' in changes || 'counterRangeMax' in changes) { this._createValidator(); } } ... }
export class CounterComponent implements ControlValueAccessor, Validator, OnChanges { ... // 動態建立RangeValidator private _createValidator(): void { this._validator = createCounterRangeValidator(this.counterRangeMax, this.counterRangeMin); } ... }
export class CounterComponent implements ControlValueAccessor, Validator, OnChanges { ... // 執行控件驗證 validate(c: AbstractControl): ValidationErrors | null { return this.counterRangeMin == null || this.counterRangeMax == null ? null : this._validator(c); } ... }
此時咱們自定義 CounterComponent 組件終於開發完成了,就差功能驗證了。具體的使用示例以下:
import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; @Component({ selector: 'exe-app', template: ` <form [formGroup]="form"> <exe-counter formControlName="counter" counterRangeMin="5" counterRangeMax="8"> </exe-counter> </form> <p *ngIf="!form.valid">Counter is invalid!</p> <pre>{{ form.get('counter').errors | json }}</pre> `, }) export class AppComponent { form: FormGroup; constructor(private fb: FormBuilder) { } ngOnInit() { this.form = this.fb.group({ counter: 5 }); } }
以上代碼成功運行後,瀏覽器頁面的顯示結果以下: