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.

  1. Stavite formControlName na element koji Angular podržava iz kutije. To su: input, textarea i select.
  2. Implementirate sučelje ControlValueAccessor. Time Angularu govorite „kako pristupiti vrijednosti vašeg kontrola“ (otuda i naziv). Ili jednostavnije: Što učiniti kada stavite formControlName na 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 ControlValueAccessor suč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 onChange i onTouch svojom 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 metode onChange i onTouch koje pruža Angular u odgovarajuće vrijeme. Vidjet ćemo kako ovo funkcionira dolje.
  • d) Također ćemo vidjeti kako metoda writeValue radi u sljedećem odjeljku, kada je implementiramo. Stavio sam je ovdje, kako bi sva potrebna svojstva na ControlValueAccessor bila 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/

Povezani članci