前端开发少不了和表单打交道; Angular中, 提供了强大的表单的支持, 响应式表单(Reactive Form) 和 模板驱动的表单(Template-driven Form) 的双向数据流给我们的开发带来了极大的便利; 借助angular, 我们除了可以使用html原生的输入控件, 也可以自定表单输入组件, 和用户更好的交互. 本文以 TagInput 组件为例, 说明在 Angular 中如何自定义表单组件;
可以先看下最终效果
github Page在线演示
ControlValueAccessor
自定义表单组件第一步, 实现 ControlValueAccessor 接口
接口定义如下:
ControlValueAccessor 接口声明export declare interface ControlValueAccessor { /** * @description * Writes a new value to the element. * * This method is called by the forms API to write to the view when programmatic * changes from model to view are requested. * * @usageNotes * ### Write a value to the element * * The following example writes a value to the native DOM element. * * ```ts * writeValue(value: any): void { * this._renderer.setProperty(this._elementRef.nativeElement, 'value', value); * } * ``` * * @param obj The new value for the element */ writeValue(obj: any): void; /** * @description * Registers a callback function that is called when the control's value * changes in the UI. * * This method is called by the forms API on initialization to update the form * model when values propagate from the view to the model. * * When implementing the `registerOnChange` method in your own value accessor, * save the given function so your class calls it at the appropriate time. * * @usageNotes * ### Store the change function * * The following example stores the provided function as an internal method. * * ```ts * registerOnChange(fn: (_: any) => void): void { * this._onChange = fn; * } * ``` * * When the value changes in the UI, call the registered * function to allow the forms API to update itself: * * ```ts * host: { * '(change)': '_onChange($event.target.value)' * } * ``` * * @param fn The callback function to register */ registerOnChange(fn: any): void; /** * @description * Registers a callback function that is called by the forms API on initialization * to update the form model on blur. * * When implementing `registerOnTouched` in your own value accessor, save the given * function so your class calls it when the control should be considered * blurred or "touched". * * @usageNotes * ### Store the callback function * * The following example stores the provided function as an internal method. * * ```ts * registerOnTouched(fn: any): void { * this._onTouched = fn; * } * ``` * * On blur (or equivalent), your class should call the registered function to allow * the forms API to update itself: * * ```ts * host: { * '(blur)': '_onTouched()' * } * ``` * * @param fn The callback function to register */ registerOnTouched(fn: any): void; /** * @description * Function that is called by the forms API when the control status changes to * or from 'DISABLED'. Depending on the status, it enables or disables the * appropriate DOM element. * * @usageNotes * The following is an example of writing the disabled property to a native DOM element: * * ```ts * setDisabledState(isDisabled: boolean): void { * this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); * } * ``` * * @param isDisabled The disabled status to set on the element */ setDisabledState?(isDisabled: boolean): void; }
这个接口包含了下面这些方法
writeValue(obj: any): void; registerOnChange(fn: any): void; registerOnTouched(fn: any): void; setDisabledState?(isDisabled: boolean): void;writeValue(obj: any): void; 表单值发生改变时 Angular 会调用这个方法给我们的表单组件赋值 registerOnChange(fn: any): void; Angular 调用这个函数给我们的自己写的组件传递一个 onChange 方法, 调用这个方法, 会更新表单中的值 registerOnTouched(fn: any): void; Angular 通过这个方法给我们在组件传递一个 onTouch 方法, 在我们的组件中调用 onTouch 会更新表单的 touched 字段
注入 NG_VALUE_ACCESSOR
除了实现 ControlValueAccessor 接口外, 我们自定义的表单组件还需要提供一个 token 为 NG_VALUE_ACCESSOR 的注入, 像下面这样
import { forwardRef, OnInit, } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'cti-tag-input', templateUrl: './tag-input.component.html', styleUrls: ['./tag-input.component.less'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TagInputComponent), multi: true, }, ], }) export class TagInputComponent implements OnInit, ControlValueAccessor { }
实例: TagInput 组件
了解了上面的内容, 就可以开始编写 TagInput 组件了;
组件的html模板<div [class.disabled]='disabled' class="tag-input-wrapper"> <div class="tag-list"> <span class="tag" *ngFor="let tag of tags"> {{tag}} </span> </div> <div> <ng-container *ngIf="!isInputting"> <button class="btn-add-tag" (click)="onClick()">新增标签</button> </ng-container> <ng-container *ngIf="isInputting"> <input type="text" #tagInputEl (keydown)="onKeyDown($event)" [(ngModel)]="tagInput" (blur)="onBlur()"> </ng-container> </div> </div>
TagInputComponent 中需要定义如下字段
// 文本输入框, 用来获取用户输入的标签的, 拿到这个可以在适当的时机对输入框进行 focus 操作 @ViewChild('tagInputEl', { read: ElementRef })tagInputEl: ElementRef<HTMLInputElement>; tags: string[] = []; // 指示表单组件是否处于禁用状态 disabled = false; // 保存用户输入的文字 tagInput = ''; // 当前是否正在输入 isInputting = false; private _onChange = (_: string[]) => {}; private _onTouch = () => {};
实现 ControlValueAccessor
writeValue(obj: any): void { if (obj instanceof Array && obj.every((x) => typeof x === 'string')) { this.tags = obj; } } registerOnChange(fn: any): void { this._onChange = fn; } registerOnTouched(fn: any): void { this._onTouch = fn; } setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; }
在适当的时机调用 Angular 传递给我们的 _onChange , _onTouch 方法, 更新表单值
onKeyDown(event: KeyboardEvent) { // 回车键键值: 13 if (event.key.toLowerCase() === 'enter' || event.key === ',') { this.emitTags(); } } onBlur = () => { this.emitTags(); }; onClick() { this.isInputting = !this.isInputting; let timer = setTimeout(() => { this.tagInputEl.nativeElement.focus(); clearTimeout(timer); timer = undefined; }, 20); } private emitTags() { if (!this.tags.includes(this.tagInput) && this.tagInput) { this.tags.push(this.tagInput); this._onChange(this.tags); } this.tagInput = ''; this.isInputting = false; this._onTouch(); }
查看完整的 TagComponent 代码
最终效果
查看完整代码
CustomTagInput
查看更多关于Angular写一个Form组件-TagInput的详细内容...