Angolare4: nessun valore di accesso per il controllo del modulo


146

Ho un elemento personalizzato:

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

Quando provo ad aggiungere formControlName, ricevo un messaggio di errore:

Errore ERRORE: nessun programma di accesso ai valori per controllo modulo con nome: 'surveyType'

Ho provato ad aggiungere ngDefaultControlsenza successo. Sembra che non ci siano input / select ... e non so cosa fare.

Vorrei associare il mio clic a questo formControl in modo che quando qualcuno fa clic sull'intera scheda che spinga il mio "tipo" nel formControl. È possibile?


Non so che il mio punto è che: formControl cerca il controllo del modulo in HTML ma div non è un controllo del modulo. Vorrei che tu legassi il mio sondaggio Digita il type.id della mia div card
jbtd

so che potrei usare il vecchio modo angolare e far sì che il mio selezionato Type si leghi ad esso, ma stavo cercando di usare e imparare la forma reattiva dall'angolo 4 e non so come usare formControl con questo tipo di caso.
jbtd,

Ok, forse è che quel caso non può essere gestito da una forma reattiva.
Grazie

Ho fatto una risposta su come suddividere enormi moduli in sottocomponenti qui stackoverflow.com/a/56375605/2398593 ma questo vale anche molto bene con solo un accessorio di valore di controllo personalizzato.
Dai

Risposte:


250

È possibile utilizzare formControlNamesolo su direttive che implementano ControlValueAccessor.

Implementa l'interfaccia

Quindi, per fare ciò che vuoi, devi creare un componente che implementa ControlValueAccessor, il che significa implementare le seguenti tre funzioni :

  • writeValue (indica ad Angular come scrivere il valore dal modello in vista)
  • registerOnChange (registra una funzione del gestore che viene chiamata quando la vista cambia)
  • registerOnTouched (registra un gestore da chiamare quando il componente riceve un evento touch, utile per sapere se il componente è stato focalizzato).

Registra un provider

Quindi, devi dire ad Angular che questa direttiva è una ControlValueAccessor(l'interfaccia non la taglierà poiché viene rimossa dal codice quando TypeScript viene compilato in JavaScript). Puoi farlo registrando un provider .

Il fornitore dovrebbe fornire NG_VALUE_ACCESSORe utilizzare un valore esistente . Avrai anche bisogno di un forwardRefqui. Si noti che NG_VALUE_ACCESSORdovrebbe essere un multi provider .

Ad esempio, se la direttiva personalizzata è denominata MyControlComponent, è necessario aggiungere qualcosa lungo le seguenti righe all'interno dell'oggetto passato a @Componentdecoratore:

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

uso

Il componente è pronto per essere utilizzato. Con i moduli basati su modelli , ngModelora l'associazione funzionerà correttamente.

Con i moduli reattivi , ora è possibile utilizzare correttamente formControlNamee il controllo dei moduli si comporterà come previsto.

risorse


72

Penso che dovresti usare formControlName="surveyType"su un inpute non su undiv


Sì certo, ma non so come trasformare il div della mia carta in qualcos'altro che sarà un controllo del modulo html
jbtd

5
Il punto di CustomValueAccessor è quello di aggiungere il controllo del modulo a TUTTO, anche un div
SoEzPz,

4
@SoEzPz Questo è un cattivo modello però. Imiti la funzionalità di input in un componente wrapper, reimplementando tu stesso i metodi HTML standard (fondamentalmente reinventando la ruota e rendendo il tuo codice dettagliato). ma nel 90% dei casi puoi realizzare tutto quello che vuoi usando <ng-content>in un componente wrapper e lasciare che il componente genitore che definisce formControlssemplicemente metta <input> all'interno di <wrapper>
Phil

3

L'errore significa che Angular non sa cosa fare quando si inserisce formControla div. Per risolvere questo problema, hai due opzioni.

  1. Inserisci l' formControlNameelemento on, che è supportato da Angular out of the box. Questi sono: input, textareae select.
  2. Si implementa l' ControlValueAccessorinterfaccia. In questo modo, stai dicendo ad Angular "come accedere al valore del tuo controllo" (da cui il nome). O in termini semplici: cosa fare, quando si inserisce formControlNameun elemento, a cui naturalmente non è associato un valore.

Ora, implementare l' ControlValueAccessorinterfaccia può essere inizialmente un po 'scoraggiante. Soprattutto perché non c'è molta buona documentazione di questo là fuori e devi aggiungere molta piastra di caldaia al tuo codice. Vorrei quindi provare a scomporlo in alcuni passaggi semplici da seguire.

Sposta il controllo del modulo nel suo componente

Per implementare il ControlValueAccessor, è necessario creare un nuovo componente (o direttiva). Spostare lì il codice relativo al controllo del modulo. In questo modo sarà anche facilmente riutilizzabile. Avere un controllo già all'interno di un componente potrebbe essere la ragione in primo luogo, perché è necessario implementare l' ControlValueAccessorinterfaccia, perché altrimenti non sarà possibile utilizzare il componente personalizzato insieme ai moduli angolari.

Aggiungi la caldaia al tuo codice

L'implementazione ControlValueAccessordell'interfaccia è piuttosto dettagliata, ecco il bollettino che ne deriva:

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
  }

Quindi cosa stanno facendo le singole parti?

  • a) Consente ad Angular di sapere durante l'esecuzione che è stata implementata l' ControlValueAccessorinterfaccia
  • b) Assicurati di implementare l' ControlValueAccessorinterfaccia
  • c) Questa è probabilmente la parte più confusa. Fondamentalmente quello che stai facendo è dare ad Angular i mezzi per sovrascrivere le proprietà / i metodi della tua classe onChangee onTouchcon la sua propria implementazione durante il runtime, in modo da poter chiamare quelle funzioni. Quindi questo punto è importante da capire: non è necessario implementare onChange e onTouch (oltre all'implementazione vuota iniziale). L'unica cosa che stai facendo con (c) è lasciare che Angular associ le proprie funzioni alla tua classe. Perché? Quindi puoi quindi chiamare i metodi onChangee onTouchforniti da Angular al momento opportuno. Vedremo come funziona di seguito.
  • d) Vedremo anche come funziona il writeValuemetodo nella prossima sezione, quando lo implementeremo. L'ho messo qui, quindi tutte le proprietà richieste ControlValueAccessorsono implementate e il tuo codice viene ancora compilato.

Implementare writeValue

Ciò che writeValuefa è fare qualcosa all'interno del componente personalizzato, quando il controllo del modulo viene modificato all'esterno . Ad esempio, se hai nominato il tuo componente di controllo del modulo personalizzato app-custom-inpute lo utilizzeresti nel componente padre in questo modo:

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

quindi writeValueviene attivato ogni volta che il componente padre modifica in qualche modo il valore di myFormControl. Questo potrebbe essere ad esempio durante l'inizializzazione del form ( this.form = this.formBuilder.group({myFormControl: ""});) o su un reset del modulo this.form.reset();.

Ciò che generalmente si desidera fare se il valore del controllo del modulo cambia all'esterno, è scriverlo in una variabile locale che rappresenta il valore del controllo del modulo. Ad esempio, se il tuo CustomInputComponentruota attorno a un controllo modulo basato su testo, potrebbe apparire così:

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

e nel codice HTML di CustomInputComponent:

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

Puoi anche scriverlo direttamente sull'elemento di input come descritto nei documenti angolari.

Ora hai gestito ciò che accade all'interno del tuo componente quando qualcosa cambia all'esterno. Ora diamo un'occhiata all'altra direzione. Come informi il mondo esterno quando qualcosa cambia all'interno del tuo componente?

Chiamando onChange

Il prossimo passo è informare il componente genitore delle modifiche all'interno del tuo CustomInputComponent. È qui che entrano in gioco le funzioni onChangee onTouchdi (c) dall'alto. Chiamando quelle funzioni è possibile informare l'esterno delle modifiche all'interno del componente. Per propagare le modifiche del valore verso l'esterno, è necessario chiamare onChange con il nuovo valore come argomento . Ad esempio, se l'utente digita qualcosa nel inputcampo nel componente personalizzato, si chiama onChangecon il valore aggiornato:

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

Se controlli di nuovo l'implementazione (c) dall'alto, vedrai cosa sta succedendo: Angular bound è la propria implementazione alla onChangeproprietà class. Tale implementazione prevede un argomento, ovvero il valore di controllo aggiornato. Quello che stai facendo ora è chiamare quel metodo e quindi far conoscere ad Angular il cambiamento. Angolare ora procederà e modificherà il valore del modulo all'esterno. Questa è la parte fondamentale in tutto questo. Hai detto ad Angular quando dovrebbe aggiornare il controllo del modulo e con quale valore chiamandoonChange . Gli hai dato i mezzi per "accedere al valore di controllo".

A proposito: il nome onChangeè stato scelto da me. Puoi scegliere qualsiasi cosa qui, ad esempio propagateChangeo simili. Comunque lo chiami, sarà la stessa funzione che accetta un argomento, che è fornito da Angular e che è legato alla tua classe dal registerOnChangemetodo durante il runtime.

Chiamata onTouch

Poiché i controlli del modulo possono essere "toccati", è necessario fornire anche ad Angular i mezzi per capire quando viene toccato il controllo del modulo personalizzato. Puoi farlo, hai indovinato, chiamando la onTouchfunzione. Quindi, per il nostro esempio qui, se vuoi rimanere conforme a come Angular lo sta facendo per i controlli del modulo predefiniti, dovresti chiamare onTouchquando il campo di input è sfocato:

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

Ancora onTouchuna volta, è un nome scelto da me, ma la sua funzione effettiva è fornita da Angular e richiede zero argomenti. Il che ha senso, dato che stai solo facendo sapere ad Angular, che il controllo del modulo è stato toccato.

Mettere tutto insieme

Quindi come appare quando si riuniscono tutti insieme? Dovrebbe sembrare come questo:

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

Altri esempi

Moduli nidificati

Notare che gli Accessor Control Value NON sono lo strumento giusto per i gruppi di moduli nidificati. Per i gruppi di moduli nidificati è possibile semplicemente utilizzare un @Input() subforminvece. Control Accessor Gli accessori sono pensati per concludere controls, no groups! Vedi questo esempio su come utilizzare un input per un modulo nidificato: https://stackblitz.com/edit/angular-nested-forms-input-2

fonti


-1

Per me è stato dovuto all'attributo "multiplo" sul controllo di input selezionato poiché Angular ha ValueAccessor diverso per questo tipo di controllo.

const countryControl = new FormControl();

E il template interno usa così

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

Maggiori dettagli si riferiscono a Documenti ufficiali


Cosa era dovuto al "multiplo"? Non vedo come il tuo codice risolva qualcosa o quale fosse il problema originale. Il tuo codice mostra il solito utilizzo di base.
Lazar Ljubenović,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.