Angular 14: Typer ses formulaires n'est plus un rêve (part 2)
Nicolas Frizzarin
Posted on May 31, 2022
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
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 });
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
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
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
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 })])
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 })])
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) {}
}
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) {}
}
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
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
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 })
});
Posted on May 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.