Angular4 - Aucun accesseur de valeur pour le contrôle de formulaire

146

J'ai un élément personnalisé:

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

Lorsque j'essaye d'ajouter le formControlName, j'obtiens un message d'erreur:

ERREUR Erreur: aucun accesseur de valeur pour le contrôle de formulaire avec le nom: 'surveyType'

J'ai essayé d'ajouter ngDefaultControlsans succès. Il semble que c'est parce qu'il n'y a pas d'entrée / sélection ... et je ne sais pas quoi faire.

Je voudrais lier mon clic à ce formControl afin que lorsque quelqu'un clique sur la carte entière, cela pousse mon «type» dans le formControl. C'est possible?

jbtd
la source
Je ne sais pas ce que je veux dire, c'est que: formControl utilise le contrôle de formulaire en html mais div n'est pas un contrôle de formulaire. Je voudrais lier mon surveyType avec le type.id de ma carte div
jbtd
Je sais que je pourrais utiliser l'ancienne méthode angulaire et faire en sorte que mon selectedType s'y lie, mais j'essayais d'utiliser et d'apprendre la forme réactive d'angular 4 et je ne sais pas comment utiliser formControl avec ce type de cas.
jbtd
Ok, c est peut-être que ce cas ne peut pas être traité par une forme réactive. Merci quand même :)
jbtd
J'ai fait une réponse sur la façon de décomposer d'énormes formulaires en sous-composants ici stackoverflow.com/a/56375605/2398593 mais cela s'applique également très bien avec juste un accesseur de valeur de contrôle personnalisé. Consultez également github.com/cloudnc/ngx-sub-form :)
maxime1992

Réponses:

251

Vous ne pouvez utiliser formControlNameque sur les directives qui implémentent ControlValueAccessor.

Implémenter l'interface

Donc, pour faire ce que vous voulez, vous devez créer un composant qui implémente ControlValueAccessor, ce qui signifie implémenter les trois fonctions suivantes :

  • writeValue (indique à Angular comment écrire la valeur du modèle dans la vue)
  • registerOnChange (enregistre une fonction de gestionnaire qui est appelée lorsque la vue change)
  • registerOnTouched (enregistre un gestionnaire à appeler lorsque le composant reçoit un événement tactile, utile pour savoir si le composant a été focalisé).

Enregistrer un fournisseur

Ensuite, vous devez dire à Angular que cette directive est une ControlValueAccessor(interface ne la coupera pas car elle est supprimée du code lorsque TypeScript est compilé en JavaScript). Pour ce faire, vous enregistrez un fournisseur .

Le fournisseur doit fournir NG_VALUE_ACCESSORet utiliser une valeur existante . Vous aurez également besoin d'un forwardRefici. Notez que cela NG_VALUE_ACCESSORdevrait être un fournisseur multiple .

Par exemple, si votre directive personnalisée est nommée MyControlComponent, vous devez ajouter quelque chose le long des lignes suivantes à l'intérieur de l'objet passé au @Componentdécorateur:

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

Usage

Votre composant est prêt à être utilisé. Avec les formulaires basés sur des modèles , la ngModelliaison fonctionnera désormais correctement.

Avec les formulaires réactifs , vous pouvez désormais utiliser correctement formControlNameet le contrôle de formulaire se comportera comme prévu.

Ressources

Lazar Ljubenović
la source
72

Je pense que vous devriez utiliser formControlName="surveyType"sur un inputet non sur undiv

Vega
la source
Oui, bien sûr, mais je ne sais pas comment transformer ma carte div en autre chose qui sera un contrôle de formulaire html
jbtd
5
Le but de CustomValueAccessor est d'ajouter un contrôle de formulaire à TOUT, même un div
SoEzPz
4
@SoEzPz C'est un mauvais modèle cependant. Vous imitez la fonctionnalité d'entrée dans un composant wrapper, réimplémentant vous-même les méthodes HTML standard (réinventant ainsi fondamentalement la roue et rendant votre code verbeux). mais dans 90% des cas, vous pouvez accomplir tout ce que vous voulez en utilisant <ng-content>dans un composant wrapper et laisser le composant parent qui définit formControlssimplement mettre le <input> à l'intérieur du <wrapper>
Phil
3

L'erreur signifie qu'Angular ne sait pas quoi faire lorsque vous mettez formControlun div. Pour résoudre ce problème, vous avez deux options.

  1. Vous mettez le formControlNamesur un élément, qui est pris en charge par Angular hors de la boîte. Ce sont: input, textareaet select.
  2. Vous implémentez l' ControlValueAccessorinterface. En faisant cela, vous dites à Angular "comment accéder à la valeur de votre contrôle" (d'où le nom). Ou en termes simples: que faire, lorsque vous mettez un formControlNamesur un élément, qui n'a naturellement pas de valeur associée.

Désormais, la mise en œuvre de l' ControlValueAccessorinterface peut être un peu intimidante au début. Surtout parce qu'il n'y a pas beaucoup de bonne documentation à ce sujet et que vous devez ajouter beaucoup de passe-partout à votre code. Alors laissez-moi essayer de décomposer cela en quelques étapes simples à suivre.

Déplacez votre contrôle de formulaire dans son propre composant

Pour implémenter le ControlValueAccessor, vous devez créer un nouveau composant (ou directive). Déplacez-y le code lié à votre contrôle de formulaire. Comme ça, il sera également facilement réutilisable. Avoir un contrôle déjà à l'intérieur d'un composant peut être la raison en premier lieu, pourquoi vous devez implémenter l' ControlValueAccessorinterface, car sinon vous ne pourrez pas utiliser votre composant personnalisé avec des formulaires angulaires.

Ajoutez le passe-partout à votre code

La mise en œuvre de l' ControlValueAccessorinterface est assez verbeuse, voici le passe-partout qui l'accompagne:

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
  }

Alors, que font les différentes parties?

  • a) Informe Angular pendant l'exécution que vous avez implémenté l' ControlValueAccessorinterface
  • b) S'assure que vous implémentez l' ControlValueAccessorinterface
  • c) C'est probablement la partie la plus déroutante. Fondamentalement, vous donnez à Angular le moyen de remplacer vos propriétés / méthodes de classe onChangeet onTouchavec sa propre implémentation pendant l'exécution, de sorte que vous puissiez ensuite appeler ces fonctions. Il est donc important de comprendre ce point: vous n'avez pas besoin d'implémenter onChange et onTouch vous-même (autre que l'implémentation vide initiale). La seule chose que vous faites avec (c) est de laisser Angular attacher ses propres fonctions à votre classe. Pourquoi? Vous pouvez donc appeler les méthodes onChangeet onTouchfournies par Angular au moment opportun. Nous verrons comment cela fonctionne ci-dessous.
  • d) Nous verrons également comment la writeValueméthode fonctionne dans la section suivante, lorsque nous l'implémenterons. Je l'ai mis ici, donc toutes les propriétés requises sur ControlValueAccessorsont implémentées et votre code se compile toujours.

Implémenter writeValue

Ce que writeValuefait, c'est faire quelque chose à l'intérieur de votre composant personnalisé, lorsque le contrôle de formulaire est modifié à l'extérieur . Ainsi, par exemple, si vous avez nommé votre composant de contrôle de formulaire personnalisé app-custom-inputet que vous l'utiliseriez dans le composant parent comme ceci:

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

puis writeValueest déclenché chaque fois que le composant parent change d'une manière ou d'une autre la valeur de myFormControl. Cela peut être par exemple lors de l'initialisation du formulaire ( this.form = this.formBuilder.group({myFormControl: ""});) ou lors d'une réinitialisation de formulaire this.form.reset();.

Ce que vous souhaiterez généralement faire si la valeur du contrôle de formulaire change à l'extérieur, c'est de l'écrire dans une variable locale qui représente la valeur du contrôle de formulaire. Par exemple, si votre CustomInputComponenttourne autour d'un contrôle de formulaire basé sur du texte, cela pourrait ressembler à ceci:

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

et dans le html de CustomInputComponent:

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

Vous pouvez également l'écrire directement dans l'élément d'entrée comme décrit dans la documentation Angular.

Vous avez maintenant géré ce qui se passe à l'intérieur de votre composant lorsque quelque chose change à l'extérieur. Regardons maintenant l'autre direction. Comment informez-vous le monde extérieur lorsque quelque chose change à l'intérieur de votre composant?

Appeler onChange

L'étape suivante consiste à informer le composant parent des modifications apportées à votre CustomInputComponent. C'est là que les fonctions onChangeet onTouchde (c) d'en haut entrent en jeu. En appelant ces fonctions, vous pouvez informer l'extérieur des changements à l'intérieur de votre composant. Afin de propager les modifications de la valeur vers l'extérieur, vous devez appeler onChange avec la nouvelle valeur comme argument . Par exemple, si l'utilisateur tape quelque chose dans le inputchamp de votre composant personnalisé, vous appelez onChangeavec la valeur mise à jour:

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

Si vous vérifiez à nouveau l'implémentation (c) ci-dessus, vous verrez ce qui se passe: Angular lié sa propre implémentation à la onChangepropriété de classe. Cette implémentation attend un argument, qui est la valeur de contrôle mise à jour. Ce que vous faites maintenant, c'est que vous appelez cette méthode et que vous informez Angular du changement. Angular va maintenant continuer et changer la valeur du formulaire à l'extérieur. C'est la partie clé de tout cela. Vous avez indiqué à Angular quand il doit mettre à jour le contrôle de formulaire et avec quelle valeur en appelantonChange . Vous lui avez donné le moyen "d'accéder à la valeur de contrôle".

Au fait: le nom onChangeest choisi par moi. Vous pouvez choisir n'importe quoi ici, par exemple propagateChangeou similaire. Quelle que soit la façon dont vous le nommez, ce sera la même fonction qui prend un argument, qui est fourni par Angular et qui est lié à votre classe par la registerOnChangeméthode pendant l'exécution.

Appeler surTouch

Puisque les contrôles de formulaire peuvent être «touchés», vous devez également donner à Angular les moyens de comprendre quand votre contrôle de formulaire personnalisé est touché. Vous pouvez le faire, vous l'avez deviné, en appelant la onTouchfonction. Donc, pour notre exemple ici, si vous voulez rester conforme à la façon dont Angular le fait pour les contrôles de formulaire prêts à l'emploi, vous devez appeler onTouchlorsque le champ de saisie est flou:

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

Encore onTouchune fois, c'est un nom que j'ai choisi, mais sa fonction réelle est fournie par Angular et ne prend aucun argument. Ce qui est logique, puisque vous faites juste savoir à Angular, que le contrôle de formulaire a été touché.

Mettre tous ensemble

Alors, à quoi cela ressemble-t-il quand tout est réuni? Ça devrait ressembler à ça:

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

Plus d'exemples

Formulaires imbriqués

Notez que les accesseurs de valeur de contrôle ne sont PAS le bon outil pour les groupes de formulaires imbriqués. Pour les groupes de formulaires imbriqués, vous pouvez simplement utiliser un @Input() subformfichier. Les accesseurs de valeur de contrôle sont destinés à envelopper controls, non groups! Voir cet exemple comment utiliser une entrée pour un formulaire imbriqué: https://stackblitz.com/edit/angular-nested-forms-input-2

Sources

bersling
la source
-1

Pour moi, cela était dû à l'attribut "multiple" sur le contrôle d'entrée sélectionné car Angular a un ValueAccessor différent pour ce type de contrôle.

const countryControl = new FormControl();

Et à l'intérieur du modèle, utilisez comme ça

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

Plus de détails réf Docs officiels

Sudhir Singh
la source
Qu'est-ce qui était dû à "multiple"? Je ne vois pas comment votre code résout quoi que ce soit, ni quel était le problème d'origine. Votre code montre l'utilisation de base habituelle.
Lazar Ljubenović