Angular寫一個Form元件-TagInput
阿新 • • 發佈:2020-12-19
前端開發少不了和表單打交道; Angular中, 提供了強大的表單的支援, 響應式表單(Reactive Form) 和 模板驅動的表單(Template-driven Form) 的雙向資料流給我們的開發帶來了極大的便利; 藉助angular, 我們除了可以使用html原生的輸入控制元件, 也可以自定表單輸入元件, 和使用者更好的互動. 本文以 TagInput
元件為例, 說明在Angular
中如何自定義表單元件;
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();
}