L'erreur signifie qu'Angular ne sait pas quoi faire lorsque vous mettez formControl
un div
. Pour résoudre ce problème, vous avez deux options.
- Vous mettez le
formControlName
sur un élément, qui est pris en charge par Angular hors de la boîte. Ce sont: input
, textarea
et select
.
- Vous implémentez l'
ControlValueAccessor
interface. 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 formControlName
sur un élément, qui n'a naturellement pas de valeur associée.
Désormais, la mise en œuvre de l' ControlValueAccessor
interface 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' ControlValueAccessor
interface, 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' ControlValueAccessor
interface 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'
ControlValueAccessor
interface
- b) S'assure que vous implémentez l'
ControlValueAccessor
interface
- 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
onChange
et onTouch
avec 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 onChange
et onTouch
fournies par Angular au moment opportun. Nous verrons comment cela fonctionne ci-dessous.
- d) Nous verrons également comment la
writeValue
méthode fonctionne dans la section suivante, lorsque nous l'implémenterons. Je l'ai mis ici, donc toutes les propriétés requises sur ControlValueAccessor
sont implémentées et votre code se compile toujours.
Implémenter writeValue
Ce que writeValue
fait, 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-input
et que vous l'utiliseriez dans le composant parent comme ceci:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
puis writeValue
est 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 CustomInputComponent
tourne 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 onChange
et onTouch
de (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 input
champ de 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 lié sa propre implémentation à la onChange
proprié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 onChange
est choisi par moi. Vous pouvez choisir n'importe quoi ici, par exemple propagateChange
ou 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 registerOnChange
mé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 onTouch
fonction. 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 onTouch
lorsque le champ de saisie est flou:
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
Encore onTouch
une 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() subform
fichier. 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