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.
- 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
.
- 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
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 :)
0 votes
J'ai fait une réponse sur la façon de décomposer des formulaires énormes en sous-composants ici stackoverflow.com/a/56375605/2398593 mais cela s'applique aussi très bien avec un simple accesseur de valeur de contrôle personnalisé. Consultez également github.com/cloudnc/ngx-sub-form :)
0 votes
J'ai eu le même problème et l'ai résolu dans ce post : stackoverflow.com/a/64617295/1190948