1. 程式人生 > 實用技巧 >Angular寫一個Form元件-TagInput

Angular寫一個Form元件-TagInput

前端開發少不了和表單打交道; 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();
}

檢視完整的 TagComponent 程式碼

最終效果

檢視完整程式碼

CustomTagInput