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 {...}
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.)
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"
]
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>
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:
+------------------------------------------------------+
|------------------------------------------------------+
|| 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.
Générer les pages login
et signup
dans le sous-dossier src/app/components
app.component.html
src/app/components
📚 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
.
Pour avoir un rendu visuel plus intéressant, intégrer le code suivant dans vos components
<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>
<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>
<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>
'' -> LoginComponent
et 'signup' -> SignupComponent
app.routes.ts
app.component.html
affichera le bon component selon la route
@@ -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
|
+
];
|
@@ -1,10 +1,9 @@ import { NgModule } from '@angular/core';
|
|
1
|
-
import {
|
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(
|
5
|
+
imports: [RouterModule.forRoot(ROUTES)],
|
7
6
|
exports: [RouterModule]
|
8
7
|
})
|
9
8
|
export class AppRoutingModule { }
|
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>
Il existe 2 stratégies pour manipuler les formulaires
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 );
@@ -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>
|
@@ -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
|
-
|
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
|
}
|
@@ -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>
|
@@ -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
|
-
|
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
|
}
|
A la création du FormControl ou du FormGroup, on peut préciser les validations à appliquer
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>
<!--
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
}
}
@@ -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
|
-
|
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
|
-
|
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>
|
@@ -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:
|
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
|
-
|
26
|
+
console.log(this.loginForm.value);
|
24
27
|
}
|
25
28
|
}
|
@@ -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
|
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
|
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
|
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>
|
@@ -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 {
|
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
}
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']);
}
}
user.model.ts
et user-credentials.ts
dans le sous-dossier models
à partir du code fourniAuthService
dans le sous-dossier services et intégrer le code fourni ci-dessousCode
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;
}
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;
}
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
AuthService
pour implémenter la fonctionnalité de connexion de l'application
*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
Sur un login/sign up valide, rediriger vers la route /recipes
qui presente le composant recipes
@@ -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",
|
@@ -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
|
];
|
@@ -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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
31
|
+
<button [disabled]="loginForm.invalid" class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
|
26
32
|
|
27
|
-
|
28
|
-
|
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>
|
@@ -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
|
-
|
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
|
}
|
@@ -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
|
|
@@ -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
|
-
|
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 {
|
@@ -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>
|
@@ -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
|
-
|
10
|
+
|
11
|
+
constructor(private authService: AuthService, private router: Router) { }
|
12
|
+
|
13
|
+
logOut() {
|
14
|
+
this.authService.logOut();
|
15
|
+
|
16
|
+
this.router.navigate(['/']);
|
17
|
+
}
|
9
18
|
}
|
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!
/profile
/profile
via la barre de navigationLe 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
Users list affiche un tableau via *ngFor.
*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
<td>
<button (click)="clicked()" type="button" class="btn btn-sm btn-warning">Click!</button>
{{ user.email }}
</td>
@@ -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
|
];
|
@@ -14,7 +14,7 @@ </li>
|
|
14
14
|
</ul>
|
15
15
|
|
16
|
-
<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>
|
@@ -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>
|
@@ -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
|
+
}
|
@@ -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>
|
@@ -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
|
+
}
|
@@ -0,0 +1,4 @@+<td>
|
|
1
|
+
<button (click)="clicked()" type="button" class="btn btn-sm btn-warning">Click!</button>
|
2
|
+
{{ user.email }}
|
3
|
+
</td>
|
@@ -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
|
+
}
|