Préparez le terrain
À la différence de la méthode template où Angular crée l'objet du formulaire, pour la méthode réactive, vous devez le créer vous-même et le relier à votre template. Même si cela a l'air plus complexe, cela vous permet de gérer votre formulaire en détail, notamment avec la création programmatique de contrôles (permettant, par exemple, à l'utilisateur d'ajouter des champs).
Pour illustrer la méthode réactive, vous allez créer une nouvelle section dans l'application des appareils électriques : vous allez permettre aux utilisateurs de créer un profil utilisateur simple. Cette démonstration utilisera des compétences que vous avez apprises tout au long de ce cours, et vous allez également créer votre premier modèle de données sous forme d'une classe TypeScript.
Commencez par le modèle User
; créez un nouveau dossier models
, et dedans un fichier User.model.ts
:
export class User {
constructor(
public firstName: string,
public lastName: string,
public email: string,
public drinkPreference: string,
public hobbies?: string[]
) {}
}
Ce modèle pourra donc être utilisé dans le reste de l'application en l'important dans les components où vous en avez besoin. Cette syntaxe de constructeur permet l'utilisation du mot-clé new
, et les arguments passés seront attribués à des variables qui portent les noms choisis ici, par exemple const user = new User('James', 'Smith', 'james@james.com', 'jus d\'orange', ['football', 'lecture'])
créera un nouvel objet User
avec ces valeurs attribuées aux variables user.firstName
, user.lastName
etc.
Ensuite, créez un UserService
simple qui stockera la liste des objets User
et qui comportera une méthode permettant d'ajouter un utilisateur à la liste :
import { User } from '../models/User.model';
import { Subject } from 'rxjs/Subject';
export class UserService {
private users: User[];
userSubject = new Subject<User[]>();
emitUsers() {
this.userSubject.next(this.users.slice());
}
addUser(user: User) {
this.users.push(user);
this.emitUsers();
}
}
Ce service contient un array privé d'objets de type personnalisé User
et un Subject pour émettre cet array. La méthode emitUsers()
déclenche ce Subject et la méthode addUser()
ajoute un objet User
à l'array, puis déclenche le Subject.
L'étape suivante est de créer UserListComponent
— pour simplifier cet exemple, vous n'allez pas créer un component pour les utilisateurs individuels :
import { Component, OnDestroy, OnInit } from '@angular/core';
import { User } from '../models/User.model';
import { Subscription } from 'rxjs/Subscription';
import { UserService } from '../services/user.service';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.scss']
})
export class UserListComponent implements OnInit, OnDestroy {
users: User[];
userSubscription: Subscription;
constructor(private userService: UserService) { }
ngOnInit() {
this.userSubscription = this.userService.userSubject.subscribe(
(users: User[]) => {
this.users = users;
}
);
this.userService.emitUsers();
}
ngOnDestroy() {
this.userSubscription.unsubscribe();
}
}
Ce component très simple souscrit au Subject dans UserService
et le déclenche pour en récupérer les informations et les rendre disponibles au template (que vous allez maintenant créer) :
<ul class="list-group">
<li class="list-group-item" *ngFor="let user of users">
<h3>{{ user.firstName }} {{ user.lastName }}</h3>
<p>{{ user.email }}</p>
<p>Cette persone préfère le {{ user.drinkPreference }}</p>
<p *ngIf="user.hobbies && user.hobbies.length > 0">
Cette personne a des hobbies !
<span *ngFor="let hobby of user.hobbies">{{ hobby }} - </span>
</p>
</li>
</ul>
Ici, vous appliquez des directives *ngFor
et *ngIf
pour afficher la liste des utilisateurs et leurs hobbies, s'ils en ont.
Afin de pouvoir visualiser ce nouveau component, ajoutez une route users
dans AppModule
, et créez un routerLink
. Ajoutez également un objet User
codé en dur dans le service pour voir les résultats :
const appRoutes: Routes = [
{ path: 'appareils', canActivate: [AuthGuard], component: AppareilViewComponent },
{ path: 'appareils/:id', canActivate: [AuthGuard], component: SingleAppareilComponent },
{ path: 'edit', canActivate: [AuthGuard], component: EditAppareilComponent },
{ path: 'auth', component: AuthComponent },
{ path: 'users', component: UserListComponent },
{ path: '', component: AppareilViewComponent },
{ path: 'not-found', component: FourOhFourComponent },
{ path: '**', redirectTo: 'not-found' }
];
<ul class="nav navbar-nav">
<li routerLinkActive="active"><a routerLink="auth">Authentification</a></li>
<li routerLinkActive="active"><a routerLink="appareils">Appareils</a></li>
<li routerLinkActive="active"><a routerLink="edit">Nouvel appareil</a></li>
<li routerLinkActive="active"><a routerLink="users">Utilisateurs</a></li>
</ul>
private users: User[] = [
new User('Will', 'Alexander', 'will@will.com', 'jus d\'orange', ['coder', 'boire du café'])
];
Dernière étape : il faut ajouter ReactiveFormsModule
, importé depuis @angular/forms
, à l'array imports
de votre AppModule
:
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
RouterModule.forRoot(appRoutes)
],
Maintenant que tout est prêt, vous allez créer NewUserComponent
qui contiendra votre formulaire réactif.
Construisez un formulaire avec FormBuilder
Dans la méthode template, l'objet formulaire mis à disposition par Angular était de type NgForm
, mais ce n'est pas le cas pour les formulaires réactifs. Un formulaire réactif est de type FormGroup
, et il regroupe plusieurs FormControl
(tous les deux importés depuis @angular/forms
). Vous commencez d'abord, donc, par créer l'objet dans votre nouveau component NewUserComponent
:
import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'app-new-user',
templateUrl: './new-user.component.html',
styleUrls: ['./new-user.component.scss']
})
export class NewUserComponent implements OnInit {
userForm: FormGroup;
constructor() { }
ngOnInit() {
}
}
Ensuite, vous allez créer une méthode qui sera appelée dans le constructeur pour la population de cet objet, et vous allez également injecter FormBuilder
, importé depuis @angular/forms
:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-new-user',
templateUrl: './new-user.component.html',
styleUrls: ['./new-user.component.scss']
})
export class NewUserComponent implements OnInit {
userForm: FormGroup;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.initForm();
}
initForm() {
}
}
FormBuilder
est une classe qui vous met à disposition des méthodes facilitant la création d'objet FormGroup
. Vous allez maintenant utiliser la méthode group
à l'intérieur de votre méthode initForm()
pour commencer à créer le formulaire :
initForm() {
this.userForm = this.formBuilder.group({
firstName: '',
lastName: '',
email: '',
drinkPreference: ''
});
}
La méthode group
prend comme argument un objet où les clés correspondent aux noms des contrôles souhaités et les valeurs correspondent aux valeurs par défaut de ces champs. Puisque l'objectif est d'avoir des champs vides au départ, chaque valeur ici correspond au string vide.
Il faut maintenant créer le template du formulaire et lier ce template à l'objet userForm
que vous venez de créer :
<div class="col-sm-8 col-sm-offset-2">
<form [formGroup]="userForm" (ngSubmit)="onSubmitForm()">
<div class="form-group">
<label for="firstName">Prénom</label>
<input type="text" id="firstName" class="form-control" formControlName="firstName">
</div>
<div class="form-group">
<label for="lastName">Nom</label>
<input type="text" id="lastName" class="form-control" formControlName="lastName">
</div>
<div class="form-group">
<label for="email">Adresse e-mail</label>
<input type="text" id="email" class="form-control" formControlName="email">
</div>
<div class="form-group">
<label for="drinkPreference">Quelle boisson préférez-vous ?</label>
<select id="drinkPreference" class="form-control" formControlName="drinkPreference">
<option value="jus d\'orange">Jus d'orange</option>
<option value="jus de mangue">Jus de mangue</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Soumettre</button>
</form>
</div>
Analysez le template :
Sur la balise
<form>
, vous utilisez le property binding pour lier l'objetuserForm
à l'attributformGroup
du formulaire, créant la liaison pour Angular entre le template et le TypeScript.Également dans la balise
<form>
, vous avez toujours une méthodeonSubmitForm()
liée à ngSubmit, mais vous n'avez plus besoin de passer le formulaire comme argument puisque vous y avez déjà accès par l'objetuserForm
que vous avez créé.Sur chaque
<input>
qui correspond à uncontrol
du formulaire, vous ajoutez l'attributformControlName
où vous passez un string correspondant au nom ducontrol
dans l'objet TypeScript.Le bouton de type
submit
déclenche l'événementngSubmit
, déclenchant ainsi la méthodeonSubmitForm()
, que vous allez créer dans votre TypeScript.
Pour tout mettre ensemble, injectez UserService
et Router
(sans oublier de les importer) dans le constructeur du component, et créez la méthode onSubmitForm()
:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { UserService } from '../services/user.service';
import { Router } from '@angular/router';
import { User } from '../models/User.model';
@Component({
selector: 'app-new-user',
templateUrl: './new-user.component.html',
styleUrls: ['./new-user.component.scss']
})
export class NewUserComponent implements OnInit {
userForm: FormGroup;
constructor(private formBuilder: FormBuilder,
private userService: UserService,
private router: Router) { }
ngOnInit() {
this.initForm();
}
initForm() {
this.userForm = this.formBuilder.group({
firstName: '',
lastName: '',
email: '',
drinkPreference: ''
});
}
onSubmitForm() {
const formValue = this.userForm.value;
const newUser = new User(
formValue['firstName'],
formValue['lastName'],
formValue['email'],
formValue['drinkPreference']
);
this.userService.addUser(newUser);
this.router.navigate(['/users']);
}
}
La méthode onSubmitForm()
récupère la value
du formulaire, et crée un nouvel objet User
(à importer en haut) à partir de la valeur des controls
du formulaire. Ensuite, elle ajoute le nouvel utilisateur au service et navigue vers /users
pour en montrer le résultat.
Il ne reste plus qu'à ajouter un lien dans UserListComponent
qui permet d'accéder à NewUserComponent
et de créer la route correspondante new-user
dans AppModule
:
<ul class="list-group">
<li class="list-group-item" *ngFor="let user of users">
<h3>{{ user.firstName }} {{ user.lastName }}</h3>
<p>{{ user.email }}</p>
<p>Cette persone préfère le {{ user.drinkPreference }}</p>
<p *ngIf="user.hobbies && user.hobbies.length > 0">
Cette personne a des hobbies !
<span *ngFor="let hobby of user.hobbies">{{ hobby }} - </span>
</p>
</li>
<a routerLink="/new-user">Nouvel utilisateur</a>
</ul>
{ path: 'users', component: UserListComponent },
{ path: 'new-user', component: NewUserComponent },
Validators
Comme pour la méthode template, il existe un outil pour la validation de données dans la méthode réactive : les Validators
. Pour ajouter la validation, vous allez modifier légèrement votre exécution de FormBuilder.group
:
initForm() {
this.userForm = this.formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
drinkPreference: ['', Validators.required]
});
}
Plutôt qu'un string simple, vous passez un array à chaque control
, avec comme premier élément la valeur par défaut souhaitée, et comme deuxième élément le ou les Validators
(dans un array s'il y en a plusieurs) souhaités. Il faut également importer Validators
depuis @angular/forms
. Dans ce cas de figure, tous les champs sont requis et la valeur du champ email
doit être sous un format valable d'adresse mail (la validité de l'adresse elle-même n'est forcément pas évaluée).
En liant la validité de userForm
à la propriété disabled
du bouton submit
, vous intégrez la validation de données :
<button type="submit" class="btn btn-primary" [disabled]="userForm.invalid">Soumettre</button>
Ajoutez dynamiquement des FormControl
Pour l'instant, vous n'avez pas encore laissé la possibilité à l'utilisateur d'ajouter ses hobbies. Il serait intéressant de lui laisser la possibilité d'en ajouter autant qu'il veut, et pour cela, vous allez utiliser un FormArray
. Un FormArray
est un array de plusieurs FormControl
, et permet, par exemple, d'ajouter des nouveaux controls
à un formulaire. Vous allez utiliser cette méthode pour permettre à l'utilisateur d'ajouter ses hobbies.
Modifiez d'abord initForm()
pour ajouter un FormArray
vide qui s'appellera hobbies avec la méthode array
:
initForm() {
this.userForm = this.formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
drinkPreference: ['', Validators.required],
hobbies: this.formBuilder.array([])
});
}
Modifiez ensuite onSubmitForm()
pour récupérer les valeurs, si elles existent (sinon, retournez un array vide) :
onSubmitForm() {
const formValue = this.userForm.value;
const newUser = new User(
formValue['firstName'],
formValue['lastName'],
formValue['email'],
formValue['drinkPreference'],
formValue['hobbies'] ? formValue['hobbies'] : []
);
this.userService.addUser(newUser);
this.router.navigate(['/users']);
}
Afin d'avoir accès aux controls
à l'intérieur de l'array, pour des raisons de typage strict liées à TypeScript, il faut créer une méthode qui retourne hobbies
par la méthode get()
sous forme de FormArray
( FormArray
s'importe depuis @angular/forms
) :
getHobbies(): FormArray {
return this.userForm.get('hobbies') as FormArray;
}
Ensuite, vous allez créer la méthode qui permet d'ajouter un FormControl
à hobbies
, permettant ainsi à l'utilisateur d'en ajouter autant qu'il veut. Vous allez également rendre le nouveau champ requis, afin de ne pas avoir un array de hobbies
avec des string vides :
onAddHobby() {
const newHobbyControl = this.formBuilder.control(null, Validators.required);
this.getHobbies().push(newHobbyControl);
}
Cette méthode crée un control
avec la méthode FormBuilder.control()
, et l'ajoute au FormArray
rendu disponible par la méthode getHobbies()
.
Enfin, il faut ajouter une section au template qui permet d'ajouter des hobbies en ajoutant des <input>
:
<div formArrayName="hobbies">
<h3>Vos hobbies</h3>
<div class="form-group" *ngFor="let hobbyControl of getHobbies().controls; let i = index">
<input type="text" class="form-control" [formControlName]="i">
</div>
<button type="button" class="btn btn-success" (click)="onAddHobby()">Ajouter un hobby</button>
</div>
<button type="submit" class="btn btn-primary" [disabled]="userForm.invalid">Soumettre</button>
Analysez cette <div> :
à la
<div>
qui englobe toute la partiehobbies
, vous ajoutez l'attributformArrayName
, qui correspond au nom choisi dans votre TypeScript ;la
<div>
de classform-group
est ensuite répété pour chaqueFormControl
dans leFormArray
(retourné pargetHobbies()
, initialement vide, en notant l'index afin de créer un nom unique pour chaqueFormControl
;dans cette
<div>
, vous avec une<input>
qui prendra commeformControlName
l'index duFormControl
;enfin, vous avez le bouton (de type
button
pour l'empêcher d'essayer de soumettre le formulaire) qui déclencheonAddHobby()
, méthode qui, pour rappel, crée un nouveauFormControl
(affichant une nouvelle instance de la<div>
de classform-group
, et donc créant une nouvelle<input>
)
Félicitations ! Maintenant, vous savez créer des formulaires par deux méthodes différentes, et comment récupérer les données saisies par l'utilisateur. Pour l'instant, la capacité d'enregistrement et de gestion de ces données a été limitée au service et donc tout est systématiquement remis à zéro à chaque rechargement de l'app. Dans les chapitres suivants, vous allez apprendre à interagir avec un serveur (et puis, plus précisément, avec un backend Firebase) afin de rendre les données permanentes et que votre application soit totalement dynamique.