自定義表單控件 ControlValueAccessor接口

ControlValueAccessor

ControlValueAccessor 是一個鏈接表單模型和視圖(DOM元素)的接口,自定義的表單控件必須實現這個接口,它的做用是:框架

  • 把 form 模型中值映射到視圖中
  • 當視圖發生變化時,通知 form directives 或 form controls

Angular 引入這個接口的緣由是,不一樣的輸入控件數據更新方式是不同的。例如,對於咱們經常使用的文本輸入框來講,咱們是設置它的 value 值,而對於複選框 (checkbox) 咱們是設置它的 checked 屬性。實際上,不一樣類型的輸入控件都有一個 ControlValueAccessor,用來更新視圖ide

Angular 中常見的 ControlValueAccessor 有:函數

  • DefaultValueAccessor - 用於 text 和 textarea 類型的輸入控件
  • SelectControlValueAccessor - 用於 select 選擇控件
  • CheckboxControlValueAccessor - 用於 checkbox 複選控件

實現ControlValueAccessor接口

首先咱們先看一下 ControlValueAccessor 接口,具體以下:this

export interface ControlValueAccessor {
  writeValue(obj: any): void;
  registerOnChange(fn: any): void;
  registerOnTouched(fn: any): void;
  setDisabledState?(isDisabled: boolean): void;
}
  • writeValue(obj: any):該方法用於將模型中的新值寫入視圖或 DOM 屬性中,即model->view
  • registerOnChange(fn: any):設置當控件接收到 change 事件後,調用的函數,能夠用來通知外部,組件已經發生變化,即view->model
  • registerOnTouched(fn: any):設置當控件接收到 touched 事件後,調用的函數
  • setDisabledState?(isDisabled: boolean):當控件狀態變成 DISABLED 或從 DISABLED 狀態變化成 ENABLE 狀態時,會調用該函數。該函數會根據參數值,啓用或禁用指定的 DOM 元素。

當表單初始化的時候,將會使用表單模型中對應的初始值做爲參數,調用 writeValue() 方法code

<form #form="ngForm">
  <exe-counter name="counter" ngModel></exe-counter>
  <button type="submit">Submit</button>
</form>

你會發現,咱們沒有爲 CounterComponent 組件設置初始值,所以咱們要調整一下 writeValue() 中的代碼,具體以下:orm

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;//每次控件view層的值發生改變,都要調用該方法通知外部
  }

  registerOnTouched(fn: any) {}
}

註冊成爲表單控件

  • NG_VALUE_ACCESSOR:token類型爲ControlValueAccessor,將控件自己註冊到DI框架成爲一個可讓表單訪問其值的控件
  • NG_VALIDATORS:將控件註冊成爲一個可讓表單獲得其驗證狀態的控件,NG_VALIDATORS的token類型爲function或Validator,配合useExisting,可讓控件只暴露對應的function或Validator的validate方法。針對token爲Validator類型來講,控件實現了validate方法就能夠實現表單控件驗證
  • forwardRef:向前引用,容許咱們引用一個還沒有定義的對象
  • multi:設爲true,表示這個token對應多個依賴項,使用相同的token去獲取依賴項的時候,獲取的是已註冊的依賴對象列表。若是不設置multi爲true,那麼對於相同token的提供商來講,後定義的提供商會覆蓋前面已定義的提供商

註冊表單控件:token

步驟一:建立Token爲NG_VALUE_ACCESSOR的提供商接口

@Component({
    selector: 'exe-counter',
    ...
    providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CounterComponent ),
      multi: true
    }
  ]
})

步驟二:建立Token爲NG_VALIDATORS的表單控件驗證器事件

@Component({
    selector: 'exe-counter',
    ...
    providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CounterComponent ),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useValue: validateCounterRange,
      multi: true
    }
  ]

CounterComponent 組件的完整代碼以下:

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--;
    }
}
相關文章
相關標籤/搜索