TP 3 : Créer une application web à l’aide de l’Angular (Partie 2)
Objectifs du TP
Dans ce TP, vous continuerez à explorer Angular en créant des services, envoyant des requêtes HTTP, manipulant les formulaires et définissant le routage.
Version complète du TP précédent
Téléchargez la version complète du tp précédent pour pouvoir compléter ce TP. Le dossier concerné est joes-robot-shop-enit-fin-partie1.
https://drive.google.com/drive/folders/1smZiwsDX7KzxQ9r1BHBaMc4zMvLgnXWU?usp=sharing
Creating Angular Services
Angular introduit les services. C’est le “bucket” où nous mettons toute notre logique métier. Pour ce faire, nous allons créer un service qui gère le panier
ng g s cart
Placez vous dans le fichier cart.service.ts, et ajoutez la variable cart de manière à avoir le code suivant:
export class CartService {
cart : IProduct[]=[];
constructor() { }
}
Ajoutons dans le service cart, cart.service.ts, la fonction déjà implémentée addToCart qui a été initialement développée dans le fichier catalog.component.ts et modifiant son nom à :
add(product:IProduct){
this.cart.push(product);
console.log(`product ${product.name} added to cart`);
}
Dans le fichier catalog.component.ts, laissez cette fonction vide pour l’instant :
addToCart(product:IProduct){
}
Les services suivent le pattern singleton. Cela veut dire qu’une seule instance du service est permise dans l’application.
Maintenant, nous allons voir comment injecter ce service dans le composant pour que le composant appelle le service au moment opportun. Au niveau du constructeur de la classe catalog.component.ts, ajoutez au paramètre du constructeur, une variable privée du service cart:
constructor(private cartSvc: CartService) {// le contenu du constructeur reste valable, à ne pas changer!!
De cette manière, vous avez utilisé le pattern dependency injection qui vous épargne d’instancier le service par vous-même. Angular s’en charge.
Ensuite, dans la même classe catalog.component.ts, au niveau de la fonction addToCart, ajoutez l’appellation du service:
addToCart(product:IProduct){
this.cartSvc.add(product);
}
Tel qu’il est écrit, le service est entrain de fonctionner d’une manière rudimentaire. Nous allons développer son fonctionnement en implémentant plus de fonctions pour faire fonctionner correctement l’ajout d’un produit à un panier. Pour ce faire, nous ajoutons ILineItem.model.ts dans le répertoire Catalog.
import { IProduct } from './product.model';
export interface ILineItem {
product: IProduct;
qty: number;
}
Dans la classe du Cart
import { Injectable } from '@angular/core';
import { IProduct } from './catalog/product.model';
import { ILineItem } from './catalog/ILineItem.model';
@Injectable({
providedIn: 'root',
})
export class CartService {
private cart: ILineItem[] = [];
constructor() {}
getTotalPrice() {
return (
Math.round(
this.cart.reduce<number>((prev, cur) => {
return (
prev + cur.qty * (cur.product.price * (1 - cur.product.discount))
);
}, 0) * 100
) / 100
);
}
findLineItem(product: IProduct) {
return this.cart.find((li) => li.product.id === product.id);
}
add(product: IProduct) {
let lineItem = this.findLineItem(product);
if (lineItem !== undefined) {
lineItem.qty++;
} else {
lineItem = { product: product, qty: 1 };
this.cart.push(lineItem);
}
console.log('added ' + product.name + ' to cart!');
console.log('Total Price: $' + this.getTotalPrice());
}
}
Making HTTP Request with Angular
Observables
Les Observables sont une partie centrale d’Angular et fournissent un puissant mécanisme pour gérer les événements asynchrones dans une application. Grâce à leur flexibilité, ils permettent de traiter les données qui arrivent de manière séquentielle, de suivre les changements d’état, et d’assurer une gestion fluide des flux de données.
Avec un Observable, vous pouvez écouter ces événements et agir lorsque de nouvelles données sont émises. Cela permet d’éviter les problèmes de blocage du thread principal et assure que l’interface reste réactive.
Les services Angular, comme le module HttpClient, utilisent largement les Observables pour traiter les données asynchrones. Par exemple, lorsqu’on fait une requête HTTP pour récupérer des données depuis un serveur, celle-ci renvoie un Observable, ce qui permet de traiter la réponse lorsque les données sont disponibles, sans bloquer le reste de l’application.
RxJS, la bibliothèque derrière les Observables, offre une multitude d’opérateurs puissants qui permettent de transformer, filtrer, et manipuler les flux de données. Cela permet de contrôler finement le comportement des événements asynchrones, notamment pour des tâches comme le debouncing (éviter trop d’appels rapprochés), le throttling (limiter la fréquence des appels), et le traitement des erreurs.
Dans la manipulation suivante, nous allons simuler une communication entre notre application Angular et une api qui utilise le style d’architecture REST à l’aide du framework Express. Vous allez créer un répertoire nommé apiserver et le mettre au même niveau que le dossier src.
A l’intérieur de ce répertoire, créez un fichier index.js:
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
app.use(bodyParser.json());
/*
IMPORTANT:
***NEVER*** store credentials unencrypted like this.
This is for demo purposes only in order to simulate a functioning API server.
*/
const users = {
"user1@joesrobotshopenit.com": {
firstName: "User1",
lastName: "User1",
email: "user1@joesrobotshopenit.com",
password: "very-secret",
},
"user2@joesrobotshop.com": {
firstName: "User2",
lastName: "User2",
email: "user2@joesrobotshopenit.com",
password: "super-secret",
},
};
let cart = [];
// use this to add a 1 second delay to all requests
// app.use(function (req, res, next) {
// setTimeout(next, 1000);
// });
app.get("/api/products", (req, res) => {
let products = [
{
id: 1,
description:
"A robot head with an unusually large eye and teloscpic neck -- excellent for exploring high spaces.",
name: "Large Cyclops",
imageName: "head-big-eye.png",
category: "Heads",
price: 1220.5,
discount: 0.2,
},
{
id: 17,
description: "A spring base - great for reaching high places.",
name: "Spring Base",
imageName: "base-spring.png",
category: "Bases",
price: 1190.5,
discount: 0,
},
{
id: 6,
description:
"An articulated arm with a claw -- great for reaching around corners or working in tight spaces.",
name: "Articulated Arm",
imageName: "arm-articulated-claw.png",
category: "Arms",
price: 275,
discount: 0,
},
{
id: 2,
description:
"A friendly robot head with two eyes and a smile -- great for domestic use.",
name: "Friendly Bot",
imageName: "head-friendly.png",
category: "Heads",
price: 945.0,
discount: 0.2,
},
{
id: 3,
description:
"A large three-eyed head with a shredder for a mouth -- great for crushing light medals or shredding documents.",
name: "Shredder",
imageName: "head-shredder.png",
category: "Heads",
price: 1275.5,
discount: 0,
},
{
id: 16,
description:
"A single-wheeled base with an accelerometer capable of higher speeds and navigating rougher terrain than the two-wheeled variety.",
name: "Single Wheeled Base",
imageName: "base-single-wheel.png",
category: "Bases",
price: 1190.5,
discount: 0.1,
},
{
id: 13,
description: "A simple torso with a pouch for carrying items.",
name: "Pouch Torso",
imageName: "torso-pouch.png",
category: "Torsos",
price: 785,
discount: 0,
},
{
id: 7,
description:
"An arm with two independent claws -- great when you need an extra hand. Need four hands? Equip your bot with two of these arms.",
name: "Two Clawed Arm",
imageName: "arm-dual-claw.png",
category: "Arms",
price: 285,
discount: 0,
},
{
id: 4,
description: "A simple single-eyed head -- simple and inexpensive.",
name: "Small Cyclops",
imageName: "head-single-eye.png",
category: "Heads",
price: 750.0,
discount: 0,
},
{
id: 9,
description:
"An arm with a propeller -- good for propulsion or as a cooling fan.",
name: "Propeller Arm",
imageName: "arm-propeller.png",
category: "Arms",
price: 230,
discount: 0.1,
},
{
id: 15,
description: "A rocket base capable of high speed, controlled flight.",
name: "Rocket Base",
imageName: "base-rocket.png",
category: "Bases",
price: 1520.5,
discount: 0,
},
{
id: 10,
description: "A short and stubby arm with a claw -- simple, but cheap.",
name: "Stubby Claw Arm",
imageName: "arm-stubby-claw.png",
category: "Arms",
price: 125,
discount: 0,
},
{
id: 11,
description:
"A torso that can bend slightly at the waist and equiped with a heat guage.",
name: "Flexible Gauged Torso",
imageName: "torso-flexible-gauged.png",
category: "Torsos",
price: 1575,
discount: 0,
},
{
id: 14,
description: "A two wheeled base with an accelerometer for stability.",
name: "Double Wheeled Base",
imageName: "base-double-wheel.png",
category: "Bases",
price: 895,
discount: 0,
},
{
id: 5,
description:
"A robot head with three oscillating eyes -- excellent for surveillance.",
name: "Surveillance",
imageName: "head-surveillance.png",
category: "Heads",
price: 1255.5,
discount: 0,
},
{
id: 8,
description: "A telescoping arm with a grabber.",
name: "Grabber Arm",
imageName: "arm-grabber.png",
category: "Arms",
price: 205.5,
discount: 0,
},
{
id: 12,
description: "A less flexible torso with a battery gauge.",
name: "Gauged Torso",
imageName: "torso-gauged.png",
category: "Torsos",
price: 1385,
discount: 0,
},
{
id: 18,
description:
"An inexpensive three-wheeled base. only capable of slow speeds and can only function on smooth surfaces.",
name: "Triple Wheeled Base",
imageName: "base-triple-wheel.png",
category: "Bases",
price: 700.5,
discount: 0,
},
];
res.send(products);
});
app.post("/api/cart", (req, res) => {
cart = req.body;
setTimeout(() => res.status(201).send(), 20);
});
app.get("/api/cart", (req, res) => res.send(cart));
app.post("/api/register", (req, res) =>
setTimeout(() => {
const user = req.body;
if (user.firstName && user.lastName && user.email && user.password) {
users[user.email] = user;
res.status(201).send({
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
});
} else {
res.status(500).send("Invalid user info");
}
}, 800)
);
/* IMPORTANT:
The code below is for demo purposes only and does not represent good security
practices. In a production application user credentials would be cryptographically
stored in a database server and the password should NEVER be stored as plain text.
*/
app.post("/api/sign-in", (req, res) => {
const user = users[req.body.email];
if (user && user.password === req.body.password) {
res.status(200).send({
userId: user.userId,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
});
} else {
res.status(401).send("Invalid user credentials.");
}
});
app.listen(8081, () => console.log("API Server listening on port 8081!"));
Nous ajountons aussi un fichier package.json:
{
"name": "angular-fundamentals-api-server",
"version": "1.0.0",
"description": "API Server for use with the Joes-Robot-Shop-ENIT Angular Fundamentals Demo App",
"main": "index.js",
"scripts": {
"start": "node ."
},
"author": "ENIT Students",
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.0",
"express": "^4.17.3"
}
}
Maintenant, avec le git bash, placez-vous sous le dossier du apiserver où il y a le package.json et lancez
npm install
Puis, lancez le serveur pour qu’il soit à l’écoute sur le port 8081 :
npm start
Nous voulons maintenant rendre cette API exposée de manière à ce qu’à chaque fois que notre application Angular peut récupérer des informations en envoyant des requêtes HTTP. Nous définissons ainsi un serveur proxy dans notre application Angular qui jouera le rôle du serveur intermédiaire. Pour ce faire, créez un fichier proxy.conf.json à l’intérieur du répertoire app (même niveau que les fichiers index.html, main.ts, et styles.css)
{
"/api": {
"target": "http://localhost:8081",
"secure": false
}
}
et au niveau du fichier angular.json, dans la partie “serve”: , rendez-vous à son enfant “development”: {
“browserTarget”: “joes-robot-shop-enit:build:development” pour ajouter sous browserTarget, la configurante de manière à avoir:
"development": {
"browserTarget": "joes-robot-shop-enit:build:development",
"proxyConfig": "src/proxy.conf.json"
}
Testez l’URL http://localhost:8081/api/products et observez le résultat.
Maintenant, nous allons faire un peu le ménage! puisque la liste des produits (pièces de robots peuvent être consultées à partir de l’API). C’est pour cette raison, nous allons supprimer la liste des produits codées en dur “hard coded” dans le constucteur du composant catalog.component.ts. Modifiez-le pour ressembler à cette forme:
constructor(
private cartSvc: CartService,
private productSvc: ProductService
) { //aucun code ici
}
Le ProduitService que vous voyez dans ce constructeur, est un nouveau service que nous allons créer avec la commande suivante (Dans le git bas, placez-vous dans le répertoire src)
ng g s catalog/produit
Dans le fichier produit.service.ts, insérez ce code qui permet d’envoyer des requêtes GET :
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { IProduct } from 'src/app/catalog/product.model';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class ProductService {
constructor(private http: HttpClient) {}
getProducts(): Observable<IProduct[]> {
return this.http.get<IProduct[]>('/api/products');
}
}
Dernière étape est de modifier le paramètre import le fichier app.module.ts, de manière à ajouter HttpClientModule ainsi
imports: [
BrowserModule, HttpClientModule
],
Visualisez votre site, vous verrez que le catalogue fonctionne!
Maintenant, nous allons implémenter le hook ngOnInit(voir le tp précédent qui contient sa définition) pour charger les données de l’API dès que nous appellons la page du catalogue. Ajoutons ce code dans le fichier catalog.component.ts et c’est là que nous définissons l’Observable:
ngOnInit() {
this.productSvc.getProducts().subscribe((products) => {
this.products = products;
});}
Comme nous avons vu dans le TP du NodeJS/Express, la requête GET est utilisée lorsque nous voulons lister les données d’une ressource. Quant à la requête POST, nous l’utilisons lorsque nous allons insérer une nouvelle donnée. Ce scénario est assurée par le panier Cart. Dans le TP précédent, nous avons ajouté Cart en tant que service. Vous allez supprimer ce service et le remplacer par tout un répertoire appelé Cart. Tous comme les autre composants home, site-header etc. Téléchargez-le (le dossier cart) à partir de lien suivant:
https://drive.google.com/drive/folders/1smZiwsDX7KzxQ9r1BHBaMc4zMvLgnXWU?usp=sharing
et puisque ce composant a été ajouté manuellement, vous devez l’ajouter manuellement au fichier app.module.ts
@NgModule({
declarations: [
AppComponent,
HomeComponent,
CatalogComponent,
SiteHeaderComponent,
ProductDetailsComponent,
CartComponent
],
Le routage
Commençons tout d’abord par créer un fichier pour cet objectif avec la commande suivante qui est à lancer sous la racine principale:
ng g m app-routing --flat --module=app
Ceci génère le fichier ``app-routing.module.ts`. Remplacez son contenu par celui-ci:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CartComponent } from './cart/cart.component';
import { CatalogComponent } from './catalog/catalog.component';
import { HomeComponent } from './home/home.component';
const routes: Routes = [
{ path: 'home', component: HomeComponent, title: "Home - Joe's Robot Shop ENIT" },
{ path: 'catalog', component: CatalogComponent, title: "Catalog - Joe's Robot Shop ENIT" },
{ path: 'cart', component: CartComponent, title: "Cart - Joe's Robot Shop ENIT" },
{ path: '', redirectTo: '/home', pathMatch: 'full' },
];
@NgModule({
declarations: [],
imports: [
RouterModule.forRoot(routes)
],
exports: [RouterModule]
})
export class AppRoutingModule { }
Rendez-vous au fichier app.component.ts et modifiez-le de manière à avoir:
<app-site-header></app-site-header>
<router-outlet></router-outlet>
est une directive utilisée dans Angular pour indiquer où les composants associés aux routes doivent être rendus dans le DOM. C’est un espace réservé dans votre template où les composants spécifiés par le routeur seront affichés en fonction de la route active.
Le retouage peut être défini soit dans le code HTML ou le code TS. Commençons par le HTML. Dans le fichier site-header.component.html, remplacez le code existant par celui-ci:
<div class="container">
<div class="left">
<img class="logo" src="/assets/images/logo.png" alt="Logo" />
<a routerLink="/home" routerLinkActive="active">Home</a>
<a routerLink="/catalog" routerLinkActive="active">Catalog</a>
<div class="cart">
<a routerLink="/cart" routerLinkActive="active">Cart</a>
</div>
</div>
<div class="right">
<a href="">Sign In</a>
<a href="" class="cta">Register</a>
</div>
</div>
Nous allons définir maintenant dans le fichier catalog.component.ts une redirection lors du chargement de la page et de l’ajout d’un produit au panier:
import { Component, inject } from '@angular/core';
import { IProduct } from './product.model';
import { CartService } from '../cart/cart.service';
import { ActivatedRoute, Router } from '@angular/router';
import { ProductService } from './produit.service';
@Component({
selector: 'app-catalog',
templateUrl: './catalog.component.html',
styleUrls: ['./catalog.component.css'],
})
export class CatalogComponent {
products: any;
filter: string = '';
constructor(
private cartSvc: CartService,
private productSvc: ProductService,
private router: Router,
private route: ActivatedRoute
) { }
ngOnInit() {
this.productSvc.getProducts().subscribe((products) => {
this.products = products;
});
this.route.queryParams.subscribe((params) => {
this.filter = params['filter'] ?? '';
})
}
addToCart(product: IProduct) {
this.cartSvc.add(product);
this.router.navigate(['/cart']);
}
getFilteredProducts() {
return this.filter === ''
? this.products
: this.products.filter(
(product: any) => product.category === this.filter
);
}
}
Mettez à jour votre fichier app.module.ts:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { CatalogComponent } from './catalog/catalog.component';
import { SiteHeaderComponent } from './site-header/site-header.component';
import { ProductDetailsComponent } from './product-details/product-details.component';
import { HttpClientModule } from '@angular/common/http';
import { CartComponent } from './cart/cart.component';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
declarations: [
AppComponent,
HomeComponent,
CatalogComponent,
SiteHeaderComponent,
ProductDetailsComponent,
CartComponent
],
imports: [
BrowserModule, HttpClientModule, AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Félicitations! Votre site est fonctionnel!