Angular2 : les formulaires

Je vous propose de poursuivre la découverte d’Angular2 en abordant dans cet article un sujet important : les formulaires. Ils sont incontournables dans la plupart des applications et ne sont pas forcément faciles à gérer. On va en profiter pour utiliser de nouvelles directives.

La structure de base

index.html

On va conserver le même fichier index.html qu’on a depuis le début de cette série d’articles avec le chargement de Bootstrap pour simplifier la mise en forme :

<html>
  <head>
    <title>Formulaire</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">

    <script src="node_modules/core-js/client/shim.min.js"></script>
    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/reflect-metadata/Reflect.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
  </head>

  <body>
    <form-test>Chargement...</form-test>
  </body>
</html>

main.ts

On va garder aussi le même composant de base main.ts :

import { bootstrap }    from '@angular/platform-browser-dynamic';
import { AppComponent } from './app.component';

bootstrap(AppComponent);

La configuration

On garde évidemment aussi les mêmes fichiers package.json, tsconfig.json, systemjs.config.js et typings.ts.

app.component.ts

En ce qui concerne le composant de l’application app.component.ts là aussi on n’a pas de grandes nouveautés :

import { Component } from '@angular/core';
import { ClientFormComponent } from './client-form.component';

@Component({
    selector: 'form-test',
    template: '<client-form></client-form>',
    directives: [ClientFormComponent]
})
export class AppComponent { }

On voit qu’on va charger un composant ClientFormComponent qui lui sera chargé de gérer le formulaire.

Une classe User

Pour les besoins de l’exemple on va créer une classe pour gérer des utilisateurs (user.ts) :

export class User {
  constructor(
    public name: string,
    public role: string
  ) {}
}

Une classe toute simple avec deux propriétés de type string.

Un formulaire de modification

Pour vous simplifier la vie j’ai mis les fichiers de cette version du formulaire ici.

On va commencer avec un exemple d’utilisation d’un formulaire pour modifier des données existantes. Imaginez qu’on a des utilisateurs, on mémorise leur nom et leur rôle. La classe User vue ci-dessus est destinée à créer les objets correspondants.

Le but n’étant pas de voir comment gérer globalement ces utilisateurs on ne va pas s’intéresser à cette partie qui nécessiterait un service spécifique. On va alimenter de façon artificielle le formulaire avec un utilisateur créé pour l’occasion.

On va créer un formulaire pour modifier les données d’un utilisateur :

img032

Avec deux contrôles :

  • une simple zone de texte pour le nom
  • une liste de choix pour le rôle

Le composant du formulaire

Pour gérer le formulaire on crée un composant (client-form-component.ts) avec ce code :

import { Component }  from '@angular/core';
import { User }       from './user'; 

@Component({
  selector: 'client-form',
  templateUrl: 'app/client-form.html',
  styles: [`
    .ng-valid { border-color: green; }
    .ng-invalid { border-color: red; }    
  `]
})
export class ClientFormComponent {
  private roles = ['Administrateur', 'Editeur', 'Rédacteur', 'Utilisateur'];
  private user = new User('Martin', this.roles[1]);
  onSubmit(): void {
    console.log(this.user); 
  }
}

Comme nouveautés par rapport à ce que nous avons vu jusqu’à présent on a du style ajouté en plus du sélecteur et du template :

styles: [`
  .ng-valid { border-color: green; }
  .ng-invalid { border-color: red; }    
`]

C’est une possibilité de localisation du style, on pourrait aussi mettre juste une référence comme on le fait pour le template, on pourrait aussi prévoir le style de façon plus classique dans la page HTML (directement ou avec une référence). On va voir bientôt à quoi nous sert ce style mais vous devez déjà un peu vous en douter…

Le template du formulaire

Voyons à présent le template du formulaire (client-form.html) :

<div class="container">
  <h1>Formulaire client</h1>
  <form (ngSubmit)="onSubmit()">
    <div class="form-group">
      <label for="name">Nom</label>
      <input id="name" type="text" class="form-control" required 
        [(ngModel)]="user.name"
        #name="ngForm">
      <span [hidden]="name.valid" class="text-danger">Vous devez entrer un nom !</span>
    </div>
    <div class="form-group">
      <label for="role">Rôle</label>
      <select id="role" class="form-control" 
        [(ngModel)]="user.role">
        <option *ngFor="let role of roles" [value]="role">{{ role }}</option>
      </select>
    </div>
    <button type="submit" class="btn btn-default">Submit</button>
  </form>
  L'utilisateur s'appelle {{ user.name }} et c'est un {{ user.role }}.
</div>

La structure du formulaire est classique avec les classes de Bootstrap. On va s’intéresser seulement à l’implantation d’Angular.

Mais pour bien comprendre il faut récapituler comment on peut établir les liaisons entre la classe du composant et la vue. On a vu qu’on peut le faire dans un sens, dans l’autre ou dans les deux, pour chaque cas la syntaxe est différente :

  • Du composant vers la vue : on a vu qu’on peut utiliser l’interpolation pour afficher la valeur d’une propriété de la classe, par exemple
{{ titre }}

Mais il y a une autre syntaxe si par exemple on veut affecter la valeur d’un attribut d’une balise, ce qu’on trouve dans notre formulaire :

[value]="role"

Remarquez l’utilisation des crochets. Ici on veut que l’attribut value ait la valeur de la variable role. On pourrait utiliser cette syntaxe également pour une classe ou un style.

  • De la vue vers le composant : dans ce sens on a vu qu’on peut avoir des événements, par exemple dans notre formulaire on trouve la soumission :
<form (ngSubmit)="onSubmit()">

Remarquez cette fois l’utilisation de parenthèses pour signifier la direction de la liaison.

  • Dans les deux sens : on veut parfois avoir une double liaison, dans ce cas on va utiliser la double syntaxe : crochets et parenthèse, par exemple dans le formulaire on trouve :
[(ngModel)]="user.name"

On veut dans la zone de texte afficher la valeur de la propriété user.name et on veut aussi que cette propriété s’actualise si on modifie la valeur dans la zone de texte.

Si vous avez compris cette syntaxe le reste sera tout simple !

ngModel

La directive ngModel a pour but d’établir une liaison entre le modèle (une donnée du composant) et un contrôle de formulaire. Comme on a deux contrôles on utilise deux fois cette directive :

<input id="name" type="text" class="form-control" required 
  [(ngModel)]="user.name"
  #name="ngForm">
...
<select id="role" class="form-control" 
  [(ngModel)]="user.role">
  <option *ngFor="let role of roles" [value]="role">{{ role }}</option>
</select>

Remarquez l’utilisation de la directive ngFor, qu’on a déjà rencontrée, pour le remplissage de la liste de choix.

Comme on utilise la double syntaxe pour la directive ngModel on a une double liaison, donc si la valeur change dans le modèle elle change dans le contrôle et réciproquement. Donc pour la mise à jour d’un utilisateur c’est automatique !

Voici une illustration de cette double liaison :

img035

On a prévu une soumission mais ce n’est pas cette action qui fait la mise à jour, on peut juste imaginer qu’elle permet par exemple d’effacer le formulaire dans un contexte global.

ngForm

La directive ngForm apparaît aussi dans ce formulaire :

<input id="name" type="text" class="form-control" required 
  [(ngModel)]="user.name"
  #name="ngForm">
<span [hidden]="name.valid" class="text-danger">Vous devez entrer un nom !</span>

Lorsqu’on crée un formulaire avec Angular il y a tout une intendance invisible mais bien présente. Pour chaque contrôle est créé un objet Control et tout ça se retrouve en général dans un objet ControlGroup. Ici on veut une référence de l’objet Control qui gère la zone de texte. On le fait simplement en utilisant la directive ngForm :

#name="ngForm"

On veut savoir si la valeur présente est « valide ». Comme on a prévu required elle est valide s’il y a quelque chose dans la zone, et invalide dans le cas contraire. pour visualiser cela on fait deux choses :

  • on fait apparaître ou disparaître un texte d’avertissement en renseignant l’attribut hidden avec la propriété valid du contrôle,
  • on utilise le fait qu’Angular ajoute automatiquement des classes selon l’état du contrôle, en particulier on aura ng-valid si l’entrée est valide, et ng-invalid dans le cas contraire. D’où le style ajouté dans le composant :
styles: [`
  .ng-valid { border-color: green; }
  .ng-invalid { border-color: red; }    
`]

Donc quand on a une entrée on a une bordure verte et pas de texte d’avertissement :

img033

Et si on n’a pas d’entrée la bordure devient rouge et on a le texte d’avertissement :

img034

On verra que ce n’est pas la seule classe ajoutée par Angular, on pourra aussi savoir si la valeur a été modifiée (ng-dirty / ng-pristine) et si le contrôle a été visité (ng-touched / ng-untouched).

La soumission

Comme je l’ai dit plus haut avec la double liaison le modèle est automatiquement actualisé dès qu’on modifie une valuer dans un contrôle du formulaire. Du coup la soumission ne sert pas à la mise à jour mais juste à savoir qu’on en a terminé avec la mise à jour.

Dans le code j’ai juste prévu un affichage des données de l’utilisateur dans la console :

onSubmit(): void {
  console.log(this.user); 
}

On se retrouve donc avec ce type d’affichage dans la console :

User {name: "Martin", role: "Editeur"}

Un formulaire de création

Pour vous simplifier la vie j’ai aussi mis les fichiers de cette version du formulaire ici.

On va maintenant considérer qu’on veut créer un nouvel utilisateur. On pourrait évidemment procéder de la même manière que ci-dessus en créant un nouvel utilisateur, en le liant au formulaire, et une fois qu’on envoie la soumission on sauvegarde cet utilisateur. Mais on va procéder différemment pour découvrir d’autres possibilités.

Cette fois on va rendre le formulaire indépendant des données du modèle, sauf pour ce qui est de remplir la liste de choix parce qu’on en a besoin. Ce n’est qu’à la soumission qu’on va créer un nouvel utilisateur avec les valeur entrées.

On ne va changer que le code du composant ClientFormComponent et de son template.

Le composant ClientFormComponent

Voici le code modifié du composant :

import { Component }  from '@angular/core';
import { User }       from './user'; 

@Component({
  selector: 'client-form',
  templateUrl: 'app/client-form.html',
  styles: [`
    .ng-valid { border-color: green; }
    .ng-invalid { border-color: red; }    
  `]
})
export class ClientFormComponent {
  private roles = ['Administrateur', 'Editeur', 'Rédacteur', 'Utilisateur'];
  onSubmit(value): void {
    let user = new User(value.name, value.role);
    console.log(user);
  }
}}
}

La seule propriété conservée est roles pour renseigner la liste de choix.

A la soumission on attend un paramètre value qui doit contenir les valeurs saisies et on crée un utilisateur avec celles-ci. On l’affiche dans la console pour vérifier que tout se passe bien.

Le template

C’est surtout au niveau du template qu’on va avoir des différences marquées, voici le nouveau code :

<div class="container">
  <h1>Formulaire client</h1>
  <form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
    <div class="form-group">
      <label for="name">Nom</label>
      <input id="name" type="text" class="form-control" required 
        #name="ngForm"
        ngControl="name">
      <span [hidden]="name.valid || name.untouched" class="text-danger">Vous devez entrer un nom !</span>
    </div>
    <div class="form-group">
      <label for="role">Rôle</label>
      <select id="role" class="form-control" 
        [ngModel]="roles[3]"
        ngControl="role">
        <option *ngFor="let role of roles" [value]="role">{{ role }}</option>
      </select>
    </div>
    <button type="submit" class="btn btn-default">Submit</button>
  </form>
</div>

Regardez déjà la balise du formulaire :

<form #f="ngForm" (ngSubmit)="onSubmit(f.value)">

J’ai déjà parlé plus haut de la directive ngForm. Cette directive représente le groupe de contrôles du formulaire, ici on en crée une référence f. A la soumission on transmet cette référence en choisissant la propriété value qui contient les contrôles avec les valeurs saisies sous la forme d’un objet :

{name: "Dupont", role: "Rédacteur"}

J’ai supprimé les ngModels qui étaient chargés de créer la liaison entre le modèle et la vue mais j’en ai toutefois gardé un pour sélectionner par défaut une valeur de la liste de choix :

[ngModel]="roles[3]"

Remarquez que j’ai utilisé juste des crochets parce que je veux une liaison dans un seul sens.

ngControl

On utilise aussi une directive qu’on n’a pas encore rencontrée : ngControl. Il faut indiquer à Angular quels contrôles sont en prendre en compte dans le formulaire et comment ils se nomment, c’est le but de cette directive. On la trouve donc deux fois dans le formulaire :

ngControl="name"
...
ngControl="role"

Le groupe de contrôles possède donc deux contrôles, ce qui permet de retrouver les valeurs saisies lors de la soumission. Quand on soumet on retrouve bien ce qu’on voulait dans la console :

User {name: "Dupont", role: "Administrateur"}

Voici une illustration du fonctionnement :

img036

J’ai aussi dû un peu changer le contrôle de la validité du nom avec la propriété untouched.

Conclusion

On voit qu’on a plusieurs possibilités pour gérer un formulaire mais je suis loin d’avoir épuisé le sujet ! En particulier je n’ai pas parlé de validation des entrées ni de la création dynamique d’un formulaire. J’y viendrai sans doute dans un autre article.

 

 

Laisser un commentaire