Archives Wild Code School
Angular

05 - Les formulaires : templates driven forms

Quête 6 sur 16

title-book
Introduction

Les formulaires et leur validation sont des éléments incontournables dans le développement d'applications web : on en utilise absolument partout : formulaire de création de compte, formulaire de connexion, formulaire de commande, formulaire de création d'un utilisateur, formulaire d'édition d'un utilisateur, etc...

Angular nous offre une multitude d'outils dédiés à la création et validation de formulaire. Il existe deux types de formulaires : les templates driven forms et les reactive forms.

→ Dans cette quête, nous allons apprendre l'utilisation des templates driven forms.

Avant de démarrer, assure-toi d'avoir terminé les quêtes suivantes👇

Objectifs

✅ Créer un template driven form ✅ Valider un template driven form ✅ Comprendre l'intérêt des directives ngForm et ngModel ✅ Comprendre les différents états possibles d'un <input> et d'un <form>

Les formulaires : templates driven forms

Les formulaires permettent à nos chers utilisateurs de saisir des données et de valider ces dernières avant de les soumettre vers une base de données.

La gestion des formulaires via Angular permet de capturer, valider et mettre à jour plus facilement les interactions utilisateurs avec les champs de saisie.

L'approche template des formulaires est préférable lorsqu'on manipule un formulaire simple avec peu de champs où la validation est moindre. Cependant, pour des formulaires plus complexes, tu pourras utiliser les reactive forms. Mais chaque chose en son temps 😌

→ Voyons déjà comment créer un template driven form !

↗
Tuto officiel Angular : template driven form

Pour réviser les notions vues précédemment, tu peux suivre le tuto jusqu'à la section "Add powers with *ngFor"

angular.io
↗
<form></form>

Un retour sur les bases des formulaires (sans angular)

developer.mozilla.org

Création d'un formulaire 📝

Pour t'entraîner, tu vas créer un nouveau formulaire de gestion de commande (ng g c my-form), qui sera constitué des champs suivants :

  • un champ de saisie de type text pour le titre
  • un champ de saisie de type number pour la quantité
  • un champ de saisie de type date pour date de commande
  • un champ de saisie de type email pour le contact commande
  • un bouton de validation
  • un bouton de réinitialisation

Tous les champs sont required. Le titre impose une minlength (une longueur min). L'attribut email impose une adresse email valide (ex : mon@adresse)

Dans l'idée, ton formulaire devrait ressembler à ceci :

html
<form>
    <fieldset>
      <label for="title-order">Title</label>
      <input id="title-order" name="title-order" type="text" required minlength="5">
    </fieldset>

    <fieldset>
      <label for="quantity-order">Quantité</label>
      <input id="quantity-order" name="quantity-order" type="number" required>
    </fieldset>

    <fieldset>
      <label for="date-order">Date de commande</label>
      <input id="date-order" name="date-order" type="date" required>
    </fieldset>

    <fieldset>
      <label for="contact-order">Contact</label>
      <input id="contact-order" name="contact-order" type="email" required email>
    </fieldset>

    <button type="reset">Restaurer</button>
    <button type="submit">Sauvegarder</button>
</form>

❌ Pour l'instant, même si tu as créé ton formulaire dans une application Angular, il n'est pas encore reconnu comme formulaire par Angular. Il manque comme une capacité spéciale.

✅ Il te suffit d'importer le module FormsModule de @angular/forms dans ton my-form.component.ts et de l'ajouter dans les tableaux des imports pour donner à ton composant l'ensemble des capacités présentes dans le FormsModule.

Info

Rappelle-toi : tu l'as déjà fait pour le two-way binding dans une quête précédente.

Quêtes liées

Une fois que tu as correctement importé le module, une des premières choses à faire est d'enrichir ton formulaire avec du langage Angular. Par exemple, si tu souhaites déclencher une méthode lors de la soumission du formulaire, tu peux écrire :

  • Dans ton fichier my-form.component.html :
html
<form (ngSubmit)="onSubmit()">
  // le contenu de ton formulaire
  <button type="submit">Sauvegarder</button>
</form>

→ Si le bouton est cliqué, l'event binding (ngSubmit) déclenche la méthode onSubmit()

  • Dans ton fichier my-form.component.ts :
typescript
//[...]
export class MyFormComponent {
  //[...]
  onSubmit(): void {
    // form submitted
    console.log('Formulaire envoyé !');
  }
}

→ On retrouve la méthode onSubmit(). Pour le moment, cette méthode ne fait que logger un message dans ta console. Pratique pour vérifier que ton code fonctionne, mais rien de très concret encore 🥸

👀 En résumé, tu as donc créé un formulaire "classique" en HTML, importé le module des formulaires dans le my-form.component.ts et ajouté un event binding pour réagir lorsque ton utilisateur soumet ton formulaire.

Info

C'est une bonne première étape. Voyons maintenant comment récupérer les données saisies par ton utilisateur.

Récupération des données

Comment récupérer les données d'un formulaire côté Typescript?

Tu vas utiliser la directive ngModel fournie par Angular. C'est la directive pour manipuler les formulaires. Elle permet de lier les données entre un template et son composant.

La modification d'un input, que ce soit dans le template ou le composant, sera répercutée des deux côtés (two-way binding 🔥).

Première étape : créer le modèle de données

Le modèle de données ?

C'est de la POO ! Ou en d'autres termes, c'est une classe Typescript représentant ton modèle de données, c'est-à-dire les données que tu vas manipuler. Dans ton cas, reprends les informations du formulaire. Avec la création d'un modèle, les données sont encapsulées et tu bénéficies de la détection des erreurs de typage, de l'autocomplétion, d'une meilleure lisibilité du code... bref du code propre !

L'interface Order représentera ton modèle de données (dans notre cas, les données du formulaire). Nomme ton fichier order.model.ts et place dans dans un dossier /models sous /app:

typescript
export interface Order {
    title: string;
    quantity: number;
    date: Date;
    contact: string;
}

Seconde étape : utiliser ton modèle de données

Crée une variable newOrder (par exemple) dans le composant my-form.component.ts et logges cette variable lors de l'envoi du formulaire :

typescript
[...]
export class MyFormComponent {

  newOrder: Order = {
    title: "",
    quantity: 0,
    date: new Date(),
    contact: ""
  };

  onSubmit(): void {
    // Formulaire envoyé
    console.log(this.newOrder);
  }
}

Troisième étape : Récupérer les données saisies

Tu dois ajouter le two-way binding [(ngModel)] sur chaque input. Chaque input est alors lié à la propriété correspondante de ta variable newOrder :

html
<form (ngSubmit)="onSubmit()">
    <fieldset>
      <label for="title-order">Title</label>
      <input id="title-order" name="title-order" type="text" [(ngModel)]="newOrder.title" required minlength="5">
    </fieldset>

    <fieldset>
      <label for="quantity-order">Quantité</label>
      <input id="quantity-order" name="quantity-order" [(ngModel)]="newOrder.quantity" type="number" required>
    </fieldset>

    <fieldset>
      <label for="date-order">Date de commande</label>
      <input id="date-order" name="date-order" [(ngModel)]="newOrder.date" type="date" required>
    </fieldset>

    <fieldset>
      <label for="contact-order">Contact</label>
      <input id="contact-order" name="contact-order" [(ngModel)]="newOrder.contact" type="email" required email>
    </fieldset>

    <button type="reset">Restaurer</button>
    <button type="submit">Sauvegarder</button>
</form>

✅ Maintenant, à l'envoi du formulaire, tu devrais voir ton modèle modifié loggé dans la console de ton navigateur ! 🎉

Info

En résumé, [(ngModel)] a mis à jour les propriétés de la variable newOrder et le clic sur le bouton "Sauvegarder" a déclenché l'événement ngSubmit, qui a lui même déclenché la méthode onSubmit(). Cette méthode a permis de logger la variable newOrder... avec les valeurs renseignées par l'utilisateur depuis le formulaire 📝

↗
La directive ngForm
angular.io
↗
La directive ngModel
angular.io

Validation des champs

💪 Tu as déjà bien avancé dans la création de ton template driven form. Mais il te manque encore la mise en place de la validation des champs du formulaire.

L'utilisation de la directive ngForm désactive la validation par défaut effectuée par le navigateur. Tu peux désormais définir un message d'erreur personnalisé pour chaque input de ton formulaire.

La directive ngForm va créer un objet FormGroup qui contient tous les champs du formulaire.

Les champs à remplir avec la directive ngModel vont créer des objets FormControl contenant les informations du champ.

Les FormControl seront ajoutés dans le FormGroup.

Les directives ngForm et ngModel exportent des variables de template qui te permettent d'accéder aux propriétés de ces directives et de connaître ainsi l'état du form et les erreurs des input à travers les propriétés .errors, .valid, .invalid, touched, etc...

Tu peux donc récupérer une variable correspondant à l'instance de la directive ngForm et ainsi accéder aux propriétés telles que .submitted, .valid, .touched, etc....

Info

Ça te paraît peut-être un peu flou tout ça. Pas de panique, ça deviendra plus simple à l'utilisation 😎

Illustrons cela.

On récupère l'instance de la directive ngForm avec #myForm="ngForm" que l'on peut ajouter dans la balise ouvrante <form> :

html
<form #myForm="ngForm" (ngSubmit)="myForm.valid && onSubmit()" >
<!-- Message d'erreurs du formulaire global -->
  <div>
    Form submitted ? {{myForm.submitted}}
    Form valid ? {{myForm.valid}}
    Form invalid ? {{myForm.invalid}}
  </div>

  [...]

  <button type="submit">Sauvegarder</button>
</form>

#myForm te permet alors d'appeler la directive ngForm.

A l'initialisation de ton formulaire, tu devrais voir les propriétés .submitted et .valid valorisées à false.

Une fois que tu cliques sur le bouton "Sauvegarder" la propriété, .submitted sera valorisée à true et .valid à false.

Si tu renseignes bien tous les champs en respectant les critères de validation, la propriété .valid est valorisée à true.

#myForm et plus généralement ngForm te permet donc d'avoir une vue d'ensemble de l'état de ton formulaire. C'est super pratique ! 👀

Mais il y a encore mieux : parce que ton formulaire possède la directive ngForm, tu peux enrichir chaque input de la directive ngModel. Elle te permet de relever l'état de chaque input de ton formulaire.

Info

En résumé, la directive ngForm relève l'état de l'ensemble de ton formulaire tandis que la directive ngModel relève l'état d'un input de ton formulaire.

Comme pour ngForm, tu vas récupérer une variable correspondant à l'instance de la directive ngModel et ainsi accéder aux propriétés de tes input telles que .invalid, .valid, .dirty, etc...

Pour ça, tu vas récupérer l'instance de la directive ngModel via #title="ngModel" (pour l'input title). Tu vas alors être en mesure d'afficher un message personnalisé en cas d'erreur de validation de cet input, via la propriété .errors:

html
<form #myForm="ngForm" (ngSubmit)="myForm.valid && onSubmit()">
  <div>
    <label for="title-order">Title</label>
    <input id="title-order" name="title-order" type="text" [(ngModel)]="model.title" #title="ngModel" required minlength="5"/>
  </div>

  <!-- Message d'erreurs de l'input title -->
  <div *ngIf="title.errors">
    <span *ngIf="title.errors?.['required']">Le titre est requis</span>
    <span *ngIf="title.errors?.['minlength']">La longueur minimale doit être supérieure à 5</span>
  </div>

  [...]

  <button type="submit">Sauvegarder</button>
</form>

C'est avec la propriété .errors, suivie de l'erreur de validation ?.['required'] ou ?.['minlength'], que tu vas pouvoir afficher et personnaliser tes messages d'erreur de validation.

La propriété .errors est un objet de type ValidationErrors, contenant une ou plusieurs erreurs de validation. Chaque clef du tableau correspond à une erreur de validation (par exemple : 'required', 'minlength', 'maxlength', 'email', 'pattern', etc...). Comme il n'y a potentiellement pas d'erreur, on doit y accéder avec un ?.

Mention spéciale sur le pattern qui permet de gérer la validité grâce aux regex. Cependant, il n'est pas très recommandé de mettre trop de complexité (regex) dans l'html.

Regex : technique difficile à maîtriser pour forcer certains formats de données : toto@mail.com, +3333, +(0)3333, 01/01/2020 ...

On teste ici en premier la propriété .errors, afin d'éviter d'éventuelles erreurs à l'initialisation du formulaire (par exemple : (...).errors is null") lorsqu'on souhaite accéder aux propriétés .required ou .minlength (la propriété .errors n'est pas tout de suite valorisée, car Angular n'a pas encore validé les input).

Tu peux maintenant appliquer la validation sur les champs restants : title, date et contact. Ton formulaire devrait ressembler à ceci :

html
<form #myForm="ngForm" (ngSubmit)="onSubmit()">
  <div>
    Form submitted ? {{myForm.submitted}}
    Form valid ? {{myForm.valid}}
    Form invalid ? {{myForm.invalid}}
  </div>

  <fieldset>
    <label for="title-order">Title</label>
    <input id="title-order" name="title-order" type="text" [(ngModel)]="newOrder.title" #title="ngModel" required minlength="5" maxlength="30"/>
  </fieldset>

  <!-- Message d'erreurs de title -->
  <div *ngIf="title.errors">
    <span *ngIf="title.errors?.['required']">Le champ titre est requis</span>
    <span *ngIf="title.errors?.['minlength']">La longueur minimale doit être supérieure à 5</span>
    <span *ngIf="title.errors?.['maxlength']">La longueur maximale doit être inférieure à 30</span>
  </div>


  <fieldset>
    <label for="quantity-order">Quantité</label>
    <input id="quantity-order" name="quantity-order" [(ngModel)]="newOrder.quantity" #quantity="ngModel" type="number" required min="1">
  </fieldset>

 <!-- Message d'erreurs de quantity -->
  <div *ngIf="quantity.errors">
    <span *ngIf="quantity.errors?.['required']">Le champ quantité est requis</span>
    <span *ngIf="quantity.errors?.['min']">La quantité doit être supérieure à 0</span>
  </div>


  <fieldset>
    <label for="date-order">Date de commande</label>
    <input id="date-order" name="date-order" type="date" [(ngModel)]="newOrder.date" #date="ngModel" required>
  </fieldset>

<!-- Message d'erreurs de date -->
  <div *ngIf="date.errors">
    <span *ngIf="date.errors?.['required']">Le champ date est requis</span>
  </div>


  <fieldset>
    <label for="contact-order">Contact</label>
    <input id="contact-order" name="contact-order" type="email" [(ngModel)]="newOrder.contact" #contact="ngModel" required email>
  </fieldset>

  <!-- Message d'erreurs de contact -->
  <div *ngIf="contact.errors">
    <span *ngIf="contact.errors?.['required']">Le champ email est requis</span>
    <span *ngIf="contact.errors?.['email']">L'adresse email doit être valide</span>
  </div>


  <button type="reset">Restaurer</button>
  <button type="submit">Sauvegarder</button>
</form>

Et voilà ! 🎉

Tu sais maintenant créer un template driven form et le valider !

→ Tu as dû remarquer que c'était en premier les erreurs required qui étaient déclenchées, avant les erreurs de type maxlength ou email. C'est normal. Angular valide d'abord la règle required. Si la validation est remplie, il passe à la règle suivante.

→ Tu as également dû remarquer que tous les messages d'erreurs s'affichent à l'initialisation du formulaire. Tu veux qu'ils s'affichent uniquement après l'envoi ? Il faut peut-être revoir l'expression des *ngIf avec la propriété .submitted 🙄

↗
Tuto complet d'Angular sur les templates driven forms
angular.io
↗
ngModel en détails
angular.io
↗
Exemple Stackblitz
stackblitz.com

Challenge 💪

Ton premier formulaire d'inscription 📝

Le but de ce challenge est de faire une page d'inscription. Si tu le fais proprement, tu pourras souvent t'en resservir (donc prends le temps de le faire bien 🙄)

  • Tu vas donc créer un composant sign-up qui contiendra un formulaire d'inscription
  • Tu ajouteras ce composant à ton app.component.html
  • Tu penseras bien à importer tout ce dont tu as besoin
  • Ce formulaire aura les champs suivants : email, firstname, lastname, password
  • Tu sais maintenant que, pour chaque formulaire, on doit créer un modèle (une interfa e ou une classe User avec chaque champ présent dans le formulaire)
  • Un bouton d'inscription de type "submit"
  • L'affichage du user créé dans la console au moment du click sur le bouton de soumission du formulaire.

Critères de validation ✅

  • firstname de type text qui doit être validé avec required, minlength à 3 et maxlength à 15
  • lastname de type text qui doit être validé avec required, minlength à 3 et maxlength à 15
  • email doit être validé avec required et email
  • Le message d'erreur pour la validation required doit être : "Le * est requis" Avec * = le nom du champ. Par exemple : Le prénom est requis
  • Le message pour la validation de l'email doit être : L'email est invalide
  • Le message pour la validation de maxlength doit être : Le * ne doit pas dépasser 15 caractères.
  • Le message pour la validation de minlength doit être : Le * doit comporter au moins 3 caractères.
  • Les messages d'erreurs doivent être affichés uniquement après l'envoi du formulaire 😈

Ressources partagées sur cette quête