DEV Community

Cover image for Angular 14: Typer ses formulaires n'est plus un rêve (part 2)
Nicolas Frizzarin for This is Angular

Posted on • Updated on

Angular 14: Typer ses formulaires n'est plus un rêve (part 2)

Introduction

Dans mon précédent article ici, j'ai porté le focus sur les composants standalone et leur fonctionnement.

Cependant Angular 14 apporte bien d'autres fabuleuses nouveautés, comme le typage strict de nos formulaires.

Eh oui :), ce n'est pas un rêve, plus la peine de trouver des workaround plus ou moins propres: le typage des formulaires est pris nativement par le framework

Comment fonctionne t'il?
Comment l'intégrer dans les applications?
Que va faire le schematics de migration vers Angular 14 ?

FormControl

La signature de la classe FormControl prend désormais un générique pour pouvoir typer son contrôle, sa valeur et le retour des méthodes exposées par cette classe.

const nameControl = new FormControl<string | null>('');

nameControl(23); // Type Error

const name = nameControl.value // string | null
Enter fullscreen mode Exit fullscreen mode

Mais pourquoi le type peut-être nul?

Le type du contrôle peut être nul dû au fait de la méthode reset. Cette dernière, par défaut, réinitialise le champs à nul.

Si ce comportement n'est pas celui souhaité une nouvelle option est disponible: nonNullable.
Cette option remplace initialValueIsDefault qui devient deprecated.

const nameControl = new FormControl('', { nonNullable: true });
Enter fullscreen mode Exit fullscreen mode

Dans le précédent exemple, le typage n'est pas explicite, ici il est implicite. Angular va comprendre automatiquement que la valeur est de type string et non nulle.

FormGroup

Comme la classe FormControl, la classe FormGroup hérite elle aussi d'un générique qui peut être implicite mais également explicite.

const addressControl = new FormGroup({
  street: new FormControl('', { nonNullable: true }),
  city: new FormControl('', { nonNullable: true }),
});

const street: addressControl.value.street // string|undefined
Enter fullscreen mode Exit fullscreen mode

Comment le type de la valeur de la variable street peut être undefined ?

Lorsqu'un FormGroup est dans un état disabled, la propriété value de la classe FormGroup ne renvoie que les valeurs des contrôles non disabled. Ce qui induit par définition que les contrôles disabled ne renverront aucune valeur.

Un moyen simple de contourner ce comportement est d'utiliser la méthode getRawValue qui renvoie toutes les valeurs d'un formulaire peut importe son état.

De manière générale, les formulaires peuvent être conséquents avec des contrôles que nous ajoutons ou supprimons au runtime.

C'est dans ce genre de cas que le typage explicite est vraiment important et intuitif.

interface PersonForm {
  firstname: FormControl<string>;
  lastname: FormControl<string>;
  username?: FormControl<string | null>
}

const personForm = new FormGroup<PersonForm>({
  firstname: new FormControl('', { nonNullable: true }),
  lastname: new FormControl('', { nonNullable: true }),
  username: new FormControl(null),
});

personForm.removeControl('firstname'); // error: firstname required

personForm.removeControl('username'); // no error
Enter fullscreen mode Exit fullscreen mode

FormRecord

Le typage apporte bien des avantages mais peut apporter aussi quelques petits inconvénients.

Un exemple très simple est le suivant: comment ajouter à un formulaire existant des contrôles dynamiquement sans en connaître par avance la clé ?
Avec un typage strict sur la classe FormGroup, ce genre de travail risque d'être compliqué.

Angular ajoute une nouvelle API pour résoudre ce problème, le FormRecord

const languages = new FormRecord({
  french: new FormControl(false, { nonNullable: true }),
  english: new FormControl(false, { nonNullable: true })
});

languages.addControl('italian', new FormControl(0, { nonNullable: true }); // error

languages.addControl('italian', new FormControl(false, { nonNullable: true }); // no error
Enter fullscreen mode Exit fullscreen mode

La classe FormRecord permet d'ajouter des contrôles dynamiquement dont les valeurs doivent avoir toutes le même type.

Contrairement à son homologue FormGroup, les méthodes setValue et removeControl n'auront aucune vérification de typage.

Cette API peut être très pratique pour représenter un ensemble de checkbox.

FormArray

La classe FormArray a également le droit à une petite transformation pour être typée génériquement.

const names = new FormArray([new FormControl('', { nonNullable: true })])
Enter fullscreen mode Exit fullscreen mode

Ainsi, chaque contrôle présent dans le FormArray sera de type de FormControl.

Une fois de plus, l'utilisation du typage explicite est possibe si le typage implicite n'est pas suffisant.

const names = new FormArray<FormControl<string>>([new FormControl('', { nonNullable: true })])
Enter fullscreen mode Exit fullscreen mode

FormBuilder

Similaire aux exemples ci-dessus, la classe FormBuilder a été mise à jour pour supporter le typage.

En plus de cette upgrade, une nouvelle injection _ NonNullableFormBuilder_ est à disposition pour éviter le boilerplate qu'impose l'option 'nonNullable'.

@Component({
  selector: 'app-form',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  addressForm = this.fb.group({
    street: '',
    city: ''
  });

  constructor(private readonly fb: NonNullableFormBuilder) {}
}
Enter fullscreen mode Exit fullscreen mode

Cette solution est la plus propre et, est la solution recommandée. Cependant une alternative est possible avec la propriété nonNullable.

@Component({
  selector: 'app-form',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  addressForm = this.fb.nonNullable.group({
    street: '',
    city: ''
  });

  constructor(private readonly fb: FormBuilder) {}
}
Enter fullscreen mode Exit fullscreen mode

Migration

Lors de la migration vers Angular 14, le schematics de migration va effectuer quelques petits changements au sein de votre code pour transformer toutes vos classes FormGroup, FormControl et FormArray vers leur 'UnTyped' version respective.

Voici les transformations qui seront effectuées:

FormControl --> UnTypedFormControl
FormGroup --> UnTypedFormGroup
FormArray --> UnTypedFormArray

Pour information: UnTypedFormControl est simplement un alias pour FormControl

Pour Rappel: cette migration sera effectuée lors de la commande suivante:

ng update @angular/core
Enter fullscreen mode Exit fullscreen mode

ou à la demande avec la commande suivante si vous avez mis à jour vos dépendances manuellement

ng update @angular/core --migrate-only=migration-v14-typed-forms
Enter fullscreen mode Exit fullscreen mode

Petit Bonus et tips

Si vous avez déjà typé vos contrôles ou vos formulaires à l'aide d'interface simple, il est tout à fait possible de ne pas perdre tout votre travail en créeant un type générique.


interface Person {
  name: string;
  username: string;
}

type ControlsFromInterface<T extends Record<string, any> = {
  [key in keyof T]: T[key] extends Record<any, any>
    ? FormGroup<ControlsFromInterface<T[key]>>
    : FormControl<T[key]>
};

const personForm = new FormGroup<ControlsFromInterface<Person>>({
  name: new FormControl('', { nonNullable: true }),
  username: new FormControl('', { nonNullable: true })
});

Enter fullscreen mode Exit fullscreen mode

Latest comments (0)