loading...
Cover image for Django & DRF & Angular 101, partie 3.1

Django & DRF & Angular 101, partie 3.1

zorky profile image DUVAL Olivier Updated on ・26 min read

3ème partie consacrée à une application de type SPA, après les 2 / 3 parties axées sur le serveur d'applications pour fournir les APIs : django & drf (django rest framework).

Sommaire

SPA, introduction

Les SPA se basent sur une approche composants et une architecture dite REST à base d'APIs, vous pouvez relire une présentation sur la partie REST du précédent article.

Dans un contexte d'applications métiers (en interne d'une organisation), le gros avantage d'une SPA est son côté fluide et son apport en termes d'UX, on se croirait sur une réelle application et non plus sur un "site Web", cela a remplacé ou remplace les applications développées MPA ou en Flash ou Silverlight ou Java Web Start et autres applets Java si on remonte encore plus loin, etc.

Dans un passé plus proche, les MPA (Multi Pages Applications) avaient / ont le défaut de leur qualité :

  • des allers-retours avec le serveur pour charger les pages et les sources javascript / CSS, souvent dans un modèle MVC, quid des modèles chargés de façon assez peu cohérent
  • on veut une meilleure interaction pour l'utilisateur, une meilleure UX, pour se faire, on utilise Javascript et ...jQuery pour les appels Ajax et des effets sur le DOM (apparitions et disparitions d'éléments etc) : on fait du SPA sans vraiment en faire, du code spaghetti augmente au fur et à mesure des vues et des versions de l'application : scripts chargés tant bien que mal, lesquels charger pour telle vue, du JS fonctionnel et non objet, des scripts de fonctions appelées les unes par rapport aux autres : l'application devient très difficile à maintenir (entropie qui s'installe avec le temps) pour les suivants (développeurs) et c'est l'expérience de beaucoup de projets qui parle

Partant de ce postulat (application), une SPA est une application qui sera embarquée dans le navigateur Web, avec un jeu de "routes" (urls) et de chargement de composants à l'intérieur de l'application, ce qui peut se schématiser par

Alt Architecture SPA

(schéma pris sur cet article)

Les sources d'une application SPA ressemble à cela, autant dire il n'y a pas grand chose et c'est assez perturbant ;)

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Ma librairie</title>
  <base href="/library/">  
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="stylesheet" href="/library/styles.a7613a87447eae269697.css"></head>
<body>

<app-libray-root></app-library-root>

<script type="text/javascript" src="/library/runtime.44f969275dacc8c108f8.js"></script>
<script type="text/javascript" src="/library/es2015-polyfills.ed18db863a6a6727d6b6.js" nomodule></script>
<script type="text/javascript" src="/library/polyfills.6daf05ca7cb3720c8e32.js"></script>
<script type="text/javascript" src="/library/scripts.2ccf57d185e6d7cc00a6.js"></script>
<script type="text/javascript" src="/library/main.080a43d15e12f96ae533.js"></script>
</body>
</html>

Une Single Page Application a une architecture à base de ces éléments :

  • un frontend : application (Javascript / HTML / CSS) embarquée dans le navigateur, des composants / des APIs : un cadre est posé
  • un backend : un serveur d'applications qui sert des APIs sur une architecture REST

A contrario d'applications classiques MPA (Multi Page Applications), qui chargent les pages au fur et à mesure de la navigation, avec des allers-retours entre le navigateur et le serveur (ces serveurs d'applications utilisent souvent le modèle MVC), les SPA, vont les charger en une fois ou de façon transparente pour les utilisateurs (javascript, les vues HTML, CSS), les données chargées au fur et à mesure grâce aux APIs et à l'Ajax, cela rend la navigation beaucoup plus fluide et améliore l'expérience utilisateur.

Parmi les frameworks et librairies existantes, je me baserai sur Angular à sa version 9. Angular que j'utilise depuis sa version 2 depuis bientôt 3 ans (la version dite "2" totalement réécrite, auparavant, j'avais également participé à 2 projets en 2+ ans avec la version "1" AngularJS qui a comme seul point commun avec sa version "2" que le modèle SPA, pour le reste, pas grand chose à voir techniquement)

Préparation de l'environnement

CORS & Django

Dans un contexte de purs appels "Ajax" (XMLHttpRequest), un mécanisme de protection est mis en place via CORS afin de contrôler et de savoir si le client qui fait l'appel (get, post, ...) a le droit ou non, surtout sur du cross-domains : un client sur domain-A.ntld effectue une requête "Ajax" sur le domain-B.ntld, il faut l'autoriser ou ...le refuser, CORS permet cela, contrôler l'origine de la requête et accepter ou refuser ce type d'appels.

En Django & DRF, nous utilisons le module django-cors-headers et l'activons dans le settings.py sur la "section" INSTALLED_APPS avec corsheaders ainsi que le MIDDLEWARE corsheaders.middleware.CorsMiddleware.

Enfin, des clés de configuration sont utilisées pour paramétrer CORS (regexp sur le(s) domaine(s) autorisé(s), les entêtes) :

  • CORS_ORIGIN_REGEX_WHITELIST : liste de regexp pour les domaines acceptés
  • CORS_ORIGIN_ALLOW_ALL = True en mode DEBUG en mode développement
  • CORS_ALLOW_HEADERS : liste des entêtes acceptés

Historiquement, avant CORS, lors de l'utilisation de jQuery, nous utilisions une astuce pour le cross-domain: le JSONP qui permettait de générer une balise script (avec l'URL de l'appel) à la volée et effectuer ainsi des appels cross-domains, jQuery permettant facilement la génération de scripts lors d'un appel de type JSONP.

Environnement Angular

Pour un environnement de développement Angular, nous aurons besoin :

  • d'un IDE : j'utilise Webstorm mais VSCode reste très bien
  • d'installer NodeJS (v10 ou v12) (pour npm, la compilation à la volée Typescript et du serveur Web local d'exécution). Remarques: en production, une application Angular n'a pas besoin de node, comme les sources "compilées" ne sont basées que sur JS, HTML et CSS
  • Créer un répertoire "frontend", créer un fichier package.json (cf. contenu ci-après) et installer Angular CLI : le client angular qui va nous aider à compiler, créer des composantes, etc. Ici, cela sert uniquement à ne pas installer Angular CLI de façon global sur votre PC (l'option -g).
$ mkdir frontend && cd frontend

Contenu du fichier package.json

{
  "name": "library",
  "version": "1.0.0",
}

Installer Angular CLI localement :

$ npm install @angular/cli --save-dev

La version d'angular cli est installé :

$ ng --version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/


Angular CLI: 9.1.0
Node: 12.13.1
OS: win32 x64

Angular: undefined
...
Ivy Workspace: <error>

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.901.0
@angular-devkit/core         9.1.0
@angular-devkit/schematics   9.1.0
@angular/cli                 9.1.0
@schematics/angular          9.1.0
@schematics/update           0.901.0
rxjs                         6.5.4

RxJs est aussi installé, nous en parlerons un peu plus tard, cela remplace les Promises qui avaient certaines limitations, RxJs ou la programmation réactive apporte aussi quelques avantages.

  • Créer l'application "library", cela va créer dans le répertoire library toute la tuyauterie (sources, package.json, etc)
$ ng new library
  • Exécuter une 1ère fois notre nouvelle application, notre squelette s'ouvre dans le navigateur sur l'adresse http://localhost:4200/
$ cd library
$ ng serve -o

Alt Text

  • Ajoutons quelques modules qui seront utiles, toujours dans "frontend/library" material, PWA et Flex layout qui nous servira pour ajuster les éléments sur une page (un site dont je me sers souvent avec des exemples)
$ ng add @angular/material
$ ng add @angular/pwa
$ ng add @angular/flex-layout
  • On vérifie que tout se passe bien en lançant l'application
$ ng serve -o

On a alors l'ensemble des fichiers suivants dans frontend/library

Alt Text

Parmi les principaux :

  • angular.json : le paramétrage de l'application Angular selon l'environnement, pour les tests unitaires, mode de compilation, etc
  • browserslist : pour la gestion compatibilité des navigateurs, Angular peut détecter et introduire des polyfills le cas échéant
  • package.json : la gestion des modules NPM
  • tsconfig.app.json : la configuration de l'application pour la compilation Typescript
  • tsconfig.json : la configuration générale Typescript, utilisé par tsconfig.app.json (cela peut être utile si nous avions plusieurs applications Angular à gérer, cela donne une configuration de base pour l'ensemble)
  • src : les sources de notre application se trouvera dans ce répertoire, ng nous a créé un semble de fichiers, notamment app-component qui représente le composant principal à partir duquel, les autres seront chargés

Alt Sources

Source de notre application sur http://localhost:4200

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Library</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&amp;display=swap" rel="stylesheet">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <link rel="manifest" href="manifest.webmanifest">
  <meta name="theme-color" content="#1976d2">
</head>
<body>
  <app-root></app-root>
  <noscript>Please enable JavaScript to continue using this application.</noscript>
<script src="runtime.js" type="module"></script><script src="polyfills.js" type="module"></script><script src="styles.js" type="module"></script><script src="vendor.js" type="module"></script><script src="main.js" type="module"></script></body>
</html>

Angular

Introduction

Angular est un framework / un cadre de développement de la famille SPA qui a été présentée (on peut trouver d'autres plateformes, des librairies : React, VueJS parmi les plus connues). C'est un framework Opensource (sous licence MIT) qui a été porté par Google, une forte communauté s'investit et tourne autour d'Angular.

Angular présente un cadre qui contraint le développeur, personnellement, c'est un bien, moins permissif et moins de code à la "jQuery", ce cadre couvre :

  • le langage, avec Typescript
  • une approche composants
  • avec un moteur de vue et de binding
  • à l'aide de services que l'on injectera dans les composants ou qui seront utilisés directement
  • un système de routing ou "routes" qui représentent les URLs et les composants à charger
  • un framework qui vous permet de créer des modules pour optimiser le chargement ou cloisonner selon les utilisateurs
  • tout un outillage de développement : de "compilation" / création / de mise à jour de versions ou de lancement d'une application grâce à Angular CLI, et de bundle et d'optimisation grâce à webpack ou Ivy (depuis la v8)
  • et bien plus : PWA, schematics (du scaffolding qui va jusqu'à de la mise à jour du code), Web elements

Autour du développement Angular, beaucoup d'éléments basés sur les Observables qui amène à la programmation réactive à l'aide de RxJS.

Du côté composants "graphiques" / UX, on trouvera Material, projet également Opensource qui constitue une excellent librairie qui implémente Material design, bien plus moderne et réactive que Bootstrap que l'on peut retrouver ailleurs.

Préambule et saisie des auteurs et livres

En attendant des interfaces d'édition des livres et auteurs sous Angular, vous pouvez utiliser l'admin de django qui n'est pas très user friendly mais ça aide. En ouvrant sur le navigateur, suivant si vous avez suivi la méthode virtualenv ou docker pour exécuter le serveur d'applications (voir la mise en place sur cet article)

Notre squelette d'application maintenant créé, on va pouvoir le modifier.

L'objectif est de lister les livres avec leur auteur.

Alt Text

Typescript

Le développement sous Angular s'effectue avec Typescript qui est un transpileur : le compilateur prend un source et le transpile en ... Javascript.

Les puristes vous diront que c'est une perte de temps certainement, que rien ne vaut JS, etc J'ai de longues années de pratique de Javascript, en POO, et il est à constater que développer en programmation objet lorsque l'on vient de langages de type C#, Java & consorts, n'est pas chose aisée car Javascript est un langage à prototype avec une syntaxe qui peut perturber.

C'est pourquoi nombre de développeurs n'utilise pas toute la puissance de Javascript pour la POO pour l'abstraction, on parlera alors encapsulation, héritage ou composition pour les grands principes mais souvent comme un langage impératif pour composer un fichier source d'une liste de fonctions à la suite des unes des autres (et génère ainsi du code spaghetti).

Typescript répond à plusieurs points que JS n'apporte pas immédiatement :

  • comment puis-je continuer à programmer objet comme mes autres langages objets que je connais, avec les mêmes principes ? Typescript intègre les classes, les interfaces et bien d'autres choses, nous sommes en terrain connu
  • TS permet aussi de typer et de vérifier à la compilation notre source, JS étant un langage à typage dynamique, et comme tous les langages de ce type, on peut avoir des surprises, TS réduit ce risque à l'exécution, la phase de transpilation permettant de réparer au plus tôt le code et d'avoir un code plus compréhensible pour les autres. Grâce au typage et selon votre IDE, celui-ci pourra vous proposer l'autocompletion des types, gain de temps assuré !
  • enfin, TS, permet d'avoir des modes de compilation, c'est à dire, la possibilité de préciser pour quelle version de Javascript je souhaite transpiler : ES 5, ES 6, ES next etc sans modifier le source d'origine de mon TS ! et ça, c'est précieux, beaucoup moins de réécrire de code. TS met même en place des mécanismes, sucres syntaxiques afin de pouvoir utiliser des techniques JS futures tout en gardant une version de JS qui n'implémente pas encore ces fonctionnalité (par exemple async / await en ES 5 n'existent pas ces 2 mots clés sont liés aux Promises, ils sont apparus à partir de ES2017, mais TS permet de les utiliser tout en produisant un code pour ES 5, magique ;) )

Pour toutes ces raisons, et il doit en manquer, Typescript est de plus en plus utilisé, dans VueJS, React, etc

En TS, par exemple, le code suivant du fichier test.ts, où on utilise le optional chaining et le nullish coalescing, ainsi que async / await (sucre syntaxique), tout ceci n'existe pas en ES5.

let obj = { attribut: 'toto' };
const variable = 0;
obj = null;
const maVarOptional = obj?.attribut;
const maVarNullish = variable ?? 1;

async function delay(n: any) {
  return n;
}

const results = (async () => await delay(400));
results().then((value: string) => console.log(value));

transpilation : tsc test.ts donnera le fichier test.js

le résultat de la transpilation avec cet outil en ligne, vous pouvez faire varier le target selon la version de Javascript que vous souhaitez (ES2017 par exemple intègre le async / await), intéressant de voir ce que Typescript génère suivant les versions (en ES2020, le optional chaining et le nullish coalescing sont maintenant intégrés, merci TS de pouvoir les utiliser dans les versions moins récentes ;-))

Alt Text

Le code TS sera traduit en Javascript de la façon suivante (attention c'est verbeux pour l'async / await en ES5) :

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var obj = { attribut: 'toto' };
var variable = 0;
obj = null;
var maVarOptional = obj === null || obj === void 0 ? void 0 : obj.attribut;
var maVarNullish = variable !== null && variable !== void 0 ? variable : 1;
function delay(n) {
    return __awaiter(this, void 0, void 0, function () {
        return __generator(this, function (_a) {
            return [2 /*return*/, n];
        });
    });
}
var results = (function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
    switch (_a.label) {
        case 0: return [4 /*yield*/, delay(400)];
        case 1: return [2 /*return*/, _a.sent()];
    }
}); }); });
results().then(function (value) { return console.log(value); });

le fichier de configuration de compilation typescript

{
        "compilerOptions": {
                "target": "ES5"
        }
}

le target peut avoir les valeurs suivantes : "ES3" (par défaut), "ES5", "ES6"/"ES2015", "ES2016", "ES2017", "ESNext".

Nota Bene : async / await sont 2 mots clés liés aux Promises et qui permettent de résoudre quand vous utilisez (les promises) ce type de code imbriqué, afin de le rendre linéaire à la lecture et donc plus lisible

getResponse(url, (response) => {
   getResponse(response.url, (secondResponse) => {
     const responseData = secondResponse.data
     getResponse(responseData.url, (thirdResponse) => {
//       ...
     })
   })
 })

en

const response = await getResponse(url)
const secondResponse = await getResponse(response.url)
const responseData = secondResponse.data
const thirdResponse = await getResponse(responseData.url)
// ...

Service

Angular propose la notion de services. Un service contient la logique que l'on doit développer, la majeure partie du temps, un service contiendra le code pour consommer une API : obtenir un livre, lister les listes, etc pour résumer le CRUD d'une entité mais on peut aussi les utiliser pour effectuer des traitements.

Les services sont injectés dans les components, on retrouve le principe de l'IoC (utilisé souvent en C#, Java, etc) : une instance est initialisée (si elle n'existe pas déjà) puis injectée dans le constructeur d'un component, un service est donc un singleton.

Créer sous src/app le répertoire services, on va créer 2 fichiers : un modèle et un service

Le modèle : créer avec votre IDE le fichier book.model.ts et author.model.ts pour notre modèle Book et Author (une interface avec les champs typés de ce que va nous renvoyer l'api books ou authors) avec le contenu suivant

export interface Book {
  id: number;
  name: string;
  nb_pages: number;
  enabled: boolean;

  dt_created?: string;
  dt_updated?: string;
}

export interface Author {
  id: number;
  first_name: string;
  last_name: string;

  dt_created?: string;
  dt_updated?: string;
}

Les champs ayant ? sont considérés optionnels lorsque de la création d'un objet. Ce typage nous permettra aussi d'avoir l'autocompletion dans notre IDE sur les champs lors de l'utilisation d'un objet dans la vue ou le ts.

Le service : en ligne de commande, dans src/app/services, on crée notre service qui pointera vers l'API books et un autre vers l'API authors, on utilise le ng cli

$ ng g service bookService
$ ng g service authorService

dans book.service.ts / author.service.ts, éditons le code pour qu'il ressemble à ceci :

  • utilisation du HttpClient qui sert à effectuer les appels (get, post, ...,), les appels sont si possible typés, pour notre liste de Book ou Author, le get utilisé renverra Book ou Author[] selon l'API utilisée, à l'aide des generics de Typescript
  • du modèle créé pour le modèle Book ou Author
  • selon le type de l'environnement, l'URL base de l'API utilisée, cela représente le serveur (en local : http://localhost:4200, en test, https://domain-test.ntld, en production : https://domain-prod.ntld, etc)
import { Injectable } from '@angular/core';
import { HttpClient} from '@angular/common/http';
import {environment} from '../../../environments/environment';
import {Book} from './book.model';

@Injectable({
  providedIn: 'root'
})
export class BookService {
  private url =  `${environment.baseUrl}/library/books/`;
  constructor(private httpClient: HttpClient) { }

  /**
   * Liste de tous les livres
   */
  public fetchAll() {
    return this.httpClient.get<Book[]>(this.url);
  }

  /**
   * Obtient un livre particulier par son id
   * @param {number} id : l'identifiant du livre
   */
  public fetch(id: number) {
    const urlId = `${this.url}${id}/`;
    return this.httpClient.get<Book>(urlId);
  }
}
import {Injectable} from "@angular/core";
import {environment} from "../../../environments/environment";
import {HttpClient} from "@angular/common/http";
import {Author} from "./author.model";

@Injectable({
  providedIn: 'root'
})
export class AuthorService {
  private url =  `${environment.baseUrl}/library/authors/`;
  constructor(private httpClient: HttpClient) { }

  /**
   * Liste de tous les auteurs
   */
  public fetchAll() {
    return this.httpClient.get<Author[]>(this.url);
  }

  /**
   * Obtient un auteur particulier par son id
   * @param {number} id : l'identifiant de l'auteur
   */
  public fetch(id: number) {
    const urlId = `${this.url}${id}/`;
    return this.httpClient.get<Author>(urlId);
  }
}

Nos 2 services sont créés, on va pouvoir les utiliser dans nos components.

Composants / components

Un component est la base d'Angular. On va créer un arbre de composants (comme les balises HTML) afin de construire notre application.

Un component va être composé de 3 fichiers :

  • la vue : component.html, dans la vue Angular intègre un moteur qui permet du traitement sur de la donnée ou composants : l'afficher en interpolant une "variable", itérer de dessus (*ngFor), tester (*ngIf), "two way data binding" etc
  • le code Typescript : component.ts
  • le CSS le cas échéant : component.css

Une commande Angular CLI permet de créer à la voléé ces fichiers ainsi qu'un squelette HTML et TS, il l'ajoutera également au module courant où se situe le component

$ ng g c monComponent

g : generate, c : component

Pour schématiser, on aura la structure suivante dans index.html puis l'arborescence à partir de app-root :

  • app-root (app.component.html)
    • app-header (header.component.html)
    • app-books-list (books-list.component.html)
    • app-footer (footer.component.html)

Communication entre components

La communication est possible entre components / composants. Les composants sont organisés en arbre, un composant peut donc avoir des enfants, qui, eux-même des enfants, etc, et un enfant un père.

componant-pere
  |--- composant-fils1
  |--- composant-fils2-pere2
       |--- composant-fils3

Pour communiquer entre composants, nous avons plusieurs options :

  • du père vers un fils : par les attributs [attribut] que propose un composant avec des @Input() : le composant fils intercepte alors la valeur via son @Input sur une propriété

Par exemple, le père utilise dans sa vue le composant fils , et lui passe la valeur "ma valeur"

<composant-fils1 [proprieteValeur]="'ma valeur'"></composant-fils1>

Côté fils, il pourra utiliser la valeur passer à proprieteValeur, ici, un string

@Component({
  templateUrl: './composant-fils1.component.html',
  styleUrls: ['./composant-fils1.component.scss']
})
export class ComposantFils1Component implements OnInit {
   @Input() proprieteValeur: string= null;
}
  • du fils vers son père : par les événements de type EventEmitter que propose le composant fils par les @Output() : le composant père intercepte alors l'événement par une fonction associés au (output_name), et potentiellement la valeur envoyée par le EventEmitter

Par exemple, le père utilise dans sa vue le composant fils , et attend de la part du fils, un événement qui lui passera une valeur (ici, un string)

<composant-fils1 (onSelected)="funInterception($event)"></composant-fils1>
FunInterception(event) {
   console.log("reçu !");
   console.log(event);
}

Côté fils, le onSelected est un EventEmitter qu'il va utiliser pour envoyer grâce à emit de la donnée (par exemple, à la suite d'un click bouton, d'un traitement fini, etc)

@Component({
  templateUrl: './composant-fils1.component.html',
  styleUrls: ['./composant-fils1.component.scss']
})
export class ComposantFils1Component implements OnInit {
   @Output() onSelected: : EventEmitter<string> = new EventEmitter<string>();

   TraitementFini() {
       this.onSelected.emit('Traitement fini, prends le relais !');
   }
}

Les Input() et Output() peuvent s'utiliser seuls ou en les combinant (on parlera de two way binding).

  • par un service en le combinant avec un objet RxJS de type Subject. Dans ce cas, peu importe si père ou fils, la communication s’effectuera dès lors que le service est utilisé, soit en émission, soit en écoute, les composants sont alors totalement découplés, en revanche, attention à bien s'y retrouver s'il en existe trop :)

Exemple concret

Le service de communication

@Injectable({ providedIn: 'root' })
export class CommunicationService {
  /** le Subject déclenché via filterUsed$ **/
   private filterUsedSource = new Subject<{name: string, value: any}>();

  /** l'observable à utiliser pour la communication pour l'interception **/
  filterReceive$ = this.filterUsedSource.asObservable();

  /** fonction d'envoi d'une valeur à intercepter par d'autres via filterUsed$ **/
  filterSend(name: string, value: any) {
    this.filterUsedSource.next({name: name, value: value, used: used});
  }
}

composant 1 qui attendra de recevoir

@Component({
  templateUrl: './composant-1.component.html',
  styleUrls: ['./composant-1.component.scss']
})
export class Composant1Component implements OnInit, OnDestroy {
   sub: Subscription;
   constructor(private commSvc: CommunicationService) {
   }

   ngOnInit() {
     this.sub = this.commSvc.filterReceive$.subscribe((valeur) => {
        console.log(valeur);
        console.log('Bien reçu !');
     });

   }

   ngOnDestroy() {
      this.sub.unsubscribe();
   }
}

composant 2 qui enverra des données à qui voudra bien l'écouter !

@Component({
  templateUrl: './composant-2.component.html',
  styleUrls: ['./composant-2.component.scss']
})
export class Composant2Component implements OnInit {
   constructor(private commSvc: CommunicationService) {
   }

   ngOnInit() {
   }

   EnvoiTraitement() {
     this.commSvc.filterSend({name: 'composant 2 envoie', value: 'cette valeur'});
   }
}

Composant app-books-list

Créons notre composant pour lister les livres, le but est de charger le service bookService, par le constructeur du component, d'appeler la fonction fetchAll() au clic du bouton, fonction qui fera l'appel à l'API et nous ramènera tous les livres de la base.

Squelette du TS d'un component généré grâce à "ng g c booksList", on a alors le constructeur du component et au moins l'événement / hook (qui sera appelé au chargement du component par Angular) ngOnInit qui est l'implémentation de l'interface OnInit, d'autres événements sont disponibles sur le cycle de vie d'un component. Angular utilise les décorateurs de Typescript, ici, @Component qui a au moins 3 attributs : le selector qui sera utilisé dans les vues "mères", la vue templateUrl du component et son CSS styleUrls

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

@Component({
  selector: 'app-books-list',
  templateUrl: './books-list.component.html',
  styleUrls: ['./books-list.component.css']
})
export class BooksListComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {
  }

}

et la vue HTML générée (attention, elle est complexe)

<p>booksList works!</p>

Modifions les 2 fichiers pour y inclure ce qu'il faut pour la liste de nos livres : un bouton où au clic, les livres sont chargés

La vue books-list.component.html dans laquelle on utilise les composants Material : une mat-card (carte), un bouton en mat-raised-button, un matBadge pour afficher sur le bouton le nombre de livres ramenés par l'API, un mat-progress-bar ("spinner") qui s'affichera lors de l'appel à l'API, une mat-list pour afficher la liste des livres,

<mat-card>
  <mat-card-title>Livres</mat-card-title>
  <mat-card-content>
    <mat-card-actions align="end" style="padding-right: 10px;">
      <button mat-raised-button
              matBadge="{{books ? books.length : 0}}" matBadgePosition="after"
              matBadgeColor="accent" matBadgeOverlap="true" matBadgeSize="large"
              (click)="fetchBooks($event)">
        Charger les livres
      </button>
    </mat-card-actions>
    <mat-progress-bar *ngIf="loading"
                      [mode]="'indeterminate'" color="warn"></mat-progress-bar>
    <mat-divider></mat-divider>
    <mat-list>
      <mat-list-item *ngFor="let book of books">
        <p mat-line>
          <span mat-list-icon><mat-icon>book</mat-icon></span>
          <span>{{book.name}}</span>
        </p>
        <p mat-line><app-author-display [author]="book?.author_obj"></app-author-display></p>
        <p mat-line><mat-divider class="mat-divider-dashed"></mat-divider></p>
      </mat-list-item>
    </mat-list>
  </mat-card-content>
</mat-card>

Le moteur de template Angular permet de faire des traitements :

  • chaque component peut avoir des événements qui permettent de communiquer du composant fils vers le père qui l'utilise, ces événements sont matérialisés notamment dans le composant fils par le décorateur @Output() sur une propriété. Par exemple, le button a un événement au clic : (click), il suffit d'y adjoindre une fonction qui sera appelée au clic du bouton, ici, la fonction fetchBooks() qui est dans le TS de app-books-list
  • *ngIf : test si true alors
  • *ngFor : itération sur une liste d'objets, ici la liste de livres
  • les
{{var}}

permettent d'interpoler la valeur d'un objet var contenu dans le TS

  • le ? : l' optional chaining, permet si l'objet en question est null de ne pas continuer la chaîne, par exemple : book?.author_obj : si book est nul, il s'arrête là sinon il ira chercher l'attribut author_obj de book, valable également en typescript depuis Angular 8 / 9, comme le nullish coalescing. C'est très utile de l'utiliser pour éviter des désagréments, et facilite une syntaxe du type "book ? book.author_obj : null", et en plus, l'optional chaining et le nulling coalescing viennent d'être adoptés pour ES 2020, what else ? Même si ES2020 ne sera pas implémenté de si tôt sur tous les navigateurs, Typescript fera en sorte d'avoir une rétro compatibilité avec des versions inférieures (ES 5, ES 6, etc)

Le source TS books-list.component.ts modifié :

import {Component, OnDestroy, OnInit} from '@angular/core';
import {SubSink} from '../../services/subsink';

import {finalize} from "rxjs/operators";
import {Book, BookService} from "../../services";

@Component({
  selector: 'app-books-list',
  templateUrl: './books-list.component.html',
  styleUrls: ['./books-list.component.css']
})
export class BooksListComponent implements OnInit, OnDestroy {
  subSink = new SubSink();
  books: Book[];
  loading = false;

  constructor(private bookSvc: BookService) { }

  ngOnInit(): void {
  }

  fetchBooks(event) {
    this.loading = true;
    this.books = [];
    this.subSink.sink = this.bookSvc
      .fetchAll().pipe(finalize(() => this.loading = false))
      .subscribe((books) => {
        this.books = books;
      });
  }

  ngOnDestroy(): void {
    this.subSink.unsubscribe();
  }
}

  • constructor() : le constructeur de l'objet BooksListComponent, initialisé par "typescript", c'est ici que l'on injecte notre service BookService, il sera automatiquement instancié
  • au click, la méthode fetchBooks() est appelée, dans celle-ci on opère les actions suivantes
    • loading à True : automatiquement le composant s'active grâce au *ngIf, pour une indication à l'utilisateur que c'est en train de charger
    • consommer l'API books : on s'inscrit via un subscribe() à la méthode fetchAll() de l'instance bookSvc. Cela suit le pattern Observer. En effet, dans fetchAll(), nous utilisons httpClient.get() qui renvoie un Observable sur lequel l'on peut s'abonner / souscrire via un subscribe(). Dès que les données arrivent, le get() enverra un "signal" et les transmettra au subscribe, il ne reste plus qu'à les intercepter, les stocker dans books et automatiquement, via le data binding, la boucle *ngFor pourra itérer dessus. Les erreurs ne sont pas traitées dans cet exemple, subscribe permet de le faire via une seconde fonction anonyme subscribe((data), (error))
    • le pipe fait parti de RxJS et permet de combiner des opérateurs de RxJs. Dans notre cas, on utilise l'opérateur finalize() qui a pour effet qu'à la fin de l'observation, le loading soit mis à false, ce qui aura pour effet de cacher le mat-progress-bar, que l'appel ait réussi ou non, comme un finally lors d'un try en C#, il y passera forcément. Le pipe peut intégrer bon nombre d'autres opérateurs que je vous laisse découvrir
  • On a une interface supplémentaire : OnDestroy, la méthode implémentée ngOnDestroy() sera alors automatiquement appelée lors de la destruction du composant par Angular, comme en C# ou autre, cela permet d'effectuer le ménage pour éviter d'avoir des références d'objets, ici on se désabonne des souscriptions aux observables (très important ! pour ne pas avoir d'effets de bords indésirables). J'utilise une classe utilitaire SubSink qui facilite l'ajout et le désabonnement aux Observable() créés.

Composant app-author-display

Le component app-books-list utilise dans sa vue un autre component que l'on va créer : qui est chargé d'afficher l'auteur du livre. app-books-list lui injecte directement l'auteur (author_obj) à l'aide de [author]="book?.author_obj" qui est un @Input côté app-author-display, charge ensuite à app-author-display de l'afficher.

Ce composant affichera l'auteur à l'aide d'un mat-chip. Côté TS, on voit un décorateur @Input() sur la propriété author. Cela permet une communication du composant père vers son fils, le père pourra injecter une valeur à l'aide d'un attribut entre [] sur le composant fils, cette valeur sera alors interceptée grâce à l'Input contenu dans le fils.

<app-author-display [author]="valeur"></app-author-display>
import {Component, Input, OnInit} from '@angular/core';
import {Author} from '../../../services';

@Component({
  selector: 'app-author-display',
  templateUrl: './author-display.component.html',
  styleUrls: ['./author-display.component.css']
})
export class AuthorDisplayComponent implements OnInit {
  @Input() author: Author;

  constructor() { }

  ngOnInit(): void {
  }

}

La vue, dès réception de la propriété author, l'objet sera affiché côté vue grâce à

{{author?.first_name}} {{author?.last_name}}

on appelle cela le data binding : toute modification de author dans le TS sera mis à jour côté vue et vis-versa, et cela de façon automatique, un gain de temps non négligeable.

<mat-chip-list>
  <mat-chip color="warn">{{author?.first_name}} {{author?.last_name}}</mat-chip>
</mat-chip-list>

Composant app-root (app.component)

J'ai aussi créé 2 autres composants que vous retrouverez dans les sources : app-header qui utilise le mat-toolbar et app-footer, on peut ainsi composer un layout propre de notre application, le composant principal app-root (app.component) ressemble maintenant à cela, avec notre composant app-books-list qui liste les livres

<div fxLayout="column" fxLayoutGap="10px" fxFlexFill>
  <div>
    <app-header></app-header>
  </div>
  <div fxLayoutAlign="center center">
    <div fxFlex="70">
      <app-books-list></app-books-list>
    </div>
  </div>
  <div fxFlexOffset="auto">
    <app-footer></app-footer>
  </div>
</div>

Le composant app-root est mis dans la (seule, SPA !) page index.html que je mets ici

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Library</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&amp;display=swap" rel="stylesheet">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <link rel="manifest" href="manifest.webmanifest">
  <meta name="theme-color" content="#1976d2">
</head>
<body>
  <app-root></app-root>
  <noscript>Please enable JavaScript to continue using this application.</noscript>
</body>
</html>

Conclusion et sources

Nous avons vu les services (singletons !), les composants / components, et quelques composants Material et opérateurs RxJS.

Dans le prochain article, nous continuerons sur notre lancée et nous ferons en sorte de pouvoir éditer les livres et auteurs avec des interfaces un peu plus user friendly que l'admin Django, un peu de refactoring en utilisant la POO qu'offre Typescript et étudier une data-table de type mat-tab pour lister les auteurs et aussi agir dessus (tri, pagination, recherche). Nous aborderons aussi le système de routing, et nous irons plus loin sur opérateurs RxJS.

Les sources complets de cet article sont disponibles sur le dépôt "library",
à la branche django-drf-angular-3.1

Posted on by:

zorky profile

DUVAL Olivier

@zorky

CP technique / Scrummaster, développeur sénior ancien blog : https://medium.com/zorky

Discussion

markdown guide