Lors d'une navigation, on peut fournir des paramètres à la route qui seront accessibles dans le component de destination.
Il existe 2 types de paramètres :
/recipes/2
/recipes?search=abc
/recipes/favorites?search=abc
Habituellement, la route identifie une ressource, tandis que la query string ajoute un contexte supplémentaire.
// Parameterized
{ path: 'profile/:email', component: ProfileDetailComponent }
// Query, pas de changement
{ path: 'profile', component: ProfileComponent }
Pour fournir les paramètres, on ajoute les données au routerLink ou router.navigate()
// Parameterized
['/profile', 'a@a']
// ou
'/profile/a@a'
// Query
// dans le component
router.navigate(['/profile'], {queryParams: {email: 'a@a'}});
// ou le HTML
[routerLink]="['/profile']" [queryParams]="{email: 'a@a'}"
// On peut sans problème utiliser une EXPRESSION pour alimenter les paramètres
Dans le component, on peut récupérer les paramètres de la route
// On doit injecter la route
constructor(private route: ActivatedRoute) { }
// Puis attendre que notre component soit initialisé
// pour acceder aux données
// Generalement, on reserve l 'initialisation dans le constructeur
// et le traitement de données via Angular dans le ngOnInit
ngOnInit(): void {
const email = this.route.snapshot.paramMap.get('email');
console.log(email);
}
Le snapshot de la route est statique est représente la valeur des parametres lors de la navigation initiale, si on modifie les paramètres sans déclencher de navigation(exemple, un paramètre pour un terme de recherche, filtre, tri qui appelle la route actuelle va réutiliser le component) il faut utiliser une méthode asynchrone pour recevoir la valeur des paramètres.
// meme principe pour paramMap ou queryParamMap
this.route.paramMap.subscribe(params => {
const asyncEmail = params.get('email');
console.log(asyncEmail);
});
Pour restreindre la navigation dans l'application, on peut utiliser le mécanisme de guard et décider si on autorise l'accès à une route ou non.
2 types de guard nous intéressent particulièrement
canActivate
, s'applique sur une seule routecanActivateChild
, s'applique sur une route et la navigation entre les sous-routes. On peut donc regrouper plusieurs routes pour leurs appliquer une validation communeng g guard guards/auth --skip-tests
{ path: 'simple', component: MyComponent, canActivate: [MyGuard] },
{ path: '', canActivateChild: [MyChildGuard], children: [
{ path: 'route', component: MyRouteComponent },
{ path: 'other', component: MyOtherComponent },
{ path: 'another/:email', component: AnotherComponent },
]}
Dans le guard, on valide ensuite si on autorise ou pas la navigation
export class MyGuard implements CanActivateChild {
// On peut injecter des dependances au besoin
constructor(private router: Router) { }
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
// Si on retourne true, la navigation a lieu
if (condition) {
return true;
} else {
// sinon, on peut fournir un URLTree pour rediriger l'utilisateur
return this.router.parseUrl('/route/...');
}
}
}
À partir du profil, qui affiche la liste des users, le clic sur un item du tableau doit naviguer vers la page profile-detail qui affiche un utilisateur précis via un paramètre de route.
AuthService
expose findUser()Autoriser l'accès aux recettes et profil si l'utilisateur est connecté, sinon rediriger au login
AuthService
offre la propriété isLoggedInguard
pour utiliser l'injection de dependanceLe client HTTP facilite la gestion des requêtes HTTP par notre application(verbe, paramètres, données, headers, transformations, interception).
On ajoute le module dans app.module.ts
import { HttpClientModule } from "@angular/common/http";
//...
imports: [
BrowserModule,
ReactiveFormsModule,
AppRoutingModule
HttpClientModule, // Apres BrowserModule
],
//...
Puis, via l'injection de dépendance, on peut récupérer le client HTTP et effectuer des requêtes
constructor(private http: HttpClient) { }
// ...
this.http.post('http://url.com').subscribe(response => {
console.log(response);
})
// body est un objet JSON à envoyer par la requête
this.http.post('http://url.com', body, { headers: {'auth' : window.btoa('user:pass')}}).subscribe(response => {
console.log(response);
})
// Pour utiliser encodage base64, il faut faire croire a TypeScript que l'objet global window existe
declare const window: any
ATTENTION Une requête HTTP n'est lancée que si on lui subscribe!
Si on veut capturer les requêtes OU réponses HTTP globalement dans l'application, il est possible d'appliquer un filtre(un peu comme les guard du routeur) pour valider et traiter les données avant que le processus ne se poursuive.
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.
+------------+ +-------------+
| | notifie | |
| Observer +<--<--<--<--<--+ Observable |
| | (pipe) | |
+-----+------+ +------+------+
| ^
| |
+-----------------------------+
subscribes
Il est possible d'interagir avec les notifications via les opérateurs RxJS.
📚 RxJS
👩 Martha
💾 dump.sql
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { InsertResult } from '../models/insert-result.model';
declare const window: any
@Injectable({
providedIn: 'root'
})
export class MarthaRequestService {
private readonly username = 'abc';
private readonly password = 'xyz';
constructor(private http: HttpClient) { }
private get headers() {
return { headers: {'auth' : window.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[] | false | null> {
return this.http.post(this.getUrl(query), body, this.headers).pipe(
map((response: any) => {
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(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);
})
);
}
}
import {JSONObject, optional, required} from 'ts-json-object'
export class InsertResult extends JSONObject {
@required
success!: boolean;
@optional
lastInsertId!: number;
}
@@ -1,6 +1,7 @@ import { BrowserModule } from '@angular/platform-browser';
|
|
1
1
|
import { NgModule } from '@angular/core';
|
2
2
|
import { ReactiveFormsModule } from '@angular/forms';
|
3
|
+
import { HttpClientModule } from "@angular/common/http";
|
3
4
|
|
4
5
|
import { AppRoutingModule } from './app-routing.module';
|
5
6
|
import { AppComponent } from './components/app/app.component';
|
@@ -26,7 +27,8 @@ import { ProfileDetailsComponent } from './components/profile-details/profile-de imports: [
|
|
26
27
|
BrowserModule,
|
27
28
|
ReactiveFormsModule,
|
28
|
-
AppRoutingModule
|
29
|
+
AppRoutingModule,
|
30
|
+
HttpClientModule
|
29
31
|
],
|
30
32
|
providers: [],
|
31
33
|
bootstrap: [AppComponent]
|
@@ -1,6 +1,9 @@ import { Injectable } from '@angular/core';
|
|
1
1
|
import { User } from '../models/user.model';
|
2
2
|
import { UserCredentials } from '../models/user-credentials.model';
|
3
|
+
import { MarthaRequestService } from './martha-request.service';
|
4
|
+
import { map } from 'rxjs/operators';
|
5
|
+
import { Observable } from 'rxjs';
|
3
6
|
|
4
7
|
@Injectable({
|
5
8
|
providedIn: 'root'
|
@@ -36,7 +39,7 @@ export class AuthService { return usersCredentials;
|
|
36
39
|
}
|
37
40
|
|
38
|
-
constructor() {
|
41
|
+
constructor(private martha : MarthaRequestService) {
|
39
42
|
const storedCurrentUser = JSON.parse(localStorage.getItem(this.CURRENT_USER_KEY) ?? 'null');
|
40
43
|
|
41
44
|
if (storedCurrentUser) {
|
@@ -56,31 +59,37 @@ export class AuthService { );
|
|
56
59
|
}
|
57
60
|
|
58
|
-
logIn(credentials: UserCredentials): boolean {
|
59
|
-
const validUser = this.usersCredentials.find(userCredentials =>
|
60
|
-
userCredentials.email == credentials.email && userCredentials.password == credentials.password
|
61
|
-
);
|
61
|
+
logIn(credentials: UserCredentials): Observable<boolean> {
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
63
|
+
return this.martha.select('users-login', credentials).pipe(
|
64
|
+
map(data => {
|
65
|
+
console.log('Auth service', data);
|
66
66
|
|
67
|
-
|
68
|
-
|
67
|
+
if (data.length == 1) {
|
68
|
+
this.setCurrentUser(new User(data[0]));
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
70
|
+
return true;
|
71
|
+
} else {
|
72
|
+
return false;
|
73
|
+
}
|
74
|
+
})
|
75
|
+
);
|
76
|
+
}
|
75
77
|
|
76
|
-
|
77
|
-
|
78
|
+
signUp(credentials: UserCredentials): Observable<boolean> {
|
79
|
+
return this.martha.insert('users-signup', credentials).pipe(
|
80
|
+
map(result => {
|
81
|
+
console.log('Auth service', result);
|
78
82
|
|
79
|
-
|
83
|
+
if (result?.success) {
|
84
|
+
this.setCurrentUser(new User(credentials));
|
80
85
|
|
81
|
-
|
82
|
-
|
86
|
+
return true;
|
87
|
+
} else {
|
88
|
+
return false;
|
89
|
+
}
|
90
|
+
})
|
91
|
+
);
|
83
92
|
}
|
84
93
|
|
85
94
|
logOut() {
|
@@ -28,10 +28,15 @@ export class LoginComponent implements OnInit { }
|
|
28
28
|
|
29
29
|
logIn() {
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
|
31
|
+
this.authService.logIn(this.loginForm.value).subscribe(success => {
|
32
|
+
console.log('Login component', success);
|
33
|
+
|
34
|
+
if (success) {
|
35
|
+
this.router.navigate(['/recipes']);
|
36
|
+
} else {
|
37
|
+
this.authError = "Invalid credentials!";
|
38
|
+
}
|
39
|
+
});
|
35
40
|
}
|
36
41
|
}
|
@@ -34,11 +34,15 @@ export class SignupComponent implements OnInit { }
|
|
34
34
|
|
35
35
|
signUp() {
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
this.authService.signUp(new UserCredentials(this.signupForm.value)).subscribe(success => {
|
37
|
+
console.log('Sign up component', success);
|
38
|
+
|
39
|
+
if (success) {
|
40
|
+
this.router.navigate(['/recipes']);
|
41
|
+
} else {
|
42
|
+
this.setUniqueEmailError(true);
|
43
|
+
}
|
44
|
+
});
|
41
45
|
}
|
42
46
|
|
43
47
|
onEmailInput() {
|
📚 i18n
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.
npm install @ngx-translate/core @ngx-translate/http-loader
ng build # --c production
# Voir defaultConfiguration dans angular.json
Le flag produit la version optimisée de notre application de le dossier dist
à la racine du projet(par défaut). Ces fichiers 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 projects > NOM_PROJET > architect > build > options
de angular.json pour indiquer à Angular de les ignorer.
"allowedCommonJsDependencies": [
"ts-json-object"
],
npx http-server