Angular 14: Typer ses formulaires n'est plus un rêve (part 2)
Tags: angular, typescript, webdev, opensource
Published: 2022-05-31 06:21:36

## Introduction

Dans mon précédent article [ici](https://dev.to/nicoss54/le-futur-dangular-part-1-5d), 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.

```ts
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.

```ts
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.

```ts
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.

```ts
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**

```ts
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.

```ts
const names = new FormArray([new FormControl('', { nonNullable: true })])
```

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

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

```ts
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'.

```ts
@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_.

```ts
@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<any>

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

```sh
ng update @angular/core
```

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

```sh
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.

```ts

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 })
});

```