Angular 4.x 自定义表单控件
当我们打算自定义表单控件前,我们应该先考虑一下以下问题:
可能还有很多事情需要考虑,但如果我们决定使用 Angular 创建自定义控件,就需要考虑以下问题:
(备注:主要浏览器上 HTML 5 当前辅助功能支持状态,可以参看 - HTML5 Accessibility) Creating a custom counter现在我们从最简单的 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 组件很快就实现了。但现在我们想在 <!-- this doesn't work YET --> <form #form="ngForm"> <exe-counter name="counter" ngModel></exe-counter> <button type="submit">Submit</button> </form> 现在我们还不能直接这么使用,要实现该功能。我们要先搞清楚 Understanding ControlValueAccessor当我们运行上面示例时,浏览器控制台中将输出以下异常信息: Uncaught (in promise): Error: No value accessor for form control with name: 'counter' 那么, ControlValueAccessor 是一个接口,它的作用是:
Angular 引入这个接口的原因是,不同的输入控件数据更新方式是不一样的。例如,对于我们常用的文本输入框来说,我们是设置它的 Angular 中常见的 ControlValueAccessor 有:
接下来我们的 CounterComponent 组件需要实现 Implementing 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; }
接下来我们先来实现 @Component(...) class CounterComponent implements ControlValueAccessor { ... writeValue(value: any) { this.counterValue = value; } } 当表单初始化的时候,将会使用表单模型中对应的初始值作为参数,调用 <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、"") 写入控件时,它才会覆盖默认值。接下来,我们来实现 @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 组件已经基本开发好了,但要能正常使用的话,还需要执行注册操作。 Registering the ControlValueAccessor对于我们开发的 CounterComponent 组件来说,实现 ControlValueAccessor 接口只完成了一半工作。要让 Angular 能够正常识别我们自定义的
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',... providers: [EXE_COUNTER_VALUE_ACCESSOR] }) 万事俱备只欠东风,我们马上进入实战环节,实际检验一下我们开发的 counter.component.ts import { Component,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--; } } Using it inside template-driven formsAngular 4.x 中有两种表单:
了解 Angular 4.x Template-Driven Forms 详细信息,请参考 - Angular 4.x Template-Driven Forms。接下来我们来看一下具体如何使用: 1.导入 FormsModule 模块app.module.ts import { FormsModule } from '@angular/forms'; @NgModule({ imports: [BrowserModule,FormsModule],... }) export class AppModule { } 2.更新 AppComponent2.1 未设置 CounterComponent 组件初始值app.component.ts import { Component,template: ` <form #form="ngForm"> <exe-counter name="counter" ngModel></exe-counter> </form> <pre>{{ form.value | json }}</pre> `,}) export class AppComponent { }
2.2 设置 CounterComponent 组件初始值 - 使用 [ngModel] 语法import { Component,template: ` <form #form="ngForm"> <exe-counter name="counter" [ngModel]="outerCounterValue"></exe-counter> </form> <pre>{{ form.value | json }}</pre> `,}) export class AppComponent { outerCounterValue: number = 5; } 2.3 设置数据双向绑定 - 使用 [(ngModel)] 语法import { Component,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; } Using it inside reactive forms了解 Angular 4.x Reactive (Model-Driven) Forms 详细信息,请参考 - Angular 4.x Reactive Forms。接下来我们来看一下具体如何使用: 1.导入 ReactiveFormsModuleapp.module.ts import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [BrowserModule,ReactiveFormsModule],... }) export class AppModule { } 2.更新 AppComponentimport { 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 // 设置初始值 }); } }
最后我们在来看一下,如何为我们的自定义控件,添加验证规则。 Adding custom validation在 Angular 4.x 基于AbstractControl自定义表单验证 这篇文章中,我们介绍了如何自定义表单验证。而对于我们自定义控件来说,添加自定义验证功能 (限制控件值的有效范围:0 <= value <=10),也很方便。具体示例如下: 1.自定义 VALIDATOR1.1 定义验证函数export const validateCounterRange: ValidatorFn = (control: AbstractControl): ValidationErrors => { return (control.value > 10 || control.value < 0) ? { 'rangeError': { current: control.value,max: 10,min: 0 } } : null; }; 1.2 注册自定义验证器export const EXE_COUNTER_VALIDATOR = { provide: NG_VALIDATORS,useValue: validateCounterRange,multi: true }; 2.更新 AppComponent接下来我们更新一下 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,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,multi: true }; export const validateCounterRange: ValidatorFn = (control: AbstractControl): ValidationErrors => { return (control.value > 10 || control.value < 0) ? { 'rangeError': { current: control.value,min: 0 } } : null; }; export const EXE_COUNTER_VALIDATOR = { provide: NG_VALIDATORS,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 配置自定义验证器之外,我们也可以在创建 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),实际上更好的方式是让用户能够灵活地配置数据的有效范围。接下来我们就来优化一下现有的功能,使得我们开发的组件更为灵活。 Making the validation configurable我们自定义 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; ... } 接着我们需要新增一个 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 自定义验证指令 文章中,我们介绍了如何自定义验证指令。要实现指令的自定义验证功能,我们需要实现 export interface Validator { validate(c: AbstractControl): ValidationErrors|null; registerOnValidatorChange?(fn: () => void): void; } 另外我们应该在检测到 import { Component,OnChanges,SimpleChanges,Validator,FormControl } from '@angular/forms'; ... export const EXE_COUNTER_VALIDATOR = { provide: NG_VALIDATORS,multi: true }; export function createCounterRangeValidator(maxValue: number,minValue: number) { return (control: AbstractControl): ValidationErrors => { return (control.value > +maxValue || control.value < +minValue) ? { 'rangeError': { current: control.value,min: minValue } } : null; } } @Component({ selector: 'exe-counter',EXE_COUNTER_VALIDATOR] }) export class CounterComponent implements ControlValueAccessor,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); } ... } 上面的代码很长,我们来分解一下: 注册 Validatorexport const EXE_COUNTER_VALIDATOR = { provide: NG_VALIDATORS,EXE_COUNTER_VALIDATOR] }) 创建 createCounterRangeValidator() 工厂函数export function createCounterRangeValidator(maxValue: number,min: minValue } } : null; } } 实现 OnChanges 接口,监听输入属性变化创建RangeValidatorexport class CounterComponent implements ControlValueAccessor,OnChanges { ... @Input() counterRangeMin: number; // 设置数据有效范围的最大值 @Input() counterRangeMax: number; // 设置数据有效范围的最小值 // 监听输入属性变化,调用内部的_createValidator()方法,创建RangeValidator ngOnChanges(changes: SimpleChanges): void { if ('counterRangeMin' in changes || 'counterRangeMax' in changes) { this._createValidator(); } } ... } 调用 _createValidator() 方法创建RangeValidatorexport class CounterComponent implements ControlValueAccessor,OnChanges { ... // 动态创建RangeValidator private _createValidator(): void { this._validator = createCounterRangeValidator(this.counterRangeMax,this.counterRangeMin); } ... } 实现 Validator 接口,实现控件验证功能export class CounterComponent implements ControlValueAccessor,OnChanges { ... // 执行控件验证 validate(c: AbstractControl): ValidationErrors | null { return this.counterRangeMin == null || this.counterRangeMax == null ? null : this._validator(c); } ... } 此时我们自定义 CounterComponent 组件终于开发完成了,就差功能验证了。具体的使用示例如下: import { Component,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 }); } } 以上代码成功运行后,浏览器页面的显示结果如下:
参考资源
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- unix crontab不能很好地处理python的subprocess.
- WebService基础教程之一(概念,如何发布和调用一
- twitter-bootstrap – Angular 4:如何包含Boots
- bootstrap + angularjs + springmvc + mybatis框
- 基金会和AngularJS
- 使用shell脚本在Jenkins中手工构建失败
- WebService动态调用方法,VS2008也支持
- scala – 我得到“不是一个有效的密钥:gen-idea
- 如何调用AngularJS指令中定义的方法?
- angularjs – 如何从angular $location获取主机基