Javascript et Frameworks

 

Master Expert Technologie de l'information EPITECH 2020.

Co-fondateur et CTO d'une startup dans l'Edtech 2019 - fin 2022. (+3 ans)

Formation PSPO-1 Agile Scrum 2022.

Co-fondateur et CTO d'une startup dans la Deeptech fin 2022 - aujourd'hui.

Valentin MONTAGNE

Déroulement du cours

1

Découverte du Javascript

3

Découverte de Angular

2

Découverte de NodeJS et Typescript

4

Concepts et notions d'Angular avancés

Chaque séance débutera par la présentation d'un concept et de l'intérêt d'utilisation de celui-ci.

1

Théorie

Après la théorie, nous verrons alors la pratique en réalisant des exercices.

2

Pratique

Nous verrons ensemble la correction des travaux pratiques. N'hésitez pas à poser vos questions.

3

Correction

Déroulement des journées

Rendu TP et exercices

Je vais noter votre investissement durant nos séances.

Pour rendre votre travail, créez un dépôt sur Github ou Gitlab et ajoutez-moi en droit de lecture pour ValentinMontagne (Github) ou vm-marvelab (Gitlab) avec tous vos exercices et TPs.

 

Ajoutez dans un README votre nom / prénom pour que je puisse vous noter correctement.

 

N'oubliez pas le .gitignore pour éviter d'ajouter les node_modules.

Découverte du Javascript

1.

Qu'est-ce que Javascript ?

Javascript a été initialement créé pour « rendre les pages web vivantes ».

 

Les programmes de ce langage sont appelés scripts. Ils peuvent être écrits directement dans le code HTML d'une page web et s'exécuter automatiquement lors du chargement de la page.

Les scripts sont fournis et exécutés sous forme de texte brut. Ils ne nécessitent pas de préparation ou de compilation particulière pour être exécutés.

 

À cet égard, Javascript est très différent d'un autre langage appelé Java.

Pourquoi "Javascript" ?

Lorsque Javascript a été créé, il portait initialement un autre nom : « LiveScript ». Mais Java était très populaire à l'époque, et il a donc été décidé qu'il serait utile de positionner un nouveau langage comme le « petit frère » de Java.

 

Mais au fil de son évolution, Javascript est devenu un langage totalement indépendant avec sa propre spécification appelée ECMAScript, et il n'a aujourd'hui plus aucun rapport avec Java.

 

Attention il y a eu beaucoup de "versions" de JavaScript et cela est encore un sujet aujourd'hui entre CommonJS et ESM, etc.

Comment lancer un script Javascript ?

Aujourd'hui, Javascript peut s'exécuter non seulement dans le navigateur, mais aussi côté serveur, ou en fait sur n'importe quel appareil doté d'un programme spécial appelé JavaScript Engine.

Le navigateur dispose d'un moteur intégré, parfois appelé « machine virtuelle Javascript ».

Par exemple :

V8 - dans Chrome, Opera et Edge.
SpiderMonkey - dans Firefox.
Il existe d'autres moteurs comme « Chakra » pour IE, « JavaScriptCore », “Nitro” et « SquirrelFish » pour Safari, etc.

Comment fonctionne un Javascript Engine ?

Les moteurs sont compliqués. Mais les principes de base sont simples :

  1. Le moteur (intégré s'il s'agit d'un navigateur) lit les scripts.
  2. Ensuite, il convertit le script en code machine.
  3. Enfin, le code machine s'exécute, assez rapidement.

 

Le moteur applique des optimisations à chaque étape du processus. Il observe même le script compilé pendant qu'il s'exécute, analyse les données qui le traversent et optimise encore le code machine sur la base de ces connaissances.

Que peut faire Javascript ?

Le Javascript moderne est un langage de programmation « sûr ». Il ne fournit pas d'accès de bas niveau à la mémoire ou au processeur, car il a été initialement créé pour des navigateurs qui ne le nécessitent pas.

Les capacités de Javascript dépendent fortement de l'environnement dans lequel il est exécuté.

Par exemple, Node.js prend en charge des fonctions qui permettent à Javascript de lire/écrire des fichiers arbitraires, d'effectuer des requêtes réseau, etc.

Dans le navigateur, Javascript peut faire tout ce qui est lié à la manipulation des pages web, à l'interaction avec l'utilisateur et avec le serveur web.

Javascript dans le Browser - 1

Ajouter un nouveau code HTML à la page, modifier le contenu existant, modifier les styles.

Réagir aux actions de l'utilisateur, s'exécuter sur les clics de souris, les mouvements du pointeur, les pressions sur les touches.

Envoyer des requêtes sur le réseau à des serveurs distants, télécharger des fichiers (AJAX et COMET).

Javascript dans le Browser - 2

Obtenir et définir des cookies, ouvrir des popups / modals au visiteur, afficher des messages.

Mémoriser les données du côté client (localStorage, sessionStorage).

Utiliser les formulaires HTML pour valider les informations et gérer l'envoi des données et les réponses.

Les limites de Javascript dans le navigateur

Ne peut pas lire/écrire des fichiers sur le disque dur, les copier ou exécuter des programmes.

Les onglets et fenêtres ne peuvent pas communiquer pour des sites différents.

Sa capacité à recevoir des données d’autres sites / domaines est limitée.

Qu'est-ce qui rends Javascript spécial ?

Intégration complète avec HTML / CSS.

Les choses simples sont faites simplement.

Pris en charge par tous les principaux navigateurs et activé par défaut.

Les outils du navigateur - 1

Les éléments permettent de consulter le HTML et le CSS de la page actuelle. Il est possible de sélectionner un élément en particulier de la page.

Les outils du navigateur - 2

La console permet l'affichage des messages de la page actuelle. On peut y voir des messages d'erreurs, de warning avec les liens vers le code ou la requête qui a échouée.

Il est aussi possible d'exécuter du code dans la console.

Les outils du navigateur - 3

Le réseau permet d'afficher toutes les requêtes réalisées par la page actuelle et de consulter toutes les informations d'une requête précisément comme la méthode, le délai, ses headers, son body ou la réponse reçu avec le code de statut.

Les outils du navigateur - 4

L'onglet Performance permet d'afficher le résultat de programme d'analyse comme Lighthouse pour vérifier les performances du site internet sur plusieurs critères comme la vitesse d'apparition de la page, le quantité d'élément à charger, etc.

Les outils du navigateur - 5

L'onglet Memory permet d'afficher la mémoire utilisé par le site internet et de découvrir des memory leaks et de les résoudre.

Les outils du navigateur - 6

L'onglet Application permet de consulter les informations stockées côté client par le site internet comme le localStorage, sessionStorage ou encore les cookies.

Découvrir le language - 1

Pour découvrir totalement JavaScript, nous allons utiliser le site javascript.info qui permet comme W3School d'apprendre tout ce qu'il faut savoir sur JavaScript.

Je vous conseille javascript.info pour la modernité de ses informations et des exercices.

Vous pouvez choisir la langue de votre choix, n'hésitez pas à star sur Github le repository au vu de la qualité du travail fourni.

 

Vous trouverez dans la slide suivante les chapitres à réaliser.

Découvrir le language - 2

Voici les chapitres à réaliser pour les bases de JavaScript :

Une introduction, Fondamentaux JavaScript, Qualité du code, Objets: les bases, Types de données, Travail avancé avec les fonctions, Classes (Pas besoin de réaliser mixins), La gestion des erreurs, Promesses, async/await, Modules.

 

N'hésitez pas à réaliser les exercices en fin de chapitre quand il y en a pour vous entrainer.

Découvrir le language - 3

Voici les chapitres à réaliser pour la communication avec le navigateur :

Document, Introduction to Events, UI Events, Forms, controls, Chargement du document et des ressources.

 

N'hésitez pas à réaliser les exercices en fin de chapitre quand il y en a pour vous entrainer.

Découvrir le language - 4

Voici les chapitres à réaliser pour des notions avancées en JavaScript :

Les données binaires et les fichiers, Requêtes réseau, Stockage des données dans le navigateur, Animation.

 

N'hésitez pas à réaliser les exercices en fin de chapitre quand il y en a pour vous entrainer.

TP - Mon Cookie Clicker

Création du projet

Nous allons créer un clone du célèbre jeu Cookie Clicker en HTML / CSS et Javascript.

Pour cela nous allons créer un dossier "MyCookieClicker" et créer notre projet grâce à Vite :

npm create vite@latest

Vous devrez choisir le Framework Vanilla et le language en JavaScript.

Ensuite, n'oubliez pas d'installer les dépendances puis lancez le projet pour vérifier que tout fonctionne :

npm install

npm run dev

Mise en place - 1

Vous devriez avoir un affichage avec un bouton permettant d'afficher combien de fois vous avez cliqué.

Vous devriez avoir une architecture de fichier où vous avez un index.html qui contiendra tout votre HTML et fera appel à votre script Javascript principal comme ici :

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- ICI ! -->
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

N'hésitez pas à modifier le titre de la page en "Mon Cookie Clicker".

Mise en place - 2

Dans votre architecture de fichier, vous avez un dossier "public" qui contiendra ici toutes les ressources de votre site internet comme des images à afficher.

Ensuite, un dossier "src" où vous pourrez ajouter votre style, vos scripts javascript et des images que vous utilisez directement dans votre code.

Pour plus de propreté, nous allons créer un dossier par type d'éléments pour mieux organiser "src" :

  • Un dossier "assets" où mettre les images.
  • Un dossier "styles" où mettre les fichiers de style.
  • Un dossier "scripts" où mettre vos scripts Javascript.

Déplacez les fichiers dans les bons dossiers et mettez à jour les différents imports dans vos fichiers.

Mise en place - 2

Dans votre architecture de fichier, vous avez un dossier "public" qui contiendra ici toutes les ressources de votre site internet comme des images à afficher.

Ensuite, un dossier "src" où vous pourrez ajouter votre style, vos scripts javascript et des images que vous utilisez directement dans votre code.

Pour plus de propreté, nous allons créer un dossier par type d'éléments pour mieux organiser "src" :

  • Un dossier "assets" où mettre les images.
  • Un dossier "styles" où mettre les fichiers de style.
  • Un dossier "scripts" où mettre vos scripts Javascript.

Déplacez les fichiers dans les bons dossiers et mettez à jour les différents imports dans vos fichiers.

Mise en place - 3

Ouvrez le fichier main.js, c'est ici le point d'entrée de votre logique pour la création de votre jeu.

Vous pouvez voir que ce script importe des ressources importantes comme le style ou certaines images et même d'autres scripts Javascript.

Ensuite, il génère dynamiquement du HTML dans la balise ayant l'id "app". C'est ici qu'on réalisera les modifications de l'affichage du jeu.

On appelle ensuite la fonction "setupCounter" pour permettre de lier à notre bouton la logique du counter.

Nous allons continuer à développer la logique ici pour réaliser notre jeu.

Création du jeu - 1

Nous allons changer le HTML de notre app pour mieux représenter notre jeu dans le fichier main.js :

 

 

 

 

Vous pouvez supprimer les images de Javascript et Vite et le counter.js, nous n'en aurons plus besoin. 

Récupérez l'image ci-dessous en l'enregistrant sur votre ordinateur et ajoutez la dans votre projet dans "assets" :

Lien vers l'image

import "../styles/style.css";

document.querySelector("#app").innerHTML = `
    <h1>Welcome to my Cookie Clicker!</h1>
    <main id="game">
    </main>
`;

Création du jeu - 2

Nous allons maintenant commencer la logique du jeu, pour cela créez un fichier "game.js" et un fichier "clickable-area.js".

Ajoutez dans clickable-area.js le code suivant :

import CookieIMG from "/cookie.png";

export class ClickableArea {
  gameElement = null;
  onClick = null;

  constructor(gameElement, onClick) {
    this.gameElement = gameElement;
    this.onClick = onClick;
  }

  render() {
    // On crée un nouvel élément du DOM.
    const clickableAreaElement = document.createElement("section");

	clickableAreaElement.id = "game-clickable-area";
    // On modifie son HTML.
    clickableAreaElement.innerHTML = `
        <img id="cookie" src=${CookieIMG} width="256px" height="256px" alt="An awesome cookie." />
    `;
    // On ajoute un listener sur l'évènement "click" à l'élément.
    clickableAreaElement.addEventListener("click", this.onClick);
    // Il faut ajouter l'élément au DOM pour pouvoir le voir
    // On l'ajoute donc à notre élément Game.
    this.gameElement.append(clickableAreaElement);
  }
}

Création du jeu - 3

Ajoutez dans game.js le code suivant :

import { ClickableArea } from "./clickable-area";

export class Game {
  // Game Properties
  cookies = 0;

  // Game Elements
  gameElement = null;
  scoreElement = null;

  // Game Components
  clickableArea = null;

  constructor(config) {
    // Récupère le nombre de cookie de base via la configuration.
    this.cookies = config.cookies;
    // Récupère l'élément avec l'id game.
    this.gameElement = document.querySelector("#game");
    // Crée le composant ClickableArea qui gère la logique de la zone cliquable.
    // On passe en argument l'élément Game pour permettre l'ajout d'HTML à l'intérieur.
    // Et une fonction Callback pour réagir à l'événement de clique.
    this.clickableArea = new ClickableArea(
      this.gameElement,
      this.onClickableAreaClick
    );
  }

  // Lance le jeu
  start() {
    this.render();
  }

  // Génère les éléments à afficher.
  render() {
    this.renderScore();
    this.clickableArea.render();
  }

  // Génère l'affichage du score.
  renderScore() {
    this.scoreElement = document.createElement("section");
	this.scoreElement.id = "game-score";
    this.gameElement.append(this.scoreElement);
    this.updateScore();
  }

  // Met à jour l'affichage du score.
  updateScore() {
    this.scoreElement.innerHTML = `
        <span>${this.cookies} cookies</span>
    `;
  }

  // Ici on utilise une fonction fléchée pour avoir encore accès au this de Game.
  // Sans fonction fléchée, le this serait celui de l'élément lié au click.
  onClickableAreaClick = () => {
    // On ajoute 1 point aux cookies pour chaque click.
    this.cookies += 1;
    // Par soucis de performance car les changements au DOM sont très lourd,
    // On demande à la Window d'attendre la prochaine frame d'animation
    // pour réaliser les changements.
    window.requestAnimationFrame(() => {
      this.updateScore();
    });
  };
}

Création du jeu - 4

Enfin mettez à jour main.js :








 

Relancez la page, essayez votre Clicker, vous devriez voir le score monter à chaque clique de votre part.

import "../styles/style.css";
import { Game } from "./game";

document.querySelector("#app").innerHTML = `
    <h1>Welcome to my Cookie Clicker!</h1>
    <main id="game">
    </main>
`;

const game = new Game({
  cookies: 0,
});

game.start();

Animons un élément - 1

Nous réalisons un jeu, notre cookie manque de punch, nous allons donc ajouter des animations à la zone cliquable.

Pour cela nous allons déjà permettre à notre cookie d'être animé de base en CSS, créez un fichier "game.css" et importez le dans game.js.

Ajoutez l'animation CSS suivante pour le cookie :

@keyframes spinAndPulse {
  0% {
    transform: rotate(0deg) scale(1);
    filter: brightness(1);
  }
  50% {
    transform: rotate(180deg) scale(1.1);
    filter: brightness(1.1);
  }
  100% {
    transform: rotate(360deg) scale(1);
    filter: brightness(1);
  }
}

#cookie {
  animation: spinAndPulse 3s linear infinite;
  transform-origin: center center;
  will-change: transform;
  cursor: pointer;
}

Animons un élément - 2

Vérifiez que l'animation fonctionne correctement.

Ensuite, ajoutons la logique permettant d'avoir une réaction lorsque que l'on clique sur le cookie :

// game.css

#game-clickable-area.active {
  transform: scale(1.2);
  transition: transform 0.1s;
}
// clickable-area.js
// Il faut ajouter dans les propriétés de la classe
// la propriété "clickableAreaElement"

render() {
    // On crée un nouvel élément du DOM.
    this.clickableAreaElement = document.createElement("section");
    this.clickableAreaElement.id = "game-clickable-area";
    // On modifie son HTML.
    this.clickableAreaElement.innerHTML = `
        <img id="cookie" src=${CookieIMG} width="256px" height="256px" alt="An awesome cookie." />
    `;
    // On ajoute un listener sur l'évènement "click" à l'élément.
    this.clickableAreaElement.addEventListener("click", () => {
    	// On ajoute ici la logique d'animation pour la réaction au clique.
      window.requestAnimationFrame(() => {
        this.clickableAreaElement.classList.add("active");
        setTimeout(() => {
          window.requestAnimationFrame(() => {
            this.clickableAreaElement.classList.remove("active");
          });
        }, 100);
      });
      this.onClick();
    });
    // Il faut ajouter l'élément au DOM pour pouvoir le voir
    // On l'ajoute donc à notre élément Game.
    this.gameElement.append(this.clickableAreaElement);
}

TP terminé

Vérifiez que tout fonctionne correctement.

Bravo, vous avez maintenant un début pour votre Cookie Clicker, nous allons maintenant réaliser des exercices pour ajouter d'autres fonctionnalités à notre jeu.

 

Appelez-moi pour que l'on puisses valider ensemble.

Exercice 1 : Gestion du Shop

Notre joueur ne peut gagner des points qu'en réalisant des clicks, comme tout jeu de clicker, on doit permettre au joueur de gagner des points passivement en achetant des améliorations.

Réalisez la logique permettant l'affichage d'une boutique sur la partie droite de l'écran, la boutique doit afficher une liste d'améliorations possibles qui donne un gain de point passif en fonction du temps.

Ajoutez la première amélioration "Cursor" qui permet l'achat d'un curseur qui va cliquer automatiquement sur le cookie pour un gain passif de cookie de 0.1 cookie par seconde par curseur acheté avec un prix qui va suivre la formule suivante : 10 + curseurs * 3.

Créez un nouveau fichier "shop.js" qui va contenir toute la logique d'affichage, nous réaliserons la logique du score plus tard.

Appelez-moi pour que l'on puisses valider ensemble.

Exercice 2 : Gestion du Score

Vous devez ajouter dans votre classe Game la logique permettant d'avoir un gain passif de cookie (passiveGain) qui doit être à 0 au début du jeu et qui doit augmenter en fonction des achats de la boutique.

Ajouter un callback dans les paramètres du constructeur de votre classe Shop pour permettre à Shop de notifier Game dès qu'il y a un achat.

Dans ce callback, réalisez la logique permettant de mettre à jour le gain passif, de retirer du score l'achat et l'affichage final du score et du gain passif.

Créez dans le start un interval permettant d'ajouter toutes les secondes le nouveau montant de cookie au score.

 

Appelez-moi pour que l'on puisses valider ensemble.

Exercice 3 : Apparition aléatoire

Pour éviter que notre joueur s'ennuie en attendant que ses cookies augmentent, nous allons créer un système d'apparition aléatoire.

Ajoutez un fichier "random-spawn.js" qui va permettre de faire apparaître à un endroit aléatoire sur l'écran un cookie doré qui disparaîtra dans 5 secondes. Ceci donne une quantité aléatoire de cookie au joueur s'il clique dessus entre 1 et passiveGain * 1000.

Ajoutez une animation CSS pour faire apparaître et disparaître le cookie doré en fondu.

Lien vers l'image




Appelez-moi pour que l'on puisses valider ensemble.

Exercice 4 : Sauvegarde

Pour éviter que notre joueur ne perde sa partie dès qu'il quitte la page, vous allez réaliser un système de sauvegarde et de chargement via le stockage local (localStorage).

Pour cela, vous devez améliorer l'objet "config" dans le constructeur de la classe Game pour que l'on puisse passer dans cet objet tous les éléments du jeu comme le gain passif de cookie et les achats déjà réalisé par le joueur.

Il faut donc une méthode .save() à Game qui enregistre dans le localStorage cet objet configuration et une méthode load() qui permet à Game de récupérer la configuration du localStorage.

 

Appelez-moi pour que l'on puisses valider ensemble.

Bonus - Exercice 5 : Améliorations

Votre jeu commence à prendre forme !

Maintenant, améliorez votre jeu en continuant de reproduire des aspects de Cookie Clicker ou en ajoutant votre touche personnelle.

Par exemple, d'autres fonctionnalités, des décors, de la musique, des animations lors des interactions avec la souris, etc.

 

Appelez-moi pour que l'on puisses valider ensemble.

Découverte de Typescript

2.

Découvrir Typescript

Typescript est la version de Javascript avec des types strictes.

Il est important pour un projet d'ampleur d'être en Typescript pour éviter des erreurs liées au typage.

 

Pour découvrir Typescript, je vous invite à réaliser le tutoriel officiel de W3School qui permet une prise en main rapide.

https://www.w3schools.com/typescript/index.php

 

Et la Cheatsheet pour vous accompagner pendant la réalisation de votre projet.

https://www.typescriptlang.org/cheatsheets

Découverte d'Angular

3.

Qu'est-ce qu'Angular ?

Framework web front-end open-source basé sur TypeScript, développé et maintenu par Google.

Offre des outils solides pour construire des applications rapides et fiables suivant les bonnes pratiques.

Offre une structure de projet et une architecture bien définie.

Pourquoi utiliser Angular ?

Parfait pour gérer des projets d'ampleurs grâce à son architecture prédéfinie même sur des grandes équipes.

Framework complet et tout-en-un, pas besoin de dépendre de librairies externes pour les bases.

Facile de prise en main pour avoir un bon rendu.

Easy to learn, Hard to master.

Les désavantages d'Angular

Plus lourd, moins rapide en terme de performance que JS Vanilla ou Svelte.

Les notions avancées sont complexes comme les Zones ou RxJS

Les migrations sont parfois lourdes au vu du framework all-in-one.

Benchmark des Frameworks

Les essentiels

Dans les frameworks Frontend pour Javascript, on retrouvera les mêmes sujets :

  • Les composants : une brique réutilisable de l’interface utilisateur. Il regroupe du code HTML, CSS et JavaScript pour afficher une partie autonome d’une page web, comme un bouton, une carte de produit ou un formulaire. 
  • La réactivité : la capacité d’une interface à se mettre automatiquement à jour dès que les données changent. Plutôt que de modifier manuellement le DOM, le framework détecte les modifications et rafraîchit uniquement les éléments concernés.
  • Les templates dynamiques : des structures HTML où l’on peut insérer des données, des conditions ou des boucles directement dans le code.

Les composants

Un composant est une brique, un block que l'on peut réutiliser partout dans notre application.

Les composants peuvent être imbriqués les uns dans les autres et échanger des données, ce qui facilite la création d’interfaces complexes, modulaires et maintenables.

Exemple :

Exemple d'un composant Angular

La réactivité

En Angular, vous utilisez des signaux pour créer et gérer l'état. Un signal est une enveloppe légère autour d'une valeur.

Cela permet plus de granularité sur la gestion des états, dans les premières versions d'Angular, le Framework vérifier l'ensemble des composants quand une valeur changeait car il était difficile de savoir ce qui était lié à cette valeur.

Il fallait activer un mode spécial et utiliser un outil interne à Angular pour définir qu'un composant devait être mis à jour.

 

Maintenant, tout est définie clairement grâce à signal et computed directement dans le code.

Exemple de réactivité en Angular

Templates dynamiques

Les templates de composants ne sont pas simplement du HTML statique - ils peuvent utiliser les données de votre classe de composants et mettre en place des Bindings pour l'interaction avec l'utilisateur.

Cela permet d'assigner la donnée au contenu d'une balise mais aussi à ses attributs ou même en réponse à un évènement.

 

Il est aussi possible de contrôler l'affichage avec des conditions ou des boucles. Par exemple, en fonction d'un booléen, afficher une information sur la vue ou boucler pour afficher tous les éléments d'une liste.

Exemple de Templates dynamiques - Binding

Exemple de Templates dynamiques - Binding Events

Exemple de Templates dynamiques - Control Flow

Et la gestion de la donnée et de la logique métier ?

Pour éviter de laisser toute la responsabilité à nos composants pour la gestion de la donnée, il y a un élément qui permet de regrouper cette logique qui est ensuite injectable partout dans notre application : les Services.

Un service représente une seule source de vérité pour un concept précis. Par exemple, pour la gestion de vos messages dans une application de chat, on utiliserait un MessagesService qui serait en charge d'un CRUD pour nos messages.

 

Pour suivre les bonnes pratiques, Angular utilise l'injection de dépendance pour les découpler des composants, ce qui facilite leur testabilité, modularité, et maintenabilité.

Exemple de Service en Angular

Les signaux

Les signaux

Un signal est une enveloppe autour d'une valeur qui notifie les composants l'utilisant lorsque cette valeur change. Les signaux peuvent contenir n'importe quelle valeur, des primitives aux structures de données complexes.

Vous lisez la valeur d'un signal en appelant sa fonction getter, ce qui permet à Angular de savoir où le signal est utilisé :

mySignal()

 

Les signaux peuvent être en écriture ou en lecture seule :

mySignal.set(3)

mySignal.update(value => value + 1)

Les signaux - OnPush

Lorsqu’un signal est lu dans le template d’un composant utilisant la stratégie de détection des changements OnPush, Angular suit ce signal comme une dépendance de ce composant. Lorsque la valeur de ce signal change, Angular marque automatiquement le composant pour s’assurer qu’il soit mis à jour lors de la prochaine exécution de la détection des changements.

 

Cela permet la réactivité automatique, tout en évitant les problèmes de performance car sans la stratégie OnPush, Angular vérifie globalement dans tout l'arbre des composants si il y a eu des changements.

Les signaux - OnPush

Les signaux - Effects

Un Effect est une opération qui s’exécute chaque fois qu’une ou plusieurs valeurs de signaux changent. Vous pouvez créer un effet à l’aide de la fonction effect.

 

Les effets sont rarement nécessaires dans la majorité du code d’une application. Voici quelques exemples de cas où un effet peut être une bonne solution :

  • Journaliser les données affichées et leurs changements, que ce soit à des fins d’analytique ou de débogage.

  • Garder des données synchronisées avec window.localStorage.

  • Ajouter un comportement personnalisé au DOM qui ne peut pas être exprimé avec la syntaxe des templates.

Les signaux - Effects - Exemple

Les signaux - LinkedSignal

En Angular, un LinkedSignal est un type spécial de signal qui est automatiquement lié à une instance de composant ou de directive.

Une fois qu'une valeur lui est assigné, il garde cette valeur jusqu'à ce que le signal dont il dépend soit mis à jour.

 

En bref :

  • Modifiable (possède les méthodes .set() et .update())

  • Peut être initialisé à partir d'autres signaux

  • Une fois modifié, il devient indépendant de sa source

  • Utile pour les contrôles de formulaire, les préférences utilisateur, etc.

Les signaux - LinkedSignal - Exemple

Les signaux - Resource

Vous pouvez utiliser une Resource pour effectuer n’importe quel type d’opération asynchrone, mais le cas d’usage le plus courant est la récupération de données depuis un serveur.

L’exemple suivant crée une resource pour récupérer des données utilisateur.

La façon la plus simple de créer une Resource est d’utiliser la fonction resource.

Les signaux - Resource - Exemple

Les composants

Qu'est-ce qu'un composant ?

Chaque composant doit avoir :

  • Une classe TypeScript avec des comportements, comme la gestion des interactions utilisateur et l'appel aux services pour la récupération de données depuis un serveur

  • Un template HTML qui contrôle ce qui est rendu dans le DOM

  • Un sélecteur CSS qui définit comment le composant est utilisé dans le HTML

Vous fournissez des informations spécifiques à Angular pour un composant en ajoutant un décorateur @Component au-dessus de la classe TypeScript.

Standalone

Dans les versions précédentes d'Angular, il y avait toute une gestion à base de Module. C'est dans ce type de fichier que l'on pouvait gérer les imports / exports et les dépendances pour chaque groupe de composants.

Maintenant, étant trop verbeux et lourd, Angular a simplifié ça avec une gestion en "Standalone", le composant peut directement via @Component décrire ses dépendances.

Selectors

Chaque composant définit un sélecteur CSS qui détermine comment le composant est utilisé :

Angular associe les sélecteurs de manière statique au moment de la compilation. Modifier le DOM à l’exécution, que ce soit via des liaisons Angular ou avec des API du DOM, n’affecte pas les composants rendus.

 

Un élément ne peut correspondre qu’à un seul sélecteur de composant. Si plusieurs sélecteurs de composants correspondent à un même élément, Angular génère une erreur.

 

Les sélecteurs de composants sont sensibles à la casse.

Selectors - 2

Selector Description Exemples
Type selector Se base sur le nom de la balise HTML. profile-photo
Attribute selector Se base sur les attributs des balises HTML ou même une valeur précise. [dropzone][type="reset"]
Class selector Se base sur les classes CSS. .menu-item

Exemples de combinaison :

  • button[type="reset"]
  • drop-zone, [dropzone]

Appel d'un composant

Un composant peut appeler d'autres composants dans sa template en écrivant son sélecteur :

Inputs

Lorsque vous utilisez un composant, vous souhaitez généralement lui transmettre des données.
Un composant spécifie les données qu’il accepte en déclarant des entrées (inputs) :

Inputs - 2

Inputs - Model

Les entrées de type Model sont un type spécial d’entrée qui permet à un composant de propager de nouvelles valeurs vers son composant parent.

Lors de la création d’un composant, vous pouvez définir une entrée de type modèle de la même manière qu’une entrée standard.

Les deux types d’entrée permettent à un utilisateur de lier une valeur à la propriété. Cependant, les entrées de type Model permettent à l’auteur du composant d’écrire des valeurs dans cette propriété.
Si la propriété est liée avec une liaison bidirectionnelle, la nouvelle valeur est propagée à cette liaison.

Inputs - Model - Exemple

Inputs - Model - Change Event

Content projection

Il est souvent nécessaire de créer des composants qui servent de conteneurs pour différents types de contenu.
Par exemple, vous pouvez vouloir créer un composant de carte :

Content projection - Placeholders

Content projection - Fallback

Content projection - ngProjectAs

Lifecycle

Phase Méthode Description
Création constructor Constructeur classique de Javascript.
Détection des changements ngOnInit S’exécute une seule fois après qu’Angular a initialisé toutes les entrées du composant.
ngOnChanges A chaque fois que les entrées changent.
ngDoCheck A chaque fois qu'il est vérifié.
ngAfterContentInit Une seule fois après que son contenu a été initialisé.
ngAfterContentChecked A chaque fois que son contenu a été vérifié.
ngAfterViewInit Une seule fois après que sa vue a été initialisée.
ngAfterViewChecked A chaque fois que sa vue a été vérifiée.
Affichage afterNextRender Une seule fois, la prochaine fois que tous les composants ont été rendus dans le DOM.
afterEveryRender A chaque fois que tous les composants ont été rendus dans le DOM.
Destruction ngOnDestroy Une seule fois juste avant qu'il ne soit détruit.

Queries

Un composant peut définir des requêtes (queries) qui permettent de rechercher des éléments enfants et de lire des valeurs à partir de leurs injecteurs.

On les utilisent le plus souvent les requêtes pour obtenir des références à des composants enfants, directives, éléments du DOM, et bien plus encore.

Toutes les fonctions de requête renvoient des signaux qui reflètent les résultats les plus à jour.

Il existe deux catégories de requêtes : les requêtes de vue (view queries) et les requêtes de contenu (content queries).

Queries - Exemple

Content Queries et Required

Les requêtes de contenu (content queries) récupèrent des résultats à partir des éléments présents dans le contenu du composant (ng-content).
Vous pouvez effectuer une requête pour obtenir un seul résultat en utilisant la fonction contentChild ou plusieurs avec contentChildren.

Dans certains cas, notamment avec viewChild, vous savez avec certitude qu’un enfant spécifique est toujours présent.
Pour ces situations, vous pouvez utiliser une requête obligatoire (required query).

Content Queries et Required - Exemple

Content Queries et Required - Exemple

Query locator

Vous pouvez également spécifier un localisateur sous forme de chaîne de caractères correspondant à une variable de référence de template.

DOM API

Angular gère la plupart des opérations de création, de mise à jour et de suppression du DOM pour vous.
Cependant, il peut arriver, dans de rares cas, que vous ayez besoin d’interagir directement avec le DOM d’un composant.
Les composants peuvent injecter ElementRef pour obtenir une référence à l’élément hôte du composant :

Héritage

Les composants Angular sont des classes TypeScript et suivent les règles standard d’héritage de JavaScript.

Un composant peut étendre n’importe quelle classe de base, d'autre composant ou de directive :

Les bonnes pratiques pour les composants

Presentational vs Container

Presentational Component : UserCardComponent

  • Conçu pour l’affichage et peut être facilement testé de manière unitaire.

  • N’a pas de dépendance métier directe (ex. services).

  • Lit des données via @Input(), émet des événements via @Output().

 

Container Component : UserProfileComponent

  • Récupère les données via des services, le store, des routes...

  • Contient souvent la logique métier ou de navigation.

  • Compose plusieurs composants de présentation.

Single Responsibility Principle

C’est un principe de design qui stipule qu’un module ou une classe ne devrait avoir qu’une seule raison de changer. (SOLID)

 

N'hésitez pas à créer un composant pour chaque élément que vous avez à afficher tout en gardant en tête que votre composant doit être découpé par le plus petit élément ayant une fonction.

 

Exemple :

ChatComponent qui représente une conversation avec un autre utilisateur ne doit pas tout gérer directement.

On doit retrouver des composants à l'intérieur comme : MessageComponent, MessageGroupComponent, AuthorComponent, MessageInputComponent, etc.

Unidirectional Data Flow

Lors de la communication entre vos composants, n'hésitez pas à prévoir en amont le flux de la donnée dans votre application entre les Services, les Pages et les Composants.

 

Pour un flux de donnée simple :

Je conseille un modèle de flux de données unidirectionnel : les données partent du parent vers l’enfant (@Input()), et les événements remontent (@Output()) permet de renforcer la clarté, la maintenabilité, et la prévisibilité des interfaces.

 

Exemple :

ChatComponent doit appeler le service ChatService avec la méthode sendMessage quand MessageInputComponent renvoie un event MessageSent.

Atomic Design

Atomic Design est une méthode de conception d’interface qui organise les composants en cinq niveaux hiérarchiques, du plus simple au plus complexe :

  1. Atoms : éléments de base (bouton, champ texte, icône).

  2. Molecules : regroupement d’atomes (ex. champ + label).

  3. Organisms : sections complètes (ex. formulaire, en-tête).

  4. Templates : structure de page avec mise en page, sans données réelles.

  5. Pages : version finale avec contenu réel.

 

Cette méthode est souvent utilisée pour réaliser des Design Systems.

Atomic Design avec Angular

Atomic Design permet :

  • Crée des composants réutilisables et cohérents.

  • Facilite la maintenance et la montée en complexité.

  • Exemple :

    • ButtonComponent (atom)

    • SearchBarComponent (molecule)

    • NavbarComponent (organism)

    • MainLayoutComponent (template)

    • HomePageComponent (page)

 

Cela favorise une architecture modulaire, claire et évolutive.

Les Pipes

Qu'est-ce qu'un Pipe ?

Les pipes sont des opérateurs spéciaux dans les expressions de template Angular qui permettent de transformer les données de manière déclarative dans le template.


Les pipes vous permettent de déclarer une fonction de transformation une seule fois et de l’utiliser ensuite dans plusieurs templates.


Les pipes Angular utilisent le caractère barre verticale (|), inspiré des pipes Unix.

Exemple d'utilisation

Built-ins

Créer un Pipe

Les Directives

Qu'est-ce qu'une Directive ?

En Angular, il y a deux types de Directive :

 

Les directives d'attribut (attribute directives) qui modifie l’apparence ou le comportement des éléments du DOM et des composants Angular.

 

Les directives structurelles (structural directives) qui sont appliquées à un élément <ng-template> qui permettent d’afficher conditionnellement ou de manière répétée le contenu de ce <ng-template>.

Exemple de directive d'attribut

Exemple de directive structurelle

Les Formulaires

Comment fonctionne les formulaires en Angular ?

En Angular, il y a deux types de formulaire :

 

Formulaires basés sur les templates (Template-driven) :
Déclarés dans le HTML avec des directives Angular comme ngModel. Simples à mettre en place, adaptés aux formulaires simples.

 

Formulaires réactifs (Reactive forms) :
Basés sur le code TypeScript avec une approche orientée modèle (FormControl, FormGroup). Plus robustes, testables et adaptés aux formulaires complexes ou dynamiques.

Qu'est-ce qu'un formulaire réactif ?

Les formulaires réactifs offrent une approche pilotée par le Model pour gérer les champs de formulaire comme :

  • Créer et mettre à jour un champ de formulaire de base.
  • Regrouper plusieurs champs.
  • Valider les valeurs du formulaire.

Créer un FormControl

Chaque élément de notre formulaire sera un FormControl, par exemple l'input qui va gérer l'username de notre utilisateur dans notre formulaire d'inscription.

Modifier un FormControl

Grouper des FromControls

Pour grouper des FormControls, deux éléments existent :

 

FormGroup : définit un formulaire avec un ensemble fixe de champs gérés ensemble. Peut être imbriqué pour créer des formulaires complexes.

 

FormArray : permet un formulaire dynamique avec ajout/suppression de champs à l'exécution. Peut aussi être imbriqué pour plus de complexité.

Exemple de FromGroup

Exemple de FromGroup - Nested

Exemple de validation et modification de FromGroup

Exemple de formulaire dynamique

Exemple de gestion des erreurs

Exemple de création d'un Validator

Les requêtes

Comment faire des requêtes en Angular ?

En Angular, il existe un service pour gérer les requêtes du nom de HttpClient qui offre les fonctionnalités principales suivantes :

  • Réponses typées

  • Gestion simplifiée des erreurs

  • Interception des requêtes et réponses

  • Outils puissants pour les tests

HttpClient propose des méthodes pour chaque méthode HTTP (GET, POST, etc.) afin de charger ou modifier des données. Chaque méthode renvoie un Observable RxJS qui envoie la requête lors du subscribe et émet la réponse du serveur.

Exemple d'utilisations

Exemple de suivi de progression

La navigation

Comment gérer la navigation en Angular ?

En Angular, la navigation est gérer par le framework avec la librairie @angular/router, elle permet :

  • Routes : associent une URL à un composant à afficher.

  • Outlets : zones dans le template où s’affichent les composants selon la route active.

  • Links : permettent de naviguer entre les routes sans recharger la page.

Gérer les routes

En Angular, les routes sont définies dans des fichiers finissant en .routes.ts, chaque route est défini par une propriété path et component pour lier un chemin à un composant.

Pour mieux différencier les composants des pages, je vous conseille de nommer la classe du composant avec Page comme suffixe, comme HomePage.

Lazy loading

Pour améliorer les performances, on priorise le loading des pages d'accueil et on permet à Angular de ne charger les autres pages que quand l'utilisateur tente d'y accéder.

Les autres propriétés

Nested Routes

Les routes imbriquées, ou routes enfants, sont une technique courante pour gérer une navigation plus complexe, où un composant possède une sous-vue qui change en fonction de l’URL.

Nested Routes - Exemple

Naviguer avec Angular

La directive RouterLink est l’approche déclarative d’Angular pour la navigation. Elle permet d’utiliser des balises <a> classiques tout en les intégrant parfaitement au système de routage d’Angular.

Naviguer avec Angular - 2

Pour les cas où la navigation dépend de la logique, des actions de l’utilisateur ou de l’état de l’application on utilise Router, vous pouvez naviguer dynamiquement, transmettre des paramètres et contrôler le comportement de la navigation.

Accéder à l'état d'une route

Les snapshots de route contiennent des informations essentielles sur la route, comme ses paramètres, ses données et ses routes enfants. De plus, les snapshots sont statiques et ne reflètent pas les changements futurs.

Gérer l'accès à une route

Utilisez les Guard pour les routes pour empêcher les utilisateurs d’accéder à certaines parties de l’application sans autorisation.

Structure du projet

TP - CookingBuddy

Création du projet

Nous allons maintenant créer une web application nommé "CookingBuddy", le but de cette solution est de permettre à un utilisateur de consulter des recettes de cuisine, d'ajouter ses propres recettes et de pouvoir créer des listes de recettes.

Pour cela nous allons créer un dossier "CookingBuddy" et créer notre projet grâce au CLI de Angular :

npm install -g @angular/cli

 

Créez le projet (toutes les valeurs par défaut, et SASS pour le CSS) :

ng new cooking-buddy

 

Lancez le projet pour vérifier que vous êtes prêt :

cd cooking-buddy && npm start

Installation de Material

Pour éviter de partir d'une page totalement blanche, on va utiliser le Design System Material de Google qui possède une librairie Angular.

Nous allons l'intégrer à notre projet :

ng add @angular/material

Acceptez le "Set up global Angular Material typography styles?" et prenez le thème de votre choix.

Retirez le placeholder dans app.html sans enlever le <router-outlet /> et nous allons ajouter un composant Material pour vérifier que tout fonctionne.

Ajoutez dans l'import de app.ts le module MatSlideToggleModule et ajoutez dans app.html la balise <mat-slide-toggle>Toggle me!</mat-slide-toggle>

Création de la page d'inscription

Après avoir vérifié que votre Toggle apparait bien sur la page d'accueil, nous allons créer une page d'inscription pour simuler une fausse authentification sur la solution.

Par soucis de simplicité, nous allons utiliser une API publique de test TheMealDB pour simuler notre backend. Pour tous les comportements particuliers que nous allons créer, nous stockerons alors nos changements dans le localStorage.

 

Créez la nouvelle page d'inscription et la page d'accueil :

ng g component features/register/register-page

ng g component features/home/home-page

Création de l'inscription

Maintenant, nous allons créer nos routes et gérer la redirection sur la page d'inscription si l'utilisateur n'est pas connecté grâce à un Guard.

 

Créez le service AuthService :

ng g service core/services/auth.service

Créez le guard :

ng g guard core/guards/auth

Créez le model user :

ng g interface core/models/user

 

Modifiez le fichier user.ts avec le code à la slide suivante.

Création de l'inscription - 2

Nous allons maintenant modifier le fichier auth.service.ts à la slide suivante.

export interface User {
  id: string;
  email: string;
  password: string;
  firstName: string;
  lastName: string;
  createdAt: Date;
}

export interface ReadOnlyUser extends Omit<User, 'password'> {}

export interface CreateUserDto {
  email: string;
  password: string;
  firstName: string;
  lastName: string;
}

export interface LoginResponse {
  user: User;
  token: string;
}

Création de l'inscription - 3

import { computed, Injectable, signal } from '@angular/core';
import {
  User,
  CreateUserDto,
  LoginResponse,
  ReadOnlyUser,
} from '../models/user';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  private readonly USERS_KEY = 'app_users';
  private readonly CURRENT_USER_KEY = 'current_user';
  private readonly TOKEN_KEY = 'auth_token';

  private _currentUser = signal<User | null>(this._getCurrentUserFromStorage());
  // Crée une version readonly du currentUser et enlève le password.
  public currentUser = computed<ReadOnlyUser | null>(() => {
    const currentUser = this._currentUser();

    if (currentUser) {
      const readOnlyUser = { ...currentUser, password: undefined };

      delete readOnlyUser.password;
      return readOnlyUser;
    }
    return null;
  });

  constructor() {
    // Initialiser avec l'utilisateur stocké s'il existe
    const storedUser = this._getCurrentUserFromStorage();

    if (storedUser) {
      this._currentUser.set(storedUser);
    }
  }

  get isAuthenticated(): boolean {
    return !!this._currentUser && !!this._getToken();
  }

  // Créer un nouvel utilisateur
  register(userData: CreateUserDto): LoginResponse {
    // Vérifier si l'email existe déjà
    const existingUsers = this._getAllUsers();
    const emailExists = existingUsers.some(
      (user) => user.email.toLowerCase() === userData.email.toLowerCase()
    );

    if (emailExists) {
      throw new Error('Cet email est déjà utilisé');
    }

    // Créer le nouvel utilisateur
    const newUser: User = {
      id: this._generateId(),
      email: userData.email,
      password: userData.password,
      firstName: userData.firstName,
      lastName: userData.lastName,
      createdAt: new Date(),
    };

    // Stocker l'utilisateur
    this._saveUser(newUser);

    // Connecter automatiquement l'utilisateur
    const token = this._generateToken();

    this._setCurrentUser(newUser, token);

    return {
      user: newUser,
      token,
    };
  }

  // Connexion utilisateur
  login(email: string, password: string): LoginResponse {
    const users = this._getAllUsers();
    const userData = users.find(
      (u) => u.email.toLowerCase() === email.toLowerCase()
    );

    if (!userData) {
      throw new Error('Email ou mot de passe incorrect');
    }

    // Vérifier le mot de passe (dans un vrai app, c'est côté serveur)
    if (userData.password !== password) {
      throw new Error('Email ou mot de passe incorrect');
    }

    const token = this._generateToken();

    this._setCurrentUser(userData, token);

    return {
      user: userData,
      token,
    };
  }

  // Déconnexion
  logout(): void {
    localStorage.removeItem(this.CURRENT_USER_KEY);
    localStorage.removeItem(this.TOKEN_KEY);
    this._currentUser.set(null);
  }

  // Vérifier si un email existe
  checkEmailExists(email: string): boolean {
    return this._getAllUsers().some(
      (user) => user.email.toLowerCase() === email.toLowerCase()
    );
  }

  // Méthodes privées
  private _getAllUsers(): User[] {
    const users = localStorage.getItem(this.USERS_KEY);

    return users ? JSON.parse(users) : [];
  }

  private _saveUser(user: User): void {
    const users = this._getAllUsers();
    users.push(user);
    localStorage.setItem(this.USERS_KEY, JSON.stringify(users));
  }

  private _setCurrentUser(user: User, token: string): void {
    localStorage.setItem(this.CURRENT_USER_KEY, JSON.stringify(user));
    localStorage.setItem(this.TOKEN_KEY, token);
    this._currentUser.set(user);
  }

  private _getCurrentUserFromStorage(): User | null {
    const user = localStorage.getItem(this.CURRENT_USER_KEY);

    return user ? JSON.parse(user) : null;
  }

  private _getToken(): string | null {
    return localStorage.getItem(this.TOKEN_KEY);
  }

  private _generateId(): string {
    return Date.now().toString() + Math.random().toString(36).substr(2, 9);
  }

  private _generateToken(): string {
    return (
      'token_' + Date.now().toString() + Math.random().toString(36).substr(2, 9)
    );
  }
}

Création de l'inscription - 4

Ajoutez la logique de auth-guard.ts :

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isAuthenticated) {
    return true;
  } else {
    router.navigate(['register']);
    return false;
  }
};

Modifiez le fichier app.routes.ts avec le code de la slide suivante.

Création de l'inscription - 5

Retirez dans app.html le composant Toggle et vérifiez que tout fonctionne correctement, essayez de naviguer sur la page d'accueil, vous devriez être redirigé automatiquement sur la page register.

import { Routes } from '@angular/router';
import { HomePage } from './features/home/home-page/home-page';
import { RegisterPage } from './features/register/register-page/register-page';
import { authGuard } from './core/guards/auth-guard';

export const routes: Routes = [
  {
    path: '',
    component: HomePage,
    canActivate: [authGuard],
  },
  {
    path: 'register',
    component: RegisterPage,
  },
];

Création de l'inscription - 6

Créez maintenant la vue de la page Register, pour cela nous allons créer en amont les validators que l'on utilisera dans le formulaire d'inscription.

Créez les différents validators dans le dossier src/core/validators/ :

password-match.ts

strong-password.ts

Ajoutez dans password-match.ts :

export function passwordMatchValidator(passwordField: string, confirmPasswordField: string): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      const password = control.get(passwordField);
      const confirmPassword = control.get(confirmPasswordField);

      if (!password || !confirmPassword) {
        return null;
      }

      return password.value === confirmPassword.value ? null : { passwordMismatch: true };
    };
}

Création de l'inscription - 7

Ajoutez dans strong-password.ts :

  export function strongPasswordValidator(): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      const value = control.value;
      if (!value) return null;

      const hasNumber = /[0-9]/.test(value);
      const hasUpper = /[A-Z]/.test(value);
      const hasLower = /[a-z]/.test(value);
      const hasSpecial = /[#?!@$%^&*-]/.test(value);
      const isLongEnough = value.length >= 8;

      const passwordValid = hasNumber && hasUpper && hasLower && hasSpecial && isLongEnough;

      return passwordValid ? null : {
        strongPassword: {
          hasNumber,
          hasUpper,
          hasLower,
          hasSpecial,
          isLongEnough
        }
      };
    };
  }

Modifier le fichier register-page.ts avec le code suivant.

Création de l'inscription - 8

import { Component, inject } from '@angular/core';
import {
  FormBuilder,
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { AuthService } from '../../../core/services/auth.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { strongPasswordValidator } from '../../../core/validators/strong-password';
import { passwordMatchValidator } from '../../../core/validators/password-match';
import { CreateUserDto } from '../../../core/models/user';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { CommonModule } from '@angular/common';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';

@Component({
  selector: 'app-register-page',
  imports: [
    CommonModule,
    ReactiveFormsModule,
    MatCardModule,
    MatFormFieldModule,
    MatInputModule,
    MatButtonModule,
    MatIconModule,
    MatProgressSpinnerModule,
    MatCheckboxModule,
  ],
  templateUrl: './register-page.html',
  styleUrl: './register-page.scss',
})
export class RegisterPage {
  private readonly fb = inject(FormBuilder);
  private readonly authService = inject(AuthService);
  private readonly router = inject(Router);
  private readonly snackBar = inject(MatSnackBar);

  readonly acceptTerms = new FormControl(false, [Validators.requiredTrue]);
  registerForm: FormGroup = this.createForm();
  isLoading = false;
  hidePassword = true;
  hideConfirmPassword = true;

  private createForm(): FormGroup {
    return this.fb.group(
      {
        firstName: new FormControl('', [
          Validators.required,
          Validators.minLength(2),
        ]),
        lastName: new FormControl('', [
          Validators.required,
          Validators.minLength(2),
        ]),
        email: new FormControl('', [Validators.required, Validators.email]),
        password: new FormControl('', [
          Validators.required,
          strongPasswordValidator(),
        ]),
        confirmPassword: new FormControl('', [Validators.required]),
      },
      {
        validators: passwordMatchValidator('password', 'confirmPassword'),
      }
    );
  }

  onSubmit(): void {
    if (this.registerForm.valid && !this.isLoading) {
      this.isLoading = true;

      const formValue = this.registerForm.value;
      const userData: CreateUserDto = {
        email: formValue.email,
        password: formValue.password,
        firstName: formValue.firstName,
        lastName: formValue.lastName,
      };

      try {
        this.authService.register(userData);
        this.snackBar.open('Inscription réussie', 'Fermer', {
          duration: 5000,
          panelClass: ['success-snackbar'],
        });
        this.router.navigate(['']);
      } catch (error) {
        this.isLoading = false;
        this.snackBar.open(
          error instanceof Error
            ? error.message
            : "Erreur lors de l'inscription",
          'Fermer',
          {
            duration: 5000,
            panelClass: ['error-snackbar'],
          }
        );
      }
    }
  }

  // Getters pour les erreurs
  get firstNameErrors() {
    const control = this.registerForm.get('firstName');

    if (control?.errors && control.touched) {
      if (control.errors['required']) return 'Le prénom est requis';
      if (control.errors['minlength'])
        return 'Le prénom doit contenir au moins 2 caractères';
    }
    return null;
  }

  get lastNameErrors() {
    const control = this.registerForm.get('lastName');

    if (control?.errors && control.touched) {
      if (control.errors['required']) return 'Le nom est requis';
      if (control.errors['minlength'])
        return 'Le nom doit contenir au moins 2 caractères';
    }
    return null;
  }

  get emailErrors() {
    const control = this.registerForm.get('email');

    if (control?.errors && control.touched) {
      if (control.errors['required']) return "L'email est requis";
      if (control.errors['email']) return "Format d'email invalide";
      if (control.errors['emailExists']) return 'Cet email est déjà utilisé';
    }
    return null;
  }

  get passwordErrors() {
    const control = this.registerForm.get('password');

    if (control?.errors && control.touched) {
      if (control.errors['required']) return 'Le mot de passe est requis';
      if (control.errors['strongPassword']) {
        const errors = control.errors['strongPassword'];
        const messages = [];
        if (!errors.isLongEnough) messages.push('8 caractères minimum');
        if (!errors.hasUpper) messages.push('une majuscule');
        if (!errors.hasLower) messages.push('une minuscule');
        if (!errors.hasNumber) messages.push('un chiffre');
        if (!errors.hasSpecial) messages.push('un caractère spécial');
        return `Le mot de passe doit contenir : ${messages.join(', ')}`;
      }
    }
    return null;
  }

  get confirmPasswordErrors() {
    const control = this.registerForm.get('confirmPassword');

    if (control?.errors && control.touched) {
      if (control.errors['required']) return 'La confirmation est requise';
    }
    if (this.registerForm.errors?.['passwordMismatch'] && control?.touched) {
      return 'Les mots de passe ne correspondent pas';
    }
    return null;
  }

  navigateToLogin(): void {
    this.router.navigate(['/login']);
  }
}

Modifier le fichier register-page.html avec le code suivant.

Création de l'inscription - 9

<div class="register-container">
  <mat-card class="register-card">
    <mat-card-header>
      <mat-card-title>Créer un compte</mat-card-title>
      <mat-card-subtitle>Rejoignez-nous dès aujourd'hui</mat-card-subtitle>
    </mat-card-header>

    <mat-card-content>
      <form
        [formGroup]="registerForm"
        (ngSubmit)="onSubmit()"
        class="register-form"
      >
        <!-- Nom et Prénom -->
        <div class="name-row">
          <mat-form-field appearance="outline" class="half-width">
            <mat-label>Prénom</mat-label>
            <input
              matInput
              formControlName="firstName"
              placeholder="Votre prénom"
              autocomplete="given-name"
            />
            <mat-icon matSuffix>person</mat-icon>
            <mat-error>{{ firstNameErrors }}</mat-error>
          </mat-form-field>

          <mat-form-field appearance="outline" class="half-width">
            <mat-label>Nom</mat-label>
            <input
              matInput
              formControlName="lastName"
              placeholder="Votre nom"
              autocomplete="family-name"
            />
            <mat-icon matSuffix>person</mat-icon>
            <mat-error>{{ lastNameErrors }}</mat-error>
          </mat-form-field>
        </div>

        <!-- Email -->
        <mat-form-field appearance="outline" class="full-width">
          <mat-label>Email</mat-label>
          <input
            matInput
            type="email"
            formControlName="email"
            placeholder="votre@email.com"
            autocomplete="email"
          />
          <mat-icon matSuffix>email</mat-icon>
          <mat-error>{{ emailErrors }}</mat-error>
        </mat-form-field>

        <!-- Mot de passe -->
        <mat-form-field appearance="outline" class="full-width">
          <mat-label>Mot de passe</mat-label>
          <input
            matInput
            [type]="hidePassword ? 'password' : 'text'"
            formControlName="password"
            placeholder="Votre mot de passe"
            autocomplete="new-password"
          />
          <button
            mat-icon-button
            matSuffix
            type="button"
            (click)="hidePassword = !hidePassword"
            [attr.aria-label]="'Hide password'"
            [attr.aria-pressed]="hidePassword"
          >
            <mat-icon
              >{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon
            >
          </button>
          <mat-error>{{ passwordErrors }}</mat-error>
        </mat-form-field>

        <!-- Confirmation mot de passe -->
        <mat-form-field appearance="outline" class="full-width">
          <mat-label>Confirmer le mot de passe</mat-label>
          <input
            matInput
            [type]="hideConfirmPassword ? 'password' : 'text'"
            formControlName="confirmPassword"
            placeholder="Confirmer votre mot de passe"
            autocomplete="new-password"
          />
          <button
            mat-icon-button
            matSuffix
            type="button"
            (click)="hideConfirmPassword = !hideConfirmPassword"
          >
            <mat-icon
              >{{ hideConfirmPassword ? 'visibility_off' : 'visibility'
              }}</mat-icon
            >
          </button>
          <mat-error>{{ confirmPasswordErrors }}</mat-error>
        </mat-form-field>

        <!-- Acceptation des conditions -->
        <div class="checkbox-container">
          <mat-checkbox [formControl]="acceptTerms" color="primary">
            J'accepte les
            <a href="/terms" target="_blank">conditions d'utilisation</a> et la
            <a href="/privacy" target="_blank">politique de confidentialité</a>
          </mat-checkbox>
          <mat-error
            *ngIf="acceptTerms.errors?.['required'] && acceptTerms.touched"
          >
            Vous devez accepter les conditions d'utilisation
          </mat-error>
        </div>

        <!-- Bouton d'inscription -->
        <button
          mat-raised-button
          color="primary"
          type="submit"
          class="full-width submit-button"
          [disabled]="registerForm.invalid || isLoading || acceptTerms.invalid"
        >
          <mat-spinner diameter="20" *ngIf="isLoading"></mat-spinner>
          <span *ngIf="!isLoading">S'inscrire</span>
          <span *ngIf="isLoading">Inscription en cours...</span>
        </button>
      </form>
    </mat-card-content>
    <mat-card-actions>
      <div
        style="
          display: flex;
          flex: 1;
          justify-content: center;
          align-items: center;
        "
      >
        <p>Déjà un compte ?</p>
        <button mat-button color="accent" (click)="navigateToLogin()">
          Se connecter
        </button>
      </div>
    </mat-card-actions>
  </mat-card>
</div>

Modifier le fichier register-page.scss avec le code suivant.

Création de l'inscription - 10

.register-container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.register-card {
  width: 100%;
  max-width: 500px;
  padding: 20px;
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
  border-radius: 12px;
}

.register-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-top: 20px;
}

.name-row {
  display: flex;
  gap: 16px;

  .half-width {
    flex: 1;
  }
}

.full-width {
  width: 100%;
}

.checkbox-container {
  margin: 16px 0;

  mat-checkbox {
    width: 100%;
  }

  mat-error {
    font-size: 12px;
    color: #f44336;
    margin-top: 4px;
  }

  a {
    color: #3f51b5;
    text-decoration: none;

    &:hover {
      text-decoration: underline;
    }
  }
}

.submit-button {
  height: 48px;
  font-size: 16px;
  margin-top: 16px;

  mat-spinner {
    margin-right: 8px;
  }
}

mat-card-header {
  margin-bottom: 8px;
}

mat-card-title {
  font-size: 24px;
  font-weight: 500;
}

mat-card-subtitle {
  font-size: 14px;
  opacity: 0.7;
}

mat-card-actions {
  padding: 16px;

  p {
    margin: 0 8px 0 0;
    color: rgba(0, 0, 0, 0.6);
  }
}

// Responsive design
@media (max-width: 600px) {
  .register-container {
    padding: 10px;
  }

  .name-row {
    flex-direction: column;
    gap: 8px;
  }

  .register-card {
    padding: 16px;
  }
}

// Styles pour les snackbars
::ng-deep .success-snackbar {
  background-color: #4caf50 !important;
  color: white !important;
}

::ng-deep .error-snackbar {
  background-color: #f44336 !important;
  color: white !important;
}

Création de l'inscription - 11

Vérifiez que tout fonctionne correctement en essayant de vous inscrire.

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 1 : PageLayout

Maintenant que l'inscription est terminée, nous devons maintenant réaliser la page Home. Pour apporter plus de maintenabilité et un standard dans la solution, nous allons créer un Layout pour éviter de réécrire la même structure d'affichage.

 

Créez un PageLayoutComponent dans le dossier shared/layouts.

Ce Layout doit afficher dans son header une Toolbar de Material pour y afficher "CookingBuddy" et les futurs liens vers nos autres pages.

Il doit aussi avoir un ng-content pour faire apparaître dans un main les éléments qu'on lui donne. Ce main doit être un container central de 1200px au maximum et centré avec un padding.

 

Appelez-moi pour que l'on puisses valider ensemble.

Exercice 2 : RecipesService

Pour préparer la page Home, nous allons créer un service RecipesService qui va gérer nos recettes en appelant directement l'API TheMealDB.

Attention, Angular utilise maintenant les signaux au lieu des BehaviorSubject / Subject de RxJS, faîtes attention en utilisant une IA qu'elle ne vous donne du code d'Angular <= 17.

Le service doit avoir un Signal représentant les catégories des recettes et doit créer une Resource pour récupérer toutes les recettes d'une catégorie : RecipesResource.

 

Appelez-moi pour que l'on puisses valider ensemble.

Exercice 3 : Composition

Pour préparer la page Home, nous allons créer les composants nécessaires :

  • RecipeCategory : le composant en charge de l'affichage d'une catégorie des recettes
  • RecipeCategories : le composant en charge de l'affichage des catégories et de la sélection d'une catégorie à afficher.
  • RecipeCard : le composant en charge de l'affichage d'une recette avec sa photo et son titre.

 

Utilisez les composants Material comme des composants de présentation de base pour ne pas devoir tout refaire.

Appelez-moi pour que l'on puisses valider ensemble.

Exercice 4 : HomePage

Maintenant que l'on a tous les éléments nécessaires, utiliser HomePage pour récupérer avec le service les catégories et recettes à afficher pour ensuite les afficher dans le HTML de la page avec vos composants.

 

Si on change de catégorie, la liste des recettes doit se mettre à jour.

 

Appelez-moi pour que l'on puisses valider ensemble.

Exercice 5 : Recherche

Ajoutez dans votre HomePage une barre de recherche au dessus de vos catégories. Lorsque l'utilisateur écrit dans la barre de recherche, le service RecipesService doit faire une requête à l'API via l'URL :

www.themealdb.com/api/json/v1/1/search.php?s=

Les catégories doivent disparaître et une liste d'un nouveau composant DetailedRecipeCard doit s'afficher avec les informations renvoyées par l'API comme sa catégorie, son Area ou encore la liste de ses ingrédients. 

Un message doit s'afficher s'il n'y a pas de résultats.

 

Appelez-moi pour que l'on puisses valider ensemble.

Exercice 6 : RecipePage

Créez une nouvelle page RecipePage qui affichera une recette en détail avec comme Header l'affichage de son nom, de sa catégorie, de son Area et du lien vers la source.

Dans son contenu, la liste des ingrédients avec leur quantité, si disponible la video youtube et ensuite les instructions pour la recette.

La page doit utiliser le service RecipesService qui doit renvoyer une RecipeResource en fonction de l'ID de la route :

/recipe/:id

On doit pouvoir naviguer vers cette page au clique d'une RecipeCard ou DetailedRecipeCard.

 

Appelez-moi pour que l'on puisses valider ensemble.

Exercice 7 : Directive

Créez une Directive appYTHoverPlay qui une fois ajoutée à l'iframe d'un player Youtube doit permettre de lancer automatiquement la video au hover de la souris.

 

Ajoutez la dans votre page RecipePage pour lancer automatiquement la video au hover de la souris.

 

Appelez-moi pour que l'on puisses valider ensemble.

Exercice 8 : Pipe

Créez un Pipe dietDetector qui permet de renvoyer plusieurs strings en fonction des ingrédients d'une Recipe que vous lui donnez en paramètre :

  • Classic
  • Vegetarian
  • Vegan

 

Ajoutez le dans votre page RecipePage dans un badge dans le Header.

 

Appelez-moi pour que l'on puisses valider ensemble.

Exercice 9 : Favoris

Créez un nouveau service FavoritesService et une page FavoritesPage pour gérer vos recettes favorites. Réaliser le stockage des favoris dans le localStorage.

Ajoutez dans les cartes un bouton pour ajouter en favori une recette.

On doit pouvoir retrouver le lien vers la page dans la Toolbar dans PageLayout.

 

La page doit afficher la liste des recettes favoris avec des cartes détaillées de chaque recette.

 

Appelez-moi pour que l'on puisses valider ensemble.

Exercice Bonus : I18n

Ajoutez l'internationalisation dans votre application en utilisant I18n, localize, et les autres outils Angular :

https://angular.dev/guide/i18n

 

Appelez-moi pour que l'on puisses valider ensemble.

Exercice Bonus : Tests

Ajoutez des tests unitaires ou E2E à votre application pour découvrir comment tester en Angular :

https://angular.dev/guide/testing

 

Appelez-moi pour que l'on puisses valider ensemble.

 30 minutes 

Soutenance

QCM

4.

N'hésitez pas à me donner votre avis.

Merci!

Javascript et Frameworks

By Valentin MONTAGNE

Javascript et Frameworks

  • 111