Pour la partie TP du module programmation frontend web, nous développerons un frontend d'une application web, basé sur le
framework Angular
.
Angular
est écrit en Typescript et permet un grand nombre d'automatisations et procure un ensemble d'outils qui rend le
développement d'un frontend agréable et simple.
Il vous faut installer NodeJS
pour pouvoir installer et utiliser Angular
.
Suivez la procédure en fonction de votre système d'exploitation.
L'IDE à utiliser est VisualCode Studio. Une fois installé, vous pouvez le lancer depuis un terminal en vous plaçant dans le répertoire de votre projet et en tapant la commande :
code .
Afin de tester le frontend, il faudra ouvrir un navigateur web. Vous pouvez utiliser la fonction console.log(message);
pour afficher des messages, qui seront visibles dans la console de votre navigateur.
Suivant le navigateur que vous utilisez, l'accès à cette console peut différer : voir plus d'info ici.
$ ng new fr-administration-front
? Do you want to enforce stricter type checking and stricter bundle budgets in the workspace?
This setting helps improve maintainability and catch bugs ahead of time.
For more information, see https://angular.io/strict (y/N)
y
y
SCSS
S'il y a une erreur liée à jasmine
, il suffit de lancer la commande : npm install -g npm@6
, puis de refaire la génération avec la commande ci-dessus.
Maintenant, on peut lancer le front :
$ ng serve --open
avec l'option --open
, un browser se lancera et se connectera au front (par défault l'url est : http://localhost:4200
).
Dans cet exercice, nous allons créer une page dédiée à une liste d'utilisateurs (user
) en utilisant Materials.
Materials est une bibliothèque qui fournit un ensemble de composants Angular.
Pour créer un nouveau composant, on utilise la commande suivante :
$ ng g component users-list
CREATE src/app/users-list/users-list.component.scss (0 bytes)
CREATE src/app/users-list/users-list.component.html (25 bytes)
CREATE src/app/users-list/users-list.component.spec.ts (648 bytes)
CREATE src/app/users-list/users-list.component.ts (291 bytes)
UPDATE src/app/app.module.ts (647 bytes)
Ici, Angular nous génère plusieurs fichiers dans le répertoire src/app/users-list
dont
src/app/users-list/users-list.component.html
qui va contenir la structure du composant Angular ;src/app/users-list/users-list.component.ts
qui va contenir le comportement du composant Angular.
L'un ne va pas sans l'autre.
Remarquez que Angular a aussi mis à jour le fichier src/app/app.module.ts
avec un nouvel import.
On va maintenant ajouter une nouvelle route pour associer une URL à notre nouveau composant Angular :
Dans src/app/app-routing.module.ts
:
const routes: Routes = [
+ {
+ path: '',
+ component: UsersListComponent,
+ },
];
On relance le front avec ng serve
et on observe notre page, en bas nous devrions voir users-list works!
qui est du texte qui a été généré en même temps que le composant.
Éditez app.component.html
pour qu'il ne contienne que la dernière ligne : <router-outlet></router-outlet>
On devrait voir que "users-list works!" maintenant sur une page blanche.
Nous allons installer Materials
:
$ ng add @angular/material
Installing packages for tooling via npm.
Installed packages for tooling via npm.
? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink [ Preview: https://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? No
? Set up browser animations for Angular Material? Yes
UPDATE package.json (1281 bytes)
✔ Packages installed successfully.
UPDATE src/app/app.module.ts (756 bytes)
UPDATE angular.json (4005 bytes)
UPDATE src/index.html (566 bytes)
UPDATE src/styles.scss (181 bytes)
Dans src/app/app.module.ts
, ajouter le MatTableModule
aux imports :
+import {MatTableModule} from '@angular/material/table';
...
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
+ MatTableModule
],
...
S'il y a un problème, relancez le frontend avec ng serve --open
.
Pour simuler un backend, nous allons créer, en dur dans le front, un tableau avec des instances de user
.
Ajoutez dans le fichier src/app/users-list/users-list.component.ts
le code suivant :
export class User {
constructor(
public id: number,
public lastname: string,
public firstname: string,
public age: number,
) {}
}
const users: User[] = [
new User(0, 'Doe', 'John', 23),
new User(1, 'Doe', 'Jane', 32),
]
On va ajouter un composant mat-table
dans src/app/users-list/users-list.component.html
:
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> id </th>
<td mat-cell *matCellDef="let user"> {{user.id}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> Lastname </th>
<td mat-cell *matCellDef="let user"> {{user.lastname}} </td>
</ng-container>
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef> Firstname </th>
<td mat-cell *matCellDef="let user"> {{user.firstname}} </td>
</ng-container>
<ng-container matColumnDef="age">
<th mat-header-cell *matHeaderCellDef> Age </th>
<td mat-cell *matCellDef="let user"> {{user.age}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
et dans src/app/users-list/users-list.component.ts
export class UsersListComponent implements OnInit {
+ displayedColumns: string[] = ['id', 'lastname', 'firstname', 'age'];
+ dataSource = users;
...
un peu de css pour que ça fasse joli dans src/app/users-list/users-list.component.scss
:
table {
margin: 10%;
width: 75%;
}
Avec la Table de Materials, il y a plein de features cool à explorer : sorting, click log, etc... voir plus d'info ici
Créez un nouveau composant pour le login :
ng g component login
On va maintenant ajouter et modifier les routes de notre frontend.
Ici, on veut que le chemin par defaut nous mène vers le nouveau composant login
, tandis que l'url /users
nous redirige vers le component précèdemment créé.
Toujours dans le fichier src/app/app-routing.module.ts
:
const routes: Routes = [
+ {
+ path: '',
+ component: LoginComponent,
+ },
{
+ path: 'users',
component: UsersListComponent,
},
];
Pour créer la page de login, on va procèder par étape et ajouter des fonctionnalités petit à petit.
On va ajouter deux champs input avec leur label dans src/app/login/login.component.html
:
<div>
<label for="username">Email</label>
<input type="text" id="username" placeholder="username" />
<label for="password">Password</label>
<input type="password" id="password" placeholder="password" />
<a><button>Login</button></a>
</div>
On va maintenant ajouter de la logique sur le click du button login :
+ <a (click)="login()"><button>Login</button></a>
Ici, la directive (click)="
va associer l'évènement du click sur le button, à une fonction.
Pour rappel, le fichier HTML donne la structure, et le fichier typescript donne la logique.
Il faut donc implémenter la fonction login()
dans notre fichier src/app/login/login.component.ts
pour que cela fonctionne.
login(): void {
console.log('click on login !')
}
Cliquez sur le button et observer dans la console (click droit inspect -> console) que le message s'affiche correctement.
On veut maintenant lire les données saisies par l'utilisateur dans les champs de la page login.
Dans les fichiers typescript des composants Angular, on a accès à la page HTML grâce à la variable globale document
.
On peut ainsi manipuler la page HTML depuis notre fichier TS.
Plus particulièrement, on peut récupérer des éléments via leur id (au sens HTML/CSS) avec la function getElementById(str)
.
Pour récupérer les informations, il suffit alors de faire :
login(): void {
- console.log('click on login !')
+ const email: string = (document.getElementById('username') as HTMLInputElement).value;
+ const password: string = (document.getElementById('password') as HTMLInputElement).value;
+ console.log(email, password);
}
On pourra alors observer dans la console les valeurs saisies par l'utilisateur.
Récupérez l'archive backend.zip
, décompressez-la et lancez le backend avec la commande suivante :
$ node dist/main.js
[Nest] 862716 - 10/22/2021, 6:25:42 PM [NestFactory] Starting Nest application...
[Nest] 862716 - 10/22/2021, 6:25:42 PM [InstanceLoader] AppModule dependencies initialized +175ms
[Nest] 862716 - 10/22/2021, 6:25:42 PM [InstanceLoader] TypeOrmModule dependencies initialized +0ms
[Nest] 862716 - 10/22/2021, 6:25:42 PM [InstanceLoader] PassportModule dependencies initialized +0ms
[Nest] 862716 - 10/22/2021, 6:25:42 PM [InstanceLoader] JwtModule dependencies initialized +0ms
[Nest] 862716 - 10/22/2021, 6:25:42 PM [InstanceLoader] AuthModule dependencies initialized +2ms
[Nest] 862716 - 10/22/2021, 6:25:42 PM [InstanceLoader] TypeOrmCoreModule dependencies initialized +78ms
...
Le backend est en train de tourner.
On va maintenant ajouter à notre front, la capacité d'envoyer des requêtes.
Pour cela, ajoutez le module HttpClientModule
dans les imports
du fichier src/app/app.module.ts
:
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatTableModule,
MatSortModule,
+ HttpClientModule,
],
Dance ce module, la classe HttpClient
nous offre des fonctions pour faire diverses requêtes sur notre backend: get()
, post()
, put()
, delete()
.
En fait, elle nous offre toutes les méthodes de requêtes dont on a besoin pour développer un service fullstack (front + back) REST.
Pour commencer en douceur la communication, on va récupérer la liste des utilisateurs depuis le backend plutôt que d'utiliser une liste en dur dans le frontend.
Ajoutez un constructor dans le composant de la liste (src/app/users-list/users-list.component.ts
) avec un HttpClient
:
constructor(
private http: HttpClient
) {}
Cela fonctionne comme l'injection de dépendances dans le backend vue l'an dernier.
On va maintenant modifier la fonction ngOnInit
de notre UsersListComponent
.
Cette fonction est appelée au moment où le composant est initialisé.
const resquest: Observable<any> = this.http.get('http://localhost:3000/users', { observe: 'response' });
resquest.toPromise().then(response => this.dataSource = response.body);
TODO: détailler un peu ce qui se passe.
Relancez le front, et connectez-vous à l'URL https://localhost:4200/users
.
On peut maintenant supprimer le tableau users du front (car maintenant on le récupère depuis le backend).
-export class User {
- constructor(
- public id: number,
- public lastname: string,
- public firstname: string,
- public age: number,
- ) {}
-}
-const users: User[] = [
- new User(0, 'Doe', 'John', 23),
- new User(1, 'Doe', 'Jane', 32),
-]
...
- dataSource = users;
+ dataSource = [];
On va maintenant modifier la function login()
de src/app/login/login.component.ts
afin de faire une requête POST
sur le backend.
On utilise ici l'api-helper, qui évite de gérer les requêtes dans les differents components. Récupérez le fichier api-helper.service.ts
et copiez-le dans un nouveau folder src/app/services
.
+constructor(
+ private api: ApiHelperService
+) {}
login(): void {
const username: string = (document.getElementById('username') as HTMLInputElement).value;
const password: string = (document.getElementById('password') as HTMLInputElement).value;
+ this.api.post({endpoint: '/auth/login', data: { username, password },}).then(response => console.log(response));
}
Retournez sur l'addresse `http://localhost:4200`, et renseignez les informations suivantes : `username = 1` et `password = password`, puis cliquez sur le bouton login.
Dans la console, vous devriez observer quelque chose comme cela :
```json
Object { access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6MSwiaWF0IjoxNjM1MzcxODUyLCJleHAiOjE2MzUzNzE5MTJ9.5GO-V4LXYdYzEZrqADODykldKKaXCpsQ-prEQjrahHM" }`
Qui est le JWT retourné par le backend en cas de login réussi.
On veut maintenant que, une fois authentifié, notre frontend sauvegarde le token et l'envoie dans toutes les requêtes suivantes.
Pour ce faire, nous allons utiliser un interceptor
, qui va intercepter toutes les requêtes pour y ajouter le token automatiquement, et ainsi sa gestion sera transparente pour le développement.
Créez le fichier src/app/services/token-storage.service.ts
et y mettre :
import { Injectable } from '@angular/core';
const TOKEN_KEY = 'token';
const USERNAME_KEY = 'username';
const IS_LOGGED_IN = 'isLoggedIn';
const IS_LOGGED = 'true';
@Injectable({
providedIn: 'root'
})
export class TokenStorageService {
public clear(): void {
localStorage.clear();
}
public save(token: string): void {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USERNAME_KEY );
localStorage.removeItem(IS_LOGGED_IN);
localStorage.setItem(TOKEN_KEY, token);
localStorage.setItem(IS_LOGGED_IN, IS_LOGGED);
}
public getToken(): string {
const token = localStorage.getItem(TOKEN_KEY);
return token === null ? '' : token;
}
public isLogged(): boolean {
return (Boolean)(localStorage.getItem(IS_LOGGED_IN));
}
}
TODO: expliquer le localStorage
Modifiez le fichier src/app/login/login.component.ts
pour enregistrer le JWT dans le TokenStorageService
:
constructor(
private api: ApiHelperService,
+ private tokenStorageService: TokenStorageService,
) {}
ngOnInit() {
}
login(): void {
const username: string = (document.getElementById('username') as HTMLInputElement).value;
const password: string = (document.getElementById('password') as HTMLInputElement).value;
this.api.post({endpoint: '/auth/login', data: { username, password },})
- .then(response => console.log(response));
+ .then(response => this.tokenStorageService.save(response.access_token));
}
Créez le fichier src/app/interceptors/token.interceptor.ts
:
import { Injectable } from '@angular/core';
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent,
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { TokenStorageService } from '../services/token-storage.service';
@Injectable({
providedIn: 'root',
})
export class TokenHttpInterceptor implements HttpInterceptor {
constructor(
private service: TokenStorageService
) {}
// C'est dans la fonction intercept qu'on implémente la logique
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
// On récupère le token depuis le TokenStorageService
const token = this.service.getToken();
// s'il n'est pas initialisé, on envoie la requête telle qu'elle est
if (!token) {
return next.handle(request);
}
// Si non, on va injecter le token dedans :
const updatedRequest = request.clone({
headers: request.headers.set('Authorization', `Bearer ${token}`),
});
// et envoyer la requête avec le token
return next.handle(updatedRequest).pipe(
tap(
(event) => {},
(error) => {}
)
);
}
}
et modifiez src/app/app.module.ts
:
+import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { LoginComponent } from './login/login.component';
import { UsersListComponent } from './users-list/users-list.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
+import { TokenHttpInterceptor } from './interceptors/token.interceptor';
@NgModule({
declarations: [
AppComponent,
LoginComponent,
UsersListComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,
MatTableModule,
MatSortModule,
],
providers: [
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: TokenHttpInterceptor,
+ multi: true,
+ },
],
bootstrap: [AppComponent]
})