Angular4 - Tidak ada accessor nilai untuk kontrol bentuk

146

Saya memiliki elemen khusus:

<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>

Ketika saya mencoba menambahkan formControlName, saya mendapatkan pesan kesalahan:

Galat ERROR: Tidak ada accessor nilai untuk kontrol formulir dengan nama: 'surveyType'

Saya mencoba menambahkan ngDefaultControltanpa hasil. Sepertinya itu karena tidak ada input / pilih ... dan saya tidak tahu harus berbuat apa.

Saya ingin mengikat klik saya ke formControl ini agar ketika seseorang mengklik seluruh kartu yang akan mendorong 'ketik' saya ke formControl. Apa itu mungkin?

jbtd
sumber
Saya tidak tahu maksud saya adalah: formControl pergi untuk kontrol bentuk di html tetapi div bukan kontrol bentuk. Saya ingin mengikat survei saya dengan type.id dari kartu saya div
jbtd
saya tahu saya bisa menggunakan cara sudut yang lama dan mengikatTypeType saya untuk itu tetapi saya sedang mencoba untuk menggunakan dan belajar bentuk reaktif dari sudut 4 dan tidak tahu cara menggunakan formControl dengan jenis kasus ini.
jbtd
Oke, mungkin saja case itu tidak bisa ditangani dengan bentuk reaktif. Terima kasih :)
jbtd
Saya telah membuat jawaban tentang cara memecah formulir besar menjadi sub komponen di sini stackoverflow.com/a/56375605/2398593 tetapi ini juga berlaku dengan sangat baik hanya dengan accessor nilai kontrol kustom. Lihat juga github.com/cloudnc/ngx-sub-form :)
maxime1992

Jawaban:

250

Anda formControlNamehanya dapat menggunakan arahan yang menerapkan ControlValueAccessor.

Implementasikan antarmuka

Jadi, untuk melakukan apa yang Anda inginkan, Anda harus membuat komponen yang mengimplementasikan ControlValueAccessor, yang berarti menerapkan tiga fungsi berikut :

  • writeValue (memberi tahu Angular cara menulis nilai dari model ke tampilan)
  • registerOnChange (mendaftarkan fungsi penangan yang dipanggil saat tampilan berubah)
  • registerOnTouched (mendaftarkan pawang untuk dipanggil ketika komponen menerima acara sentuh, berguna untuk mengetahui apakah komponen telah difokuskan).

Daftarkan penyedia

Kemudian, Anda harus memberi tahu Angular bahwa arahan ini adalah ControlValueAccessor(antarmuka tidak akan memotongnya karena dilucuti dari kode ketika TypeScript dikompilasi ke JavaScript). Anda melakukan ini dengan mendaftarkan penyedia .

Penyedia harus menyediakan NG_VALUE_ACCESSORdan menggunakan nilai yang ada . Anda juga perlu di forwardRefsini. Catatan yang NG_VALUE_ACCESSORharus menjadi penyedia multi .

Misalnya, jika arahan khusus Anda bernama MyControlComponent, Anda harus menambahkan sesuatu di sepanjang baris berikut di dalam objek yang diteruskan ke @Componentdekorator:

providers: [
  { 
    provide: NG_VALUE_ACCESSOR,
    multi: true,
    useExisting: forwardRef(() => MyControlComponent),
  }
]

Pemakaian

Komponen Anda siap digunakan. Dengan formulir yang digerakkan templat , ngModelpenjilidan sekarang akan berfungsi dengan baik.

Dengan formulir reaktif , Anda sekarang dapat menggunakan dengan benar formControlNamedan kontrol formulir akan berperilaku seperti yang diharapkan.

Sumber daya

Lazar Ljubenović
sumber
72

Saya pikir Anda harus menggunakan formControlName="surveyType"pada inputdan bukan padadiv

Vega
sumber
Ya tentu, tetapi saya tidak tahu cara mengubah div kartu saya menjadi sesuatu yang lain yang akan menjadi kontrol bentuk html
jbtd
5
Inti CustomValueAccessor adalah menambahkan kontrol bentuk ke ANYTHING, bahkan div
SoEzPz
4
@ SoEzPz Ini adalah pola yang buruk. Anda meniru fungsi Input dalam komponen wrapper, menerapkan kembali sendiri metode-HTML standar (dengan demikian pada dasarnya menciptakan kembali roda dan membuat kode Anda bertele-tele). tetapi dalam 90% kasus Anda dapat mencapai semua yang Anda inginkan dengan menggunakan <ng-content>komponen pembungkus dan membiarkan komponen induk yang mendefinisikan formControlscukup masukkan <input> di dalam <wrapper>
Phil
3

Kesalahan berarti, bahwa Angular tidak tahu apa yang harus dilakukan ketika Anda memakai formControla div. Untuk memperbaikinya, Anda memiliki dua opsi.

  1. Anda meletakkan formControlNameelemen, yang didukung oleh Angular di luar kotak. Mereka adalah: input, textareadan select.
  2. Anda mengimplementasikan ControlValueAccessorantarmuka. Dengan melakukannya, Anda memberi tahu Angular "cara mengakses nilai kendali Anda" (karena itu namanya). Atau secara sederhana: Apa yang harus dilakukan, ketika Anda meletakkan formControlNameelemen, itu tidak secara alami memiliki nilai yang terkait dengannya.

Sekarang, mengimplementasikan ControlValueAccessorantarmuka bisa sedikit menakutkan pada awalnya. Terutama karena tidak ada dokumentasi yang bagus tentang ini di luar sana dan Anda perlu menambahkan banyak boilerplate ke kode Anda. Jadi izinkan saya mencoba untuk memecah ini dalam beberapa langkah mudah diikuti.

Pindahkan kontrol formulir Anda ke komponennya sendiri

Untuk menerapkannya ControlValueAccessor, Anda perlu membuat komponen baru (atau arahan). Pindahkan kode yang terkait dengan kontrol formulir Anda di sana. Seperti ini juga akan mudah digunakan kembali. Memiliki kontrol yang sudah ada di dalam komponen mungkin menjadi alasan di tempat pertama, mengapa Anda perlu mengimplementasikan ControlValueAccessorantarmuka, karena jika tidak, Anda tidak akan dapat menggunakan komponen kustom Anda bersama-sama dengan bentuk Angular.

Tambahkan boilerplate ke kode Anda

Menerapkan ControlValueAccessorantarmuka cukup verbose, inilah boilerplate yang menyertainya:

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
  }

Jadi apa yang dilakukan masing-masing bagian?

  • a) Memungkinkan Angular tahu selama runtime bahwa Anda mengimplementasikan ControlValueAccessorantarmuka
  • b) Pastikan Anda mengimplementasikan ControlValueAccessorantarmuka
  • c) Ini mungkin bagian yang paling membingungkan. Pada dasarnya yang Anda lakukan adalah, Anda memberi Angular sarana untuk mengganti properti / metode kelas Anda onChangedan onTouchdengan implementasinya sendiri selama runtime, sehingga Anda dapat memanggil fungsi-fungsi tersebut. Jadi poin ini penting untuk dipahami: Anda tidak perlu mengimplementasikan onChange dan onTouch sendiri (selain dari implementasi kosong awal). Satu-satunya hal yang Anda lakukan dengan (c) adalah membiarkan Angular melampirkan fungsinya sendiri ke kelas Anda. Mengapa? Sehingga Anda kemudian dapat memanggil yang onChangedan onTouchmetode yang disediakan oleh angular pada waktu yang tepat. Kita akan melihat bagaimana ini bekerja di bawah.
  • d) Kita juga akan melihat bagaimana writeValuemetode ini bekerja di bagian selanjutnya, ketika kita mengimplementasikannya. Saya telah meletakkannya di sini, jadi semua properti yang diperlukan ControlValueAccessordiimplementasikan dan kode Anda masih dikompilasi.

Terapkan writeValue

Apa yang writeValuedilakukan, adalah melakukan sesuatu di dalam komponen kustom Anda, ketika kontrol formulir diubah di luar . Jadi misalnya, jika Anda telah menamai komponen kontrol formulir kustom app-custom-inputAnda dan Anda akan menggunakannya dalam komponen induk seperti ini:

<form [formGroup]="form">
  <app-custom-input formControlName="myFormControl"></app-custom-input>
</form>

kemudian writeValueakan terpicu setiap kali komponen induk mengubah nilai myFormControl. Ini bisa misalnya selama inisialisasi form ( this.form = this.formBuilder.group({myFormControl: ""});) atau pada form reset this.form.reset();.

Apa yang biasanya ingin Anda lakukan jika nilai kontrol formulir berubah di luar, adalah menulisnya ke variabel lokal yang mewakili nilai kontrol formulir. Misalnya, jika Anda CustomInputComponentberputar di sekitar kontrol bentuk berbasis teks, itu bisa terlihat seperti ini:

writeValue(input: string) {
  this.input = input;
}

dan dalam html dari CustomInputComponent:

<input type="text"
       [ngModel]="input">

Anda juga bisa menulisnya langsung ke elemen input seperti yang dijelaskan dalam dokumen Angular.

Sekarang Anda telah menangani apa yang terjadi di dalam komponen Anda ketika sesuatu berubah di luar. Sekarang mari kita lihat ke arah yang lain. Bagaimana Anda memberi tahu dunia luar ketika ada sesuatu yang berubah di dalam komponen Anda?

Memanggil di Ubah

Langkah selanjutnya adalah memberi tahu komponen induk tentang perubahan di dalam Anda CustomInputComponent. Di sinilah onChangedan onTouchfungsi dari (c) dari atas ikut bermain. Dengan memanggil fungsi-fungsi itu, Anda dapat memberi tahu pihak luar tentang perubahan di dalam komponen Anda. Untuk menyebarkan perubahan nilai ke luar, Anda perlu memanggil onChange dengan nilai baru sebagai argumen . Misalnya, jika pengguna mengetik sesuatu di inputbidang di komponen khusus Anda, Anda menelepon onChangedengan nilai yang diperbarui:

<input type="text"
       [ngModel]="input"
       (ngModelChange)="onChange($event)">

Jika Anda memeriksa implementasi (c) dari atas lagi, Anda akan melihat apa yang terjadi: Angular terikat penerapannya sendiri ke onChangeproperti kelas. Implementasi itu mengharapkan satu argumen, yang merupakan nilai kontrol yang diperbarui. Apa yang Anda lakukan sekarang adalah Anda memanggil metode itu dan dengan demikian membiarkan Angular tahu tentang perubahan itu. Angular sekarang akan maju dan mengubah nilai formulir di luar. Ini adalah bagian penting dari semua ini. Anda memberi tahu Angular kapan harus memperbarui kontrol formulir dan dengan nilai apa dengan menelepononChange . Anda telah memberinya sarana untuk "mengakses nilai kontrol".

Ngomong-ngomong: Nama onChangeitu dipilih oleh saya. Anda dapat memilih apa pun di sini, misalnya propagateChangeatau serupa. Namun apa pun nama Anda, itu akan menjadi fungsi yang sama yang mengambil satu argumen, yang disediakan oleh Angular dan yang terikat ke kelas Anda dengan registerOnChangemetode selama runtime.

Memanggil onTouch

Karena kontrol formulir dapat "disentuh", Anda juga harus memberikan sudut kepada Angular cara untuk memahami ketika kontrol bentuk kustom Anda disentuh. Anda dapat melakukannya, Anda dapat menebaknya, dengan memanggil onTouchfungsi. Jadi untuk contoh kami di sini, jika Anda ingin tetap mematuhi bagaimana Angular melakukannya untuk kontrol bentuk out-of-the-box, Anda harus memanggil onTouchketika bidang input kabur:

<input type="text"
       [(ngModel)]="input"
       (ngModelChange)="onChange($event)"
       (blur)="onTouch()">

Sekali lagi, onTouchadalah nama yang dipilih oleh saya, tetapi apa fungsi sebenarnya disediakan oleh Angular dan tidak membutuhkan argumen. Yang masuk akal, karena Anda hanya memberi tahu Angular, bahwa kontrol bentuk telah disentuh.

Menyatukan semuanya

Jadi, bagaimana itu terlihat ketika datang bersama-sama? Seharusnya terlihat seperti ini:

// 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>

Lebih banyak contoh

Formulir Bersarang

Perhatikan bahwa Access Control Value BUKAN alat yang tepat untuk grup formulir bersarang. Untuk grup formulir bersarang Anda bisa menggunakan @Input() subformsaja. Access Control Value dimaksudkan untuk membungkus controls, bukan groups! Lihat contoh ini cara menggunakan input untuk formulir bersarang: https://stackblitz.com/edit/angular-nested-forms-input-2

Sumber

bersling
sumber
-1

Bagi saya itu disebabkan oleh atribut "berganda" pada kontrol input terpilih karena Angular memiliki ValueAccessor yang berbeda untuk jenis kontrol ini.

const countryControl = new FormControl();

Dan di dalam template gunakan seperti ini

    <select multiple name="countries" [formControl]="countryControl">
      <option *ngFor="let country of countries" [ngValue]="country">
       {{ country.name }}
      </option>
    </select>

Detail lebih lanjut, ref Documents Resmi

Sudhir Singh
sumber
Apa yang disebabkan oleh "banyak"? Saya tidak melihat bagaimana kode Anda memecahkan apa pun, atau apa masalah aslinya. Kode Anda menunjukkan penggunaan dasar yang biasa.
Lazar Ljubenović