網站搜尋

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 不知道該怎麼做。要解決這個問題,你有兩個選擇。

  1. 你將 formControlName 放在一個元素上,這是 Angular 開箱即用支持的。這些元素是:input、textarea 和 select。
  2. 你實現了 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/