当咱们打算自定义表单控件前,咱们应该先考虑一下如下问题: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 }); } }
以上代码成功运行后,浏览器页面的显示结果以下: