1.2 - Atelier dirigé


Projet Angular

📚 Sommaire des commandes

L'outil en ligne de commande Angular CLI permet d'accelerer et faciliter le developpement avec le framework.

ng new {PROJECT NAME} --standalone=false
# standalone est la nouvelle architecture Angular
# on reste en mode 'classique' pour la session

cd {PROJECT NAME}
# initialise un repo git par defaut, on peut le lier a un remote
# git remote add origin [URL]
# git push -u origin master

ng serve
# VS Code Remote SSH offre le port forwarding
# Donc pas necessaire d'utiliser l'IP de la VM
# localhost de la machine hote redirige dans la VM avec le port

ng serve --port={PORT} --host={IP}
# ng serve offre le live-reload
# VM Minimum 1 CPU, 3 Go RAM
# echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

ng build # angular.json "defaultConfiguration":"production"

ng build -c development --watch # -c Preciser le nom d'une configuration
                                # --watch Rebuild automatiquement si les fichiers changent

ng generate {...}

Hiérarchie des fichiers

angular.json:    Configurations du framework

src/
  styles.css:    Feuille de style globale
  main.ts:       Point d'entree du programme
  index.html:    Fichier HTML racine, cadre de l'application
  assets/:       Ressources(images, pdf, etc.)

src/app/ : Fichiers de code de l'application, architecture ~MVC
  app.module.ts: Configurations du projet
  app.component.(ts, html et css): Controller, View
  xyz.model.ts: Model
  On regroupe les components et models par dossier
  Ce dossier accueillera egalement les autres composants logiciels
  (services, interceptors, etc.)

📚 Ressources Angular

📚 Référence complète

✅ Recipeasy

  • Créer un nouveau projet Angular recipeasy avec le Routing.

  • Option 1: Installer uniquement Bootstrap

npm install bootstrap

Indiquer à Angular de charger Bootstrap

// angular.json

// projects > {PROJECT NAME} > architect > build > styles
"styles": [
    "src/styles.css",
    "node_modules/bootstrap/dist/css/bootstrap.min.css"
]
  • Option 2: Librairie ngx-bootstrap

Actuellement ne fonctionne pas en Angular 17...

Pour faciliter l'utilisation des composants UI de Bootstrap, on peut utiliser le wrapper ngx-bootstrap

ng add ngx-bootstrap
# Consulter la documentation pour la compatibilite avec la version d'Angular

La commande ng add permet d'exploiter le mécanisme de schematics lors de l'ajout d'une dépendance.

Il existe d'autres librairies de composants UI selon vos goûts et besoins, par exemple :

On peut valider la configuration de Bootstrap en remplaçant le contenu de app.component.html

<h1 class="text-success">Recipeasy!</h1>

<button type="button" 
        class="btn btn-primary"
        tooltip="This is a tooltip!">
        Hover me
</button>

<bs-datepicker-inline></bs-datepicker-inline>
  • Démarrer le serveur de développement pour valider.

Components

📚 Les components

Une application Angular est en fait la composition de plusieurs components. Un component racine heberge les autres components qui peuvent a leur tour heberger un ou plusieurs components. Par exemple, l'interface ci-dessous pourrait etre separee selon les components suivants:

  • Application
    • Navigation
      • (Menu)
      • (Breadcrumbs)
    • Products List
      • Product Row(on peut reutiliser un component avec differentes donnees)
      • (Pagination)
+------------------------------------------------------+
|------------------------------------------------------+
||  Home  |  Products  |  Users  |           [Log out] |
+---------+------------+-------------------------------+
|                                                      |
| Products > Inventory > Backorder                     |
|                                                      |
| +--------------------------------------------------+ |
| | +---+                                            | |
| | |   | Product A                           12.34$ | |
| | +---+                                            | |
| +--------------------------------------------------+ |
| +--------------------------------------------------+ |
| | +---+                                            | |
| | |   | Product B                           34.56$ | |
| | +---+                                            | |
| +--------------------------------------------------+ |
| +--------------------------------------------------+ |
| | +---+                                            | |
| | |   | Product C                           56.99$ | |
| | +---+                                            | |
| +--------------------------------------------------+ |
|                                                      |
|                  ≤ 1 2 3 ... 9 ≥                     |
+------------------------------------------------------+

Un component permet donc d'associer un comportement(.ts) et un visuel(le template .html) a des balises réutilisables. Par exemple, pour intégrer une liste de produits, je peux céeer un component et l'ajouter à la page:

<app-products-list></app-products-list>

<!-- <app-products-list /> depuis 15.1 -->

Angular interprète ensuite le HTML et injecte le comportement aux endroits nécessaires.

L'utilitaire en ligne de commande simplifie la création des components et leur inscription auprès d'Angular dans app.module.ts.

ng g component {the--component-name} --skip-tests

# On peut regrouper les components dans le dossier app/components
# On peut deplacer le app component dans le dossier et 
# VS Code devrait offrir de mettre a jour les imports

# Pour generer directement dans un sous-dossier
# Pour un component occupant la page entiere via le routing
# il peut etre interessant d'inscrire le type 'page'
ng g component components/sub/directory --type page --skip-tests 

Le préfix app du sélecteur est ajouté automatiquement lors de la génération pour différencier les balises appartenant à notre application.

✅ Recipeasy

Générer les pages login et signup dans le sous-dossier src/app/components

  • Afficher les components dans app.component.html
  • Déplacer le component app dans le sous-dossier src/app/components

Routing

📚 Router

On peut intégrer les components dans la page mais il serait plus intéressant de pouvoir naviguer d'un component a l'autre.

Dans app-routing.module.ts on retrouve la configuration des routes, ce qui permet d'injecter le bon component dans un page selon l'URL. On doit définir la coquille qui accueillera des components avec la balise :

<router-outlet></router-outlet>

Puis

// On ne precise PAS le / indiquant la racine

{ path: '', component: MyComponent } // route = /

{ path: 'login', component: MyOtherComponent } // route = /login

Pour indiquer a Angular que le routeur doit procéder à la navigation sur le clic des liens en utilisant la propriété suivante à la place du href sur les balises a:

<a routerLink="/route">Naviguer!</a>
<!-- OU -->
<a [routerLink]="['/route']">Naviguer!</a>

<!-- Attention aux liens relatifs VS absolus -->

Il peut être intéressant d'extraire la déclaration des routes, le tableau routes, de app-routing.module.ts pour la mettre dans un fichier distinct app/app.routes.ts.

✅ Recipeasy

Pour avoir un rendu visuel plus intéressant, intégrer le code suivant dans vos components

app.component.html

<div class="fixed-top">
  <div class="row">
    <div class="col text-center">
      <h1>Recipeasy!</h1>
    </div>
  </div>

  <nav class="navbar navbar-expand navbar-dark bg-primary">
    <div class="container-fluid">
      <div class="collapse navbar-collapse justify-content-between">
        <ul class="navbar-nav">
          <li class="nav-item">
            <a href="#" class="nav-link text-light">Recipes</a>
          </li>
        </ul>

        <ul class="navbar-nav">
          <li class="nav-item">
            <a href="#" class="nav-link text-light">Profile</a>
          </li>
          <li>
            <button type="button" class="btn btn-outline-light">Log out</button>
          </li>
        </ul>
      </div>
    </div>
  </nav>
</div>

<div class="pt-4" style="margin: 112px 0px 24px 0px;">
  <router-outlet></router-outlet>
</div>

login.page.html

<div class="container text-center">
  <div class="row">
    <div class="col-12 col-sm-10 offset-sm-1">
      <form class="row g-3">
          <input type="email" placeholder="Email" class="form-control">

          <input type="password" placeholder="Password" class="form-control">

          <button class="btn btn-primary" type="submit">Sign in</button>

          <a href="/signup" class="btn btn-sm btn-outline-success mt-4" role="button">Sign up</a>
      </form>
    </div>
  </div>
</div>

signup.page.html

<div class="container text-center">
  <div class="row">
    <div class="col-sm-10 offset-sm-1 ">
      <form class="row g-3">
        <input type="email" placeholder="Email" class="form-control">

        <input type="password" placeholder="Password" class="form-control">

        <input type="password" placeholder="Password confirmation" class="form-control">

        <button class="btn btn-success" type="submit">Create my account</button>

        <a href="/" class="btn btn-sm btn-outline-secondary mt-4" role="button">Cancel</a>
      </form>
    </div>
  </div>
</div>

  • Declarer les routes '' -> LoginComponent et 'signup' -> SignupComponent
    • Extraire la declaration dans app.routes.ts
    • Le routeur-outlet dans app.component.html affichera le bon component selon la route
  • Mettre en place la navigation sur les bouton Sign Up et Cancel
Résultat
src/app/app.routes.ts CHANGED
@@ -0,0 +1,8 @@+import { Routes } from "@angular/router";
1
+ import { LoginComponent } from "./components/login/login.component";
2
+ import { SignupComponent } from "./components/signup/signup.component";
3
+
4
+ export const ROUTES: Routes = [
5
+ { path: '', component: LoginComponent },
6
+ { path: 'signup', component: SignupComponent }
7
+ ];
src/app/app-routing.module.ts CHANGED
@@ -1,10 +1,9 @@ import { NgModule } from '@angular/core';
1
- import { Routes, RouterModule } from '@angular/router';
2
-
3
- const routes: Routes = [];
1
+ import { RouterModule } from '@angular/router';
2
+ import { ROUTES } from './app.routes';
4
3
 
5
4
  @NgModule({
6
- imports: [RouterModule.forRoot(routes)],
5
+ imports: [RouterModule.forRoot(ROUTES)],
7
6
  exports: [RouterModule]
8
7
  })
9
8
  export class AppRoutingModule { }

Templates

📚 Directives

Les directives augmentent le HTML des templates pour les rendre dynamiques. Elles permettent une interaction, qui peut etre birectionnelle, entre le controleur(.ts) et la vue(.html).

{{ expression }} : Évaluation de l'expression/variable dans le template

#variable : Ajoute une référence locale vers l'élément disponible dans le template uniquement

[property]="expression" : Assigne une propriété, lorsque la valeur de l'expression change la propriété se met à jour(one-way binding)

(event)="expression" : Inscrit un événement à l'élément, l'expression est exécuté lors de l'événement

[(variable)]="expression" : Data-binding bidirectionnel

ngDirective OU ngDirective="valeur" : Associe un comportement à l'élément(form, model)

*directive="expression" : Modifie la structure du DOM(if, for, etc.)

Par exemple, pour afficher le tableau items dans une liste

let items = [
    { id: 1,  isActive: true, image: 'cat.jpg', name: 'Garfield' },
    { id: 2,  isActive: true, image: 'dog.jpg', name: 'Fido' },
    { id: 3,  isActive: false, image: 'bird.jpg', name: 'Tweetie' },
];
<ul>
  <li *ngFor="let item of items">
    <img *ngIf="item.isActive" [src]="item.image">

    {{ item.name }}

    <button (click)="delete(item.id)">X</button>
  </li>
</ul>

Formulaires

📚 Formulaires

Il existe 2 stratégies pour manipuler les formulaires

  • Template-driven : Annotations dans le HTML, comportements simples
  • Reactive : Déclaration dans le controller, plus flexible

ℹ️ Template vs Reactive

Nous allons nous concentrer sur la méthode Reactive Forms

Ajouter le module dans la app.module.ts

@NgModule({
  imports: [
    // ...
    ReactiveFormsModule // VS Code devrait suggerer le import { ReactiveFormsModule } from '@angular/forms';
  ],
})

Identifier le formulaire et ses éléments dans le HMTL

<form [formGroup]="loginForm">

<input formControlName="email">
<!-- formControlName n'évalue PAS une expression, donc pas de [] -->

Puis établir la correspondance dans le contrôleur

this.loginForm = = new FormGroup({
  email: new FormControl('a@a.com'),
  password: new FormControl('aaaaaaaa')
});

On peut exécuter une méthode du contrôleur lors de l'événement submit

<form ... (ngSubmit)="logIn()">

On peut récupérer les valeurs du formulaire

console.log( this.loginForm.value );
// value est un objet JSON, donc on peut lire une propriété précise avec value.xyz

// Ou les contrôles individuellement
const control = this.loginForm.controls.email;
// puis manipuler l'element, control.xyz();
console.log( control.value );

✅ Recipeasy

  • Ajouter le module ReactiveForms au projet
  • Intercepter la soumission du formulaire login
    • Récupérer et afficher ses valeurs dans la console/alerte
  • Intercepter la soumission du formulaire sign up
    • Récupérer et afficher ses valeurs dans la console/alerte
Résultat
Login
src/app/components/login/login.component.html CHANGED
@@ -2,13 +2,13 @@ <div class="row mt-2">
2
2
  <div class="col-10 offset-1">
3
3
  <!-- https://getbootstrap.com/docs/4.5/components/forms -->
4
- <form>
4
+ <form [formGroup]="loginForm" (ngSubmit)="logIn()">
5
5
  <div class="form-group">
6
- <input type="email" placeholder="Email" class="form-control" >
6
+ <input formControlName="email" type="email" placeholder="Email" class="form-control" >
7
7
  </div>
8
8
 
9
9
  <div class="form-group">
10
- <input type="password" placeholder="Password" class="form-control" >
10
+ <input formControlName="password" type="password" placeholder="Password" class="form-control" >
11
11
  </div>
12
12
 
13
13
  <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
src/app/components/login/login.component.ts CHANGED
@@ -1,4 +1,5 @@ import { Component, OnInit } from '@angular/core';
1
+ import { FormControl, FormGroup } from '@angular/forms';
1
2
 
2
3
  @Component({
3
4
  selector: 'app-login',
@@ -7,9 +8,19 @@ import { Component, OnInit } from '@angular/core'; })
7
8
  export class LoginComponent implements OnInit {
8
9
 
9
- constructor() { }
10
+ loginForm: FormGroup;
11
+
12
+ constructor() {
13
+ this.loginForm = new FormGroup({
14
+ email: new FormControl(),
15
+ password: new FormControl()
16
+ });
17
+ }
10
18
 
11
19
  ngOnInit(): void {
12
20
  }
13
21
 
22
+ logIn() {
23
+ console.log(this.loginForm.value);
24
+ }
14
25
  }
Sign up
src/app/components/signup/signup.component.html CHANGED
@@ -1,17 +1,17 @@ <div class="container text-center">
1
1
  <div class="row mt-2">
2
2
  <div class="col-10 offset-1">
3
- <form>
3
+ <form [formGroup]="signupForm" (ngSubmit)="signUp()">
4
4
  <div class="form-group">
5
- <input type="email" placeholder="Email" class="form-control" >
5
+ <input formControlName="email" type="email" placeholder="Email" class="form-control" >
6
6
  </div>
7
7
 
8
8
  <div class="form-group">
9
- <input type="password" placeholder="Password" class="form-control" >
9
+ <input formControlName="password" type="password" placeholder="Password" class="form-control" >
10
10
  </div>
11
11
 
12
12
  <div class="form-group">
13
- <input type="password" placeholder="Password confirmation" class="form-control" >
13
+ <input formControlName="passwordConfirmation" type="password" placeholder="Password confirmation" class="form-control" >
14
14
  </div>
15
15
 
16
16
  <button class="btn btn-success btn-block" type="submit">Create my account</button>
src/app/components/signup/signup.component.ts CHANGED
@@ -1,4 +1,5 @@ import { Component, OnInit } from '@angular/core';
1
+ import { FormControl, FormGroup } from '@angular/forms';
1
2
 
2
3
  @Component({
3
4
  selector: 'app-signup',
@@ -7,9 +8,20 @@ import { Component, OnInit } from '@angular/core'; })
7
8
  export class SignupComponent implements OnInit {
8
9
 
9
- constructor() { }
10
+ signupForm: FormGroup;
11
+
12
+ constructor() {
13
+ this.signupForm = new FormGroup({
14
+ email: new FormControl(),
15
+ password: new FormControl(),
16
+ passwordConfirmation: new FormControl()
17
+ });
18
+ }
10
19
 
11
20
  ngOnInit(): void {
12
21
  }
13
22
 
23
+ signUp() {
24
+ console.log(this.signupForm.value);
25
+ }
14
26
  }

Validations des formulaires

📚 Validation Reactives

A la création du FormControl ou du FormGroup, on peut préciser les validations à appliquer

  • Attention On peut appliquer les validations sur un champ ou sur le formulaire pour vérifier une concordance entre plusieurs champs
new FormControl(null, [Validators.required, Validators.email, ...])
// Le 1er parametre est la valeur initiale du contrôle

Il est également possible d'utiliser une fonction de validation personnalisée en fournissant un item qui répond à l'interface ValidatorFn

Ensuite dans le template on peut vérifier le statut d'un FormControl ou FormGroup, voir leur super classe AbstractControl pour toutes les propriétés et méthodes.

formOrControl.valid
formOrControl.invalid
formOrControl.errors

formOrControl.dirty   // la valeur a changée via le UI
formOrControl.touched // le champ a été focus puis quitté

form.get('myFormControlName')?.xyz // Recuperer un form control du form
// peut etre null, donc ? pour le chaînage optionel

On peut exploiter les variables locales des templates pour récupérer si le formulaire à été soumis

<form #form="ngForm">
  <!-- form.submitted est disponible dans les expressions -->
  <div [class.is-invalid]="form.submitted">
    ...
  </div>
</form>

✅ Recipeasy

<!-- 
  Un input possedant la classe is-invalid suivi du bloc 
  invalid-feedback pour afficher l'erreur 
-->
<div>
  <!-- Attention d'encapsuler dans un bloc -->
  <input class="form-control is-invalid">
  <div class="invalid-feedback text-start">
    Message
  </div>
</div>
  • Désactiver le bouton sign up si le formulaire est invalide

  • Exploiter les validations sur plusieurs champs pour valider que la confirmation du mot de passe correspond au mot de passe dans le sign up

private passwordMatch(form: AbstractControl): ValidationErrors | null {
  // AbstractControl est la super classe des FormGroup ET FormControl
  // attention sur lequel la validation personnalisée est appliquée

  if (form.value?.password != form.value?.passwordConfirmation) {
    return { passwordConfirmationMustMatch: true };
  } else {
    return null
  }
}
Résultat
Login
src/app/components/login/login.component.html CHANGED
@@ -4,14 +4,26 @@ <!-- https://getbootstrap.com/docs/4.5/components/forms -->
4
4
  <form [formGroup]="loginForm" (ngSubmit)="logIn()">
5
5
  <div class="form-group">
6
- <input formControlName="email" type="email" placeholder="Email" class="form-control" >
6
+ <input
7
+ formControlName="email"
8
+ [class.is-invalid]="loginForm.get('email')?.invalid && loginForm.get('email')?.dirty && loginForm.get('email')?.touched"
9
+ type="email" placeholder="Email" class="form-control" >
10
+ <div class="invalid-feedback text-left">
11
+ Email must be valid.
12
+ </div>
7
13
  </div>
8
14
 
9
15
  <div class="form-group">
10
- <input formControlName="password" type="password" placeholder="Password" class="form-control" >
16
+ <input
17
+ formControlName="password"
18
+ [class.is-invalid]="passwordControl.invalid && passwordControl.dirty && passwordControl.touched"
19
+ type="password" placeholder="Password" class="form-control" >
20
+ <div *ngIf="passwordControl.invalid && passwordControl.dirty && passwordControl.touched" class="text-left text-danger">
21
+ Password must be at least 3 characters long.
22
+ </div>
11
23
  </div>
12
24
 
13
- <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
25
+ <button [disabled]="loginForm.invalid" class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
14
26
 
15
27
  <a [routerLink]="['/signup']" class="btn btn-outline-success btn-block mt-4" role="button">Sign up</a>
16
28
  </form>
src/app/components/login/login.component.ts CHANGED
@@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core';
1
- import { FormControl, FormGroup } from '@angular/forms';
1
+ import { FormControl, FormGroup, Validators } from '@angular/forms';
2
2
 
3
3
  @Component({
4
4
  selector: 'app-login',
@@ -9,11 +9,14 @@ import { FormControl, FormGroup } from '@angular/forms'; export class LoginComponent implements OnInit {
9
9
 
10
10
  loginForm: FormGroup;
11
+ passwordControl: FormControl
11
12
 
12
13
  constructor() {
14
+ this.passwordControl = new FormControl(null, [Validators.required, Validators.minLength(3)]);
15
+
13
16
  this.loginForm = new FormGroup({
14
- email: new FormControl(),
15
- password: new FormControl()
17
+ email: new FormControl(null, [Validators.required, Validators.email]),
18
+ password: this.passwordControl
16
19
  });
17
20
  }
18
21
 
@@ -21,6 +24,6 @@ export class LoginComponent implements OnInit { }
21
24
 
22
25
  logIn() {
23
- console.log(this.loginForm.value);
26
+ console.log(this.loginForm.value);
24
27
  }
25
28
  }
Sign up
src/app/components/signup/signup.component.html CHANGED
@@ -3,18 +3,36 @@ <div class="col-10 offset-1">
3
3
  <form [formGroup]="signupForm" (ngSubmit)="signUp()">
4
4
  <div class="form-group">
5
- <input formControlName="email" type="email" placeholder="Email" class="form-control" >
5
+ <input
6
+ formControlName="email"
7
+ [class.is-invalid]="signupForm.get('email')?.invalid && signupForm.get('email')?.dirty && signupForm.get('email')?.touched"
8
+ type="email" placeholder="Email" class="form-control" >
9
+ <div class="invalid-feedback text-left">
10
+ Email must be valid.
11
+ </div>
6
12
  </div>
7
13
 
8
14
  <div class="form-group">
9
- <input formControlName="password" type="password" placeholder="Password" class="form-control" >
15
+ <input
16
+ formControlName="password"
17
+ [class.is-invalid]="signupForm.get('password')?.invalid && signupForm.get('password')?.dirty && signupForm.get('password')?.touched"
18
+ type="password" placeholder="Password" class="form-control" >
19
+ <div class="invalid-feedback text-left">
20
+ Password must be at least 3 characters long.
21
+ </div>
10
22
  </div>
11
23
 
12
24
  <div class="form-group">
13
- <input formControlName="passwordConfirmation" type="password" placeholder="Password confirmation" class="form-control" >
25
+ <input
26
+ formControlName="passwordConfirmation"
27
+ [class.is-invalid]="signupForm.errors?.passwordConfirmationMustMatch && signupForm.get('passwordConfirmation')?.dirty && signupForm.get('passwordConfirmation')?.touched"
28
+ type="password" placeholder="Password confirmation" class="form-control">
29
+ <div class="invalid-feedback text-left">
30
+ Password and confirmation must match.
31
+ </div>
14
32
  </div>
15
33
 
16
- <button class="btn btn-success btn-block" type="submit">Create my account</button>
34
+ <button [disabled]="signupForm.invalid" class="btn btn-success btn-block" type="submit">Create my account</button>
17
35
 
18
36
  <a [routerLink]="['/']" class="btn btn-outline-secondary btn-block mt-4" role="button">Cancel</a>
19
37
  </form>
src/app/components/signup/signup.component.ts CHANGED
@@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core';
1
- import { FormControl, FormGroup } from '@angular/forms';
1
+ import { AbstractControl, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms';
2
2
 
3
3
  @Component({
4
4
  selector: 'app-signup',
@@ -12,10 +12,20 @@ export class SignupComponent implements OnInit {
12
12
  constructor() {
13
13
  this.signupForm = new FormGroup({
14
- email: new FormControl(),
15
- password: new FormControl(),
14
+ email: new FormControl(null, [Validators.required, Validators.email]),
15
+ password: new FormControl(null, [Validators.required, Validators.minLength(3)]),
16
16
  passwordConfirmation: new FormControl()
17
- });
17
+ }, this.passwordMatch);
18
+ }
19
+
20
+ private passwordMatch(form: AbstractControl): ValidationErrors | null {
21
+ console.log(form.value);
22
+
23
+ if (form.value?.password != form.value?.passwordConfirmation) {
24
+ return { passwordConfirmationMustMatch: true };
25
+ } else {
26
+ return null
27
+ }
18
28
  }
19
29
 
20
30
  ngOnInit(): void {

Services

📚 Injection de dépendance

On vise a simplifier l'instantiation de ressources partagées. Plutôt que de devoir nous-même créer manuellement ces objets, le framework fournit un mécanisme pour les récupérer facilement. Les instances de services sont des Singleton.

ng g service services/{the-name} --skip-tests

On peut ensuite injecter la dépendance via les constructor

constructor(private authService: AuthService) {
  // Le mot-clé private est un raccourci 
  // pour créer un membre de classe au même moment
}

Injection de dépendances

L'injection de dépendance permet à différents composants de l'application d'intéragir entre eux ou avec des mécanismes du framework(Router, Client HTTP, Services, DOM, etc.). C'est le framework qui gère l'instantiation des objets injectés et les rends disponibles lorsque demandés.

Par exemple, on peut manipuler la navigation dans les contrôleur TypeScript via le Router

@Component({
    selector: 'app-demo'
})
export class DemoComponent implements OnInit {
    constructor(private router: Router) { }

    clicked() {
        this.router.navigate(['/some/route']);
    }
}

✅ Recipeasy

  • Créer les fichiers user.model.ts et user-credentials.ts dans le sous-dossier models à partir du code fourni
  • Générer le AuthService dans le sous-dossier services et intégrer le code fourni ci-dessous
  • Pour faciliter la manipulation du JSON, nous ajoutons la librairie ts-json-object.
  • Créer la page recipes et mettre à jour les routes

Code

models/user-credentials.model.ts

import {custom, JSONObject, required} from 'ts-json-object'

export class UserCredentials extends JSONObject {
  @required
  @custom((user: UserCredentials, key:string, value: string) => {
    return value.toLowerCase();
  })
  email!: string;

  @required
  password!: string;
}
models/user.model.ts

import { custom, JSONObject, required } from "ts-json-object";

export class User extends JSONObject {
  @required
  @custom((user: User, key:string, value: string) => {
    return value.toLowerCase();
  })
  email!: string;
}
services/auth.service.ts

private readonly CURRENT_USER_KEY = 'recipeasy.currentUser';
private readonly USERS_KEY = 'recipeasy.users';

private _currentUser : User | null = null;

get currentUser(): User | null {
  return this._currentUser;
}

get isLoggedIn(): boolean {
  return !!this._currentUser;
}

get users(): User[] {
  return this.usersCredentials.map(userCredentials => new User(userCredentials));
}


private get usersCredentials(): UserCredentials[] {
  let usersCredentials : UserCredentials[] = [];
  const storedUsers = JSON.parse(localStorage.getItem(this.USERS_KEY) ?? 'null');

  if (storedUsers) {
    usersCredentials = (storedUsers as UserCredentials[]).map(obj =>
      new UserCredentials(obj)
    );
  }

  return usersCredentials;
}

constructor() {
  const storedCurrentUser = JSON.parse(localStorage.getItem(this.CURRENT_USER_KEY) ?? 'null');

  if (storedCurrentUser) {
    this._currentUser = new User(storedCurrentUser);
  }
}

private setCurrentUser(user: User | null) {
  this._currentUser = user;
  localStorage.setItem(this.CURRENT_USER_KEY, JSON.stringify(user));
}

private emailExists(email: string): boolean {
  return !!this.usersCredentials.find(credentials =>
    credentials.email == email
    );
}

findUser(email: string): User | null {
    return this.users.find(user =>
        user.email == email.toLowerCase()
    ) ?? null;
}

logIn(credentials: UserCredentials): boolean {
  const validUser = this.usersCredentials.find(userCredentials =>
    userCredentials.email == credentials.email && userCredentials.password == credentials.password
  );

  if (validUser) {
    this.setCurrentUser(new User(validUser))
  }

  return !!validUser;
}

signUp(credentials: UserCredentials): boolean {
  if (this.emailExists(credentials.email)) {
    return false;
  } else {
    let usersCredentials = this.usersCredentials;

    usersCredentials.push(credentials);
    localStorage.setItem(this.USERS_KEY, JSON.stringify(usersCredentials));

    this.setCurrentUser(new User(credentials))

    return true;
  }
}

logOut() {
  this.setCurrentUser(null);
}

Fonctionnalités

  • Utiliser le AuthService pour implémenter la fonctionnalité de connexion de l'application
    • Afficher à l'utilisateur si les informations de connexion sont invalides(la méthode logIn retourne false)
    • *ngIf pour modifier la structure de la page selon une condition
<!-- https://getbootstrap.com/docs/5.1/components/alerts/ -->
<div class="alert alert-danger" role="alert">
  Message
</div>


  • Utiliser le AuthService pour implémenter la fonctionnalité d'inscription de l'application

    • Afficher à l'utilisateur si le email est deja utilisé(la methode signUp retourne false)
  • Sur un login/sign up valide, rediriger vers la route /recipes qui presente le composant recipes

  • Implémenter l'événement click du bouton Log out qui déconnecte l'utilisateur courant et redirige à la racine(le login)
Résultat
ts-json-object
tsconfig.json CHANGED
@@ -12,6 +12,7 @@ "declaration": false,
12
12
  "downlevelIteration": true,
13
13
  "experimentalDecorators": true,
14
+ "emitDecoratorMetadata": true,
14
15
  "moduleResolution": "node",
15
16
  "importHelpers": true,
16
17
  "target": "es2015",
Route
src/app/app.routes.ts CHANGED
@@ -1,8 +1,10 @@ import { Routes } from "@angular/router";
1
1
  import { LoginComponent } from "./components/login/login.component";
2
+ import { RecipesComponent } from "./components/recipes/recipes.component";
2
3
  import { SignupComponent } from "./components/signup/signup.component";
3
4
 
4
5
  export const ROUTES: Routes = [
5
6
  { path: '', component: LoginComponent },
6
- { path: 'signup', component: SignupComponent }
7
+ { path: 'signup', component: SignupComponent },
8
+ { path: 'recipes', component: RecipesComponent },
7
9
  ];
Login
src/app/components/login/login.component.html CHANGED
@@ -1,32 +1,38 @@ <div class="container text-center">
1
1
  <div class="row mt-2">
2
2
  <div class="col-10 offset-1">
3
- <!-- https://getbootstrap.com/docs/4.5/components/forms -->
4
- <form [formGroup]="loginForm" (ngSubmit)="logIn()">
5
- <div class="form-group">
6
- <input
7
- formControlName="email"
8
- [class.is-invalid]="loginForm.get('email')?.invalid && loginForm.get('email')?.dirty && loginForm.get('email')?.touched"
9
- type="email" placeholder="Email" class="form-control" >
10
- <div class="invalid-feedback text-left">
11
- Email must be valid.
12
- </div>
3
+
4
+ <!-- https://getbootstrap.com/docs/4.5/components/alerts/#examples -->
5
+ <div *ngIf="authError" class="alert alert-danger" role="alert">
6
+ {{ authError }}
7
+ </div>
8
+
9
+ <!-- https://getbootstrap.com/docs/4.5/components/forms -->
10
+ <form [formGroup]="loginForm" (ngSubmit)="logIn()">
11
+ <div class="form-group">
12
+ <input
13
+ formControlName="email"
14
+ [class.is-invalid]="loginForm.get('email')?.invalid && loginForm.get('email')?.dirty && loginForm.get('email')?.touched"
15
+ type="email" placeholder="Email" class="form-control" >
16
+ <div class="invalid-feedback text-left">
17
+ Email must be valid.
13
18
  </div>
19
+ </div>
14
20
 
15
- <div class="form-group">
16
- <input
17
- formControlName="password"
18
- [class.is-invalid]="passwordControl.invalid && passwordControl.dirty && passwordControl.touched"
19
- type="password" placeholder="Password" class="form-control" >
20
- <div *ngIf="passwordControl.invalid && passwordControl.dirty && passwordControl.touched" class="text-left text-danger">
21
- Password must be at least 3 characters long.
22
- </div>
21
+ <div class="form-group">
22
+ <input
23
+ formControlName="password"
24
+ [class.is-invalid]="passwordControl.invalid && passwordControl.dirty && passwordControl.touched"
25
+ type="password" placeholder="Password" class="form-control" >
26
+ <div *ngIf="passwordControl.invalid && passwordControl.dirty && passwordControl.touched" class="text-left text-danger">
27
+ Password must be at least 3 characters long.
23
28
  </div>
29
+ </div>
24
30
 
25
- <button [disabled]="loginForm.invalid" class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
31
+ <button [disabled]="loginForm.invalid" class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
26
32
 
27
- <a [routerLink]="['/signup']" class="btn btn-outline-success btn-block mt-4" role="button">Sign up</a>
28
- </form>
33
+ <a [routerLink]="['/signup']" class="btn btn-outline-success btn-block mt-4" role="button">Sign up</a>
34
+ </form>
29
35
  </div>
30
36
  </div>
31
37
  </div>
src/app/components/login/login.component.ts CHANGED
@@ -1,5 +1,7 @@ import { Component, OnInit } from '@angular/core';
1
- import { FormControl, FormGroup, Validators } from '@angular/forms';
1
+ import { FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms';
2
+ import { Router } from '@angular/router';
3
+ import { AuthService } from 'src/app/services/auth.service';
2
4
 
3
5
  @Component({
4
6
  selector: 'app-login',
@@ -8,10 +10,12 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; })
8
10
  export class LoginComponent implements OnInit {
9
11
 
12
+ authError: string | null = null;
13
+
10
14
  loginForm: FormGroup;
11
15
  passwordControl: FormControl
12
16
 
13
- constructor() {
17
+ constructor(private authService: AuthService, private router: Router) {
14
18
  this.passwordControl = new FormControl(null, [Validators.required, Validators.minLength(3)]);
15
19
 
16
20
  this.loginForm = new FormGroup({
@@ -24,6 +28,10 @@ export class LoginComponent implements OnInit { }
24
28
 
25
29
  logIn() {
26
- console.log(this.loginForm.value);
30
+ if (this.authService.logIn(this.loginForm.value)) {
31
+ this.router.navigate(['/recipes']);
32
+ } else {
33
+ this.authError = "Invalid credentials!";
34
+ }
27
35
  }
28
36
  }
Sign up
src/app/components/signup/signup.component.html CHANGED
@@ -6,9 +6,10 @@ <input
6
6
  formControlName="email"
7
7
  [class.is-invalid]="signupForm.get('email')?.invalid && signupForm.get('email')?.dirty && signupForm.get('email')?.touched"
8
+ (input)="onEmailInput()"
8
9
  type="email" placeholder="Email" class="form-control" >
9
10
  <div class="invalid-feedback text-left">
10
- Email must be valid.
11
+ {{ uniqueEmailError || 'Email must be valid.' }}
11
12
  </div>
12
13
  </div>
13
14
 
src/app/components/signup/signup.component.ts CHANGED
@@ -1,5 +1,8 @@ import { Component, OnInit } from '@angular/core';
1
1
  import { AbstractControl, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms';
2
+ import { Router } from '@angular/router';
3
+ import { UserCredentials } from 'src/app/models/user-credentials.model';
4
+ import { AuthService } from 'src/app/services/auth.service';
2
5
 
3
6
  @Component({
4
7
  selector: 'app-signup',
@@ -8,9 +11,10 @@ import { AbstractControl, FormControl, FormGroup, ValidationErrors, Validators } })
8
11
  export class SignupComponent implements OnInit {
9
12
 
13
+ uniqueEmailError: string | null = null;
10
14
  signupForm: FormGroup;
11
15
 
12
- constructor() {
16
+ constructor(private authService: AuthService, private router: Router) {
13
17
  this.signupForm = new FormGroup({
14
18
  email: new FormControl(null, [Validators.required, Validators.email]),
15
19
  password: new FormControl(null, [Validators.required, Validators.minLength(3)]),
@@ -32,6 +34,23 @@ export class SignupComponent implements OnInit { }
32
34
 
33
35
  signUp() {
34
- console.log(this.signupForm.value);
36
+ if (this.authService.signUp(new UserCredentials(this.signupForm.value))) {
37
+ this.router.navigate(['/recipes']);
38
+ } else {
39
+ this.setUniqueEmailError(true);
40
+ }
41
+ }
42
+
43
+ onEmailInput() {
44
+ if (this.uniqueEmailError) {
45
+ this.setUniqueEmailError(false);
46
+
47
+ this.signupForm.get('email')?.updateValueAndValidity();
48
+ }
49
+ }
50
+
51
+ private setUniqueEmailError(isUsed: boolean) {
52
+ this.uniqueEmailError = isUsed ? 'This email is already used!' : null;
53
+ this.signupForm.get('email')?.setErrors(isUsed ? { emailNotUnique: true } : null);
35
54
  }
36
55
  }
@@ -19,8 +23,6 @@ export class SignupComponent implements OnInit { }
19
23
 
20
24
  private passwordMatch(form: AbstractControl): ValidationErrors | null {
21
- console.log(form.value);
22
-
23
25
  if (form.value?.password != form.value?.passwordConfirmation) {
24
26
  return { passwordConfirmationMustMatch: true };
25
27
  } else {
Log out
src/app/components/app/app.component.html CHANGED
@@ -15,7 +15,7 @@ </ul>
15
15
 
16
16
  <a href="#" class="text-light mr-4">Profile</a>
17
- <button type="button" class="btn btn-outline-light">Log out</button>
17
+ <button (click)="logOut()" type="button" class="btn btn-outline-light">Log out</button>
18
18
  </div>
19
19
  </nav>
20
20
  </div>
src/app/components/app/app.component.ts CHANGED
@@ -1,4 +1,6 @@ import { Component } from '@angular/core';
1
+ import { Router } from '@angular/router';
2
+ import { AuthService } from 'src/app/services/auth.service';
1
3
 
2
4
  @Component({
3
5
  selector: 'app-root',
@@ -6,5 +8,12 @@ import { Component } from '@angular/core'; styleUrls: ['./app.component.css']
6
8
  })
7
9
  export class AppComponent {
8
- title = 'recipeasy';
10
+
11
+ constructor(private authService: AuthService, private router: Router) { }
12
+
13
+ logOut() {
14
+ this.authService.logOut();
15
+
16
+ this.router.navigate(['/']);
17
+ }
9
18
  }

Communication entre les components

📚 Inputs/Outputs

On peut declarer des propriétés dans un component qui seront visible via la balise HTML permettant au component parent d'envoyer des donnees.

Input

Dans le contrôleur, on declare un membre de classe en ajoutant l'annotation @Input()

@Input() myInput: string; // Le type peut etre un objet

Puis dans la balise HTML, le parent peut fournir la donnée

<app-demo-component [myInput]="expression"></app-demo-component>

Output

Dans la même logique, on peut définir un événement qui peut etre alimenté par un component auxquel s'enregistre le component parent

Dans le contrôleur, on déclare un membre de classe en ajoutant l'annotation @Output()

@Output() readonly myOutput = new EventEmitter();

//Puis pour déclencher l'événement
myOutput.emit(paramOptionnel) // le parametre peut servir à ajouter une valeur à l'événement

Puis dans la balise HTML, le parent peut écouter l'événement

<app-demo-component (myOutput)="expression"></app-demo-component>

<!-- Pour récuperer le paramètre, on utilise la variable $event -->
<app-demo-component (myOutput)="myFunction($event)"></app-demo-component>

On peut sans problème combiner un ou plusieurs Input et Output sur un même component!

✅ Recipeasy

  • Ajouter la page profile
    • Créer la route /profile
  • Naviguer vers /profile via la barre de navigation
  • Le profile affiche l'utilisateur actuel via la JsonPipe

  • À partir du profile, afficher la liste de tous les utilisateur existants via le getter users du AuthService en encapsulant la liste dans le component users-list et chaque item de la liste dans users-list-item

  • Créer les components users-list et users-list-item

Users list affiche un tableau via *ngFor.

  • Utiliser *ngIf pour gérer un layout alternatif s'il n'y a qu'un user dans le système, sinon afficher le tableau
<table class="table table-hover">
    <thead>
        <tr> <th scope="col">Email</th> </tr>
    </thead>
    <tbody>
        <!-- 
          Les tableau Bootstrap doivent respecter la hierarchie table > tr > td 
          donc il faut indiquer au tr de se comportant EN TANT QUE users-list-item
          en modifiant le selector du component dans le controller .ts pour [app-user-list-item]
          et ensuite ajouter cet attribut sur le tr
        -->
        <tr *ngFor="let user of users" app-users-list-item [user]="user" (userClicked)="userClicked($event)"></tr>
    </tbody>
</table>

Users list item affiche un élément de la liste reçu via un input

  • Un bouton offre l'événement click
  • Le click est propagé à la liste qui affiche la valeur sélectionnée dans une alert
<td>
    <button (click)="clicked()" type="button" class="btn btn-sm btn-warning">Click!</button>
    {{ user.email }}
</td>
Résultat
Route vers /profile
src/app/app.routes.ts CHANGED
@@ -1,5 +1,6 @@ import { Routes } from "@angular/router";
1
1
  import { LoginComponent } from "./components/login/login.component";
2
+ import { ProfileComponent } from "./components/profile/profile.component";
2
3
  import { RecipesComponent } from "./components/recipes/recipes.component";
3
4
  import { SignupComponent } from "./components/signup/signup.component";
4
5
 
@@ -7,4 +8,5 @@ export const ROUTES: Routes = [ { path: '', component: LoginComponent },
7
8
  { path: 'signup', component: SignupComponent },
8
9
  { path: 'recipes', component: RecipesComponent },
10
+ { path: 'profile', component: ProfileComponent },
9
11
  ];
src/app/components/app/app.component.html CHANGED
@@ -14,7 +14,7 @@ </li>
14
14
  </ul>
15
15
 
16
- <a href="#" class="text-light mr-4">Profile</a>
16
+ <a [routerLink]="['/profile']" class="text-light mr-4">Profile</a>
17
17
  <button (click)="logOut()" type="button" class="btn btn-outline-light">Log out</button>
18
18
  </div>
19
19
  </nav>
Profile
src/app/components/profile/profile.component.html CHANGED
@@ -0,0 +1,9 @@+<div class="container">
1
+ <h1>Current user</h1>
2
+
3
+ {{ currentUser | json }}
4
+
5
+ <div class="mt-4">
6
+ <app-users-list></app-users-list>
7
+ </div>
8
+ </div>
src/app/components/profile/profile.component.ts CHANGED
@@ -0,0 +1,20 @@+import { Component, OnInit } from '@angular/core';
1
+ import { AuthService } from 'src/app/services/auth.service';
2
+
3
+ @Component({
4
+ selector: 'app-profile',
5
+ templateUrl: './profile.component.html',
6
+ styleUrls: ['./profile.component.css']
7
+ })
8
+ export class ProfileComponent implements OnInit {
9
+
10
+ get currentUser() {
11
+ return this.authService.currentUser;
12
+ }
13
+
14
+ constructor(private authService: AuthService) { }
15
+
16
+ ngOnInit(): void {
17
+ }
18
+
19
+ }
Users list
src/app/components/users-list/users-list.component.html CHANGED
@@ -0,0 +1,19 @@+<h2>All users</h2>
1
+
2
+ <div *ngIf="users.length == 1; else showTable">
3
+ You are the only user!
4
+ </div>
5
+ <ng-template #showTable>
6
+ <!-- https://getbootstrap.com/docs/4.5/content/tables/#hoverable-rows -->
7
+
8
+ <table class="table table-hover">
9
+ <thead>
10
+ <tr>
11
+ <th scope="col">Email</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <tr *ngFor="let user of users" app-users-list-item [user]="user" (userClicked)="userClicked($event)"></tr>
16
+ </tbody>
17
+ </table>
18
+ </ng-template>
src/app/components/users-list/users-list.component.ts CHANGED
@@ -0,0 +1,23 @@+import { Component, OnInit } from '@angular/core';
1
+ import { User } from 'src/app/models/user.model';
2
+ import { AuthService } from 'src/app/services/auth.service';
3
+
4
+ @Component({
5
+ selector: 'app-users-list',
6
+ templateUrl: './users-list.component.html',
7
+ styleUrls: ['./users-list.component.css']
8
+ })
9
+ export class UsersListComponent implements OnInit {
10
+
11
+ get users(): User[] {
12
+ return this.authService.users;
13
+ }
14
+ constructor(private authService: AuthService) { }
15
+
16
+ ngOnInit(): void {
17
+ }
18
+
19
+ userClicked(userEmail: string) {
20
+ alert(userEmail);
21
+ }
22
+ }
Users list item
src/app/components/users-list-item/users-list-item.component.html CHANGED
@@ -0,0 +1,4 @@+<td>
1
+ <button (click)="clicked()" type="button" class="btn btn-sm btn-warning">Click!</button>
2
+ {{ user.email }}
3
+ </td>
src/app/components/users-list-item/users-list-item.component.ts CHANGED
@@ -0,0 +1,22 @@+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
1
+ import { User } from 'src/app/models/user.model';
2
+
3
+ @Component({
4
+ selector: '[app-users-list-item]',
5
+ templateUrl: './users-list-item.component.html',
6
+ styleUrls: ['./users-list-item.component.css']
7
+ })
8
+ export class UsersListItemComponent implements OnInit {
9
+
10
+ @Input() user!: User;
11
+ @Output() userClicked = new EventEmitter();
12
+
13
+ constructor() { }
14
+
15
+ ngOnInit(): void {
16
+ }
17
+
18
+ clicked() {
19
+ this.userClicked.emit(this.user.email);
20
+ }
21
+ }