javascript - Angular4 - 表單控制項沒有值存取器
我有一個自訂元素:
<div formControlName="surveyType">
<div *ngFor="let type of surveyTypes"
(click)="onSelectType(type)"
[class.selected]="type === selectedType">
<md-icon>{{ type.icon }}</md-icon>
<span>{{ type.description }}</span>
</div>
</div>
當我嘗試添加 formControlName 時,我收到一個錯誤訊息:
錯誤 錯誤:表單控制名稱為 'surveyType' 的控制元件沒有值存取器
我試著添加 ngDefaultControl 但沒有成功。似乎是因為沒有 input/select... 我也不知道該怎麼辦。
我想將我的點擊綁定到這個 formControl,以便當有人點擊整個卡片時,會將我的『type』推送到 formControl。這可能嗎?
答案#1
您只能在實現了 ControlValueAccessor 的指令上使用 formControlName。
實作介面
所以,為了做你想做的事情,你必須創建一個實現 ControlValueAccessor 的元件,這意味著 需要實現以下三個函數:
writeValue(告訴 Angular 如何將模型的值寫入視圖)registerOnChange(註冊一個當視圖改變時會被呼叫的處理函數)registerOnTouched(註冊一個處理程序,在元件接收到觸碰事件時被呼叫,對於知道元件是否已獲取焦點非常有用)。
註冊服務提供者
然後,你必須告訴 Angular,這個指令是一個 ControlValueAccessor(接口不行,因為 TypeScript 編譯成 JavaScript 時會被移除)。你可以透過 註冊一個提供者 來做到這一點。
提供者應該提供 NG_VALUE_ACCESSOR 並使用現有的值。這裡你還需要一個 forwardRef。請注意 NG_VALUE_ACCESSOR 應該是一個多重提供者。
例如,如果你的自訂指令名稱為 MyControlComponent,你應該在傳給 @Component 裝飾器的物件內添加如下類似內容:
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => MyControlComponent),
}
]
使用
您的元件已準備好使用。在模板驅動表單中,ngModel 綁定現在將正常運作。
使用反應式表單時,你現在可以正確使用 formControlName,表單控制項將如預期般運作。
資源
- Angular 中的自訂表單控制元件 作者:Thoughtram
- 使用反應式表單和 NgModel 的 Angular 自訂表單控制元件 作者:Cory Rylan
答案#2
你應該在 input 上使用 formControlName="surveyType",而不是在 div 上使用
答案#3
這個錯誤意味著,當你在 div 上使用 formControl 時,Angular 不知道該怎麼做。要解決這個問題,你有兩個選擇。
- 你將
formControlName放在一個元素上,這是 Angular 開箱即用支持的。這些元素是:input、textarea和select。 - 你實現了
ControlValueAccessor介面。這麼做的意思是你在告訴 Angular「如何存取你的控制項的值」(因此得名)。或者簡單來說:當你在一個本身沒有自然值的元素上放置formControlName時,要做什麼。
現在,實作 ControlValueAccessor 介面一開始可能會有點令人生畏。特別是因為外面沒有太多好的文件,而且你需要在程式碼中加入很多樣板程式碼。所以讓我試著將這分解成一些簡單易懂的步驟。
將你的表單控制移到它自己的元件中
為了實現 ControlValueAccessor,你需要創建一個新的元件(或指令)。將與表單控制相關的代碼移到那裡。這樣它也會更容易被重複使用。控制已經在元件內可能就是你需要實現 ControlValueAccessor 介面的原因,因為否則你將無法將自定義元件與 Angular 表單一起使用。
將樣板代碼添加到你的程式碼中
實作 ControlValueAccessor 介面相當繁瑣,這裡是隨之而來的樣板程式碼:
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
@Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.scss'],
// a) copy paste this providers property (adjust the component name in the forward ref)
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
// b) Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {
// c) copy paste this code
onChange: any = () => {}
onTouch: any = () => {}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// d) copy paste this code
writeValue(input: string) {
// TODO
}
那各個部分在做什麼呢?
- a) 在運行時讓 Angular 知道您已實作
ControlValueAccessor介面 - b) 確保你正在實作
ControlValueAccessor介面 - c) 這可能是最讓人困惑的部分。基本上你所做的是,你讓 Angular 有能力在運行時用它自己的實作覆寫你的類別屬性/方法
onChange和onTouch,這樣你就可以呼叫那些函式。因此這一點需要理解:你不需要自己實作 onChange 和 onTouch(除了最初的空實作之外)。你在 (c) 中唯一做的事情就是讓 Angular 將它自己的函式附加到你的類別上。為什麼?這樣你就可以在適當的時候呼叫 Angular 提供的onChange和onTouch方法。我們將在下面看到這是如何運作的。 - d) 我們也會在下一節中,當我們實作
writeValue方法時,看到它如何運作。我把它放在這裡,是為了確保ControlValueAccessor上所有必需的屬性都已經實作,而且你的程式碼仍然可以編譯。
實作 writeValue
writeValue 所做的是 當表單控制項在外部被更改時,對你的自訂元件執行某些操作。例如,如果你將你的自訂表單控制元件命名為 app-custom-input,並且在父元件中這樣使用它:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
然後,writeValue 會在父元件以某種方式更改 myFormControl 的值時被觸發。例如,這可能是在表單初始化期間 (this.form=this.formBuilder.group({myFormControl: ""});) 或在表單重設時 this.form.reset();。
如果表單控制項的值在外部發生變化,你通常想要做的是將它寫入一個代表表單控制項值的本地變量。例如,如果你的 CustomInputComponent 圍繞一個基於文字的表單控制項,它可能看起來像這樣:
writeValue(input: string) {
this.input = input;
}
以及在 CustomInputComponent 的 HTML 中:
<input type="text"
[ngModel]="input">
你也可以像 Angular 文件中描述的那樣,直接將它寫入輸入元素。
現在你已經處理了當外部發生變化時,元件內部會發生什麼情況。現在讓我們看看另一個方向。當元件內部發生變化時,你如何通知外部世界?
呼叫 onChange
下一步是通知父元件關於你在 CustomInputComponent 裡的變更。這就是上面 (c) 提到的 onChange 和 onTouch 函數發揮作用的地方。透過呼叫這些函數,你可以將元件內的變更通知到外部。為了將值的變更傳播到外部,你需要 以新值作為參數呼叫 onChange。例如,如果使用者在你自訂元件的 input 欄位輸入內容,你就呼叫 onChange 並傳入更新後的值:
<input type="text"
[ngModel]="input"
(ngModelChange)="onChange($event)">
如果你再次查看上面的實現 (c),你會看到發生了什麼:Angular 將它自己的實現綁定到了 onChange 類屬性。該實現期望一個參數,即更新後的控制值。你現在所做的是調用該方法,從而讓 Angular 知道這個變化。Angular 現在將繼續在外部更改表單值。這是整個過程中的關鍵部分。你通過調用 onChange 告訴了 Angular 何時應該更新表單控制以及使用何種值。你已經給了它 "訪問控制值" 的方式。
順便說一句:名稱 onChange 是我選的。你在這裡可以選擇任何名字,例如 propagateChange 或類似的名稱。然而無論你如何命名,它都將是同一個函數,這個函數接受一個由 Angular 提供的參數,並且在運行時透過 registerOnChange 方法綁定到你的類上。
呼叫 onTouch
由於表單控制項可以被「觸碰」,你也應該讓 Angular 能夠了解你的自訂表單控制項何時被觸碰。你可以做到,你猜對了,就是透過呼叫 onTouch 函數。因此在我們這個範例中,如果你想要遵循 Angular 對內建表單控制項的做法,當輸入欄位失去焦點時,你應該呼叫 onTouch:
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
再次強調,onTouch 是我選擇的名稱,但它的實際功能是由 Angular 提供的,而且它不接受任何參數。這是有道理的,因為你只是讓 Angular 知道表單控制已被觸碰。
綜合考量
那當這一切整合在一起時會是什麼樣子呢?它應該看起來像這樣:
// custom-input.component.ts
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
@Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.scss'],
// Step 1: copy paste this providers property
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
// Step 2: Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {
// Step 3: Copy paste this stuff here
onChange: any = () => {}
onTouch: any = () => {}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// Step 4: Define what should happen in this component, if something changes outside
input: string;
writeValue(input: string) {
this.input = input;
}
// Step 5: Handle what should happen on the outside, if something changes on the inside
// in this simple case, we've handled all of that in the .html
// a) we've bound to the local variable with ngModel
// b) we emit to the ouside by calling onChange on ngModelChange
}
// custom-input.component.html
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
// parent.component.html
<app-custom-input [formControl]="inputTwo"></app-custom-input>
// OR
<form [formGroup]="form" >
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
更多範例
- 範例與輸入:https://stackblitz.com/edit/angular-control-value-accessor-simple-example-tsmean
- 帶有延遲加載輸入的範例:https://stackblitz.com/edit/angular-control-value-accessor-lazy-input-example-tsmean
- 按鈕範例:https://stackblitz.com/edit/angular-control-value-accessor-button-example-tsmean
巢狀表單
請注意,控制值存取器(Control Value Accessors)並不是用於巢狀表單群組(nested form groups)的正確工具。對於巢狀表單群組,你可以簡單地使用 @Input() subform。控制值存取器是用來封裝 controls,而不是 groups!參見此範例,了解如何為巢狀表單使用輸入: https://stackblitz.com/edit/angular-nested-forms-input-2
來源
- https://angular.io/api/forms/ControlValueAccessor
- https://www.tsmean.com/articles/angular/angular-control-value-accessor-example/
