javascript - Angular4 - Nema pristupnika vrijednosti za kontrolu obrasca
Imam prilagođeni element :
<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>
Kada pokušam dodati formControlName, dobivam poruku o pogrešci:
GREŠKA Greška: Nema pristupnika vrijednosti za kontrolu obrasca s imenom: 'surveyType'
Pokušao sam dodati ngDefaultControl bez uspjeha. Čini se da je to zato što nema input/select... i ne znam što da radim.
Želio bih povezati svoj klik s ovim formControl-om kako bi, kada netko klikne na cijelu karticu, to gurnulo moj 'type' u formControl. Je li to moguće?
Odgovor #1
Možete koristiti formControlName samo na direktivama koje implementiraju ControlValueAccessor.
Implementirajte sučelje
Dakle, da biste učinili ono što želite, morate stvoriti komponentu koja implementira ControlValueAccessor, što znači implementaciju sljedeće tri funkcije:
writeValue(kaže Angularu kako upisati vrijednost iz modela u prikaz)registerOnChange(registrira funkciju rukovatelja koja se poziva kada se prikaz promijeni)registerOnTouched(registrira obrađivača koji će se pozvati kada komponenta primi događaj dodira, korisno za znati je li komponenta bila u fokusu).
Registrirajte pružatelja
Zatim, morate reći Angularu da je ova direktiva ControlValueAccessor (sučelje nije dovoljno jer se uklanja iz koda kada se TypeScript kompajlira u JavaScript). To radite registriranjem providera.
Pružatelj usluga trebao bi osigurati NG_VALUE_ACCESSOR i koristiti postojeću vrijednost. Također će vam ovdje trebati forwardRef. Imajte na umu da NG_VALUE_ACCESSOR treba biti višestruki pružatelj.
Na primjer, ako se vaša prilagođena direktiva zove MyControlComponent, trebali biste dodati nešto slično sljedećem unutar objekta proslijeđenog dekoratoru @Component:
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => MyControlComponent),
}
]
Upotreba
Vaša komponenta je spremna za upotrebu. Kod obrazaca vođenih predloškom, ngModel povezivanje sada će raditi ispravno.
S reaktivnim obrascima sada možete ispravno koristiti formControlName i kontrola obrasca će se ponašati kako se očekuje.
Resursi
- Prilagođene kontrole obrazaca u Angularu od Thoughtram
- Prilagođeni Angular obrasci s reaktivnim obrascima i NgModelom autora Coryja Rylana
Odgovor #2
Trebali biste koristiti formControlName="surveyType" na input elementu, a ne na div elementu
Odgovor #3
Pogreška znači da Angular ne zna što učiniti kada stavite formControl na div. Da biste to popravili, imate dvije opcije.
- Stavite
formControlNamena element koji Angular podržava iz kutije. To su:input,textareaiselect. - Implementirate sučelje
ControlValueAccessor. Time Angularu govorite „kako pristupiti vrijednosti vašeg kontrola“ (otuda i naziv). Ili jednostavnije: Što učiniti kada staviteformControlNamena element koji prirodno nema pridruženu vrijednost.
Sada, implementacija sučelja ControlValueAccessor može biti pomalo zastrašujuća na početku. Posebno zato što ne postoji mnogo dobre dokumentacije o tome i potrebno je dodati puno osnovnog koda u vaš projekt. Dakle, dopustite mi da pokušam ovo razložiti u nekoliko jednostavnih koraka koje je lako pratiti.
Premjesti svoj kontrolni element obrasca u vlastitu komponentu
Da biste implementirali ControlValueAccessor, trebate stvoriti novu komponentu (ili direktivu). Premjestite kod koji se odnosi na vaš kontrolu obrasca tamo. Na ovaj način bit će i lako ponovno upotrebljiv. To što kontrola već postoji unutar komponente može biti razlog zbog kojeg u prvom redu trebate implementirati ControlValueAccessor sučelje, jer inače nećete moći koristiti vašu prilagođenu komponentu zajedno s Angular obrascima.
Dodajte predložak u svoj kod
Implementacija sučelja ControlValueAccessor prilično je opsežna, evo osnovnog koda koji dolazi s njom:
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
}
Pa što rade pojedini dijelovi?
- a) Obavijestite Angular tijekom izvođenja da ste implementirali sučelje
ControlValueAccessor - b) Osigurava da implementirate
ControlValueAccessorsučelje - c) Ovo je vjerojatno najzbunjujući dio. U osnovi, ono što radite je da Angularu dajete mogućnost da prepiše svojstva/metode vaše klase
onChangeionTouchsvojom vlastitom implementacijom tijekom izvođenja, tako da možete potom pozivati te funkcije. Dakle, ovo je važno razumjeti: Ne morate sami implementirati onChange i onTouch (osim početne prazne implementacije). Jedino što radite s (c) je da omogućite Angularu da priključi svoje funkcije na vašu klasu. Zašto? Tako da tada možete pozvati metodeonChangeionTouchkoje pruža Angular u odgovarajuće vrijeme. Vidjet ćemo kako ovo funkcionira dolje. - d) Također ćemo vidjeti kako metoda
writeValueradi u sljedećem odjeljku, kada je implementiramo. Stavio sam je ovdje, kako bi sva potrebna svojstva naControlValueAccessorbila implementirana i vaš kod se i dalje kompajlirao.
Implementiraj writeValue
Što writeValue radi je to da učini nešto unutar vaše prilagođene komponente, kada se vanjski oblikovni kontrola promijeni. Dakle, na primjer, ako ste nazvali svoju prilagođenu komponentu kontrolu obrasca app-custom-input i koristili biste je u roditeljskoj komponenti ovako:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
tada se writeValue pokreće kad god roditeljska komponenta na neki način promijeni vrijednost myFormControl. To bi, na primjer, moglo biti tijekom inicijalizacije obrasca (this.form=this.formBuilder.group({myFormControl: ""});) ili prilikom resetiranja obrasca this.form.reset();.
Ono što ćete obično htjeti učiniti ako se vrijednost kontrola obrasca promijeni izvana je zapisati je u lokalnu varijablu koja predstavlja vrijednost kontrole obrasca. Na primjer, ako se vaš CustomInputComponent vrti oko tekstualne kontrole obrasca, to bi moglo izgledati ovako:
writeValue(input: string) {
this.input = input;
}
i u HTML-u CustomInputComponent:
<input type="text"
[ngModel]="input">
Također biste to mogli napisati izravno u element unosa kao što je opisano u Angular dokumentaciji.
Sada ste obradili što se događa unutar vaše komponente kada se nešto promijeni izvana. Sada pogledajmo drugi smjer. Kako obavijestiti vanjski svijet kada se nešto promijeni unutar vaše komponente?
Pozivanje onChange
Sljedeći korak je obavijestiti roditeljsku komponentu o promjenama unutar vašeg CustomInputComponent. Ovo je mjesto gdje dolaze funkcije onChange i onTouch iz (c) gore. Pozivanjem tih funkcija možete obavijestiti vanjski svijet o promjenama unutar vaše komponente. Kako biste prenijeli promjene vrijednosti na vanjski svijet, morate pozvati onChange s novom vrijednošću kao argumentom. Na primjer, ako korisnik upiše nešto u input polje u vašoj prilagođenoj komponenti, pozivate onChange s ažuriranom vrijednošću:
<input type="text"
[ngModel]="input"
(ngModelChange)="onChange($event)">
Ako ponovno provjerite implementaciju (c) gore, vidjet ćete što se događa: Angular je vezao vlastitu implementaciju za onChange vlasničku klasu. Ta implementacija očekuje jedan argument, a to je ažurirana vrijednost kontrola. Ono što sada radite je da pozivate tu metodu i time obavještavate Angular o promjeni. Angular će sada nastaviti i promijeniti vrijednost obrasca izvana. Ovo je ključni dio svega ovoga. Rekli ste Angularu kada treba ažurirati kontrolu obrasca i s kojom vrijednošću pozivom onChange. Dali ste mu sredstva za "pristup vrijednosti kontrole".
Usput: Ime onChange odabrao sam ja. Ovdje možete odabrati bilo što, na primjer propagateChange ili slično. Bez obzira kako ga nazvali, bit će to ista funkcija koja prima jedan argument, koji osigurava Angular i koji je povezan s vašom klasom putem metode registerOnChange tijekom izvođenja.
Pozivanje onTouch
Budući da se kontrolama obrasca može pristupiti kao "dodirnutima", trebali biste Angularu također omogućiti da razumije kada je vaša prilagođena kontrola obrasca dodirnuta. To možete učiniti, pogodili ste, pozivom funkcije onTouch. Dakle, za naš primjer ovdje, ako želite ostati u skladu s načinom na koji Angular to radi za standardne kontrole obrasca, trebali biste pozvati onTouch kada polje za unos izgubi fokus:
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
Opet, onTouch je ime koje sam odabrao, ali njegova stvarna funkcija je osigurana od strane Angulara i ne prima argumente. Što ima smisla, jer samo obavještavate Angular da je kontrola forme dotaknuta.
Stavljajući sve zajedno
Pa kako to izgleda kada se sve spoji? Trebalo bi izgledati ovako:
// 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>
Više primjera
- Primjer s unosom: https://stackblitz.com/edit/angular-control-value-accessor-simple-example-tsmean
- Primjer s lenjo učitanim unosom: https://stackblitz.com/edit/angular-control-value-accessor-lazy-input-example-tsmean
- Primjer s gumbom: https://stackblitz.com/edit/angular-control-value-accessor-button-example-tsmean
Ugniježđeni obrasci
Imajte na umu da Control Value Accessors NIJE pravi alat za ugneždene form grupe. Za ugneždene form grupe jednostavno možete koristiti @Input() subform. Control Value Accessors su namijenjeni omotavanju controls, a ne groups! Pogledajte ovaj primjer kako koristiti input za ugneždeni obrazac: https://stackblitz.com/edit/angular-nested-forms-input-2
Izvori
- https://angular.io/api/forms/ControlValueAccessor
- https://www.tsmean.com/articles/angular/angular-control-value-accessor-example/
