Aller au contenu

Angular Forms: destructurer pour mieux structurer

Découvrez comment #Angular simplifie la gestion de formulaires complexes ! Utilisez FormControl et l'API ControlValueAccessor pour une meilleure structuration et réutilisabilité. #DevTips #WebDevelopment

Angular simplifie la gestion de formulaires complexes

Introduction

Depuis sa sortie, il y a maintenant 6 ans, les développeurs Angular ont assez de recul pour comprendre comment s'assemble les différentes composantes du framework mais aussi comment réaliser des applications puissantes.

Les formulaires font partie d'une des briques les plus importantes d'Angular et sont présents dans nos applications au quotidien à travers la création de compte, d'authentification ou autres attentes business.

Nos formulaires peuvent donc devenir très complexes notamment lorsque l'on mixe au sein de ceux-ci plusieurs FormGroup, FormControl et FormArray. Cette complexité a pour conséquence une maintenance compliquée.

Comment donc simplifier nos formulaires ?
Et bien simplement en destructurant en FormControl les valeurs de structures complexes et en utilisant l'API ControlValueAccessor.

Le FormControl

Le FormComtrol est une classe qui provient du module @angular/forms. L'instance de cette classe prend deux paramètres:

  • une valeur d'initialisation
  • un objet d'options (optionnel)
@Component({...})
export class UserComponent implements OnInit {
  firstname = new FormControl('Nicolas');

  ngOnInit(): void {
    console.log(this.firstname.value); // Nicolas
  }
}

La valeur d'initalisation peut être de tout type. Ce qui signifie que celle-ci peut être un objet, un tableau, un tableau d'objets, etc...

On peut donc écrire:

@Component({...})
export class UserComponent implements OnInit {
  user = new FormControl({ firstname:  'Nicolas' });

  ngOnInit(): void {
    console.log(this.user.value); //{ firstname: 'Nicolas'}
  }
}

L'API ControlValueAccessor

L'API ControlValueAccessor indique à Angular comment accéder à la valeur d'un contrôle. Il s'agit en quelque sorte d'un pont entre un contrôle et un élément natif.

Comment implémenter l'API ControlValueAccessor ?

Le ControlValueAccessor est une simple interface, il est donc logique d'écrire le code suivant:

export class AddressComponent implements ControlValueAccessor{}

Cette interface nous permet d'implémenter les méthodes suivantes:

writeValue -- model -> value

Cette méthode permet d'écrire une nouvelle valeur dans votre élément. Angular appellera cette méthode dans les deux cas suivants:

  • 1. À l'initialisation de votre contrôle
  • 2. Lorsque vous appelez this.control.patchValue()/setValue()
export class AddressComponent implements ControlValueAccessor {
  writeValue(value: any): void {
    /** 
     * Value est la valeur de votre contrôle
     * Vous pouvez réaliser la logique dont vous avez besoin 
     * pour affecter la valeur à votre élément 
     */ 
  }
}

registerOnChange -- vue -> model

Cette méthode vous permet de définir une fonction à appeler pour mettre le contrôle à jour lorsque votre élément change.
À travers cette méthode, Angular vous procure une fonction et vous demande de l'appeler à chaque fois que votre élément change et que vous souhaitez mettre à jour le contrôle.

export class AddressComponent implements ControlValueAccessor {
  private _onChange: (x: any) => void;

  writeValue(value: any): void {
    /** 
     * Value est la valeur de votre contrôle
     * Vous pouvez réaliser la logique dont vous avez besoin 
     * pour affecter la valeur à votre élément 
     */ 
  }

  registerOnChange(fn: (x: any) => void): void {
    this._onChange = fn;
  }
}

registerOnTouched -- vue -> model

Cette méthode est similaire à la méthode registerOnChange à l'exception qu'elle est appelée lorsque votre composant a été "touché", en d'autres termes quand l'utilisateur a intéragi avec votre composant.

export class AddressComponent implements ControlValueAccessor {
  private _onChange: (x: any) => void;
  private _onTouched: () => void;

  writeValue(value: any): void {
    /** 
     * Value est la valeur de votre contrôle
     * Vous pouvez réaliser la logique dont vous avez besoin 
     * pour affecter la valeur à votre élément 
     */ 
  }

  registerOnChange(fn: (x: any) => void): void {
    this._onChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this._onTouched = fn;
  }
}

setDisabledState

Cette méthode est appelée lorsque le statut du contrôle change vers le statut DISABLE ou non.

Angular appelle cette méthode dans les cas suivants

  • 1. À l'instanciation d'un contrôle avec la propriété disabled égale à true: new ForomControl({value: null, disabled: true}).
  • 2. Quand vous appelez la fonction control.disable() ou la fonction control.enable().
export class AddressComponent implements ControlValueAccessor {
  private _onChange: (x: any) => void;
  private _onTouched: () => void;

  writeValue(value: any): void {
    /** 
     * Value est la valeur de votre contrôle
     * Vous pouvez réaliser la logique dont vous avez besoin 
     * pour affecter la valeur à votre élément 
     **/ 
  }

  registerOnChange(fn: (x: any) => void): void {
    this._onChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this._onTouched = fn;
  }

  setDisabledState(isDisable: boolean): void {}
}

Pour enregistrer ce composant comme un composant de formulaire, il nous faut "pousser" ce composant dans le service global NG_VALUE_ACCESSOR.

@Component({
  selector: 'address',
  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AddressComponent), multi: true}]
})
export class AddressComponent implements ControlValueAccessor {
  private _onChange: (x: any) => void;
  private _onTouched: () => void;

  writeValue(value: any): void {
    /** 
     * Value est la valeur de votre contrôle
     * Vous pouvez réaliser la logique dont vous avez besoin 
     * pour affecter la valeur à votre élément 
     */ 
  }

  registerOnChange(fn: (x: any) => void): void {
    this._onChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this._onTouched = fn;
  }

  setDisabledState(isDisable: boolean): void {}
}

Comment destructurer pour mieux structurer

L'idée est de mettre des valeurs plus complexes dans nos FormControl pour créer le formulaire associé à cette valeur dans un composant enfant.

Imaginons le formulaire d'un utilisateur comme ceci:

  • nom
  • prénom
  • addresse
  • ville
  • pays
  • code postal
  • rue

Instinctivement, le formulaire associé à cette description est le suivant:

@Component({...})
export class UserComponent implements OnInit {
  userForm = new FormGroup({
    name: new FormControl(null),
    firstname: new FormControl(null),
    address: new FormGroup({
      city: new FormControl(null),
      country: new FormControl(null),
      zipCode: new FormControl(null),
      street: new FormControl(null)
    })
  });

  ngOnInit(): void {
    console.log(this.userForm.value); //{ firstname: 'Nicolas'}
  }
}

Bien que ce formualaire soit petit, il devient compliqué à gérer si nous avons beaucoup de règles business à traiter notamment sur la partie addresse.

Pourquoi ne pas créer un composant custom qui ne gère que l'addresse ?

La valeur d'un contrôle peut-être de tout type de structure.

@Component({...})
export class UserComponent implements OnInit {
  user = new FormGroup({
    name: new FormControl(null),
    firstname: new FormControl(null),
    address: new FormControl(null)
  });

  ngOnInit(): void {
    console.log(this.user.value); //{ name, ... }
  }
}

L'API ControlValueAccessor nous permet de créer un "pont" entre un contrôle et un élément custom"

@Component({
  selector: 'address',
  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AddressComponent), multi: true}]
})
export class AddressComponent implements OnDestroy, ControlValueAccessor {
  addressForm = new FormGroup({
    city: new FormControl(null),
    country: new FormControl(null),
    zipCode: new FormControl(null),
    street: new FormControl(null) 
  })
  private _unsubscribe$: Subject<boolean> = new Subject();
  private _onTouched: () => void;

  ngOnDestroy():void {
    this._unsubscribe$.next(true);
    this._unsubscribe$.complete();
  }


  writeValue(address Adress): void {
    address && this.addressForm.patchValue(address);
  }

  registerOnChange(fn: (x: Address) => void): void {
    this.addressForm.valueChanges
      .pipe(takeUntil(this._unsubscribe$))
      .subscribe(address => {
        fn(address);
        this._onTouched();
      })
  }

  registerOnTouched(fn: () => void): void {
    this._onTouched = fn;
  }

  setDisabledState(isDisable: boolean): void {}
}

Dans la méthode registerOnChange, nous souscrivons à l'observable valueChanges qui renvoie la nouvelle valeur du formulaire à chaque modification de celui-ci.

Chaque modification provoque un appel à la fonction de notification du changement de valeur du contrôle.

Dans le template associé au composant UserComponent, il devient facile d'écrire:

<form [formGroup]="userForm">
  <input type="text" formControlName="name" />
  <input type="text" formControlName="firstname" />
  <address formControlName="address"></address>
</form>

Ainsi le contrôle address aura comme valeur:
{ city, country, street, zipCode }
à chaque modification réalisée sur le formulaire du composant AddressComponent.

Avantages et Incovénients ?

Les avantages sont nombreux:

  • simplification de votre logique de formulaire
  • atomicité de certaines parties de votre formulaire
  • composant custom form réutilisable

Le réel inconvénient de cette solution reste le design du composant custom. Peu importe où vous appellerez ce composant, son design sera toujours le même et il ne sera pas si facile de le modifier.

Dernier