184 votes

Angular4 - Pas d'accesseur de valeur pour le contrôle de formulaire

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'essaie d'ajouter le formControlName, j'obtiens un message d'erreur :

ERROR Erreur : Aucun accesseur de valeur pour le contrôle de formulaire avec le nom : 'surveyType'.

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

J'aimerais lier mon clic à ce formControl afin que, lorsque quelqu'un clique sur la carte entière, mon "type" soit introduit dans le formControl. Cela est-il possible ?

0 votes

Je ne sais pas, ce que je veux dire, c'est que : formControl correspond à un 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.

0 votes

Je sais que je pourrais utiliser l'ancienne méthode angulaire et faire en sorte que mon selectedType se lie à lui, mais j'essayais d'utiliser et d'apprendre le formulaire réactif d'angular 4 et je ne sais pas comment utiliser formControl dans ce type de cas.

0 votes

Ok, c'est peut-être juste que ce cas ne peut pas être géré par un formulaire réactif. Merci quand même :)

311voto

Lazar Ljubenović Points 9208

Vous pouvez utiliser formControlName uniquement sur les directives qui mettent en œuvre 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 mettant en œuvre les trois fonctions suivantes :

  • writeValue (indique à Angular comment écrire la valeur du modèle dans la vue)
  • registerOnChange (enregistre une fonction de gestion 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é mis au point).

Enregistrer un fournisseur

Ensuite, vous devez dire à Angular que cette directive est une ControlValueAccessor (l'interface ne suffira pas, car elle est supprimée du code lorsque TypeScript est compilé en JavaScript). Pour ce faire, il faut enregistrement d'un fournisseur .

Le prestataire doit fournir NG_VALUE_ACCESSOR y utiliser une valeur existante . Vous aurez également besoin d'un forwardRef ici. Notez que NG_VALUE_ACCESSOR devrait être un multi-fournisseur .

Par exemple, si votre directive personnalisée s'appelle MyControlComponent, vous devez ajouter quelque chose du type suivant à l'intérieur de l'objet passé à la commande @Component décorateur :

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

Utilisation

Votre composant est prêt à être utilisé. Avec des formulaires pilotés par des modèles , ngModel La liaison fonctionne désormais correctement.

Avec formes réactives vous pouvez maintenant utiliser correctement formControlName et le contrôle de formulaire se comportera comme prévu.

Ressources

2 votes

N'oubliez pas non plus le ngDefaultControl sur l'entrée concernée.

78voto

Vega Points 13451

Vous devez utiliser formControlName="surveyType" sur un input et non sur un div

0 votes

Oui, bien sûr, mais je ne sais pas comment transformer mon div de carte en quelque chose d'autre qui sera un contrôle de formulaire html.

9 votes

Le but de CustomValueAccessor est d'ajouter un contrôle de formulaire à N'IMPORTE QUOI, même un div.

6 votes

@SoEzPz C'est un mauvais modèle cependant. Vous imitez la fonctionnalité Input dans un composant wrapper, en réimplémentant vous-même les méthodes HTML standard (ce qui revient à réinventer la roue et à rendre 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 formControls il suffit de placer le <input> à l'intérieur du <wrapper>.

31voto

bersling Points 5004

L'erreur signifie qu'Angular ne sait pas quoi faire lorsque l'on met un fichier formControl sur un div . Pour résoudre ce problème, vous avez deux possibilités.

  1. Vous mettez le formControlName sur un élément, qui est pris en charge par Angular dès le départ. Ces éléments sont : input , textarea y select .
  2. Vous mettez en œuvre le ControlValueAccessor l'interface. Ce faisant, vous indiquez à Angular "comment accéder à la valeur de votre contrôle" (d'où son nom). Ou en termes simples : Que faire, lorsque vous mettez un formControlName sur un élément, qui n'a pas naturellement une valeur associée à lui.

Maintenant, la mise en œuvre de la ControlValueAccessor peut être un peu décourageante au début. D'autant plus qu'il n'y a pas beaucoup de documentation sur ce sujet et que vous devez ajouter beaucoup de texte passe-partout à votre code. Permettez-moi donc d'essayer de décomposer cela en quelques étapes simples à suivre.

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

Afin de mettre en œuvre le ControlValueAccessor vous devez créer un nouveau composant (ou directive). Déplacez-y le code relatif à votre contrôle de formulaire. Ainsi, il sera facilement réutilisable. Le fait d'avoir déjà un contrôle dans un composant peut être la raison pour laquelle vous devez implémenter la directive ControlValueAccessor car sinon, vous ne pourrez pas utiliser votre composant personnalisé avec les formulaires Angular.

Ajoutez le boilerplate à votre code

Mise en œuvre de la ControlValueAccessor est assez verbeuse, voici le texte 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) Permet à Angular de savoir, lors de l'exécution, que vous avez implémenté l'option ControlValueAccessor interface
  • b) S'assurer que vous mettez en œuvre le programme ControlValueAccessor interface
  • c) C'est probablement la partie la plus déroutante. En gros, ce que vous faites, c'est que vous donnez à Angular les moyens de surcharger les propriétés/méthodes de votre classe. onChange y onTouch avec sa propre implémentation pendant l'exécution, de sorte que vous pouvez ensuite appeler ces fonctions. Il est donc important de comprendre ce point : Vous n'avez pas besoin d'implémenter vous-même onChange et onTouch. (autre que l'implémentation initiale vide). La seule chose que vous faites avec (c) est de laisser Angular attacher ses propres fonctions à votre classe. Pourquoi ? Pour que vous puissiez ensuite appelez le site onChange y onTouch les méthodes fournies par Angular au moment opportun. Nous allons voir comment cela fonctionne ci-dessous.
  • d) Nous verrons également comment le writeValue La méthode fonctionne dans la section suivante, lorsque nous l'implémentons. Je l'ai mis ici, ainsi toutes les propriétés requises sur ControlValueAccessor sont mises en œuvre et votre code se compile toujours.

Mettre en œuvre writeValue

Quoi writeValue fait, c'est de 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-input et vous l'utiliserez dans le composant parent comme ceci :

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

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

En général, si la valeur du contrôle de formulaire change à l'extérieur, il faut l'écrire dans une variable locale qui représente la valeur du contrôle de formulaire. Par exemple, si votre CustomInputComponent tourne autour d'un contrôle de formulaire basé sur le texte, il 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 d'Angular.

Vous avez maintenant géré ce qui se passe à l'intérieur de votre composant lorsque quelque chose change à l'extérieur. Maintenant, regardons dans l'autre sens. Comment informer 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 à l'intérieur de votre CustomInputComponent . C'est là que le onChange y onTouch Les fonctions du point c) ci-dessus 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 champ input dans votre composant personnalisé, vous appelez onChange avec 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 a lié sa propre implémentation à l'objet onChange de la classe. Cette implémentation attend un argument, qui est la valeur de contrôle mise à jour. Ce que vous êtes en train de faire, c'est d'appeler cette méthode et d'informer Angular de la modification. Angular va maintenant aller de l'avant et changer la valeur du formulaire à l'extérieur. C'est la partie clé de tout ça. Vous avez dit à Angular quand il devait mettre à jour le contrôle de formulaire et avec quelle valeur en appelant onChange . Vous lui avez donné les moyens d'"accéder à la valeur de contrôle".

Au fait : Le nom onChange est choisi par moi. Vous pouvez choisir n'importe quoi ici, par exemple propagateChange ou autre. Quel que soit le nom que vous lui donnez, il s'agira de la même fonction qui prend un argument, qui est fournie par Angular et qui est liée à votre classe par l'attribut registerOnChange pendant l'exécution.

Appeler onTouch

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 fonction onTouch fonction. Ainsi, pour notre exemple, si vous voulez rester conforme à la façon dont Angular procède pour les contrôles de formulaires prêts à l'emploi, vous devez appeler onTouch lorsque le champ de saisie est flou :

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

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

Tout mettre en place

Alors, à quoi ça ressemble 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>

Autres exemples

Formes imbriquées

Notez que les accesseurs de valeurs 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 une balise @Input() subform au contraire. Les accesseurs de valeurs de contrôle sont destinés à envelopper controls pas groups ! Voir cet exemple pour savoir comment utiliser une entrée pour un formulaire imbriqué : https://stackblitz.com/edit/angular-nested-forms-input-2

Sources

-1voto

Sudhir Singh Points 1

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

const countryControl = new FormControl();

Et dans le modèle, utilisez comme ceci

    <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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X