Angular: [ControlValueAccessor] 自定義表單控件
我們在實際開發中,通常會遇到各種各樣的定製化功能,會遇到有些組件會與 Angular 的表單進行交互,這時候我們一般會從外部傳入一個 FormGroup 對象,然後在組件的內部寫相應的邏輯對 Angular 表單進行操作。如果我們只是對錶單中的一個項進行定製,將整個表單對象傳入顯然不合適,並且組件也會顯得臃腫。
<form [formGroup]="simpleForm">
<other-component [form]="simpleForm"></other-component>
</form>
那麼,我們能不能像原生表單一樣去使用這些自定義組件呢?目前,開源組件 ng-zorro-antd 表單組件能和原生表單一樣使用 formControlName 這個屬性,這類組件就叫自定義表單組件。
如何實現自定義表單控件
在 Angular 中,使用 ControlValueAccessor 可以實現組件與外層包裹的 form 關聯起來。
ControlValueAccessor是用於處理以下內容的接口:
- 將表單模型中的值寫入視圖/ DOM
- 在視圖/ DOM更改時通知其他表單指令和控件
ControlValueAccessor
ControlValueAccessor 接口定義了四個方法:
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
writeValue(obj:any)
:將表單模型中的新值寫入視圖或DOM屬性(如果需要)的方法,它將來自外部的數據寫入到內部的數據模型。數據流向: form model -> component。
registerOnChange(fn:any)
:一種註冊處理程序的方法,當視圖中的某些內容發生更改時應調用該處理程序。它具有一個告訴其他表單指令和表單控件以更新其值的函數。通常在 registerOnChange 中需要保存該事件觸發函數,在數據改變的時候,可以通過調用事件觸發函數通知外部數據變了,同時可以將修改後的數據作爲參數傳遞出去。數據流向: component -> form model。
registerOnTouched(fn: any)
:註冊 onTouched 事件,基本同 registerOnChange ,只是該函數用於通知表單組件已經處於 touched 狀態,改變綁定的 FormControl 的內部狀態。狀態變更: component -> form model。
setDisabledState(isDisabled: boolean)
:當調用 FormControl 變更狀態的 API 時得表單狀態變爲 Disabled 時調用 setDisabledState() 方法,以通知自定義表單組件當前表單的讀寫狀態。狀態變更: form model -> component。
如何使用 ControlValueAccessor
搭建控件框架
@Component({
selector: 'app-test-control-value-accessor',
templateUrl: './test-control-value-accessor.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TestControlValueAccessorComponent),
multi: true
}]
})
export class TestControlValueAccessorComponent implements ControlValueAccessor {
_counterValue = 0;
private onChange = (_: any) => {};
constructor() { }
get counterValue() {
return this._counterValue;
}
set counterValue(value) {
this._counterValue = value;
// 觸發 onChange,component 內部的值同步到 form model
this.onChange(this._counterValue);
}
increment() {
this.counterValue++;
}
decrement() {
this.counterValue--;
}
// form model 的值同步到 component 內部
writeValue(obj: any): void {
if (obj !== undefined) {
this.counterValue = obj;
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void { }
setDisabledState?(isDisabled: boolean): void { }
}
註冊 ControlValueAccessor
爲了獲得 ControlValueAccessor
用於表單控件,Angular 內部將注入在 NG_VALUE_ACCESSOR
令牌上註冊的所有值,這是將控件本身註冊到 DI
框架成爲一個可以讓表單訪問其值的控件。因此,我們需要做的就是NG_VALUE_ACCESSOR
使用我們自己的值訪問器實例(這是我們的組件)擴展 multi-provider 。所以設置 multi: true
,是聲明這個 token
對應的類很多,分散在各處。
這裏我們必須使用 useExisting
,因爲TestControlValueAccessorComponent
可能在使用它的組件中被其創建爲指令依賴項。這就得用到 forwardRef
了,這個函數允許我們引用一個尚未定義的對象。
@Component({
...
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TestControlValueAccessorComponent ),
multi: true
}
]
})
export class TestControlValueAccessorComponent implements ControlValueAccessor {
...
}
控件界面
- test-control-value-accessor.component.html
<div class="panel panel-primary">
<div class="panel-heading">自定義控件</div>
<div class="panel-body">
<button (click)="increment()">+</button>
{{counterValue}}
<button (click)="decrement()">-</button>
</div>
</div>
在表單中使用
- app.component.html
<div class="constainer">
<form #form="ngForm">
<app-test-control-value-accessor name="message" [(ngModel)]="message"></app-test-control-value-accessor>
<button type="button" (click)="submit(form.value)">Submit</button>
</form>
<pre>{{ message }}</pre>
</div>
- app.component.ts
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
message = 5;
submit(value: any): void {
console.log(value);
}
}