Comment créer facilement un composant input réutilisable piloté par l'interface ControlValueAccessor ?

L'une des problématiques majeures en Angular est de créer des composants réutilisables ce qui constitue l'essence même du framework. Souvent, nous sommes amenés à créer des composants input pour les utiliser un peu partout dans nos projets suivant le besoin. Ces composants input doivent être fonctionnels lorsque nous mettons en place un formulaire piloté par un Template-Driven Form ou un Reactive Form.

Le but de cet article est de mettre la lumière sur comment créer facilement un composant input réutilisable que nous pourrions utiliser dans nos formulaires réactifs.

Etape 1 : Création du composant app-input

Commençons par générer un nouveau composant avec la commande Angular CLI :

  ng generate component input

Nous modifierons le template de notre composant pour avoir ceci de façon très basique :

  <input type="text" [formControl]="control"/>

Etape 2: Implémentation de ControlValueAccessor

Pour que notre composant puisse être utilisé dans un formulaire réactif ou un formulaire basé sur des modèles, nous devons implémenter l'interface ControlValueAccessor.

Nous avons besoin de l'interface ControlValueAccessor parce que lorsqu'on crée le composant et qu'on désire l'utiliser dans un composant parent, nous ne devons pas oublier qu'il sera encapsulé dans son sélecteur app-input (dans notre cas ici). Ainsi, tous les attributs auxquels l'on avait accès directement sur un input ne seront plus disponibles. C'est davantage plus difficile si l'on veut avoir le contrôle sur notre composant via un formulaire réactif ou un formulaire basé sur des modèles.

Cette interface fournit une série de méthodes que nous pouvons utiliser pour synchroniser la valeur de notre composant avec un FormControl.

Nous pouvons le faire en ajoutant les méthodes suivantes à notre composant :

onChange: any = () => {};

onTouched = () => {};

writeValue(value: any): void {
    // mettre à jour la valeur de votre composant
}

registerOnChange(fn: any): void {
 // enregistrer une fonction qui sera appelée lorsque la valeur du composant change
 this.onChange = fn;
}

registerOnTouched(fn: any): void {
 // enregistrer une fonction qui sera appelée lorsque le composant est touché
 this.onTouched = fn;
}

Lorsqu'on implémente l'interface ControlValueAccessor, ces trois méthodes sont automatiquement redéfinies dans notre composant. Ces méthodes permettront à notre composant d'écouter les changements et de les enregistrer suivant le besoin.

Il faut aussi penser à rendre accessible l'objet AbstractControl afin de pouvoir effectuer des manipulations dans le template HTML de notre composant pour soit afficher des messages d'erreur ou gérer des scénarios. Voici ce qu'il faut faire :

...

public control: AbstractControl;

...

constructor(private injector: Injector) {}

ngAfterViewInit(): void {
    const ngControl = this.injector.get(NgControl, null);
    if (ngControl) {
      setTimeout(() => {
        this.control = Object.assign(new FormControl(), ngControl.control) as FormControl;
      });
    }
  }

Cette initialisation dans la méthode ngAfterViewInit nous permet d'accéder facilement à l'objet AbstractControl de notre composant input et de prévenir un problème lié à l'appel de notre composant dans un composant parent. En effet, le setTimeout est utilisé pour s'assurer que le DOM a été bien initialisé par le parent pour permettre l'utilisation de notre composant enfant.

Nous devons également fournir un provider pour notre composant qui indique à Angular d'utiliser notre composant comme un ControlValueAccessor :

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

Etape 3 : Utilisation du composant dans un formulaire

Maintenant que notre composant est un ControlValueAccessor, nous pouvons l'utiliser dans un formulaire réactif. Voici un exemple :

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

Nous pouvons obtenir la valeur du composant en utilisant form.get('firstname').value et nous pouvons définir la valeur du composant en utilisant form.get('firstname').setValue(value).

Est-ce que ça fonctionne aussi avec [(ngModel)] ?

La question de savoir si ngModel est pris en compte par le ControlValueAccessor est très récurrente !

La réponse est OUI. Nous pouvons utiliser ngModel sur un composant piloté par ControlValueAccessor et ça fonctionne parfaitement :

 <app-input [(ngModel)]="value"></app-input>

En somme, lorsque nous décidons de créer des composants réutilisables de type input, pensons toujours à implémenter l'interface ControlValueAccessor pour avoir cette flexibilité à pouvoir l'utiliser dans un formulaire réactif (Reactive Form) ou un formulaire basé sur des modèles (Template-Driven Form).

Pour plus d'informations :

👉 ControlValueAccessor

👉 Reactive Forms

👉 Template-Driven Forms

Did you find this article valuable?

Support Ezéchiel Amen AGBLA by becoming a sponsor. Any amount is appreciated!