TP3 Building web App using Angular

Troisième TP

By Mariem ZAOUALI

TP 3 : Créer une application web à l’aide de l’Angular (Partie 1)

À la fin de ce TP, vous serez capable de développer une application à l’aide du framework Angular. Vous allez manipuler la notion de composants en Angular. Vous sereiez apte à les créer et à les personnaliser. Vous manipulerez la syntaxe Template, la création et l’envoi des requêtes HTTP, le routage et la navigation et la création des formulaires.

Ce cours est inspriré du celui de Jim Cooper https://jcoop.io/author/jimcooperps/

Préparer l’environnement

Nous utiliserons pour ce TP la version 16 de Angular. Pour ce faire, placez vous devant votre terminal et lancez la commande :

npm install -g @angular/cli@16.0.0

Il se trouve parfois que la version de NodeJS n’est pas conforme à celle d’Angular. Pour régler ce problème, et d’ailleurs c’est le problème que nous faisons face pour ce TP, nous installon NVM (Node Version Manager) en suivant ces commandes et en les lançant dans le terminal de VS code.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.4/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

Pour bien vérifier que NVM a été bien installée, lancez la commande suivante:

nvm --version

Il reste maintenant d’installer la bonne version de Node JS en tapant la commande suivante:

nvm install 18

Puis:

nvm use 18

Création du projet

Dans votre VS code, ouvrez le répertoire là où vous allez créer votre projet et lancez la commande suivante:

ng new joes-robot-shop-enit

On vous demande quelques questions pour achever l’installation, vous allez répondre avec No quand on vous demande d’ajouter un Angular routing. Vous choisissez CSS pour le format de stylesheet.

Un fois créé et les dépendences installées, VS code va vous demander si vous voulez installer Angular Language Service. Acceptez sa proposition. Cette extension va vous aider en écrivant du code.

Ce qui a été généré à présent est une application complète. C’est une version bootstrap sur laquelle vous pourvez bâtir et personnaliser votre application. Testez cette application en tapant dans votre terminal du VS code tout en étant dans le répertoire de l’application ainsi créée:

npm start

Regardez la structure générée. Vous avez rencontré dans le TP précédent le fichier package.json. En l’ouvrant, vous allez le reconnaître puisqu’il garde la même structure et rôle que le TP précédent.

Vous avez également d’autres fichiers. Ouvrez le fichier index.html. Remarquez qu’il ne contient pas suffisamment de code qui lui permet d’afficher la page d’accueil que vous avez eu.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>JoesRobotShopEnit</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

Remarquez particulièrement le tag app-root qui n’est pas une balise HTML usuelle! Ceci est un composant angular. Regardons particulièrement les fichiers qui commencent par le mot clé app. C’est le composant qui se chargent au sommet de tous les composants d’Angular. Remarquez que vous avez le même préfixe app.component mais des suffixes différents : css, html, ts et finalement spec.ts. Ce sont les fichiers qui définissent le concept de composants en angular.

Examinez le code du fichier app.component.ts. Le tag rencontré dans le fichier HTML provient en fait du paramètre selector de ce fichier.

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'joes-robot-shop-enit';
}

En examinant, le fichier app.component.html, vous allez trouver le HTML affiché dans votre page index.html. Maintenant remplacez son contenu par simplement:

<h1>Hello World!</h1>

et dans le fichier app.component.ts, supprimez la ligne du code :

 title = 'joes-robot-shop-enit';

Dans votre navigateur, vérifiez que vous avez bien la fameuse “Hello World!”.

Creating angular componenent

Nous allons commencer par la création de la page d’accueil. Nous créons pour ce faire le composant home.

ng generate component home

Placez-vous au niveau du fichier home.component.ts. La première partie conceprne l’importation des modules nécessaires pour le fonctionnement du code

import { Component } from '@angular/core';

La deuxième partie est un décorateur. C’est un concept du Javascript utilisé pour ajouter des metadonnées sur les classes. Remarquez dans cette partie, que vous pouvez définir le tag à utiliser dans la partie HTML en le spécifiant devant le selector. Vous mentionnez aussi le fichier HTML template est app.component.html. Finalement, remarquez que devant styleUrls, nous pouvons mettre toute une liste de fichiers de styles. Dans notre cas ici, un seul a été mentionné pour définir le style à appliquer à notre composant.

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})

La dernière partie concerne le code métier à développer pour lancer un traitement spécifique.

export class HomeComponent {

}

Faisons un petit test pour bien assimiler le concept. Allez au fichier app.component.html et ajoutez au-dessous de Hello World!, le tag app:

<app-home></app-home>

Ajoutez un dans le fichier home.component.htmlle style suivant :

<p class="red">home works</p>

Et finalement, ajoutez un dans le fichier home.component.css la règle suivante :

.red{
  color: red;
}

Vérrifiez dans le fichier app.module.ts, l’existance de votre module HomeComponent qui a été ajouté automatiquement par Angular. Si vous avez ajouté un composant manuellement, n’oubliez pas de passer par ce fichier pour le déclarer!

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Maintenant, nous allons ajouter du contenu à notre page home. Copiez ce code dans votre fichier home.component.html:

<div class="container">
    <div class="hero"></div>
  
    <div class="promoted">
      <img src="" alt="Friendly Robot Head" />
      <div class="promo-text">
        <div class="promo-main-text">DISPELL THE ROBOT APOCALYPSE MYTH</div>
        <div class="promo-sub-text cta">
          <div>SAVE 20% ON OUR FRIENDLIEST</div>
          <div>ROBOT HEADS</div>
        </div>
      </div>
      <img src="" alt="Big Eye Head" />
    </div>
  
    <ul class="robot-parts-cta">
      <li>
        <a class="part">
          <img src="" alt="Robot Heads" />
          <div>ROBOT HEADS</div>
        </a>
      </li>
      <li>
        <a class="part">
          <img src="" alt="Robot Arms" />
          <div>ROBOT ARMS</div>
        </a>
      </li>
      <li>
        <a class="part">
          <img src="" alt="Robot Torsos" />
          <div>ROBOT TORSOS</div>
        </a>
      </li>
      <li>
        <a class="part">
          <img src="" alt="Robot Bases" />
          <div>ROBOT BASES</div>
        </a>
      </li>
    </ul>
  
    <div class="white-paper">
      <img src="" alt="Robot Apocalyse" />
      <div class="text">
        <div>
          <div class="header-text cta">Will they kill us all?</div>
          <div class="sub-text">
            <p>10 Myths About the</p>
            <p>Robot Apocalyse</p>
          </div>
        </div>
        <div class="large-text">WHITE PAPER</div>
        <a href="" class="learn-more">Learn More</a>
      </div>
    </div>
  </div>

et dans le fichier home.component.css:

.container {
    display: flex;
    flex-direction: column;
  }
  
  .hero {
    background-repeat: no-repeat;
    height: 300px;
    background-size: cover;
    background-position: center center;
    text-align: center;
    margin-left: -8px;
    margin-right: -8px;
  }
  
  .promoted {
    display: flex;
    justify-content: space-between;
    margin: 25px;
    border-top: 2px solid #888;
    border-bottom: 2px solid #888;
    padding: 10px 150px 10px 150px;
  }
  
  .promoted img {
    width: 150px;
    height: 150px;
  }
  
  .promo-text {
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    margin: 40px 0 40px 0;
  }
  
  .promo-main-text {
    font-size: 24px;
    text-align: center;
  }
  
  .promo-sub-text {
    font-size: 20px;
    text-align: center;
  }
  
  .robot-parts-cta {
    display: flex;
    justify-content: space-between;
    margin: 0px 100px 25px 100px;
    padding: 10px 0;
  }
  
  .part {
    display: flex;
    flex-direction: column;
    font-size: 18px;
    text-align: center;
    cursor: pointer;
  }
  
  .part img {
    width: 180px;
    margin-bottom: 10px;
    padding: 10px;
    background-color: #888;
  }
  
  .white-paper {
    display: flex;
    height: 100%;
    margin: 0 25px;
  }
  
  .white-paper img {
    width: 75%;
  }
  
  .white-paper .text {
    display: flex;
    width: 25%;
    flex-direction: column;
    background-color: #333;
    text-align: center;
    justify-content: space-between;
  }
  
  .white-paper .header-text {
    margin-top: 50px;
    font-size: 30px;
  }
  
  .white-paper .sub-text {
    font-size: 20px;
    color: #5cadd2;
    margin-top: 10px;
    padding: 0 15%;
  }
  
  .white-paper .sub-text p {
    margin: 0;
  }
  
  .white-paper .large-text {
    border-top: 2px solid #666;
    border-bottom: 2px solid #666;
    margin-top: -50px;
    padding: 20px 0;
    font-size: 35px;
    color: #fff;
  }
  
  .white-paper .learn-more {
    background-color: #d25ca1;
    color: white;
    padding: 15px 25px;
    text-decoration: none;
  }

Nous définissons aussi le style gloabal de l’application en ajoutant ce contenu au fichier qui s’appelle style.css dans la racine principale de votre projet:

body {
    background-color: #fff;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 16px;
    color: #444;
  }
  
  a {
    color: #444;
    text-decoration: none;
  }
  
  a.active {
    padding-bottom: 5px;
    border-bottom: 2px solid #d25ca1;
  }
  
  a.button.active {
    padding-bottom: 5px;
    border-bottom: 2px solid #d25ca1;
  }
  
  a:visited {
    color: #444;
  }
  
  a:hover {
    color: #5cadd2;
  }
  
  a.cta {
    color: #d25ca1;
  }
  
  a.cta:visited {
    color: #d25ca1;
  }
  
  a.cta:hover {
    color: #f27cb1;
  }
  
  .cta {
    color: #d25ca1;
  }
  
  a.cta {
    color: #d25ca1;
  }
  
  a.cta:visited {
    color: #d25ca1;
  }
  
  a.cta:hover {
    color: #f27cb1;
  }
  
  .cta {
    color: #d25ca1;
  }
  
  ul {
    list-style-type: none;
    padding: 0;
  }
  
  button {
    font-size: 15px;
    padding: 15px 25px;
    background-color: #5cadd2;
    color: #fff;
    border: 0;
    box-shadow: none;
    border-radius: 5px;
    cursor: pointer;
  }
  
  a.button {
    font-size: 15px;
    padding: 15px 25px;
    background-color: #5cadd2;
    color: #fff;
    border: 0;
    box-shadow: none;
    border-radius: 5px;
    cursor: pointer;
  }
  
  a.button:hover {
    color: #1c6d92;
  }
  
  button:disabled {
    background-color: #777;
  }
  
  button:hover {
    color: #1c6d92;
  }
  
  button.cta {
    font-family: Arial, Helvetica, sans-serif;
    background-color: #d25ca1;
    color: #ddd;
  }
  
  button.cta:hover {
    color: #fff;
  }
  
  input {
    padding: 15px;
    font-size: 20px;
  }
  
  .button.cta:disabled {
    background-color: #f590c4;
    color: #aaa;
  }

Lancez votre projet et visualisez votre page d’accueil. Votre site est en vie sur l’adresse localhost:4200 :

npm start

Enlevez Hello World! en se plaçant dans le fichier app.component.html.

Nous allons ajouter des images à notre page. Téléchargez le dossier images et mettez le sous assets dans votre projet de manière à avoir /assets/images.

https://drive.google.com/drive/folders/19U2t0RVDHoZeiYOMtH8_17qJA4xhf-iM?usp=sharing

Dans le fichier ‘home.component.html`, vous ajoutez pour les deux images ayant respectivement les alt suivant dans leur tag img : “Friendly Robot Head” et “Big Eye Head”. Remarque : les deux images ont presque le même chemin, vous pouvez donc écrire en parallèle dans deux emplacements dans VS code, en cliquant sur le premier en placement, ppuiant sur la touche “alt” et cliquant sur le deuxième emplacement. Quand vous commencez l’écriture dans un emplacement, vous verez que l’autre emplacement vous suit. Amusez-vous!

<img src="/assets/images/robot-parts/head-friendly.png" alt="Friendly Robot Head" />
<img src="/assets/images/robot-parts/head-big-eye.png" alt="Big Eye Head" />

Maintenant, allons au fichier home.component.css et allons à la classe .hero pour ajouter background-image. Ajoutons cette règle de manière à avoir :

.hero {
    background-image: url("/assets/images/hero-banner.png");
    background-repeat: no-repeat;
    height: 300px;
    background-size: cover;
    background-position: center center;
    text-align: center;
    margin-left: -8px;
    margin-right: -8px;
  }

Lifecycle component hook

Chaque composant dans Angular a un cycle de vie, défini par une série d’événements qui surviennent tout au long de la vie du composant. Lorsque l’un de ces événements se produit, vous pouvez exécuter votre propre code en utilisant des hooks de cycle de vie. Certains de ces hooks se déclenchent une seule fois, tandis que d’autres se produisent plusieurs fois lorsque les données changent pendant la durée de vie du composant.

Cependant, la plupart de ces hooks ne sont pas souvent utilisés. Une compréhension approfondie de chacun dépasse le cadre de ce cours, mais vous pouvez consulter la documentation d’Angular pour en savoir plus. Les hooks les plus couramment utilisés sont OnChanges, OnInit, et OnDestroy, avec OnInit étant le plus fréquent. OnInit est généralement utilisé pour récupérer des données pour le composant.

Si vous devez effectuer une action lorsque les données du composant changent, vous utiliserez OnChanges. Enfin, OnDestroy est utilisé pour nettoyer les ressources et éviter les fuites de mémoire, bien que ce ne soit pas toujours nécessaire.

En pratique, OnInit est le hook de cycle de vie le plus utilisé. Vous utiliserez ce hook pour récupérer des données dans plusieurs endroits de votre code. Pour utiliser un hook de cycle de vie dans Angular, trois étapes sont nécessaires : importer l’interface de l’événement de cycle de vie, l’implémenter dans la classe du composant, puis créer une méthode qui contient la logique à exécuter lorsque le hook se déclenche. Par exemple, les données sont souvent récupérées dans la méthode ngOnInit.

Il est important de noter que le nom de la méthode commence par “ng”, contrairement au nom de l’interface. Assurez-vous donc de toujours ajouter ce préfixe “ng” dans les noms de vos méthodes de hooks de cycle de vie.

Ajoutons d’autres composants

Lançons la commande :

ng generate component catalog

Vous ajoutez le code suivant dans le fichier catalog.component.html:

<div class="container">
    <div class="filters">
      <a class="button">Heads</a>
      <a class="button">Arms</a>
      <a class="button">Torsos</a>
      <a class="button">Bases</a>
      <a class="button">All</a>
    </div>
  
    <ul class="products">
      <li class="product-item">
        <div class="product">
          <div class="product-details">
            <img src="" alt="" />
            <div class="product-info">
              <div class="name"></div>
              <div class="description"></div>
              <div class="category">Part Type: </div>
            </div>
          </div>
          <div class="price">
            <div></div>
            <button class="cta">Buy</button>
          </div>
        </div>
      </li>
    </ul>
  </div>

et ce contenu dans le fichier catalog.componenet.css:

.container {
    display: flex;
    flex-direction: column;
  }
  
  .filters {
    display: flex;
    justify-content: space-between;
    padding: 25px 200px;
  }
  
  .filters button {
    width: 100px;
  }
  
  .products {
    margin: 0 100px;
    border-top: 2px solid #999;
  }
  
  .product-item {
    border-bottom: 2px solid #999;
  }
  
  /* Product Details */
  .product {
    display: flex;
    justify-content: space-between;
    padding: 20px 25px;
  }
  
  .product .product-details {
    display: flex;
    align-items: center;
  }
  
  .product img {
    width: 125px;
  }
  
  .product .product-info {
    margin-left: 25px;
  }
  
  .product .name {
    font-size: 22px;
    font-weight: bold;
  }
  
  .product .description {
    margin-top: 3px;
    font-size: 18px;
  }
  
  .product .category {
    margin-top: 20px;
    color: #777;
  }
  
  .product .price {
    display: flex;
    flex-direction: column;
    font-size: 25px;
    justify-content: space-around;
    align-items: center;
    min-width: 190px;
    color: #555;
    border-left: 2px solid #aaa;
    margin-left: 50px;
  }
  
  .product .price button {
    padding: 10px;
    width: 100px;
  }

Pour pouvoir visualiser ce code, allez au fichier app.component.html et ajoutez le tag de ce nouveau composant:

<!--<app-home></app-home>-->
<app-catalog></app-catalog>

Maintenant, nous allons ajouter le composant header de notre site qui contient les liens de navigation. Lancez donc votre commande.

ng generate component site-header

Ajoutons son code HTML:

<div class="container">
  <div class="left">
    <img class="logo" src="/assets/images/logo.png" alt="Logo" />
    <a>Home</a>
    <a href="">Catalog</a>
    <div class="cart">
      <a href="">Cart</a>
    </div>
  </div>
  <div class="right">
    <a href="">Sign In</a>
    <a href="" class="cta">Register</a>
  </div>
</div>

et sa partie CSS:

.container {
  display: flex;
  justify-content: space-between;
  font-size: 24px;
  margin-bottom: 10px;
  margin: -3px -8px 0 -8px;
  padding: 0 8px 5px 8px;
  border-bottom: 2px solid #818285;
}

.left {
  display: flex;
  align-items: center;
}

.left * {
  margin-right: 25px;
}

.logo {
  height: 65px;
}

.cart {
  display: flex;
  position: relative;
}

.cartCount {
  position: absolute;
  display: flex;
  justify-content: space-around;
  align-items: center;
  width: 20px;
  height: 20px;
  top: -7px;
  right: -15px;
  border-radius: 25px;
  background-color: #f590c4;
  color: white;
  font-size: 14px;
}

.cartCount div {
  margin: auto;
}

.right {
  display: flex;
  align-items: center;
  margin-right: 25px;
}

.right * {
  margin-left: 25px;
}

.right img {
  height: 40px;
  cursor: pointer;
}

.sign-out {
  position: absolute;
  top: 60px;
  right: 30px;
}

Revenons à notre fichier app.component.htmlpour ajouter le selector de ce nouveau composant pour faire apparaître le header:

<app-site-header></app-site-header>
<!--<app-home></app-home>-->
<app-catalog></app-catalog>

Angular Template Syntax

Ca commence maintenant à être plus intéressant! Nous allons essayer dans cette section de relier les données à nos pages et non seulement celà nous allons essayer de faire de sorte que les données à afficher dans la page HTML sont alimentées d’une manière dynamique. Pour ce faire, nous allons utiliser la technique de Angular Template.

Pour cette première manipumation, nous allons essayer d’afficher dans la page du catalogue, juste les données d’un seul produit pour comprendre le concept, puis nous allons le généraliser. Sous le dossier catalog, créez un fichier qui s’appelle product.model.ts, où nous allons définir le contenu suivant:

export interface IProduct {
  id: number;
  description: string;
  name: string;
  imageName: string;
  category: string;
  price: number;
  discount: number;
}

Ouvrez le fichier catalog.component.ts et modifiez seulement la partie CatalogComponent. Notez bien que nous pouvons utiliser private devant product mais celà le rend inaccessible dans le template HTML. Donc, nous le laissons sans rien spécifier et dans ce cas, angular comprend que c’est une variable publique.

export class CatalogComponent {
  product : IProduct;
  constructor() {
   this.product= {
        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,
      };
    }
}

Allez maintenat à la page catalog.component.html et faisons un peu de magie pour rendre les données du fichier précédent, affichées dans la page du catalog. C’est l’interpolation ou aussi appelée Data binding. Remarquez comment on utilise “” et comment on appelle l’objet product défini dans la partie typescript. Votre code final du fichier doit ressembler à celà.

<div class="container">
    <div class="filters">
      <a class="button">Heads</a>
      <a class="button">Arms</a>
      <a class="button">Torsos</a>
      <a class="button">Bases</a>
      <a class="button">All</a>
    </div>
  
    <ul class="products">
      <li class="product-item">
        <div class="product">
          <div class="product-details">
            <img src="/assets/images/robot-parts/" alt="" />
            <div class="product-info">
              <div class="name"></div>
              <div class="description"></div>
              <div class="category">Part Type: </div>
            </div>
          </div>
          <div class="price">
           <div>$</div>
            <button class="cta">Buy</button>
          </div>
        </div>
      </li>
    </ul>
  </div>

Nous pouvons améliorer de sorte de passer de :

 <img src="/assets/images/robot-parts/" alt="" />

à cette écriture :

 <img [src]="'/assets/images/robot-parts/' + product.imageName" [alt]="product.name" />

En écrivant ainsi, vous êtes entrain de définir un flux d’alimentation des données qui commence de la classe du composant vers l’attribut HTML. C’est un sens unique. Celà veut dire, vous ne pouvez pas avec cette écriture aller de l’attribut HTML vers la classe du composant.

Nous allons maintenant créer une fonction et l’appler en HTML. Revenez donc vers la classe du composant dans le fichier catalog.component.ts et ajoutez la fonction suivante:

 getImageUrl(product: IProduct) {
    if (!product) return '';
    return '/assets/images/robot-parts/' + product.imageName;
  }

et changez la balise concernée dans le fichier template HTML du composant:

<img [src]="getImageUrl(product)" [alt]="product.name" />

Maintenant, nous allons afficher un ensemble de produits dans le catalogue. Pour ce faire, revenez vers le fichier classe du composant et changez le contenu de la méthode constructeur de la manière suivante:

 products : IProduct[];
  constructor() {
    this.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,
      },
    ];
  }

Nous revenons maintenant vers la template HTML du composant et nous allons ajouter une directive ** *ngFor** . Cette directive va changer la structure du HTML en le recopiant autant de fois. Nous l’utilisons dans notre cas pour aficher tous les produits de la même manière sans avoir besoin de passer par copier et coller le même code pour chaque produit.

 <ul class="products">
      <li class="product-item" *ngFor="let product of products">
        <div class="product" >
          <div class="product-details">
            <img [src]="getImageUrl(product)" [alt]="product.name" />
            <div class="product-info">
              <div class="name"></div>
              <div class="description"></div>
              <div class="category">Part Type: </div>
            </div>
          </div>
          <div class="price">
           <div>$</div>
            <button class="cta">Buy</button>
          </div>
        </div>
      </li>
    </ul>
  </div>

Maintenant, nous implémentons des actions allant de la template vers la classe du composant. Nous voulons laisser notre catalog soit filtré en cliquant sur les boutons. Pour ce faire, nous ajoutons (click) dans la partie template:

<div class="container">
    <div class="filters">
      <a class="button" (click)="filter='Heads'">Heads</a>
      <a class="button" (click)="filter='Arms'">Arms</a>
      <a class="button" (click)="filter='Torsos'">Torsos</a>
      <a class="button" (click)="filter='Bases'">Bases</a>
      <a class="button" (click)="filter=''">All</a>
    </div>

Modifiez le tag li de manière à avoir:

 <li class="product-item" *ngFor="let product of getFilteredProducts()">

et dans la partie classe du composant, nous ajoutons la fonction suivante:

getFilteredProducts() {
    return this.filter === ''
      ? this.products
      : this.products.filter((product: any) => product.category === this.filter);
  }

Ajoutez ausssi sous le tableau Products, toujours dans le cadre de la classe du composant:

filter : string = '';

Visualisez le résultat et voyez que le filtre marche très bien.

Mais si jamais les données products contiennent une valeur NULL? que faire à cette instant. Nous allons modifier notre code pour gérer cette exception. Changer le type de products dans la classe du composant de sorte à avoir celà:

  products : any;

Faites glisser une valeur NULL dans votre tableau de produits, toujours dans la classe du composant catalog:

  constructor() {
    this.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,
      },
      null,
      ....

Maintenant, nous allons décorer notre template HTML avec ? de sorte à avoir ce contenu:

<ul class="products">
      <li class="product-item" *ngFor="let product of getFilteredProducts()">
        <div class="product" >
          <div class="product-details">
            <img [src]="getImageUrl(product)" [alt]="product?.name" />
            <div class="product-info">
              <div class="name"></div>
              <div class="description"></div>
              <div class="category">Part Type: </div>
            </div>
          </div>
          <div class="price">
           <div>$</div>
            <button class="cta">Buy</button>
          </div>
        </div>
      </li>
    </ul>

Gérons à ce stade là le prix. Nous voulons afficher le prix barré s’il y a une remise. Pour ce faire, nous utiliserons ngIf. Toujours sur votre page template, ajoutez ce code

<div class="price">
   <div>$</div>
       <div class="discount">
              $ 
       </div>
       <button class="cta">Buy</button>
</div>

Dans le fichier CSS du composant, ajoutez:


  .discount{
    margin-top: -15px;
    color : #d25ca1;
  }

Visualisez le contenu. Ajoutez maintenant ce code à la template

 <div class="price">
          <div *ngIf="product.discount === 0"></div>
          <div *ngIf="product.discount > 0" class="discount">
            
          </div>
          <button class="cta">Buy</button>
        </div>

Encore un tout petit peut de CSS pour restyler votre application

Communiquer entre les composants d’Angular

Nous allons créer de prime abord le composant product-details qui servira à afficher les informations descriptives sur un produit donné. Vous lancez la commande usuelle ng generate component de cette manière simplifiée :

ng g c product-details

Nous allons extraire un peu du HTML déjà développé dans la partie catalog pour la mettre dans la template du nouveau composant product-details. Donc, au niveau du fichier product-details.component.html, copiez le code suivant :

<div class="product" >
    <div class="product-details">
      <img [src]="getImageUrl(product)" [alt]="product.name" />
      <div class="product-info">
        <div class="name"></div>
        <div class="description"></div>
        <div class="category">Part Type: </div>
      </div>
    </div>
    <div class="price">
      <div *ngIf="product.discount === 0"></div>
      <div *ngIf="product.discount > 0" class="discount">
        
      </div>
      <button class="cta" (click)="buyButtonClicked(product)">Buy</button>
    </div>
  </div>

Dans la classe du composant product-details, ajoutez ce contenu. Remarque ** !** devant la variable product. Nous mentionnons à l’interpréteur que cette variable sera initialisée ultérieurement.

export class ProductDetailsComponent {
  product!:IProduct;

  getImageUrl(product: IProduct) {
    if (!product) return '';
    return '/assets/images/robot-parts/' + product.imageName;
  }


}

Vous revenez à la template HTML du composant catalog et vous ajoutez le tag du composant product-details de la manière suivante :

<div class="container">
    <div class="filters">
      <a class="button" (click)="filter='Heads'">Heads</a>
      <a class="button" (click)="filter='Arms'">Arms</a>
      <a class="button" (click)="filter='Torsos'">Torsos</a>
      <a class="button" (click)="filter='Bases'">Bases</a>
      <a class="button" (click)="filter=''">All</a>
    </div>
  
    <ul class="products">
      <li class="product-item" *ngFor="let product of getFilteredProducts()">
        <app-product-details></app-product-details>
      </li>
    </ul>
  </div>

Allez maintenant vers le CSS du composant catalog et coupez la partie commentée comme /*Product details. Vous la copiez dans le CSS du composant product-details :

/* Product Details */
  .product {
    display: flex;
    justify-content: space-between;
    padding: 20px 25px;
  }
  
  .product .product-details {
    display: flex;
    align-items: center;
  }
  
  .product img {
    width: 125px;
  }
  
  .product .product-info {
    margin-left: 25px;
  }
  
  .product .name {
    font-size: 22px;
    font-weight: bold;
  }
  
  .product .description {
    margin-top: 3px;
    font-size: 18px;
  }
  
  .product .category {
    margin-top: 20px;
    color: #777;
  }
  
  .product .price {
    display: flex;
    flex-direction: column;
    font-size: 25px;
    justify-content: space-around;
    align-items: center;
    min-width: 190px;
    color: #555;
    border-left: 2px solid #aaa;
    margin-left: 50px;
  }
  
  .product .price button {
    padding: 10px;
    width: 100px;
  }

  .discount{
    margin-top: -15px;
    color : #d25ca1;
  }

Dans la template du composant catalog, nous allons ajouter du code pour passer le produit de la fonction getFilteredProducts() vers le composant product details. Nous modifions le code de tout à l’heure de manière à avoir :

  <ul class="products">
      <li class="product-item" *ngFor="let product of getFilteredProducts()">
        <app-product-details [product]="product"></app-product-details>
      </li>
    </ul>

Dans la classe du composant product-details, vous allez ajouter @Input pour injecter la valeur product dans cette classe. On va vous demander d’ajouter un import de angular/core, faites-le :

  @Input() product!:IProduct;

Maintenant, nous ajoutons le lien entre les deux composants. Dans le fichier classe du composant catalog, nous ajoutons un tableau qui s’appelle cart. C’est là où nous allons stocker les produits sélectionnés :

cart: IProduct[] =[];

Nous ajoutons également dans la même classe, une fonction qui va vous permettre d’ajouter des produits dans le cart :

  addToCart(product:IProduct){
    this.cart.push(product);
    console.log(`product ${product.name} added to cart`);
  }

Passons à la classe du composant product-details. Nous y ajoutons maintenant un EventEmitter qui va nous permettre de passer un produit du child vers le parent, du composant product-details vers catalog :

 @Output() buy= new EventEmitter();

Et en bas de la même classe, ajoutez la fonction suivante :

 buyButtonClicked(product:IProduct){
    this.buy.emit();
  }

Modifiez juste la balise bouton dans la template du composant product-details pour qu’elle puisse lancer la fonction buyButtonClicked.

 <button class="cta" (click)=" buyButtonClicked (product)">Buy</button>

Placez-vous maintenant au niveau du HTML du composant catalog et ajoutez ce code :

<app-product-details [product]="product" (buy)="addToCart(product)"></app-product-details>

Testez maintenant votre application dans votre navigateur. Allez à l’onglet Console que vous faites apparaître à l’aide du menu du click-droit de la souris Inspecter. Vous allez remarque que le nom du produit sélectionné va apparaître comme étant ajouté à votre Cart.

A vous de jouer

Vous allez transformer le code du premier TP du HTML/CSS natif vers Angular en utilisant les composants. Au moins deux composants sont à développer. Le routage n’est pas demandé.