Pour favoriser la séparation des responsabilités, les composants doivent être en mesure de communiquer entre eux de façon bi-directionnelle: Input et Output.
👉 Gérer la navigation vers la page détail d'un bâtiment dans le Home
: LocationComponent -- (output) --> Home
import {Component, inject} from '@angular/core';
import {Router, ActivatedRoute} from '@angular/router';
import {LocationComponent} from './location.component';
import {Location} from 'models/location.interface';
@Component({
selector: 'app-home',
imports: [
LocationComponent
],
template: `
...
<app-location [location]="location" (detailsClick)="handleLocationClicked($event)"></app-location>
...
`,
styles: ` ... `
})
export class HomePage {
...
handleLocationClicked(id: number) {
console.log({ id })
this.router.navigate(['/details', id])
}
}
import {Component, Input, output} from '@angular/core';
import {CommonModule} from '@angular/common';
import {Location} from 'models/location.interface';
@Component({
selector: 'app-location',
imports: [
CommonModule, RouterModule,
],
template: `
...
<button mat-raised-button (click)="handleClick()">Details</button>
...
`,
styles: `...`
})
export class LocationComponent {
@Input() location!: Location
detailsClick = output<number>()
handleClick() {
this.detailsClick.emit(this.location.id)
}
}
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.
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 -->
@if(form.submitted) {
...
}
</form>
En utilisant Angular Material, plusieurs mécanisme encadrent l'affichage des erreurs
👉 Ajouter quelques validations sur le formulaire d'inscription
import {Component, inject} from '@angular/core';
import {JsonPipe} from '@angular/common';
import {RouterModule, Router} from '@angular/router';
import {FormControl, FormGroup, FormGroupDirective, NgForm, AbstractControl, ReactiveFormsModule, Validators, ValidationErrors} from '@angular/forms';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatButtonModule} from '@angular/material/button';
import {ErrorStateMatcher} from '@angular/material/core';
@Component({
selector: 'app-signup',
imports: [
RouterModule, ReactiveFormsModule, JsonPipe,
MatFormFieldModule, MatInputModule, MatIconModule, MatButtonModule
],
template: `
<form [formGroup]="signupForm" (submit)="handleSubmit()">
<mat-form-field appearance="outline">
<mat-label>Username</mat-label>
<input formControlName="username" matInput>
@if(usernameControl.hasError('required')) {
<mat-error>Please enter a username</mat-error>
}
@else if(usernameControl.hasError('email')) {
<mat-error>Username must be an email</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Password</mat-label>
<input formControlName="password" matInput type="password">
<mat-error>Please enter a password</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Password confirmation</mat-label>
<input formControlName="passwordConfirmation" [errorStateMatcher]="confirmationMatcher" matInput type="password">
@if(passwordConfirmationControl.hasError('required')) {
<mat-error>Please enter a password confirmation</mat-error>
}
@else if(signupForm.hasError('passwordConfirmationMustMatch')) {
<mat-error>Password confirmation must match password</mat-error>
}
</mat-form-field>
<div id="buttons">
<button mat-flat-button>Sign up</button>
<a mat-button routerLink="/login">Cancel</a>
</div>
</form>
<pre>
{{
{
username: signupForm.get('username')?.errors,
password: signupForm.get('password')?.errors,
passwordConfirmation: signupForm.get('passwordConfirmation')?.errors,
signupForm: signupForm.errors,
signupFormValid: signupForm.valid
} | json
}}
</pre>
`,
styles: ` ... `
})
export class SignupPage {
private readonly router = inject(Router)
passwordControl = new FormControl('', [Validators.required])
passwordConfirmationControl = new FormControl('', [Validators.required])
signupForm = new FormGroup({
username: new FormControl('', [Validators.required, Validators.email]),
password: this.passwordControl,
passwordConfirmation: this.passwordConfirmationControl
}, [this.passwordMatch])
confirmationMatcher = new ConfirmationMatcher()
private passwordMatch(form: AbstractControl): ValidationErrors | null {
if (form.value?.password != form.value?.passwordConfirmation) {
return { passwordConfirmationMustMatch: true };
} else {
return null
}
}
get usernameControl(): FormControl {
return this.signupForm.get('username') as FormControl
}
handleSubmit() {
if (this.signupForm.valid) {
this.router.navigate(['/'])
}
}
}
class ConfirmationMatcher implements ErrorStateMatcher {
// Pour lier l'affichage de l'erreur sur le FORM avec le CONTROL
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(
control
&& (control.invalid || form?.hasError('passwordConfirmationMustMatch')) // FORM
&& (control.dirty || control.touched || isSubmitted)
);
}
}
MVC?
👉 Importer les fichiers modèles dans votre projet
export class InsertResult {
readonly success: boolean
readonly lastInsertId: number
constructor(value: InsertResult) {
this.success = value.success
this.lastInsertId = value.lastInsertId
}
}
export class User {
constructor(
public email: string
) { }
}
export class UserCredentials {
readonly email: string
readonly password: string
constructor(value: { username: string, password: string }) {
this.email = value.username.toLowerCase()
this.password = value.password
}
}
http://martha.jh.shawinigan.info
create table users (
email varchar(50) not null,
passwd varchar(64) not null,
primary key (email)
);
insert into users(email, passwd)
values
("a@a", sha2('aaa', 256)),
("b@b", sha2('bbb', 256)),
("c@c", sha2('ccc', 256));
[
{
"name": "users-login",
"string": "select email from users where email = '?email' && passwd = sha2('?password', 256);",
"type": 0
},
{
"name": "users-signup",
"string": "insert into users(email, passwd) values ('?email', sha2('?password', 256));",
"type": 1
}
]
Les Observables sont utilisés pour la gestion de plusieurs tâches traitées de façon asynchrone par Angular(HTTP, Routing, Forms). Il est possible de créer ses propres Observables pour communiquer des données entre différents éléments de notre application.
ATTENTION Une requête HTTP n'est lancée que si on lui subscribe!
+------------+ +-------------+
| | notifie | |
| Observer +<--<--<--<--<--+ Observable |
| | (pipe) | |
+-----+------+ +------+------+
| ^
| |
+-----------------------------+
subscribes
Si on veut capturer les requêtes OU réponses HTTP globalement dans l'application, il est possible d'appliquer un filtre/middleware(un peu comme les guard du routeur) pour valider et traiter les données avant que le processus ne se poursuive.
👉 Implémenter la connexion et l'inscription via Martha
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {map, catchError, tap} from 'rxjs/operators';
import {InsertResult} from '../models/insert-result.model';
@Injectable({
providedIn: 'root'
})
export class MarthaRequestService {
private readonly username = 'USERNAME';
private readonly password = 'PASSWORD';
constructor(private http: HttpClient) { }
private get headers() {
return { headers: {'auth' : btoa(`${this.username}:${this.password}`)}};
}
private getUrl(query: string) {
return `http://martha.jh.shawinigan.info/queries/${query}/execute`;
}
select(query: string, body: any = null): Observable<any | null> {
return this.http.post<any>(this.getUrl(query), body, this.headers).pipe(
map(response => {
console.log('Martha select', response);
if (response.success) {
return response.data;
} else {
return false;
}
}),
catchError(error => {
console.log('Error', error);
return of(null);
})
);
}
insert(query: string, body: any = null): Observable<InsertResult | null> {
return this.http.post<any>(this.getUrl(query), body, this.headers).pipe(
map(result => {
console.log('Martha insert', result);
return new InsertResult(result);
}),
catchError(error => {
console.log('Error', error);
return of(null);
})
);
}
statement(query: string, body: any = null): Observable<boolean | null> {
return this.http.post<any>(this.getUrl(query), body, this.headers).pipe(
tap(response => console.log('Martha statement', response)),
map(result => result.success ?? false),
catchError(error => {
console.log('Error', error);
return of(null);
})
);
}
}
import {Injectable} from '@angular/core';
import {User} from 'models/user.model';
import {UserCredentials} from 'models/user-credentials.model';
import {MarthaRequestService} from 'services/martha-request.service';
import {map} from 'rxjs/operators';
import {Observable} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private readonly CURRENT_USER_KEY = 'recipeasy.currentUser';
private _currentUser : User | null = null;
get currentUser(): User | null {
return this._currentUser;
}
get isLoggedIn(): boolean {
return !!this._currentUser;
}
constructor(private martha : MarthaRequestService) {
const storedCurrentUser = JSON.parse(localStorage.getItem(this.CURRENT_USER_KEY) ?? 'null');
if (storedCurrentUser) {
this._currentUser = new User(storedCurrentUser.email);
}
}
private setCurrentUser(user: User | null) {
this._currentUser = user;
localStorage.setItem(this.CURRENT_USER_KEY, JSON.stringify(user));
}
logIn(credentials: UserCredentials): Observable<boolean> {
return this.martha.select('users-login', credentials).pipe(
map(data => {
if (data.length == 1) {
this.setCurrentUser(new User(data[0].email));
return true;
} else {
return false;
}
})
);
}
signUp(credentials: UserCredentials): Observable<boolean> {
console.log({ credentials })
return this.martha.insert('users-signup', credentials).pipe(
map(result => {
if (result?.success) {
this.setCurrentUser(new User(credentials.email));
return true;
} else {
return false;
}
})
);
}
logOut() {
this.setCurrentUser(null);
}
}
import {Component, inject} from '@angular/core';
import {JsonPipe} from '@angular/common';
import {RouterModule, Router} from '@angular/router';
import {FormControl, FormGroup, FormGroupDirective, NgForm, AbstractControl, ReactiveFormsModule, Validators, ValidationErrors} from '@angular/forms';
import {AuthService} from 'services/auth.service';
import {UserCredentials} from 'models/user-credentials.model';
@Component({
selector: 'app-signup',
imports: [
RouterModule, ReactiveFormsModule, JsonPipe,
MatFormFieldModule, MatInputModule, MatIconModule, MatButtonModule
],
template: `
<form [formGroup]="signupForm" (submit)="handleSubmit()">
@if (error) {
<div id="error">
{{ error }}
</div>
}
<mat-form-field appearance="outline">
<mat-label>Username</mat-label>
<input formControlName="username" matInput>
@if(usernameControl.hasError('required')) {
<mat-error>Please enter a username</mat-error>
}
@else if(usernameControl.hasError('email')) {
<mat-error>Username must be an email</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Password</mat-label>
<input formControlName="password" matInput type="password">
<mat-error>Please enter a password</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Password confirmation</mat-label>
<input formControlName="passwordConfirmation" [errorStateMatcher]="confirmationMatcher" matInput type="password">
@if(passwordConfirmationControl.hasError('required')) {
<mat-error>Please enter a password confirmation</mat-error>
}
@else if(signupForm.hasError('passwordConfirmationMustMatch')) {
<mat-error>Password confirmation must match password</mat-error>
}
</mat-form-field>
<div id="buttons">
<button mat-flat-button>Sign up</button>
<a mat-button routerLink="/login">Cancel</a>
</div>
</form>
<pre>
{{
{
username: signupForm.get('username')?.errors,
password: signupForm.get('password')?.errors,
passwordConfirmation: signupForm.get('passwordConfirmation')?.errors,
signupForm: signupForm.errors,
signupFormValid: signupForm.valid
} | json
}}
</pre>
`,
styles: `
#error {
background-color: pink;
border-radius: 4px;
padding: 8px;
margin-bottom: 16px;
color: darkred;
}
`
})
export class SignupPage {
private readonly router = inject(Router)
private readonly auth = inject(AuthService)
error: string | null = null
passwordControl = new FormControl('', [Validators.required])
passwordConfirmationControl = new FormControl('', [Validators.required])
signupForm = new FormGroup({
username: new FormControl('', [Validators.required, Validators.email]),
password: this.passwordControl,
passwordConfirmation: this.passwordConfirmationControl
}, [this.passwordMatch])
confirmationMatcher = new ConfirmationMatcher()
private passwordMatch(form: AbstractControl): ValidationErrors | null {
if (form.value?.password != form.value?.passwordConfirmation) {
return { passwordConfirmationMustMatch: true };
} else {
return null
}
}
get usernameControl(): FormControl {
return this.signupForm.get('username') as FormControl
}
handleSubmit() {
if (this.signupForm.valid) {
console.log(this.signupForm.value)
const { username, password } = this.signupForm.value
const credentials = new UserCredentials({ username: username!, password: password! })
this.auth.signUp(credentials).subscribe( success => {
if (success) {
this.router.navigate(['/'])
}
else {
this.error = 'Could not create account'
}
})
}
}
}
class ConfirmationMatcher implements ErrorStateMatcher {
// Pour lier l'affichage de l'erreur sur le FORM avec le CONTROL
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(
control
&& (control.invalid || form?.hasError('passwordConfirmationMustMatch'))
&& (control.dirty || control.touched || isSubmitted)
);
}
}
import { Component, inject } from '@angular/core';
import { RouterModule, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import {AuthService} from 'services/auth.service'
import {UserCredentials} from 'models/user-credentials.model'
@Component({
selector: 'app-login',
imports: [
RouterModule, FormsModule,
MatFormFieldModule, MatInputModule, MatIconModule, MatButtonModule
],
template: `
<form (submit)="handleSubmit(usernameInput.value, passwordInput.value)">
@if (error) {
<div id="error">
{{ error }}
</div>
}
<mat-form-field appearance="outline">
<mat-label>Username</mat-label>
<input #usernameInput matInput>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Password</mat-label>
<input #passwordInput matInput type="password">
</mat-form-field>
<div id="buttons">
<button mat-flat-button>Log in</button>
<a mat-button routerLink="/signup">Create an account!</a>
</div>
</form>
`,
styles: `
#error {
background-color: pink;
border-radius: 4px;
padding: 8px;
margin-bottom: 16px;
color: darkred;
}
`
})
export class LoginPage {
private readonly router = inject(Router)
private readonly auth = inject(AuthService)
error: string | null = null
handleSubmit(username: string, password: string) {
const credentials = new UserCredentials({ username, password })
this.auth.logIn(credentials).subscribe( success => {
if (success) {
this.router.navigate(['/'])
}
else {
this.error = 'Invalid credentials'
}
})
}
}
import {Component, inject} from '@angular/core';
import {RouterModule} from '@angular/router';
import {AuthService} from 'services/auth.service';
@Component({
selector: 'app-root',
imports: [
RouterModule,
MatToolbarModule, MatIconModule
],
template: `
<mat-toolbar>
<a routerLink="/" id="home">
<mat-icon>home</mat-icon>
<span>Housing</span>
</a>
<span class="spacer"></span>
@if (auth.isLoggedIn) {
<a routerLink="/" (click)="handleLogOut()">Log out</a>
}
@else {
<a routerLink="/login">Log in</a>
}
</mat-toolbar>
...
`,
styles: ` ... `
})
export class AppComponent {
auth = inject(AuthService)
handleLogOut() {
this.auth.logOut()
}
}
Le mécanisme d'internationalisation d'Angular est très complet mais également imposant à mettre en place. Plus particulièrement, il demande de déployer une version de l'application pour chaque langue et rend difficile de changer la langue dynamiquement à l'exécution.
La librairie ngx-translate nous permets de supporter facilement plusieurs langues dans notre application.
ng build # --c production
Les fichiers générés sont des artéfacts statiques pouvant être servis par n'importe quel serveur web, assurez-vous de rediriger toutes les requêtes qui auraient menée à un 404 Not Found vers le fichiers index.html de votre application pour gérer les routes via le code client Angular(ex. avec Apache).
Il est possible d'utiliser des variables d'environnement qui seront différentes en développement vs en production(ex: URL de serveur, log de debug);
Certaines librairie, comme ts-json-object
, ne sont pas optimisées pour le processus de build Angular. Il faut ajouter une exception dans la configuration du projet dans angular.json pour indiquer à Angular de les ignorer.
"allowedCommonJsDependencies": [
"ts-json-object"
],