摘要:在里我們簡單保存了對回調(diào)函數(shù)的引用,回調(diào)函數(shù)是由指令傳入的譯者注參考,只要每次組件值發(fā)生改變,就會觸發(fā)這個回調(diào)函數(shù)。
原文鏈接:Never again be confused when implementing ControlValueAccessor in Angular?forms
如果你正在做一個復(fù)雜項目,必然會需要自定義表單控件,這個控件主要需要實現(xiàn) ControlValueAccessor 接口(譯者注:該接口定義方法可參考 API 文檔說明,也可參考 Angular 源碼定義)。網(wǎng)上有大量文章描述如何實現(xiàn)這個接口,但很少說到它在 Angular 表單架構(gòu)里扮演什么角色,如果你不僅僅想知道如何實現(xiàn),還想知道為什么這樣實現(xiàn),那本文正合你的胃口。
首先我解釋下為啥需要 ControlValueAccessor 接口以及它在 Angular 中是如何使用的。然后我將展示如何封裝第三方組件作為 Angular 組件,以及如何使用輸入輸出機(jī)制實現(xiàn)組件間通信(譯者注:Angular 組件間通信輸入輸出機(jī)制可參考官網(wǎng)文檔),最后將展示如何使用 ControlValueAccessor 來實現(xiàn)一種針對 Angular 表單新的數(shù)據(jù)通信機(jī)制。
FormControl 和 ControlValueAccessor如果你之前使用過 Angular 表單,你可能會熟悉 FormControl ,Angular 官方文檔將它描述為追蹤單個表單控件值和有效性的實體對象。需要明白,不管你使用模板驅(qū)動還是響應(yīng)式表單(譯者注:即模型驅(qū)動),FormControl 都總會被創(chuàng)建。如果你使用響應(yīng)式表單,你需要顯式創(chuàng)建 FormControl 對象,并使用 formControl 或 formControlName 指令來綁定原生控件;如果你使用模板驅(qū)動方法,FormControl 對象會被 NgModel 指令隱式創(chuàng)建(譯者注:可查看 Angular 源碼這一行):
@Directive({ selector: "[ngModel]...", ... }) export class NgModel ... { _control = new FormControl(); <---------------- here
不管 formControl 是隱式還是顯式創(chuàng)建,都必須和原生 DOM 表單控件如 input,textarea 進(jìn)行交互,并且很有可能需要自定義一個表單控件作為 Angular 組件而不是使用原生表單控件,而通常自定義表單控件會封裝一個使用純 JS 寫的控件如 jQuery UI"s Slider。本文我將使用原生表單控件術(shù)語來區(qū)分 Angular 特定的 formControl 和你在 html 使用的表單控件,但你需要知道任何一個自定義表單控件都可以和 formControl 指令進(jìn)行交互,而不是原生表單控件如 input。
原生表單控件數(shù)量是有限的,但是自定義表單控件是無限的,所以 Angular 需要一種通用機(jī)制來橋接原生/自定義表單控件和 formControl 指令,而這正是 ControlValueAccessor 干的事情。這個對象橋接原生表單控件和 formControl 指令,并同步兩者的值。官方文檔是這么描述的(譯者注:為清晰理解,該描述不翻譯):
?ControlValueAccessor?acts as a bridge between the Angular forms API and a native element in the DOM.
任何一個組件或指令都可以通過實現(xiàn) ControlValueAccessor 接口并注冊為 NG_VALUE_ACCESSOR,從而轉(zhuǎn)變成 ControlValueAccessor 類型的對象,稍后我們將一起看看如何做。另外,這個接口還定義兩個重要方法——writeValue 和 registerOnChange (譯者注:可查看 Angular 源碼這一行):
interface ControlValueAccessor { writeValue(obj: any): void registerOnChange(fn: any): void registerOnTouched(fn: any): void ... }
formControl 指令使用 writeValue 方法設(shè)置原生表單控件的值(譯者注:你可能會參考 L186 和 L41);使用 registerOnChange 方法來注冊由每次原生表單控件值更新時觸發(fā)的回調(diào)函數(shù)(譯者注:你可能會參考這三行,L186 和 L43,以及 L85),你需要把更新的值傳給這個回調(diào)函數(shù),這樣對應(yīng)的 Angular 表單控件值也會更新(譯者注:這一點可以參考 Angular 它自己寫的 DefaultValueAccessor 的寫法是如何把 input 控件每次更新值傳給回調(diào)函數(shù)的,L52 和 L89);使用 registerOnTouched 方法來注冊用戶和控件交互時觸發(fā)的回調(diào)(譯者注:你可能會參考 L95)。
下圖是 Angular 表單控件 如何通過 ControlValueAccessor 來和原生表單控件交互的(譯者注:formControl 和你寫的或者 Angular 提供的 CustomControlValueAccessor 兩個都是要綁定到 native DOM element 的指令,而 formControl 指令需要借助 CustomControlValueAccessor 指令/組件,來和 native DOM element 交換數(shù)據(jù)。):
再次強(qiáng)調(diào),不管是使用響應(yīng)式表單顯式創(chuàng)建還是使用模板驅(qū)動表單隱式創(chuàng)建,ControlValueAccessor 都總是和 Angular 表單控件進(jìn)行交互。
Angular 也為所有原生 DOM 表單元素創(chuàng)建了 Angular 表單控件(譯者注:Angular 內(nèi)置的 ControlValueAccessor):
Accessor | Form Element |
---|---|
DefaultValueAccessor | input,textarea |
CheckboxControlValueAccessor | input[type=checkbox] |
NumberValueAccessor | input[type=number] |
RadioControlValueAccessor | input[type=radio] |
RangeValueAccessor | input[type=range] |
SelectControlValueAccessor | select |
SelectMultipleControlValueAccessor | select[multiple] |
從上表中可看到,當(dāng) Angular 在組件模板中中遇到 input 或 textarea DOM 原生控件時,會使用DefaultValueAccessor 指令:
@Component({ selector: "my-app", template: ` ` }) export class AppComponent { ctrl = new FormControl(3); }
所有表單指令,包括上面代碼中的 formControl 指令,都會調(diào)用 setUpControl 函數(shù)來讓表單控件和DefaultValueAccessor 實現(xiàn)交互(譯者注:意思就是上面代碼中綁定的 formControl 指令,在其自身實例化時,會調(diào)用 setUpControl() 函數(shù)給同樣綁定到 input 的 DefaultValueAccessor 指令做好安裝工作,如 L85,這樣 formControl 指令就可以借助 DefaultValueAccessor 來和 input 元素交換數(shù)據(jù)了)。細(xì)節(jié)可參考 formControl 指令的代碼:
export class FormControlDirective ... { ... ngOnChanges(changes: SimpleChanges): void { if (this._isControlChanged(changes)) { setUpControl(this.form, this);
還有 setUpControl 函數(shù)源碼也指出了原生表單控件和 Angular 表單控件是如何數(shù)據(jù)同步的(譯者注:作者貼的可能是 Angular v4.x 的代碼,v5 有了點小小變動,但基本相似):
export function setUpControl(control: FormControl, dir: NgControl) { // initialize a form control // 調(diào)用 writeValue() 初始化表單控件值 dir.valueAccessor.writeValue(control.value); // setup a listener for changes on the native control // and set this value to form control // 設(shè)置原生控件值更新時監(jiān)聽器,每當(dāng)原生控件值更新,Angular 表單控件值也更新 valueAccessor.registerOnChange((newValue: any) => { control.setValue(newValue, {emitModelToViewChange: false}); }); // setup a listener for changes on the Angular formControl // and set this value to the native control // 設(shè)置 Angular 表單控件值更新監(jiān)聽器,每當(dāng) Angular 表單控件值更新,原生控件值也更新 control.registerOnChange((newValue: any, ...) => { dir.valueAccessor.writeValue(newValue); });
只要我們理解了內(nèi)部機(jī)制,就可以實現(xiàn)我們自定義的 Angular 表單控件了。
組件封裝器由于 Angular 為所有默認(rèn)原生控件提供了控件值訪問器,所以在封裝第三方插件或組件時,需要寫一個新的控件值訪問器。我們將使用上文提到的 jQuery UI 庫的 slider 插件,來實現(xiàn)一個自定義表單控件吧。
簡單的封裝器最基礎(chǔ)實現(xiàn)是通過簡單封裝使其能在屏幕上顯示出來,所以我們需要一個 NgxJquerySliderComponent 組件,并在其模板里渲染出 slider:
@Component({ selector: "ngx-jquery-slider", template: ` `, styles: ["div {width: 100px}"] }) export class NgxJquerySliderComponent { @ViewChild("location") location; widget; ngOnInit() { this.widget = $(this.location.nativeElement).slider(); } }
這里我們使用標(biāo)準(zhǔn)的 jQuery 方法在原生 DOM 元素上創(chuàng)建一個 slider 控件,然后使用 widget 屬性引用這個控件。
一旦簡單封裝好了 slider 組件,我們就可以在父組件模板里使用它:
@Component({ selector: "my-app", template: `Hello {{name}}
` }) export class AppComponent { ... }
為了運行程序我們需要加入 jQuery 相關(guān)依賴,簡化起見,在 index.html 中添加全局依賴:
這里是安裝依賴的源碼。
交互式表單控件上面的實現(xiàn)還不能讓我們自定義的 slider 控件與父組件交互,所以還得使用輸入/輸出綁定來是實現(xiàn)組件間數(shù)據(jù)通信:
export class NgxJquerySliderComponent { @ViewChild("location") location; @Input() value; @Output() private valueChange = new EventEmitter(); widget; ngOnInit() { this.widget = $(this.location.nativeElement).slider(); this.widget.slider("value", this.value); this.widget.on("slidestop", (event, ui) => { this.valueChange.emit(ui.value); }); } ngOnChanges() { if (this.widget && this.widget.slider("value") !== this.value) { this.widget.slider("value", this.value); } } }
一旦 slider 組件創(chuàng)建,就可以訂閱 slidestop 事件獲取變化的值,一旦 slidestop 事件被觸發(fā)了,就可以使用輸出事件發(fā)射器 valueChanges 通知父組件。當(dāng)然我們也可以使用 ngOnChanges 生命周期鉤子來追蹤輸入屬性 value 值的變化,一旦其值變化,我們就將該值設(shè)置為 slider 控件的值。
然后就是父組件中如何使用 slider 組件的代碼實現(xiàn):
源碼在這里。
但是,我們想要的是,使用 slider 組件作為表單的一部分,并使用模板驅(qū)動表單或響應(yīng)式表單的指令與其數(shù)據(jù)通信,那就需要讓其實現(xiàn) ControlValueAccessor 接口了。由于我們將實現(xiàn)的是新的組件通信方式,所以不需要標(biāo)準(zhǔn)的輸入輸出屬性綁定方式,那就移除相關(guān)代碼吧。(譯者注:作者先實現(xiàn)標(biāo)準(zhǔn)的輸入輸出屬性綁定的通信方式,又要刪除,主要是為了引入新的表單組件交互方式,即 ControlValueAccessor。)
實現(xiàn)自定義控件值訪問器實現(xiàn)自定義控件值訪問器并不難,只需要兩步:
注冊 NG_VALUE_ACCESSOR 提供者
實現(xiàn) ControlValueAccessor 接口
NG_VALUE_ACCESSOR 提供者用來指定實現(xiàn)了 ControlValueAccessor 接口的類,并且被 Angular 用來和 formControl 同步,通常是使用組件類或指令來注冊。所有表單指令都是使用NG_VALUE_ACCESSOR 標(biāo)識來注入控件值訪問器,然后選擇合適的訪問器(譯者注:這句話可參考這兩行代碼,L175 和 L181)。要么選擇DefaultValueAccessor 或者內(nèi)置的數(shù)據(jù)訪問器,否則 Angular 將會選擇自定義的數(shù)據(jù)訪問器,并且有且只有一個自定義的數(shù)據(jù)訪問器(譯者注:這句話參考 selectValueAccessor 源碼實現(xiàn))。
讓我們首先定義提供者:
@Component({ selector: "ngx-jquery-slider", providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: NgxJquerySliderComponent, multi: true }] ... }) class NgxJquerySliderComponent implements ControlValueAccessor {...}
我們直接在組件裝飾器里直接指定類名,然而 Angular 源碼默認(rèn)實現(xiàn)是放在類裝飾器外面:
export const DEFAULT_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DefaultValueAccessor), multi: true }; @Directive({ selector:"input", providers: [DEFAULT_VALUE_ACCESSOR] ... }) export class DefaultValueAccessor implements ControlValueAccessor {}
放在外面就需要使用 forwardRef,關(guān)于原因可以參考 What is forwardRef in Angular and why we need it 。當(dāng)實現(xiàn)自定義 controlValueAccessor,我建議還是放在類裝飾器里吧(譯者注:個人建議還是學(xué)習(xí) Angular 源碼那樣放在外面)。
一旦定義了提供者后,就讓我們實現(xiàn) controlValueAccessor 接口:
export class NgxJquerySliderComponent implements ControlValueAccessor { @ViewChild("location") location; widget; onChange; value; ngOnInit() { this.widget = $(this.location.nativeElement).slider(this.value); this.widget.on("slidestop", (event, ui) => { this.onChange(ui.value); }); } writeValue(value) { this.value = value; if (this.widget && value) { this.widget.slider("value", value); } } registerOnChange(fn) { this.onChange = fn; } registerOnTouched(fn) { }
由于我們對用戶是否與組件交互不感興趣,所以先把 registerOnTouched 置空吧。在registerOnChange 里我們簡單保存了對回調(diào)函數(shù) fn 的引用,回調(diào)函數(shù)是由 formControl 指令傳入的(譯者注:參考 L85),只要每次 slider 組件值發(fā)生改變,就會觸發(fā)這個回調(diào)函數(shù)。在 writeValue 方法內(nèi)我們把得到的值傳給 slider 組件。
現(xiàn)在我們把上面描述的功能做成一張交互式圖:
如果你把簡單封裝和 controlValueAccessor 封裝進(jìn)行比較,你會發(fā)現(xiàn)父子組件交互方式是不一樣的,盡管封裝的組件與 slider 組件的交互是一樣的。你可能注意到 formControl 指令實際上簡化了與父組件交互的方式。這里我們使用 writeValue 來向子組件寫入數(shù)據(jù),而在簡單封裝方法中使用 ngOnChanges;調(diào)用 this.onChange 方法輸出數(shù)據(jù),而在簡單封裝方法中使用 this.valueChange.emit(ui.value)。
現(xiàn)在,實現(xiàn)了 ControlValueAccessor 接口的自定義 slider 表單控件完整代碼如下:
@Component({ selector: "my-app", template: `Hello {{name}}
Current slider value: {{ctrl.value}}` }) export class AppComponent { ctrl = new FormControl(11); updateSlider($event) { this.ctrl.setValue($event.currentTarget.value, {emitModelToViewChange: true}); } }
你可以查看程序的最終實現(xiàn)。
Github項目的 Github 倉庫。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/93895.html
摘要:由于的屬性提供了令牌,并且該令牌指向的對象就是,所以構(gòu)造函數(shù)中注入的令牌包含的對象數(shù)組只有一個。這樣的構(gòu)造函數(shù)里就會包含一個對象,然后把這個傳給對象,最后注冊回調(diào),這樣以后值更新時就會運行。整個包的設(shè)計也是按照這種數(shù)據(jù)流形式,并不復(fù)雜。 我們知道,Angular 的 @angular/forms 包提供了 NgModel 指令,來實現(xiàn)雙向綁定,即把一個 JS 變量(假設(shè)為 name)與...
摘要:大多數(shù)初學(xué)者會認(rèn)為也有封裝規(guī)則,但實際上沒有。第二個規(guī)則是最后導(dǎo)入模塊的,會覆蓋前面導(dǎo)入模塊的。 原文鏈接:Avoiding common confusions with modules in Angular showImg(https://segmentfault.com/img/remote/1460000015298243?w=270&h=360); Angular Modul...
摘要:在自定義表單控件,有時你想要的輸入不是標(biāo)準(zhǔn)的文本輸入選擇或復(fù)選框。它獲取一個函數(shù),告訴其他表單指令和表單控件更新其值。與此類似,它專門為控件接收觸摸事件時注冊一個處理程序。 在 Angular 自定義表單控件,有時你想要的輸入不是標(biāo)準(zhǔn)的文本輸入、選擇或復(fù)選框。通過實現(xiàn)ControlValueAccessor 接口并將組件注冊為 NG_VALUE_ACCESSOR,您可以將自定義表單控件...
摘要:構(gòu)建一個自定義輸入組件今天我們來學(xué)習(xí)如何正確的構(gòu)建和一個具有和同樣作用,但同時也具有自己的邏輯的輸入組件。值訪問器在完成上面的一些步驟之后,我們的組件基本功能完成了,但是接下來還有最重要的一部分內(nèi)容,那就是讓我們的自定義組件獲得值訪問權(quán)限。 構(gòu)建一個自定義 angular2 輸入組件 今天我們來學(xué)習(xí)如何正確的構(gòu)建和一個具有和 同樣作用,但同時也具有自己的邏輯的輸入組件。 在讀這篇文章...
摘要:構(gòu)建一個自定義輸入組件今天我們來學(xué)習(xí)如何正確的構(gòu)建和一個具有和同樣作用,但同時也具有自己的邏輯的輸入組件。值訪問器在完成上面的一些步驟之后,我們的組件基本功能完成了,但是接下來還有最重要的一部分內(nèi)容,那就是讓我們的自定義組件獲得值訪問權(quán)限。 構(gòu)建一個自定義 angular2 輸入組件 今天我們來學(xué)習(xí)如何正確的構(gòu)建和一個具有和 同樣作用,但同時也具有自己的邏輯的輸入組件。 在讀這篇文章...
閱讀 3916·2021-11-16 11:44
閱讀 3116·2021-11-12 10:36
閱讀 3373·2021-10-08 10:04
閱讀 1257·2021-09-03 10:29
閱讀 391·2019-08-30 13:50
閱讀 2605·2019-08-29 17:14
閱讀 1735·2019-08-29 15:32
閱讀 1081·2019-08-29 11:27