Vous êtes sur la page 1sur 231

APPRENDRE ANGULAR

Développez facilement votre première application


Angular avec TypeScript
DIENY, SIMON
Reproduction totale ou partielle interdite sur quelque support que ce soit sans l’accord de
l’auteur. Il est donc protégé par les lois internationales sur le droit d’auteur et la protection de la
propriété intellectuelle. Il est strictement interdit de le reproduire, dans sa forme ou son
contenu, totalement ou partiellement, sans un accord écrit de son auteur. La loi du 11 mars
1957, n’autorisant, au terme des alinéas 2 et 3 de l’article 4, d’une part, que « les copies ou
reproductions strictement réservées à l’usage privé du copiste et non destinées à une utilisation
collective et, d’autre part, que les analyses et les courtes citations dans un but d’exemple et
d’illustration, toute représentation ou reproduction , inté- grale ou partielle, faite sans le
consentement de l’auteur ou de ses ayants cause, est illicite » (alinéa premier de l’article 40).
Cette représentation ou reproduction constituerait donc une contrefaçon sanctionnée par les
articles 425 et suivants du Code pénal.
Droits d'auteur 2018 © Simon Dieny
Tous droits réservés
TABLE DES MATIÈRES
TABLE DES MATIÈRES
Préambule
Avant-propos
Avant de commencer le cours … (Important !)
A propos de l’auteur
Partie 1
Découvrir Angular
Chapitre 1 : Présentation de Angular
Chapitre 2 : ECMAScript 6
Chapitre 3 : Découvrir TypeScript
Chapitre 4 : Les Web Components
Chapitre 5 : Premiers pas avec Angular
Questionnaire n°1
Partie 2
Acquérir les bases d’Angular
Chapitre 6 : Les composants
Chapitre 7 : Les templates
Chapitre 8 : Les directives
Chapitre 9 : Les pipes
Chapitre 10 : Les routes
Chapitre 11 : Les modules
Chapitre 12 : Les services et l'injection de dépendances
Questionnaire n°2
Partie 3
Aller plus loin avec Angular
Chapitre 13 : Formulaires pilotés par le template
Chapitre 14 : Formulaires pilotés par le modèle
Chapitre 15 : La programmation réactive
Chapitre 16 : Effectuer des requêtes HTTP standards
Chapitre 17 : Effectuer des traitements asynchrones avec RxJS
Chapitre 18 : Authentification
Chapitre 19 : Déployer votre application
Questionnaire n°3
Partie 4
Annexes
Annexe 1 : Des titres pour vos pages
Annexe 2 : Bonnes pratiques de développement
Annexe 3 : Configuration de TypeScript
Annexe 4 : Dépendances d'un projet Angular
Préambule
Avant-propos
Bienvenue à tous, et à toutes, dans ce cours consacré au développement de votre première
application avec Angular !
Ce cours s'adresse aux développeurs web qui souhaiteraient créer des applications web
réactives: les développeurs novices, comme les développeurs plus expérimentés qui avaient
l'habitude d'utiliser AngularJS.
Ce cours va vous permettre de prendre en main rapidement Angular.
Vous verrez dans ce cours: Pourquoi choisir Angular ? Comment mettre en place un
environnement de développement ? Comment récupérer des données depuis un serveur distant ?
Comment concevoir un site réactif avec plusieurs pages ?
Sachez qu'il n'y a pas besoin de connaître AngularJS v.1 pour suivre ce cours, nous partons
de zéro !
Cependant, il y a quelques pré-requis nécessaires. Mais pas d'inquiétude, il s'agit de
connaissances élémentaires sur lesquelles vous pouvez vous formez, et il y a des cours très bien
faits là-dessus sur Internet.
Je vous conseille donc de voir (ou de revoir) les cours suivants si vous en ressentez le
besoin :
· Connaître le HTML et le CSS.
· Avoir déjà entendu parler de Javascript, car c'est le langage que nous allons utiliser tout le
long de ce cours.
· Connaître un peu la programmation orienté objet (savoir ce qu'est une classe, une
méthode, une propriété…)
Si vous êtes trop impatients, vous pouvez commencer à suivre le cours dès maintenant, car
les premiers chapitres sont assez théoriques, mais vous sentirez rapidement le besoin de vous
mettre à niveau par la suite !
Pendant ce cours, je vous propose de développer une application pour gérer des Pokémons.
La démonstration réalisée avec Angular 6 est disponible en ligne à cette adresse.
Voici un aperçu :
Figure 1 - Voici l'application que vous réaliserez pendant ce cours !

IMPORTANT : Dans ce cours, le terme AngularJS désigne la version 1.x d'Angular, et les
versions supérieures du framework sont appelées simplement Angular. C'est l'appellation qui
est recommandée par la documentation officielle :
"Angular is the name for the Angular of today and tomorrow. AngularJS is the name for all
v1.x versions of Angular."
J'utiliserai donc le terme Angular pour désigner les versions d'Angular supérieures ou égales à
la version 2.
PS : La version actuelle du cours correspond à la version 6.0.3, qui est une notation sémantique
du numéro de version. Mais on parle bien du "nouvel Angular", rassurez-vous !

Comment est structuré le cours ?


Ce cours est structuré en quatre parties distinctes :
I. Une vue d’ensemble d’Angular : Cette section théorique vous permet de savoir où vous
mettez les pieds, et comment réaliser un magnifique « Hello, World ! » avec Angular.
II. Acquérir les bases d’Angular : Nous verrons comment maîtriser les éléments de base
d’une application Angular avec les composants, les templates, la gestion des routes…
III. Aller plus loin avec Angular : Nous lèverons le voile sur les formulaires, les requêtes
HTTP, l’authentification, le déploiement…
IV. Annexes : Composeés de quatre chapitres sur les bonnes pratiques de développement
avec Angular, la configuration de TypeScript, les dépendances de base d’une application
Angular et la gestion des titres pour vos pages.
Avant de commencer le cours … (Important
!)
Le code de l’application
L’ensemble du code de l’application est disponible librement à l’adresse suivante :
https://github.com/moumoune/ng6-pokemons-app
Ce dépôt est organisé avec des tags Git, et vous donne accès à une correction complète à la
fin de chaque chapitre :
Git checkout 4.directives // Correction du chapitre sur les directives

N'hésitez pas à vous y référer en cas de doute !

Si vous ne connaissez ou n’utilisez pas Git, consultez directement la correction en ligne grâce
au lien ci-dessus.

Les extraits de code


Ce cours est très orienté pratique et est donc composé de nombreux extraits de code. Plutôt
que de les recopier manuellement, vous pouvez les copier-coller depuis l'adresse suivante :
https://awesome-angular.com/ebook/
Attention : Le code disponible correspond à l'état du fichier lorsque vous le rencontrez
pour la première fois dans le chapitre, sans les modifications ultérieures qui peuvent lui être
apportées plus loin. Il s'agit de vous faire économiser le temps de recopie d'un fichier long,
ensuite c'est à vous de travailler dessus en suivant le cours !

Questionnaires
Il y a en tout trois questionnaires dans ce cours disponibles en ligne :
https://awesome-angular.com/ebook/

Google is everywhere
Les questionnaires et le chapitre sur le déploiement de l’application réalisée pendant le
cours nécessite de posséder un compte Google, car nous effectuons le déploiement grâce à
Firebase, une entreprise appartenant à Google, et les questionnaires sont des Google Forms.

Règles typographiques
Ce cours respecte la typographie suivante :

Ceci est une bulle contenant des informations supplémentaires sur le point qui vient d’être
abordé.

Ceci est un message d’avertissement qu’il est recommandé de prendre en compte !


Cette bulle contient une question que vous pouvez vous poser, et à laquel nous
apporterons une réponse !

Ci-dessous se trouve un extrait de code. Les numéros de lignes sont indiqués à gauche :
01 function Vehicule(couleur, nombreRoueMotrice) {
02 this.couleur = couleur;
03 this.nombreRoueMotrice = nombreRoueMotrice;
04 this.moteurAllumer = false;
05 }

Pratiquez, Pratiquez !
N’attendez pas de finir ce cours pour commencer à travailler, nous vous recommandons de
faire vos tests au fur et à mesure sur votre poste de travail habituel.

De A à Z
Pour une première lecture, lisez le livre dans l’ordre, chapitre après chapitre.
Ensuite vous pourrez revenir sur certains chapitres sur lesquels vous souhaitez approfondir
vos connaissances.

Ça bouge !
Angular évolue constamment, comme beaucoup de technologies dans le monde du
développement Web. Ce cours est stabilisé pour la version 6.0.3 du Framework. Vous pouvez
vous former sans problèmes sur des versions ultérieures (6.1.0, 6.2.0, etc), car les évolutions
d’Angular concernent des éléments très spécifiques, et qui n’ont pas d’impact sur les éléments de
base abordés dans ce cours.
Les différences apportées dans ce cours par rapport à la version 5.0.0, sont disponibles sur
mon blog.

Je suis bloqué, … je ne trouve rien sur Google, et personne ne m’aide


sur les forums !
La courbe d’apprentissage d’Angular n’est pas simple. Mais c’est en cherchant et en
trouvant les réponses à vos questions par vous-même, que vous réussirez. Toutefois, il arrive à
tout le monde d’être bloqué, et je peux vous donner un petit coup de main si nécessaire.
De plus, si vous avez une remarque sur l’ouvrage (exemple de code pas très parlant,
coquilles, etc), faites en moi part, et je pourrai continuer à améliorer ce cours pour ceux qui
apprendront après vous.
A propos de l’auteur

Figure 2 - L'auteur, Simon Dieny


Je suis un jeune ingénieur logiciel, passionné par le développement web et mobile depuis
mes études. J’ai eu un vrai coup de foudre pour Angular, depuis qu’il est en bêta !
Mon objectif est simple : Permettre à un maximum d’étudiants et de professionnels de se
former facilement sur cette nouvelle technologie. Et pourquoi pas d’aller plus loin avec Angular :
Ionic, NativeScript, ElectronJS, etc.
Vous pourrez retrouver sur mon blog des ressources complémentaires pour ce cours, ainsi
que des articles techniques sur Angular, son environnement et son actualité :
https://awesome-angular.com/blog/
Bon, ce n’est pas tout, mais quand commence t’on ? Et bien, maintenant ! C'est parti !
Partie 1

Découvrir Angular
Chapitre 1 : Présentation de Angular
Angular, de quoi parle-t-on exactement ?
Alors comme ça vous souhaitez vous former au développement d'applications Web avec la
nouvelle version d'Angular ? Vous aussi vous rêvez de construire des sites dynamiques, qui
réagissent immédiatement aux moindres interactions de vos utilisateurs, avec une performance
optimale ? Eh, ça tombe bien, vous êtes au bon endroit !
Nous vivons une époque excitante pour le développement Web avec JavaScript. Il y a une
multitude de nouveaux Frameworks disponibles, et encore une autre multitude qui éclot jour
après jour. Nous allons voir pourquoi vous devez faire le pari de vous lancer avec Angular, et ce
que vous allez pouvoir faire avec ce petit bijou, sorti tout droit de la tête des ingénieurs de
Google.
Cette nouvelle version d'Angular est une réécriture complète de la première version
d'Angular, nommée AngularJS. C'est donc une bonne nouvelle pour ceux qui ne connaîtraient
pas cette version d'Angular, ou qui en auraient juste entendu parler : pas besoin de connaître
AngularJS, vous pouvez vous lancer dans l'apprentissage d'Angular dès maintenant !

Pour rappel et pour être tout à fait clair : Angular désigne le "nouvel" Angular (version 2 et
supérieure), et AngularJS désigne la version 1 de cet outil. Ce sont ces appellations que
j'utiliserai dans le cours.

Introduction
Alors commençons par le commencement, qu'est-ce que c'est Angular, au fait ? Et bien
c'est un Framework.

« Et c’est quoi, un frame… machin ? »

Un Framework est un mot Anglais qui signifie "Cadre de travail". En gros c'est un outil qui
permet aux développeurs (c'est-à-dire vous) de travailler de manière plus efficace et de façon
plus organisée. Vous avez sûrement remarqué que vous avez souvent besoin de faire les mêmes
choses dans vos applications : valider des formulaires, gérer la navigation, lever des erreurs...
Souvent les développeurs récupèrent des fonctions qu'ils ont développées pour un projet, puis les
réutilisent dans d'autres projets. Et bien dans ce cas-là, on peut dire que vous avez développé une
sorte de mini-Framework personnel !
L'avantage d'un Framework professionnel, est qu'il permet à plusieurs développeurs de
travailler sur le même projet, sans se perdre dans l'organisation du code source. En effet, lorsque
vous développez des fonctions "maison", vous êtes le seul à les connaître, et si un jour vous
devez travailler avec un autre développeur, il devra d'abord prendre connaissance de toutes ces
fonctions. En revanche, un développeur qui rejoint un projet qui utilise un Framework, connaît
déjà les conventions et les outils à sa disposition pour pouvoir se mettre au travail.

« Oui d'accord, c'est sympa d'avoir un cadre de travail commun, mais pour travailler sur
quoi exactement ? »

Effectivement, quels genres d'applications peuvent être développés avec Angular ? Et bien
Angular permet de développer des applications web, de manière robuste et efficace. Nous allons
voir la différence entre une application web, et un site web, car cette distinction est très
importante pour bien comprendre dans quoi vous mettez les pieds.

Site Web versus Application Web


La tendance actuelle en développement web est de vouloir séparer complètement :
· La partie client du site : c'est-à-dire les fichiers HTML, CSS et JavaScript, qui sont
interprétés par le navigateur.
· La partie serveur du site : les fichiers PHP, si vous développez votre site en PHP, les
fichiers Java si vous utilisez Java... qui sont interprétés côté serveur. C'est dans cette partie
que l'on fait des requêtes aux bases de données, par exemple.
Un "site web" au sens traditionnel du terme, est donc une application serveur qui envoie
des pages HTML dans le navigateur du client à chaque fois que celui-ci le demande. Quand
l'utilisateur navigue sur votre site et change de page, il faut faire une requête au serveur. Quand
l'utilisateur remplit et soumet un formulaire, il faut faire une requête au serveur... bref, tout cela
est long, coûteux, et pourrait être fait plus rapidement en JavaScript.
Une "application web" est une simple page HTML, qui contient suffisamment de
JavaScript pour pouvoir fonctionner en autonomie une fois que le serveur l'a envoyé au client. Je
vous ai fait un petit schéma d’explication :

Figure 3 - L'architecture d'un site web et d'une application web


A votre avis, quel est le schéma représentant un site web, et celui représentant une
application Web ?
Le schéma de gauche représente un site web : à chaque fois que l'utilisateur demande une
page, le serveur s'occupe de la renvoyer : /accueil, /forum, etc... Dans le cas d'une application
web, le serveur ne renvoie qu'une page pour l'ensemble du site, puis le JavaScript prend le relais
pour gérer la navigation, en affichant ou masquant les éléments HTML nécessaires, pour donner
l'impression à l'internaute qu'il navigue sur un site traditionnel !
L'avantage de développer un site de cette façon, c'est qu'il est incroyablement plus réactif.
Imaginez, vous remplacez le délai d'une requête au serveur par un traitement JavaScript ! De
plus, comme vous n'avez pas à recharger toute la page lors de la navigation, vous pouvez
permettre à l'utilisateur de naviguer sur votre site tout en discutant avec ses amis par exemple !
(Comme la version web de Facebook).
Vous avez remarqué que l'ensemble d'une application web est contenu sur une seule page ?
Eh bien, on appelle ce genre d'architecture une architecture SPA, ce qui signifie simplement
Single Page Application.
Même si Angular ne vous plait pas au final (ce dont je doute fort !), vous serez sûr d'avoir
appris beaucoup de choses sur l'écosystème bourgeonnant du développement d'applications web.
La première partie de ce cours contient essentiellement des chapitres théoriques qui permettront
de poser le décor avant d'attaquer le vif du sujet. Nous terminerons la première partie de ce cours
avec le fameux "Hello, World !" et un questionnaire pour vérifier que vous n'avez pas fait
semblant de suivre ce cours.
Le "Hello, World !" est un grand classique en programmation. Dans chaque langage que vous
aborderez, la première chose à faire est de développer une petite application qui affiche un
message de bienvenue à l'utilisateur pour vérifier que tout marche bien ! On appelle cette petite
application un "Hello, World ! ».

Différences entre Angular et AngularJS


C'était quoi, AngularJS ?
AngularJS était très populaire et a été utilisé pour développer des applications clientes
complexes, exactement comme son grand-frère Angular. Il permettait de réaliser de grosses
applications et de les tester avec efficacité. Il a été développé dans les locaux de Google en 2009
et était utilisé pour quelques-unes de ces applications en interne (je ne pourrai pas vous dire
lesquelles par contre).
Pour les développeurs d'AngularJS, je vous ai préparé une petite liste ci-dessous des
changements majeurs entre la version un et la deux. Voici résumé en six points les changements
qui me paraissent le plus important :
1. Les contrôleurs : L'architecture traditionnelle MVC est remplacée par une architecture
réactive à base de composants web. L'architecture MVC est une architecture classique que
l'on retrouve dans beaucoup de Framework (Symfony, Django, Spring) permettant de
découper le Modèle, la Vue et le Contrôleur.
2. Les directives : La définition existante de l'objet Directive est retirée, et remplacée par
trois nouveaux types de directives à la place : les composants, les directives d'attributs et
les directives structurelles.
3. Le $scope : Les scopes et l'héritage de scope sont simplifiés et la nécessité d'injecter des
$scopes est retirée.
4. Les modules : Les modules AngularJS sont remplacés par les modules natifs d'ES6.
5. jQLite : Cette version plus légère de jQuery était utilisée dans AngularJS. Elle est retirée
dans Angular, principalement pour des raisons de performance.
6. Le two-way data-binding : Pour les même raisons de performances, cette fonctionnalité
n'est pas disponible de base. Cependant, et il est toujours possible d'implémenter ce
mécanisme avec Angular.

Pourquoi Angular ?
L'équipe de Google qui travaille sur AngularJS a officiellement annoncé Angular à la
conférence européenne Ng-Conf en Octobre 2014 (Une conférence consacrée spécialement à
Angular). Ils ont annoncé que cette nouvelle version ne serait pas une mise à jour, mais plutôt
une réécriture de l'ensemble du Framework, ce qui causera des changements de ruptures
important. D'ailleurs, il a été annoncé qu’AngularJS ne serait plus maintenu à partir de 2018. On
dirait qu'ils veulent faire passer un message, vous ne trouvez pas ?
Les motivations pour développer un nouveau Framework, tout en sachant que cette version
ne serait pas rétro-compatible, étaient les suivantes :
1. Les standards du web ont évolué, en particulier l'apparition des Web Components (nous
y reviendrons) qui ont été développés après la sortie d'AngularJS, et qui fournissent des
meilleures solutions de manière native que celles qui existent actuellement dans les
implémentations spécifiques d'AngularJS. Angular a été l'un des premiers Frameworks
conçu pour intégrer sérieusement avec les Web Components.
2. JavaScript ES6 - la plupart des standards d'ES6 ont été finalisés et le nouveau standard a
été adopté mi-2015 (Nous y reviendrons également). ES6 fournit des fonctionnalités qui
peuvent remplacer les implémentations existantes d'AngularJS et qui améliorent leurs
performances. Pourquoi s'en priver ?
3. Performance - AngularJS peut être amélioré avec de nouvelles approches et les
fonctionnalités d'ES6. Différents modules du noyau d'Angular ont été également retirés,
notamment jqlite (une version de JQuery allégée pour Angular) ce qui résulte en de
meilleures performances. Ces nouvelles performances font d'Angular un outil parfaitement
approprié pour développer des applications web mobiles.

La philosophie d'Angular
Angular est un Framework orienté composants. Lors du développement de nos
applications, nous allons coder une multitude de petits composants, qui une fois assemblés tous
ensemble, constitueront une application à part entière. Un composant est l'assemblage d'un
morceau de code HTML, et d'une classe JavaScript dédiée à une tâche particulière.

Hé mais attends, ... depuis quand il y a des classes en JavaScript ?

Oui, je sais, les classes n'existent pas en JavaScript... mais je vous dis qu'on va quand
même en utiliser dans nos développements, soyez patient !
Ce qu'il faut bien comprendre, c'est que ces composants reposent sur le standard des Web
Components, que nous verrons dans un chapitre dédié. Ce standard n'est pas encore supporté par
tous les navigateurs, mais ça pourrait le devenir un jour. Il a été pensé pour découper votre page
web en fonction de leur rôle : barre de navigation, boîte de dialogue pour tchatter, contenu
principal d'une page... Un composant est censé être une partie qui fonctionne de manière
autonome dans une application.
Angular n'est pas le seul à s'intéresser à ce nouveau standard, mais c'est le premier (enfin,
je n'en connais aucun autre avant lui) à considérer sérieusement l'intégration des Web
Components.

Le fonctionnement global d'une application


Sans rentrer dans les détails, je vais vous présenter quelques briques élémentaires qui
forment les bases de toute application Angular qui se respecte. Cela fait beaucoup de nouvelles
choses d'un coup, du vocabulaire et des concepts en pagaille, mais je ne vous demande pas de
tout retenir, lisez simplement cette partie de haut en bas, et revenez-y quand vous ne vous
rappelez plus de certains éléments et que vous avez besoin d'un rafraichissement de mémoire !
Comme nous l'avons vu précédemment, toute votre application tiendra dans une simple
page HTML : cela change radicalement la manière de concevoir vos sites web. Heureusement,
Angular nous aide à organiser et à développer efficacement tout le code dont nous aurons besoin.
Il fournit également plusieurs librairies, dont certaines d’entre elles forment le cœur du
Framework.
Nous allons voir quatre éléments à la base de toute application Angular :
· Le Module
· Le Composant. (ou 'Component' en anglais)
· Le Template.
· Les métadonnées. (j'utiliserai le terme annotation dans la suite de ce cours)

Nous verrons chacun de ces éléments de manière plus poussée par la suite.

Le Module
Un module est une brique de votre application, qui une fois assemblée avec d'autres
briques, forme une application à part entière. Chaque module est dédié à une fonctionnalité
particulière.
Par exemple, la librairie @angular/core est une librairie Angular qui contient la plupart des
éléments de base dont nous aurons besoin pour construire notre application. Si nous voulons
créer un composant, il faudra importer la classe de base d'un composant depuis ce composant :
01 import { Component } from ‘@angular/core’ ;
Souvent, on aura besoin d'importer des éléments depuis notre propre code, sans passer par
une librairie. Dans ce cas on renseigne un chemin relatif à la place du nom de la librairie :
01 import { MaClasse } from ‘./ce-dossier’ ;

Heu ... c'est quoi cette syntaxe exactement ?

On importe et exporte des éléments grâce à la syntaxe des modules ECMAScript 2015. Si
ça ne vous avance pas beaucoup, sachez que cette syntaxe a vu le jour pour ré- pondre à un
problème précis.
Si vous avez déjà développé un site avec une grosse dose de JavaScript, vous vous êtes
rendu compte que l'on se retrouve vite avec beaucoup de fichiers JavaScript qui dépendent tous
les uns des autres, et on ne sait plus dans quel ordre les charger. Il a vite fallu améliorer
l’organisation de ce code, et les développeurs se sont tournés vers des outils comme require.js.
Vous n'avez pas besoin de connaître ces outils pour continuer à suivre le cours, mais sachez que
nous utiliserons System.js avec Angular, et que cet outil s'occupe pour nous de charger les
modules et leurs dépendances.
ECMAScript 2015, ES6 ou ES2015 désigne le même standard, que je vous présenterai dans un
chapitre dédié.

Le Composant
Les composants sont la base des applications Angular. Vous allez écrire des composants
tout le temps, pour gérer les interactions avec l'utilisateur, appeler des services, traiter les
formulaires. Chaque composant contrôlera un petit bout de votre interface utilisateur, que nous
appellerons une vue.
On définit la logique d’application d’un composant - ce qu’il doit faire pour supporter la
vue - à l’intérieur d’une classe, et cette classe interagit avec la vue à travers un ensemble de
propriétés et de méthodes.
Par exemple, un composant FilmsComponent pourrait avoir une propriété films, qui
retourne un tableau de films. La vue associée à ce composant pourrait contenir un tableau HTML
qui afficherait ses films.

Il est recommandé de suffixer le nom de vos composants par 'Component'. Vous verrez que ce
sera utile pour vous y retrouvez quand vous aurez des classes de services, de modèles, …

Le Template
Un template ? C'est quoi ? Ne vous inquiétez pas, c'est simplement une "vue", associée à
un composant. En fait, à chaque fois que l'on définit un composant, on lui associe toujours un
template. Un template est une forme de HTML spéciale qui dit à Angular ce que doit afficher le
composant. Parfois, un template contient simplement du HTML traditionnel. Par exemple, le
code ci-dessous pourrait être un template Angular, sans aucun problème :
01 <h1>Blog sur Angular2</h1>
02 <p>Bienvenue sur mon blog, je m'appelle Jean Dupond !</p>

Mais parfois on a besoin d'injecter dans notre template des informations issues du
composant lui-même :
01 <h1>Blog de Angular2</h1>
02 <p>Bienvenue sur mon blog, je m'appelle {{ prenomAuteur }} !</p>

Avez-vous compris ? - La syntaxe avec les accolades {{prenomAuteur}} indique à Angular,


que cette variable n'est pas disponible dans le template, et que la valeur de cette variable se
trouve dans le composant qui gère ce template. En dehors de cette nouvelle syntaxe, que nous
verrons dans un chapitre dédié, vous reconnaissez les balises HTML classiques <h1> et <p>.
Les Annotations
Les métadonnées indiquent à Angular comment il doit traiter une classe. Par exemple,
regardez la classe ci-dessous :
01 export class AppComponent { … }

Comme son nom l'indique, il s'agit bien d'une classe de composant, puisqu'elle est suffixée
par 'Component'.
Cependant, comment Angular sait qu'il s'agit vraiment d'un composant et pas d'un modèle,
ou d'un service par exemple ? Et bien nous devons ajouter des annotations sur la classe
AppComponent. Nous allons ajouter l'annotation @component pour indiquer à Angular que cette
classe est un composant :
01 @Component({
02 selector: 'mon-app',
03 template: '<p>Ici le template de mon composant</p>'
04 })
05 export class AppComponent { ... }
Les annotations ont souvent besoin d'un paramètre de configuration. Le décorateur
@Component n'échappe pas à cette règle, et prend en paramètre un object qui contient les
informations dont Angular a besoin pour créer et lier le composant et son template. Il y a
plusieurs options de configuration possible, mais nous n'allons voir que les deux plus importants
pour le moment, qui sont obligatoires lorsque vous définissez un composant :
· Selector : un sélecteur est un identifiant unique dans votre application, qui indique à
Angular de créer et d'insérer une instance de ce composant à chaque fois qu'il trouve une
balise <mon-app></mon-app> dans du code HTML d'un composant parent. Le nom de
cette balise vient de la ligne 2 du code ci-dessus.
· Template : Le code du template lui-même. Il est également possible d'écrire le code du
template dans un fichier à part si vous le souhaitez. Dans ce cas, remplacez template par
templateUrl, et indiquez chemin relatif vers le fichier du template.

Hey, mais ce n'est pas du HTML valide ça : <mon-app></mon-app> !

Et bien détrompez-vous, ce code est parfaitement valide, et vous pourrez même le faire
valider par le W3C. Nous verrons pourquoi dans la suite de ce cours.

Il existe bien sûr d'autres annotations : @Injectable, @Input ou encore @Pipe par exemple.

Conclusion
Il y a bien sûr beaucoup d'autres éléments qui peuvent constituer une application Angular,
mais nous ne sommes qu'au début de notre apprentissage !
Si à un moment ou à un autre vous bloquez sur le cours, ne paniquez pas. Pensez à prendre
une grande respiration, et à reprendre lentement la partie qui vous bloque, depuis le début. Il est
important de noter que Angular est une technologie récente, et que par conséquent elle évolue
rapidement. C'est pourquoi savoir s'adapter au fur et à mesure et ne pas baisser les bras est
important.
C’est pourquoi il est important de s’adopter au fur et à mesure et ne pas baisser les bras.
Pour les curieux, sachez que la documentation officielle d’Angular se trouve sur le site
https://angular.io/. Par contre, cette documentation n'est disponible qu'en anglais. Qui a dit que
l'anglais était partout en informatique ?

En résumé
· Les sites web deviennent de plus en plus de véritables applications, et une utilisation
intensive du langage JavaScript devient nécessaire.
· Angular est un Framework orienté composant, votre application entière est un
assemblage de composants.
· Les quatre éléments à la base de toute application sont : le module, le composant, le
Template et les annotations.
· Nous utiliserons SystemJS pour charger les modules de nos applications.
· Angular est conçu pour le web de demain et intègre déjà la norme ECMAScript6 (ES6) et
les Web Components.
· Enfin, retenez que tout cet écosystème est bourgeonnant et change très vite. Soyez
persévérant lors de votre apprentissage et ne vous découragez pas, et vous prendrez vite
plaisir à utiliser Angular !
Chapitre 2 : ECMAScript 6
Le nouveau visage de JavaScript
Si vous n'avez jamais entendu parler d'ECMAScript6 (ou ES6), c'est que vous n'avez pas
encore percé tous les mystères de JavaScript ! Vous avez sans doute remarqué que JavaScript est
un langage un peu à part : un système d'héritage dit "prototypale", des fonctions "anonymes", ...
Bref, le besoin d'une nouvelle standardisation s'est fait sentir pour fournir à JavaScript les
moyens de développer des applications web robustes.

C'est quoi, ECMAScript 6 ?


ECMAScript 6 est le nom de la dernière version standardisé de JavaScript. Ce standard a
été approuvé par l'organisme de normalisation en Juin 2015 : cela signifie que ce standard va être
supporté de plus en plus par les navigateurs dans les temps à venir. ECMAScript 6 a été annoncé
pour la première fois en 2008, et bientôt il deviendra inévitable (Il est important de noter toutes
les versions futures de ECMAScript ne prendront pas autant de temps).

« Mais ça fait quelque temps que je développe en JavaScript, je n'ai jamais entendu
parler de tout ça ! »

En fait, sans le savoir, vous deviez surement développer en ES5 car c'est le standard le plus
courant utilisé depuis quelques années.

Pour votre information, ECMAScript6, ECMAScript 2015 et ES6 désignent la même chose.
Nous utiliserons comme petit nom ES6, car c'est son surnom le plus populaire.

Même si toutes les nouveautés d'ES6 ne fonctionnent pas encore dans certains navigateurs,
beaucoup de développeurs ont commencé à développer avec ES6 (Pourquoi ne pas prendre un
peu d’avance ?) et utilisent un transpilateur pour convertir leur code ES6 en du code ES5. Ainsi
leur code peut être interprété par tous les navigateurs.

« Un transpilateur ? »

Le transpilateur est un outil qui permet de publier son code pour les navigateurs qui ne
supportent pas encore l'ES6 : le rôle du transpilateur est de traduire le code ES6 en code ES5.
Comme certaines fonctionnalités d'ES6 ne sont pas disponibles dans ES5, leur comportement est
simulé.
Dans ce chapitre nous n'allons pas voir comment utiliser un transpilateur : nous allons juste
nous consacrer à la découverte théorique d'ES6, et vous verrez qu'il y a déjà pas mal de choses à
voir. Mais dès le prochain chapitre, nous utiliserons un transpilateur ! Patience…
La bonne nouvelle c'est que nous allons coder en ES6, et être ainsi à la pointe de la
technologie !
Voyons tout de suite ce que ES6 apporte et ce qu'il est possible de faire avec, en avant !
Sachez qu’ECMAScript 6 est une spécification standardisée qui ne concerne pas seulement
JavaScript, mais également le langage Swift d’ Apple, entre autres ! JavaScript est une des
implémentations de la spécification standardisée ECMAScript 6.

Le nouveau monde d'ES6


Commençons tout de suite avec une des nouveautés les plus intéressantes d'ES6 : il est
désormais possible d'utiliser des classes en Javascript !

Les classes
Pour créer une simple classe Vehicule avec ES5, nous aurions dû faire comme ceci :
01 function Vehicle(color, drivingWheel) {
02 this.color = color;
03 this.drivingWheel = drivingWheel;
04 this.isEngineStart = false;
05 }
06 Vehicle.prototype.start = function start(){
07 this.isEngineStart = true;
08 }
09 Vehicle.prototype.stop = function stop(){
10 this.isEngineStart = false;
11 }
Le code ci-dessus était le moyen détourné utilisé pour créer un objet, en utilisant la
mécanique des prototypes, propre à JavaScript.

Si vous ne voyez pas ce qu'est l'héritage prototypal, vous pouvez regarder cette vidéo très bien
faite sur Youtube, en français.

Mais ES6 introduit une nouvelle syntaxe : class. C'est le même mot clé que dans d'autres
langages, mais sachez que c'est toujours de l'héritage par prototype qui tourne derrière, mais vous
n'avez plus à vous en soucier.
01 class Vehicle {
02 constructor(color, drivingWheel) {
03 this.color = color;
04 this.drivingWheel = drivingWheel;
05 this.isEngineStart = false;
06 }
07 start() {
08 this.isEngineStart = true;
09 }
10 stop() {
11 this.isEngineStart = false;
12 }
13 }
Voilà une classe JavaScript, bien différente de ce que l'on avait précédemment. Si vous
avez bien remarqué à la ligne 2, on a même droit à un constructeur !
C’est merveilleux !

L'héritage
En plus d'avoir ajouté les classes en JavaScript, ES6 continue avec l'héritage. Plus besoin
de l'héritage prototypal.
Avant, il fallait appeler la méthode call pour hériter du constructeur. Par exemple, pour
développer une classe Voiture et Moto, héritant de la classe Vehicule, on écrivait quelque chose
comme ça :
01 function Vehicle (color, drivingWheel) {
02 this.color = color;
03 this.drivingWheel = drivingWheel;
04 this.isEngineStart = false;
05 }
06 Vehicle.prototype.start = function start() {
07 this.isEngineStart = true;
08 }
09 Vehicle.prototype.stop = function stop() {
10 this.isEngineStart = false;
11 };
12 // Une voiture est un véhicule.
13 function Car(color, drivingWheel, seatings) {
14 Vehicle.call(this, color, drivingWheel);
15 this.seatings = seatings;
16 }
17 Vehicle.prototype = Object.create(Vehicle.prototype);
18 // Une moto est un véhicule également.
19 function Motorbike (color, drivingWheel, unleash) {
20 Vehicle.call(this, color, drivingWheel);
21 this.unleash = unleash;
22 }
23 Motorbike.prototype = Object.create(Vehicle.prototype);

Maintenant on peut utiliser le mot clé extends en JavaScript (non, non ce n'est pas une
blague), et le mot-clé super, pour rattacher le tout à la "superclasse", ou "classe-mère" si vous
préférez.
01 class Vehicle {
02 constructor(color, drivingWheel, isEngineStart = false) {
03 this.color = color;
04 this.drivingWheel = drivingWheel;
05 this.isEngineStart = isEngineStart;
06 }
07 start() {
08 this.isEngineStart = true;
09 }
10 stop() {
11 this.isEngineStart = false;
12 }
13 }
14 class Car extends Vehicle {
15 constructor(color, drivingWheel, isEngineStart = false, seatings) {
16 super(color, drivingWheel, isEngineStart);
17 this.seatings = seatings;
18 }
19 }
20 class Moto extends Vehicle {
21 constructor(color, drivingWheel, isEngineStart = false, unleash) {
22 super(color, drivingWheel, isEngineStart);
23 this.unleash = unleash;
24 }
25 }

Le code est quand même plus clair et concis avec cette nouvelle syntaxe.

Les paramètres par défaut


En JavaScript, on ne peut pas restreindre le nombre d'arguments attendus par une fonction,
ni définir des paramètres comme facultatifs. Voici l'implémentation traditionnelle de la fonction
Somme en JavaScript (Sum en Anglais), qui prend un nombre quelconque d'arguments en
paramètre, les additionne, puis retourne le résultat :
01 function sum(){
02 var result = 0;
03 for (var i = 0; i < arguments.length; i++) {
04 result += arguments[i];
05 }
06 return result;
07 }
Comme vous pouvez le constater, il n'y a pas d'arguments dans la signature de la fonction,
mais le mot-clé arguments permet de récupérer le tableau des paramètres passés à la fonction et
ainsi de le traiter dans le code de la fonction. Par contre, ce n'est pas très pratique si l'on veut
définir un nombre déterminé de paramètres.
Et pour définir des valeurs par défaut ? Les développeurs avaient l'habitude de bricoler
pour faire comme si les variables par défaut étaient supportées :
01 function someFunction (defaultValue) {
02 defaultValue = defaultValue || undefined;
03 return defaultValue;
04 }
Dans le code ci-dessus, si aucun paramètre n'est passé en paramètre, la fonction renverra
undefined, sinon elle renverra la valeur passée en paramètre.
Imaginons une fonction qui multiplie deux nombres passés en paramètres. Mais le
deuxième paramètre est facultatif, et vaut un par défaut :
01 function multiply (a, b) {
02 // b est facultatif
03 var b = typeof b !== 'undefined' ? b : 1;
04 return a*b;
05 }
06 multiply (2, 5); // 10
07 multiply (1, 5); // 5
08 multiply (5); // 5
A la ligne 3, nous utilisons un opérateur ternaire pour attribuer la valeur 1 à la variable b si
aucun nombre n'a été passé en deuxième paramètre. Ce code semble très compliqué pour réaliser
quelque chose de très simple.
Heureusement, ES6 nous permet d'utiliser une syntaxe plus élégante :
01 function multiply (a, b = 1) {
02 return a*b;
03 }
04 multiply (5); // 5

Avec ES6, il suffit de définir une valeur par défaut dans la signature même de la fonction.
C'est quand même beaucoup plus pratique non ?

Les nouveaux mots clés : « let » & « const »


Le mot-clé let permet de déclarer une variable locale dans le contexte où elle a été assignée
(Un contexte est le terme français pour désigner un scope en Anglais).
Par exemple, les instructions que vous écrivez dans le corps d'une fonction ou à l'extérieur
n'ont pas le même contexte. Normalement une instruction if n'a pas de contexte en soi, mais
maintenant si, avec le mot clé let. Cela peut être utile pour effectuer beaucoup d'opérations sur
une variable, sans polluer d'autres contextes avec des variables qui ne sont pas nécessaires :
01 var x = 1;
02 if(x < 10) {
03 let v = 1;
04 v = v + 21;
05 console.log(v);
06 }
07 // v n'est pas définie, car v a été déclarée avec 'let' et non 'var'.
08 console.log(v);

Et cela fonctionne pour les boucles également !


01 for (let i = 0; i < 10; i++) {
02 console.log(i); // 0, 1, 2, 3, 4 ... 9
03 }
04 // i n'est pas défini hors du contexte de la boucle
05 console.log(i);
En général, garder un contexte global propre est vivement conseillé et c'est pourquoi ce
mot clé est vraiment le bienvenu ! Sachez que let a été pensé pour remplacer var, alors nous
n'allons pas nous en priver dans ce cours.
Le mot clé const quant à lui, permet de déclarer ... des constantes ! Voyons comment cela
fonctionne :
01 const PI = 3.141592 ;
Une déclaration de constante ne peut se faire qu'une fois, une fois définie, vous ne pouvez
plus changer sa valeur :
01 PI = 3.14 // Erreur : la valeur de PI ne peut plus être modifié

C'est bien pratique si vous avez besoin de définir des valeurs une bonne fois pour toutes
dans votre application.
Cependant, le comportement est un peu différent pour une constante de tableau ou d’objet.
Vous ne pouvez pas modifier la référence vers le tableau ou l'objet, mais vous pouvez continuer
à modifier les valeurs du tableau, ou les propriétés de l'objet :
01 const MATHEMATICAL_CONSTANTS = {PI: 3.141592, e: 2,718281};
02 MATHEMATICAL_CONSTANTS.PI = 3.142; // Aucun problème.

Une dernière chose. En JavaScript, comme dans beaucoup d'autres langages, il existe des
mots-clés réservés. Ce sont des mots que vous ne devez pas utiliser comme noms de variables, de
fonctions ou de méthodes, car JavaScript a une utilité spéciale pour eux. Voici quelques
exemples de mots-clés : if, else, new, var, for ... mais également class ou super !

Retrouvez la liste exhaustive des mots-clés réservés JavaScript (ES6 compris) ici.

Un raccourci pour créer des objets


ES6 apporte un raccourci sympathique pour créer des objets. Observez le code suivant :
01 function createCar() {
02 let color = ‘green’;
03 let wheel = 4;
04 return { color, wheel };
05 }
Cela vous semble étrange ? Vous avez l'impression qu'il manque quelque chose ? Et bien
vous avez raison, normalement nous aurions dû écrire :
01 function createCar() {
02 let color = ‘green’;
03 let wheel = 4;
04 return { color: color, wheel: wheel };
05 }
Comme vous pouvez le constater, à la ligne 4, si la propriété de l'objet a le même nom que
la variable utilisé comme valeur pour l'attribut, nous pouvons utiliser le raccourcis d'ES6 comme
ci-dessus.

Les promesses
Les promesses, ou "promises" en anglais, étaient déjà présentes dans AngularJS. Pour ceux
qui ne voient pas de quoi on parle, nous allons voir ça tout de suite.
L'objectif des promesses est de simplifier la programmation asynchrone. En général, on
utilise les fonctions de callbacks (des fonctions anonymes qui sont appelées à certains moments
dans votre application), mais les Promesses sont plus pratiques que les Callbacks. Voici par
exemple un code avec des callbacks, pour afficher la liste d'amis d'un utilisateur quelconque :
01 getUser(userId, function(user) {
02 getFriendsList(user, function(friends) {
03 showFriends(friends);
04 });
05 });
L'exemple ci-dessus permet de récupérer un objet user à partir de son identifiant, puis de
récupérer la liste de ses amis, et enfin d'afficher cette liste. On constate que ce code est très
verbeux, et qu'il sera vite compliqué de le maintenir. Les promesses nous proposent un code plus
efficace et plus élégant :
01 getUser(userId)
02 .then(function(user) {
03 getFriendsList(user);
04 })
05 .then(function(friends) {
06 showFriends(friends);
07 });

Il n'y a même pas besoin d'explications j’espère : le code est assez clair, il parle de lui-
même !

Heu, oui peut-être, mais c'est quoi cette méthode then ?

Vous avez raison, je ne vous ai pas encore tout dit. En fait, lorsque vous créez une
promesse (avec la classe Promise), vous lui associez implicitement une méthode then, et cette
méthode prend deux arguments : une fonction en cas de succès et une fonction en cas d'erreur.
Ainsi, lorsque la promesse est réalisée, c'est la fonction de succès qui est appelée, et en cas
d'erreur, c'est la fonction d'erreur qui est invoquée.
Voici un exemple d'une promesse qui récupère un utilisateur depuis un serveur distant, à
partir de son identifiant :
01 let getUser = function(userId) {
02 return new Promise(function(resolve, reject) {
03 // appel asynchrone au serveur pour récupérer
04 // les informations d'un utilisateur...
05 // à partir de la réponse du serveur,
06 // j'extrais les données de l'utilisateur :
07 let user = response.data.user;
08 if(response.status === 200) {
09 resolve(user);
10 } else {
11 reject('Cet utilisateur n\'existe pas.');
12 }
13 })
14 }

Et voilà, vous venez de créer une promesse ! Vous pouvez ensuite l'utiliser avec la
méthode then dans votre application :
01 getUser(userId)
02 .then(function (user) {
03 console.log(user); // en cas de succés
04 }, function (error) {
05 console.log(error); // en cas d'erreur
06 });

Voilà, vous en savez déjà un peu plus sur les Promesses. Je voudrais justement vous
montrer autre chose qui pourrait vous intéresser, les arrow functions, qui vous permettent de
simplifier l'écriture des fonctions anonymes : cela se combine parfaitement avec l'utilisation des
promesses.

Les fonctions fléchées, ou « Arrow Functions »


Les arrows functions (ou fonctions 'fléchées' en français) sont une nouveauté d'ES6. Le
premier cas d'utilisation des arrows functions est avec this. L'utilisation de this peut être
compliquée, surtout dans le contexte de fonctions à l'intérieur d'autres fonctions. Prenons
l'exemple ci-dessous :
01 class Person {
02 constructor(firstName, email, button) {
03 this.firstName = firstName;
04 this.email = email;
05 button.onclick = function() {
06 // ce 'this' fait référence au bouton,
07 // et non à une instance de Personne.
08 sendEmail(this.email);
09 }
10 }
11 }
Comme vous le voyez en commentaire, le this de la ligne 8 ne fait pas référence à
l'instance de Person mais au bouton sur lequel l'utilisateur a cliqué. Hors, c'est le contraire que
nous souhaitons, nous voulons avoir accès au mail de la personne, pas au bouton ! Les
développeurs JavaScript ont donc pensé à utiliser une variable intermédiaire à l'extérieur du
contexte de la fonction, souvent nommé that, comme ceci :
01 class Person {
02 constructor(firstName, email, button) {
03 this.firstName = firstName;
04 this.email = email;
05 // 'this' fait référence ici à l'instance de Personne
06 var that = this;
07
08 button.onclick = function() {
09 // 'that' fait référence à la même instance de Personne
10 sendEmail(that.email);
11
12 }
13 }
14 }

Cette fois-ci, nous obtenons le comportement souhaité, mais avouez que ce n'est pas très
élégant.
C'est là que les arrows functions entrent en scène. Nous pouvons écrire la fonction onclick
comme ceci :
01 button.onclick = () => { sendEmail(this.email); }

Les arrow functions ne sont donc pas tout à fait des fonctions classiques, parce qu’elles ne
définissent pas un nouveau contexte comme les fonctions traditionnelles. Nous les utiliserons
beaucoup dans le cadre de la programmation asynchrone qui nécessite beaucoup de fonctions
anonymes.
C'est également pratique pour les fonctions qui tiennent sur une seule ligne :
01 someArray = [1, 2, 3, 4, 5];
02 let doubleArray = someArray.map((n) => n*2);
03 console.log(doubleArray); // [2, 4, 6, 8, 10]

La fonction map de JavaScript permet d'appliquer un traitement à chaque élément d'un tableau
grâce à une fonction passé en paramètre.

Pour reprendre l'exemple avec les promesses, on peut écrire ça :


01 // un traitement asynchrone en quelques lignes !
02 getUser(userId)
03 .then(user => getFriendsList(user))
04 .then(friends => showFriends(friends));

La collection Map
Map est l'équivalent d'un dictionnaire dans lequel vous ajoutez des paires de clé-valeur.
Imaginons par exemple le cas où vous souhaitez stocker le classement d'un joueur :
01 let zlatan = {rank: 1, name: 'Zlatan'};
02 // Je crée un nouveau dictionnaire
03 let players = new Map();
04 // J'ajoute lobjet 'zlatan' à la clé '1'
05 players.set(zlatan.rank, zlatan.name);
Vous disposez maintenant d'un dictionnaire contenant un joueur et son classement. Nous
verrons ci-dessous les opérations que nous pouvons effectuer sur ce dictionnaire.

La collection Set
Il est également possible de créer des listes sur le même principe. Une liste se comporte
comme un dictionnaire, mais sans clés.
01 let players = new Set(); // Je crée une nouvelle liste
02 players.add(zlatan); // j'ajoute un joueur dans cette liste
Vous remarquez que j'utilise la méthode add et non set comme pour les dictionnaires.
Enfin, sachez que vous pouvez faire ensuite tout un tas d'opérations sur vos collections,
qu'il s'agisse de dictionnaires ou de listes. Regardez le code ci-dessous, il est suffisamment
explicite pour pouvoir se passer d'explications.
01 // affiche le nombre d'éléments dans la collection
02 players.size;
03 // Dictionnaire: affiche si oui ou non,
04 // le dictionnaire contient la clé correspondant au rang de Zlatan.
05 players.has(zlatan.rang);
06 // Liste: affiche si oui ou non, la liste contient le joueur Zlatan.
07 players.has(zlatan);
08 // Dictionnaire: supprime un élément d'après une clef.
09 players.delete(zlatan.rang);
10 // Liste: supprime l'élément passé en paramètre.
11 players.delete(zlatan);

Les 'Template Strings'


Avant de terminer avec les nouveautés d’ES6, je voulais vous expliquer comment utiliser
les templates strings. C'est une petite amélioration d’ES6 bien sympathique !
Jusqu'à maintenant, concaténer des chaînes de caractères était pénible en JavaScript, il
fallait ajouter des symboles "+" les uns à la suite des autres :
01 let someText = "duTexte";
02 let someOtherText = "unAutreTexte";
03 let embarrassingString = someText;
04 embarrassingString += " blabla";
05 embarrassingString += someText;
06 embarrassingString += "blabla";
07 return embarrassingString;

Comme vous pouvez le constater, c'était assez pénible, et cela augmente les erreurs
d'inattention.
Mais avec ES6, on peut utiliser des templates strings, qui commencent et se terminent par
un backtick (`). Il s'agit d'un symbole particulier qui permet d'écrire des chaînes de caractères sur
plusieurs lignes.
01 // On peut écrire des strings sur plusieurs ligne grâce au backtick
02 let severalLinesString = `bla
03 blablalbalblalballb
04 balblablalabla
05 b
06 ablablablabbl`;
07 // .. mais pas avec des guillemets !
08 // Regardez la couleur syntaxique, vous comprendrez que ce code risque
09 // de lever une erreur !
10 let severalLinesStringWithError = "bla
11 blba
12 blbla
13 blabla"
On peut insérer des variables dans la chaîne de caractères avec ${}, comme ceci :
01 return ‘${this.name} a pour email : ${this.email}’;

Bref, il est bien pratique ce backtick pas vrai ?

Conclusion
Nous avons vu dans ce chapitre plusieurs changements majeurs apportés par ES6 à
JavaScript, et qui nous servirons avec Angular. Sachez que ES6 est rétro-compatible, donc vous
pouvez toujours développer de la façon dont vous le faites actuellement, puis migrer petit à petit
vers la syntaxe d'ES6. Je vous recommande fortement d'adopter ES6 assez rapidement, vous
gagnerez en productivité et en lisibilité avec cette nouvelle syntaxe. Vous vous demandez
maintenant quand est-ce que vous allez pouvoir mettre en pratique tout ce que vous venez
d'apprendre ? Eh bien, direction le prochain chapitre, où nous allons voir le transpilateur
TypeScript !

En résumé
· JavaScript profite de la nouvelle spécification standardisée ECMAScript 6, également
nommée ES6.
· On peut développer en ES6 dès aujourd'hui, et utiliser un transpilateur pour convertir
notre code d’ES6 vers ES5, afin qu'il soit compréhensible par tous les navigateurs.
· ES6 nous permet d'utiliser les classes et l'héritage en JavaScript.
· ES6 introduit deux nouveaux mots-clés : let et const. Le premier permet de déclarer des
variables et tend à remplacer var, et const permet de déclarer des constantes.
· Les Promesses offrent une syntaxe plus efficace que les callbacks, et tendent à les
remplacer, surtout pour les développements relatifs à la programmation asynchrone.
Chapitre 3 : Découvrir TypeScript
Le JavaScript sous stéroïdes
Dans cette troisième partie nous allons aborder un des piliers d'Angular : TypeScript.
Certains d'entre vous en ont peut-être déjà entendu parler, mais les autres ne vous en faites pas,
nous allons démystifier tout cela immédiatement !
Avant de voir plus en détail ce qu'on va faire avec ce langage, et parce que je ne suis pas
très inspiré, je vous propose la définition de Wikipédia :
"TypeScript est un langage de programmation libre et open-source développé par
Microsoft qui a pour but d'améliorer et de sécuriser la production de code JavaScript. C'est un
sur-ensemble de JavaScript (c'est-à-dire que tout code JavaScript correct peut être utilisé avec
TypeScript). Le code TypeScript est transcompilé en JavaScript, pouvant ainsi être interprété par
n'importe quel navigateur web ou moteur JavaScript. Il a été co-créé par Anders Hejlsberg,
principal inventeur de C#)"
Je n'aurais pas été plus clair. Mais je vous rassure on va creuser ça tout de suite, je ne vais
pas vous laisser avec une simple définition de Wikipédia.

C'est quoi, "TypeScript" ?


Le langage JavaScript a toujours présenté les mêmes faiblesses : pas de typage des
variables, absence de classes comme dans les autres langages... Ces faiblesses font que lorsque
l'on commencer à écrire beaucoup de code, on arrive vite à un moment où on s'emmêle les
pinceaux ! Notre code devient redondant, perd en élégance, et devient de moins en moins lisible :
on parle de code spaghetti. (Vous comprendrez tous seuls la référence aux célèbres pâtes
italiennes !)
La communauté des développeurs, ainsi que certaines entreprises, se sont donc mises à
développer des métalangages pour JavaScript : CoffeeJS, Dart, et TypeScript en sont les
exemples les plus célèbres. Ces outils apportent également de nouvelles fonctionnalités qui font
défaut au JavaScript natif, avec une syntaxe moins verbeuse. Par exemple, TypeScript permet de
typer vos variables, ce qui permet d'écrire du code plus robuste. Une fois que vous avez
développé votre application avec le métalangage de votre choix, vous devez le compiler, c'est-à-
dire le transformer en du code JavaScript que le navigateur pourra interpréter.
En effet, votre navigateur ne sait pas interpréter le CoffeeJS, ni le Dart, ni le TypeScript.
Vous devez d'abord compiler ce code en JavaScript pour qu'il soit lisible par votre navigateur.

Puisque ces méta-languages produisent du JavaScript au final, pourquoi je ne


développerais pas directement en JavaScript ? (Pourquoi s'embêter avec cette étape
intermédiaire ?)

En effet, c'est une excellente question. En théorie, on pourrait effectivement se passer de


métalangages et réécrire nous-même les éléments dont nous avons besoin. En pratique, c'est très
différent :
· D'abord vous n'allez pas réécrire tous ce que le métalangage vous apporte en JavaScript,
vous en auriez pour plusieurs mois ou même plusieurs années pour réécrire ce que les
équipes de développement de ces outils ont réalisé.
· Et vous devriez recommencer à chaque nouveau projet !
Autant vous dire que vous en auriez pour trop longtemps pour que ça en vaille la peine. Ne
perdons plus de temps et voyons tous de suite le lien entre TypeScript et ce qui nous intéresse :
Angular.

Angular et TypeScript
Ce que vous devez savoir, c'est qu’Angular vous recommande de développer vos
applications avec TypeScript. Si vous allez sur la documentation officielle, vous verrez que
plusieurs versions de la documentation vous sont proposées :

Figure 4 - Il est possible d'utiliser Dart ou TypeScript pour développer des applications Angular
Alors je ne vais pas tourner autour du pot : il est chaudement recommandé d'utiliser
TypeScript avec Angular, c'est comme ça que vous trouverez le plus d'aide par la suite. Et il y a
une bonne raison à cela : c'est que l'équipe de chez Google chargée de développer le Framework
recommande explicitement d'utiliser TypeScript.
En bonus, Angular lui-même est écrit avec TypeScript. C'est pourquoi nous utiliserons ce
langage, qui est d'ailleurs proposé par défaut sur la documentation officielle.
(Oubliez donc AngularDart pour le moment).

"Hello, TypeScript !"


Pourquoi ne pas se familiariser avec TypeScript à travers une petite initiation ?
Je ne vous propose rien d'original, nous allons commencer par le traditionnel "Hello,
World !" avec TypeScript.
C'est parti, je vous propose de créer un dossier où vous le souhaitez sur votre ordinateur,
l'emplacement n'a pas beaucoup d'importance. A la racine de ce dossier, créez un fichier test.ts,
et ajoutez-y le code suivant :
01 // Fonction qui retourne un message de bienvenue
02 function sayHello(person) {
03 return "Hello, " + person;
04 }
05 // Création d'une variable "Jean"
06 var Jean = "Jean";
07 // On ajoute dans notre la page HTML un message
08 // pour affiche "Hello, Jean".
09 document.body.innerHTML = sayHello(Jean);

L'extension des fichiers TypeScript est .ts, et non .js . Pensez-y lorsque vous nommez vos
fichiers !
Rien de passionnant vous me direz, et pourtant regardez tout ce qu'implique ce petit fichier
:
· Vous avez créé un fichier TypeScript. Si, si, même si vous pensez que c'est du JavaScript,
il s'agit bien de TypeScript ! (regardez l'extension de votre fichier, il s'agit bien de test.ts et
non de test.js). Et oui, le TypeScript sera compilé en JavaScript : donc si vous écrivez
directement du JavaScript, cela ne pose pas de problème pour TypeScript. C'est l'avantage,
TypeScript ne vous impose pas d'abandonner le JavaScript, il vient juste l'améliorer, et
cette souplesse est très agréable !
· Le revers de la médaille, c'est que le navigateur est bien incapable de lire du TypeScript,
tant que celui-ci n'a pas été compilé en JavaScript !
Pour remédier à ce problème, nous allons installer le nécessaire pour transformer notre
TypeScript en JavaScript. Toujours motivé ? Alors c'est parti !
Pour commencer, ouvrez un terminal et placez-vous à la racine de votre projet. Assurez-
vous d'abord que Node.js et Npm sont installés, et vérifier leur versions respectives grâce aux
commandes suivantes :
node –v
v8.11.2
Puis vérifiez aussi que vous avez aussi Npm d'installé :
npm –v
5.9.0

Si vous n'avez pas exactement les mêmes versions que moi, ce n'est pas grave. Par contre vous
devez avoir au moins la version 8 pour Node. Si les deux commandes ci-dessus vous affichent
une erreur, c'est probablement que vous n'avez pas installé Node. Vous allez en avoir besoin
pour la suite, revenez à ce chapitre une fois que vous l'aurez installé.

Bon, ce n’est pas tout, mais nous devons installer le compilateur TypeScript. Il suffit d'une
seule commande pour cela :
npm install –g typescript
Ensuite, vérifiez rapidement que l'installation a bien fonctionné :
tsc –v
Version 2.9.1

Voilà, c'est aussi simple que ça !


Là aussi, la version a de l'importance. Essayez d'avoir au moins la version 2.7, mais pas en-
dessous, ou vous serez bloqués dans la suite du cours. Je vous avais prévenu que c'était un
écosystème bourgeonnant !
Mais le meilleur arrive, nous allons transformer notre code TypeScript en JavaScript grâce
à la commande suivante :
tsc test.ts

Si vous regardez le contenu du dossier de votre application, vous verrez qu'un nouveau
fichier est apparu : test.js !
Qu'est-ce que nous attendons pour ouvrir ce fichier qui a été créé spécialement pour nous ?
Ouvrez donc le fichier test.js avec votre éditeur de texte, voici ce qu'il devrait contenir :
01 // Fonction qui retourne un message de bienvenu
02 function sayHello(person) {
03 return "Hello, " + person;
04 }
05 // Création d'une variable "Jean"
06 var Jean = "Jean";
07 // On ajoute dans notre la page HTML un message
08 // pour affiche "Hello, Jean".
09 document.body.innerHTML = sayHello(Jean);

Mais rien n'a changé dans ce fichier, à part son extension ts en js !


Ben oui, c'est du JavaScript ! La seule chose que le compilateur a fait, c'est de supprimer
les espaces entre les lignes ! Qu'est-ce que vous vouliez qu'il fasse, notre fichier initial ne
contenait que du JavaScript.
Bon je vous rassure, on va faire des choses un peu plus intéressantes dans la suite de ce
chapitre, c'était surtout pour vous montrer comment ça marche !

A quoi servent les fichiers de définition dans TypeScript ? Les fichiers avec l'extension
*.d.ts ?

Si vous voulez utiliser des bibliothèques externes écrites en JavaScript, vous ne pouvez pas
savoir le type des paramètres attendus par telle ou telle fonction d'une bibliothèque. C'est
pourquoi la communauté TypeScript a créé des interfaces pour les types et les fonctions exposés
par les bibliothèques JavaScript les plus populaires (comme jQuery par exemple). Les fichiers
contenant ces interfaces ont une extension spéciale : *.d.ts. Ils contiennent une liste de toutes les
fonctions publiques des librairies utilisées.
Mais concernant votre code, sachez que depuis TypeScript 1.6, le compilateur est capable
de trouver tout seul ces interfaces avec les dépendances de notre répertoire node_modules. On n'a
donc plus à le faire par nous-même !

Pas de limites avec TypeScript


Bon, nous allons maintenant voir ce que TypeScript a dans le ventre, et ce qu'il va nous
apporter avec Angular. Et quoi de plus normal pour commencer, que de voir les types de
TypeScript.

Le typage avec TypeScript


La syntaxe pour déclarer une variable typée avec TypeScript est la suivante :
01 // Syntaxe pour déclarer une variable typé en TypeScript
02 var variable: type;

Vous êtes libre de choisir le type que vous voulez. Vous pouvez utiliser des types basiques
ou alors créer vos propres types :
01 // On déclare un nombre
02 var lifePoint: number = 100;
03 // On déclare une chaîne de caractère
04 var name: string = 'Green Lantern';
05 // On déclare une variable qui correspond
06 // à une classe de notre application !
07 var greenLantern: Hero = new Hero(lifePoint, name);
08 // On peut créer un autre héros de type Hero
09 var superMan: Hero = new Hero(lifePoint, 'Superman');
10 // Je peux même créer un tableau de héros,
11 // qui contient tous les héros de mon application !
12 var heros: Array<Hero> = [greenLantern, superMan];

Le typage de nos variables est très pratique, puisque cela vous permet d'être sûrs du type
des variables que vous utilisez. Pouvoir être sûr que notre tableau héros ne contient que des héros
et pas des nombres ou autres choses, est plutôt confortable. Notre code devient plus robuste et
plus élégant.
Pour vous prouver ce que je viens de vous dire, essayons d'ajouter une chaîne de caractères
à la variable point de vie. Redéfinissez le code de votre fichier test.ts comme ceci :
01 var lifePoint: number = 100;
02 lifePoint = ‘some-string’;

Ensuite essayez de compiler ce code :


tsc test.ts
Vous obtiendrez alors cette magnifique erreur dans votre console :
Test.ts(7,1) :error TS2322 :Type ‘string’ is not assignable to type ‘number’.
L'erreur est explicite, à la ligne 2 de votre fichier test.js, vous avez essayé d'assigner une
string à une variable que vous aviez déclarée comme étant un nombre, et TypeScript vous
l'interdit bien sûr.
En revanche si vous faites :
01 // Ce code ne causera pas d'erreur car lifePoint est bien de type number.
02 lifePoint = 50;

Et sachez que TypeScript s'occupe de vérifier le type de toutes les variables typées pour
nous. Si dans notre tableau de héros nous essayons d'ajouter un nouvel élément qui ne soit pas un
héros, TypeScript nous retournerait une erreur. Bref, c'est magique non ?

Sachez que TypeScript propose également un type nommé 'any' qui signifie 'tous les types’.

TypeScript et les fonctions


TypeScript permet de spécifier un type de retour pour nos fonctions. Imaginons que nous
voulons créer une fonction pour générer des Heros :
01 // Un constructeur pour notre classe Hero
02 // On spécifie le type de retour après les ':', ici Hero.
03 function createHero(lifePoint: number, name: string): Hero {
04 var hero = new Hero();
05 hero.lifePoint = lifePoint;
06 hero.name = name;
07 return hero;
08 }

Cette fonction doit retourner une instance de la classe Hero, comme indiqué après les ':' à
la ligne 3. Vous avez également remarqué qu'on a pu typer les paramètres de notre fonction ? Là-
aussi, il s'agit d'un gros plus qu'apporte TypeScript, et qui nous permet de développer un code
plus sérieux qu'avec le JavaScript natif !
Vous pouvez également ajouter des paramètres optionnels à vos fonctions. Par exemple,
ajoutons à notre constructeur précédent un paramètre facultatif pour indiquer la planète d'origine
d'un héros, grâce à l'opérateur '?' :
01 // Le '?' indique que le paramètre ‘planet’ est facultatif.
02 function createHero(lifePoint: number, name: string,
03 planet?: string): Hero {
04 var hero = new Hero();
05 hero.lifePoint = lifePoint;
06 hers.name = name;
07 if(planet) hero.planet = planet;
08
09 return heros;
10 }

Les classes avec TypeScript


Nous allons faire une petite expérience intéressante. Nous allons créer une classe vide, et
nous allons la transcompiler vers ES5, puis vers ES6. Créer donc un fichier test.ts quelque part
sur votre ordinateur, et ajoutez-y le code TypeScript suivant :
01 // test.ts
02 class Test {}

Maintenant, nous allons compiler ce code vers du JavaScript ES5, c'est-à-dire vers une
spécification de JavaScript qui ne supporte pas encore le mot-clé class. Pour cela, exécutons la
commande TypeScript que nous avons déjà vu, mais en rajoutant une option pour demander
explicitement à TypeScript de compiler vers du ES5 :
tsc --t ES5 test.ts

Maintenant, si vous allez jeter un coup d'œil au fichier test.js qui vient d'être généré par
TypeScript, voici ce que vous trouverez à l'intérieur :
01 // Ma classe en ES5 (test.js)
02 var Test = (function () {
03 function Test() {
04 }
05 return Test;
06 }());
Mais, c'est une classe ça ???

Et bien d'une certaine manière, oui ! Vous avez sous les yeux une classe, mais avec la
syntaxe d'ES5. Vous y reconnaitrez une IIFE.

IIFE : Immediately-invoked Function Expression. Ce sont des fonctions qui s’exécutent


immédiatement, et dans un contexte privé.

Essayons maintenant de compiler le même code, mais vers ES6 :


tsc --t ES6 test.ts

Si vous affichez maintenant le code généré dans test.js :


01 // Ma classe en ES6 (test.js)
02 class Test {
03 }

Mais, on est revenu au point de départ, non ? C'est exactement le code TypeScript qu'on
avait au début !
Oui, vous avez raison, et c'est parfaitement normal. Vous savez pourquoi ? Parce qu’ES6
supporte parfaitement les classes, comme nous l'avons vu dans le chapitre précédent. Comme
TypeScript et ES6 supportent les classes, et bien vous ne voyez pas de différences !

Les Décorateurs
Vous vous rappelez des métadonnées que nous avons vues dans le premier chapitre ? Non
? Tant pis, rappelez-vous simplement qu'il s'agissait d'ajouter des informations à nos classes via
des annotations. TypeScript propose un moyen d'implémenter ces métadonnées grâce aux
décorateurs.
Prenons un cas simple, vous avez développé une classe, et vous souhaitez indiquer à
Angular qu'il s'agit bien d'une classe de composant, et pas d'une classe lambda, et bien pour cela
vous utiliserez les décorateurs TypeScript :
01 // // Eexemple d'utilisation des décorateurs avec TypeScript
02 @Component({
03 selector: 'my-component',
04 template: 'my-template.html'
05 })
06 export class MyComponent {}

Les Décorateurs sont assez récents dans TypeScript, depuis la version 1.5 exactement. Voilà
pourquoi il est important de veiller à avoir une version à jour.
En Angular, on utilisera régulièrement des décorateurs, qui sont fournis par le Framework.
Retenez également que tous les décorateurs sont préfixés par @.

Conclusion
TypeScript apporte évidemment d'autres fonctionnalités : les valeurs énumérées, les
interfaces, la généricité, ... TypeScript est donc un méta-language qui est surtout connu pour
apporter, entre autres, le typage à JavaScript, mais il s'agit également d’un transpilateur, capable
de générer du code vers ES5 ou ES6 !
L'objectif de ce chapitre était de vous initier à TypeScript, pour voir ensuite son
fonctionnement avec Angular. Si vous voulez pousser plus loin vos connaissances en TypeScript,
je vous invite à regarder la documentation offcielle. La documentation est disponible uniquement
en anglais, mais il s'agit d'anglais technique et la majorité de la documentation est composée
d'exemples de code, vous devriez largement vous y retrouvez même si vous n'êtes pas très à l'aise
avec cette langue.

En résumé
· Angular a été construit pour tirer le meilleur parti d'ES6 et de TypeScript.
· Il est possible d'utiliser JavaScript, ou TypeScript pour ses développements avec Angular.
· Angular a été développé avec TypeScript. Il est fortement recommandé d'adopter
TypeScript pour vos développements avec Angular.
· L'extension des fichiers TypeScript est *.ts et non *.js.
· Le navigateur ne peut pas interpréter le TypeScript, il faut donc compiler le TypeScript
vers du code JavaScript.
· TypeScript apporte beaucoup de fonctionnalités complémentaires à JavaScript comme le
typage des variables, la signature des fonctions, les classes, la généricité, les annotations, ...
· Les annotations TypeScript permettent d'ajouter des informations sur nos classes, pour
indiquer par exemple que telle classe est un composant de l'application, ou telle autre un
service.
Chapitre 4 : Les Web Components
L’avenir du web ?
Avant de commencer à développer notre toute première application Angular, où nous
allons réaliser un splendide "Hello, World !", je tenais à vous présenter les Web Components, ou
Composant Web.
Les Composants Web sont indépendants d'Angular. Cependant les concepteurs d'Angular
ont fait un effort particulier pour les intégrer dans leur Framework, et je pense que ce serait une
bonne chose que vous sachiez de quoi il s'agit. Ce chapitre est très théorique, vous pouvez le lire
en diagonale si vous êtes pressé de passer à la pratique, et revenir lire ce chapitre plus tard. C'est
comme vous voulez !

Introduction aux Web Components


Les Web Components désignent un standard qui permet aux développeurs de créer des
sections complètement autonomes au sein de leurs pages web. On peut par exemple créer un
composant web qui gère l'affichage d'articles dans notre application : ce composant web
fonctionnera indépendamment du reste de la page, il possèdera son propre code HTML, son
propre code CSS et son propre code JavaScript, encapsulé dans le composant web. Il faudra
ensuite insérer ce composant web dans la page principale de notre application pour indiquer que,
à tel endroit, nous voulons afficher des articles grâce à ce composant web.
On peut voir les Web Components comme des widgets réutilisables.
Les Web Components utilisent des capacités standards, nouvelles ou en cours de
développement des navigateurs. Il s'agit d'une technologie récente qui n'est pas encore supportée
par tous les navigateurs. Pour les utiliser dès aujourd'hui, nous devrons utiliser des polyfills pour
combler les lacunes de couverture des navigateurs.

Un polyfill est un ensemble de fonctions, souvent sous forme de scripts JavaScript, permettant
de simuler sur un navigateur web ancien des fonctionnalités qui ne sont pas nativement
disponibles.

Les Web Components sont composés de quatre technologies différentes, qui peuvent
chacune être utilisées séparément, mais qui une fois assemblées forment le standard des Web
Components :
· Les éléments personnalisés (Custom Elements) ;
· Le DOM de l'ombre (Shadow DOM) ;
· Les templates HTML (HTML Templates) ;
· Les imports HTML (HTML Imports).

Pour ceux qui ne s'en souviennent plus, le DOM est une représentation de votre code HTML
sous forme d'arbre. Ainsi un élément <ul> a des éléments fils <li>. L'élément racine du DOM
est donc la balise <html>.
Je vais vous présenter chacune de ces technologies : le plus important est que vous
compreniez pourquoi telle ou telle technologie a été inventée et à quoi elle sert.
Par ailleurs, je vous conseille vivement de lire ce chapitre de haut en bas, il y a un certain
enchainement logique entre tous ces éléments !

Les éléments personnalisés


Les éléments personnalisés permettent de créer des balises HTML personnalisées dans vos
pages web, selon les besoins de votre application.
Vous vous demandez sûrement quel est l'intérêt de ces éléments personnalisés, étant donné
que vous pouvez déjà mettre dans votre code une balise du type <balise-inventée>, et ensuite
appliquer du JavaScript dessus, ou même du CSS :
01 <!-- Je crée une balise au hasard -->
02 <une-balise-inventee></une-balise-inventee>
03 <style>
04 /* Je rajoute un peu de style à ma nouvelle balise */
05 une-balise-inventee {
06 width: 200px;
07 height: 50px;
08 border: 1px solid orange;
09 }
10 </style>
11 <script>
12 // Ce script permet d'afficher le nombre de
13 // balises personnalisées sur la page
14 var elementPerso =
15 document.getElementsByTagName("une-balise-inventee");
16 var quantite = elementPerso.length;
17 alert("Il y a " + quantite +
18 " éléments <une-balise-inventee> dans ce document.");
19 </script>
Alors je vous recommande tout de suite de ne jamais faire ça, et ce pour plusieurs raisons :
· Votre code n'est pas valide, et développer du code invalide n'est pas très bien vu pour un
développeur !
· Si tout le monde faisait comme vous, il serait impossible de s'y retrouver : imaginez
qu’une personne récupère votre code, et qu'à la place des balises <p>, <ul>, <h1>, … elle
se retrouve face à des balises <une-balise-inventee>, <element-important>… cette
personne ne saurait plus où donner de la tête ! Même si l'idée de créer ses propres balises
peut être intéressante, vous êtes d'accord sur l'idée qu'il faut que tout le monde respecte les
mêmes standards pour pouvoir s'y retrouver, non ?
· Vous ne profitez pas des spécifications des Web Components : en respectant ces
standards, vous pourriez profiter d'un coup d'un code valide, d'un standard respecté par
tous les développeurs, et également des lifecycle callbacks. Bref, ça vaut le coup !

Heu, 'lifecycle callbacks' ???

Il n'y a rien de compliqué ! Prenons un mot après l’autre :


· Lifecycle ou cycle de vie : désigne le fait que vos éléments personnalisés vont passer par
plusieurs étapes au cours de leur existence (création, modification, suppression).
· Callbacks : désigne simplement une fonction qui sera appelé lorsque votre élément
passera par une des étapes de son cycle de vie.
Par exemple, vous pouvez attacher une fonction sur un de vos éléments, qui ne sera
appelée que lorsque l'élément sera créé. Ainsi vous pouvez attacher des comportements à
différentes étapes du cycle de vie d'un composant.
Il y a quatre lifecycle callback à avoir en tête si vous voulez vous en servir :
1. createdCallback - Le comportement à définir quand l'élément est enregistré auprès du
navigateur via Document.registerElement (Nous allons voir de quoi il s'agit ci-dessous).
2. attachedCallback - La fonction qui est appelée quand l'élément est inséré dans le DOM.
3. detachedCallback - La fonction qui est appelée quand l'élément est retiré du DOM.
4. attributeChangedCallback - Le comportement de l'élément quand un de ses attributs est
ajouté, modifié ou retiré.

Bon ok, tu m’as convaincu d'utiliser les standards du Web Component ! Mais comment
on fait, tu ne nous as rien dit !

Alors, la clé pour utiliser les éléments personnalisés, c'est la méthode


Document.registerElement. On utilise cette méthode pour enregistrer de nouveaux éléments, en
lui passant le nom de notre élément ainsi qu'un tableau d'options, notamment un prototype pour
notre nouvel élément. Ainsi le navigateur sait que l'élément que nous venons de lui déclarer est
un élément personnalisé à part entière, et qu'il ne s'agit pas juste d'une balise malformée. En
effet, si vous utilisez cette méthode, elle ajoutera à votre élément personnalisé l'interface
HTMLElement. Cette interface représente n'importe quel élément HTML, ce qui signifie que
votre code sera valide !
En revanche, si vous n'utilisez pas cette méthode et que vous faites votre élément
personnalisé dans votre coin, alors votre code ne sera pas valide, et implémentera probablement
l'interface HTMLUnknownElement !
Vous pouvez aussi construire votre propre élément personnalisé à partir d'un élément natif.
Prenons <button> par exemple. Alors vous ne pourrez pas utiliser un nom personnalisé comme
<my-button>, vous devrez utiliser une syntaxe comme <button is=’my-button’>.
Bon je sens que vous êtes impatient de voir du code, et d'arrêter de m'écouter parler tout
seul !
Rassurez-vous, voici un exemple de qualité pour me rattraper. Nous allons créer un petit
élément personnalisé pour afficher la vignette et quelque informations sur des super-héros.
Créons tout de suite un élément personnalisé nommé <super-heros>. Pour cela nous avons
besoin d'un nom et d'un prototype. C'est parti !
Faites attention, le nom de votre élément personnalisé doit contenir un tiret pour indiquer au
navigateur qu'il ne s'agit pas d'un élément natif. Généralement on préfixe notre élément par un
diminutif du nom de notre application, comme : 'monApp-monElement'.
Vous devez également associer un template à votre élément, sinon le navigateur ne saura pas ce
qu'il doit afficher.
Créer donc une page index.html avec le code suivant :
01 <!doctype html>
02 <html>
03 <head>
04 <meta charset="UTF-8">
05 <title>Test</title>
06 </head>
07 <body>
08 <script>
09 // On crée une classe pour notre élément personnalisé
10 // et on lui ajoute un template avec la méthode ‘innerHTML’.
11 class SuperHero extends HTMLElement {
12 constructor() {
13 super();
14 this.innerHTML = '<h1>Superman</h1>';
15 }
16 }
17
18 // On définit notre élément personnalisé avec la méthode 'define'.
19 customElements.define('super-hero', SuperHeros);
20 // On ajoute notre élément sur la page web !
21 document.body.appendChild(new SuperHero());
22 </script>
23 </body>
24 </html>

Affichez ensuite cette page dans votre navigateur préféré : vous verrez affiché Superman
en titre sur votre page !
Si vous inspectez votre code, vous verrez que le DOM de votre page contient notre
nouvelle balise :

Figure 5 - Console du navigateur lors de l'inspection de mon titre 'Superman'


Pour vous prouver que notre code est bien valide, essayez de faire valider cette page
HTML par le W3C Validator (en copiant et collant le code ci-dessus), vous obtiendrez alors ce
magnifique message :

Figure 6 - Notre code est valide, en voici la preuve !


Vous voyez bien que nous ne sommes pas fous, nous avons bien créé une nouvelle balise
HTML valide !
Qu'attendons-nous pour conquérir le monde !?!

Oui mais on aurait pu faire ça avec <h1>Superman</h1>, non ?

Ah, la question qui fâche ! Bon, oui, sur la forme vous avez raison, mais vous auriez perdu
les avantages des éléments personnalisés :
· Réutilisabilité : Maintenant que nous avons défini notre élément personnalisé, nous
pouvons le réutiliser autant de fois que vous le voulez. Imaginons que vous ayez développé
un élément permettant d'afficher la présentation d'un héros, et bien il vous suffit d'appeler
cet élément autant de fois que vous avez de héros !
· Personnalisation : Je ne vous l'ai pas encore montré, mais nous pouvons ajouter des
propriétés et des méthodes personnalisées encapsulées dans notre élément, ce que nous ne
pouvons pas faire avec une simple balise.
· Encapsulation : Tout le code que nous développons au sein de notre élément peut être
encapsulé dedans, et ainsi nous pouvons développer nos pages HTML comme un
assemblage d’éléments personnalisés !
Attend tu viens de dire que l'on peut encapsuler notre code HTML, avec le code Javascript et
CSS associés, mais comment éviter les conflits entre les éléments alors ?
En effet, que-ce passe-t-il si vous avez sur votre page un élément A qui indique que tous
ces paragraphes doivent être écrit en jaune, et un élément B qui spécifie que tous les paragraphes
doivent être écrit en rouge ? Lequel l'emporte ? Et que deviennent les paragraphes qui
n'appartiennent à aucun élément, et qui sont simplement présents sur la page ?
Je vous rassure, la spécification des composants Web prend en compte ce genre de
problématique, et c'est pour ça qu'on va tout de suite voir ce qu'est le DOM de l'ombre. Il
permettra d'éviter les conflits entre les éléments personnalisés.

Pour rappel, les éléments personnalisés que nous venons de voir sont un des quatre standards
des composant web, mais vous pouvez tout à fait les utiliser pour eux-mêmes, indépendamment
des composants web.

Le DOM de l'ombre
Ce nom est génial, avouons-le. Imaginez la tête que vont faire vos camarades ou vos
collègues quand vous leur direz que vous utilisez le DOM de l'ombre pour vos développements !
Dans la suite du cours j'emploierai le terme Anglais : le Shadow DOM, c'est encore plus
impressionnant !

Les spécifications du Shadow DOM


L'objectif du shadow DOM est de permettre d'encapsuler nos éléments personnalisés de
manière sûre (c’est-à-dire de les isoler du reste de la page), afin d'éviter tout conflit avec les
éléments de la page. Retenez ce principe, et vous avez déjà fait la moitié du travail !
Le Shadow DOM représente donc un ensemble de spécifications qui est supporté par les
standards récents du Web, afin d'isoler le JavaScript, le CSS et le HTML d'un élément
personnalisé du reste de la page.
Cela permet donc de créer un nouveau DOM qui n'est pas rattaché au DOM principal, donc
le DOM principal ne peut plus accéder à nos shadow DOM, et c'est là que ça devient intéressant.

Pourquoi tu dis 'nos shadow DOM', on peut en créer plusieurs ?

Oui, vous aurez exactement un nouveau Shadow DOM pour chaque élément personnalisé,
afin que chacun d'eux puisse profiter du système d'encapsulation.
Vous voulez voir en pratique comment cela fonctionne ? Pas de soucis, nous allons créer
un Shadow DOM.
Un Shadow DOM doit toujours être rattaché à un élément existant, que ce soit un élément
présent dans votre fichier HTML, ou un élément créé depuis un script. Et il est bien sûr possible
de rattacher un Shadow DOM à un élément personnalisé, comme <mon-element> par exemple !
Créons simplement un paragraphe avec un identifiant unique :
01 <p id=”paragraphe-shadow”><p> <!—Un simple paragraphe -->
Et maintenant ajoutons un Shadow DOM sur ce paragraphe :
01 // On crée un nouveau Shadow DOM sur un élément de notre page,
02 // qui possède l'indentifiant 'paragraphe-shadow'
03 var shadow =
04 document.querySelector('#paragraphe-shadow').attachShadow({mode: ‘open’});
05 // Pour l'instant notre Shadow DOM est vide,
06 // mais nous pouvons lui ajouter du contenu.
07 shadow.innerHTML = "<p id='shadow'>Salut, Shadow DOM !</p>";
08 // Si on recherche notre contenu caché depuis la console du navigateur,
09 // alors la commande suivante ne retourne rien
10 // car le DOM n'a pas accès au Shadow DOM !
11 document.querySelectorAll('#paragraphe-shadow');

Comme vous le constatez, il suffit d'utiliser la méthode Element.attachShadow pour


instancier un nouveau Shadow DOM. Cette méthode prend un paramètre qui permet d'indiquer si
on souhaite accéder au DOM de l'ombre en utilisant du code JavaScript écrit dans le contexte
principal de la page. C'est le cas pour nous car nous avons définis ce paramèter à 'open'. La
méthode attachShadow retourne ensuite un nouveau Shadow DOM si vous avez définit le
paramètre à open, et retourne null si vous avez définis le paramètre à closed.
Comme promis, vous pouvez ajouter du CSS qui ne sera appliqué qu'aux éléments de ce
Shadow DOM, en l'occurrence votre élément 'paragraphe-shadow' :
01 // On ajoute un peu de style à notre Shadow DOM
02 shadow.innerHTML += '<style>p {color: red;}</style>';

Si vous testez ce code, vous vous apercevrez que seuls les paragraphes de notre Shadow
DOM ont un texte rouge !

Angular et le Shadow DOM


Vous devez savoir que Angular n'utilise pas directement les spécifications du Shadow
DOM que nous venons de voir.
QUOI ? On apprend des trucs qui ne servent à rien depuis le début ?!

Non, je ne vous ai pas exactement menti, disons que tout cela était nécessaire. Derrière les
spécifications du Shadow DOM, vous avez déjà compris le mécanisme d'encapsulation sous-
jacent. Et bien Angular utilise lui aussi son propre mécanisme d'encapsulation en interne, qui
simule un comportement d'encapsulation, et qui a l'avantage de fonctionner sur tous les
navigateurs, contrairement au Shadow DOM qui est assez récent et qui n'est pas supporté par les
navigateurs plus anciens. Du coup, c'est bénéfique pour nous !
En fait, Angular peut quand même utiliser les spécifications du Shadow DOM si on le lui
demande explicitement. Pour être exact, Angular propose trois techniques d'encapsulation des
éléments personnalisés. Pour chacun de nos composants que nous créons avec Angular, nous
avons le choix entre les systèmes d'encapsulation suivants :
· Emulated : C'est la méthode par défaut qu'utilise Angular, qui simule le fonctionnement
du Shadow DOM mais qui a l'avantage de fonctionner sur tous les navigateurs. En interne,
Angular simule le Shadow DOM en suffixant les sélecteurs utilisés pour cibler les
composants. C'est cette méthode que nous utiliserons dans ce cours. Comme il s'agit de la
méthode par défaut, le comportement de notre application sera le même sans ajouter de
code particulier, Angular s'occupe de tout pour nous ! Elle n’est pas belle la vie ?
· Native : Cette méthode d'encapsulation supporte les spécifications du Shadow DOM,
nous obtenons donc le même comportement d'encapsulation mais avec une portabilité
moindre sur les anciens navigateurs.
· None : Pour une raison quelconque, vous pouvez vouloir ne pas utiliser le mécanisme
d'encapsulation des vues, à vos risques et périls !

Les templates HTML


Un template HTML permet de déclarer des petits morceaux de HTML qui ne seront pas
mis en forme lors du chargement de la page, mais qui pourront être copiés, complétés, puis
insérés dans le document grâce au JavaScript : on pourrait parler de module HTML. Ce concept
de découpe en module existe déjà beaucoup côté serveur (JavaEE, .NET, Symfony, Django...)
mais HTML propose désormais un mécanisme similaire. On peut donc créer de petits éléments
de contenu HTML pouvant être réutilisé dans différents endroit de notre application.
Le contenu d'un template HTML est tout de même parsé pendant le chargement de la page, afin
de s'assurer qu'il soit valide.
Créons sans plus tarder un template HTML, qui a pour objectif d'afficher un héros. Pour
cela nous utilisons la balise <template> dédiée :
01 <template id="super-heros">
02 <style>
03 h1 { color: green; };
04 </style>
05 <h1>Green Lantern</h1>
06 </template>

Vous voyez, il n'y a rien de bien compliqué, nous utilisons simplement la balise dédié à la
création de template HTML : <template>. Pour l'instant, évidemment ce code ne fait rien qui
soit visible à l'écran pour l'utilisateur, puisqu'il est destiné à être appelé en JavaScript. Ajoutons
donc un script sur notre page afin d'interagir avec notre template HTML :
// On reprend notre élément personnalisé 'super-heros' précédent
customElements.define('super-hero',
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById('super-hero');
// On récupère le contenu de notre template
let templateContent = template.content;
// On ajoute le contenu de notre template au DOM de l'ombre
const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(templateContent.cloneNode(true));
}
})

Désormais, dans votre page, vous pouvez utiliser : <super-hero></super-hero>.


Comme vous pouvez le voir, on commence à avoir un système d'encapsulation solide en
HTML : composants personnalisés, Shadow DOM et templates HTML. Si seulement on pouvait
déclarer tout ça dans un fichier à part, et qu'il suffirait ensuite d'importer dans notre page web
principale, ça serait vraiment la classe ! Attendez, mais... je crois bien que c'est possible avec les
imports HTML !

Les imports HTML


Les imports HTML sont le dernier standard qu'il nous manque pour pouvoir développer de
véritables composants web dans un fichier dédié, fonctionnant indépendamment du reste de la
page.
Ils rendent désormais possible d'importer un fichier HMTL en utilisant la balise <link>
dans un autre document HTML :
01 <link rel=’import’ href=’un-autre-fichier.html’>

En fait, on peut importer du HTML dans du HTML, et le fichier importé fonctionne en


autonomie, il contient son propre HTML, CSS et JavaScript et ne vient pas interférer avec votre
DOM !
Certains sites proposent même des composants prêts à l'emploi ! Il y a également des
bibliothèques connues qui ont été conçu pour faciliter l'intégration des Web Composants, comme
la librairie Polymer de Google, et qui vous permet de faire ça :
01 <!-- Polyfill pour les Composants Web,
02 afin de supporter les navigateurs plus anciens -->
03 <script
04 src="components/webcomponentsjs/webcomponents-lite.min.js"></script>
05 <!-- Un import de HTML -->
06 <link rel="import" href="components/google-map/google-map.html">
07 <!-- On utilise l'élément personnalisé importé ! -->
08 <google-map latitude="37.790" longitude="-122.390"></google-map>

Le code parle de lui-même, et il vous affichera une Google map centrée sur San Francisco,
rien que ça !
Bon je pense que je vous en ai déjà pas mal dit, n'hésitez pas à continuer à voir ce que sont
les Composants Web si le sujet vous intéresse !

Voici un lien sur lequel vous pouvez voir quelles spécifications sont actuellement supportées
par les navigateurs les plus populaires.

Conclusion
Voilà, c’est tout pour ce chapitre. J’espère que vous aurez appris plein de choses sur
l’avenir du développement web. Les Web Components auront certainement un rôle à jouer dans
le web de demain, et il serait dommage de passé à côté !
Retenez que les Web Components sont composés de quatre technologies différentes qui
fonctionnent ensemble, pour nous permettre de réaliser des applications web avec du HTML
modulaire.

En résumé
· Les Composants Web permettent d'encapsuler du code HTML, CSS et JavaScript qui
n'interfère pas avec le DOM principal de la page web.
· Les Composants Web sont un assemblage de quatre standards indépendants : les éléments
personnalisés, le Shadow DOM, les templates HTML et les imports HTML.
· Les éléments personnalisés permettent de créer ses propres éléments HTML valides.
· Les templates HTML permettent de développer des morceaux de code HTML qui ne sont
pas interprétés au chargement de la page.
· Les imports HTML permettent d'importer du HTML dans du HTML.
· Les spécifications des Composants Web sont assez récentes et ne fonctionnent pas
forcément sur tous les navigateurs, mais Angular propose de simuler en interne certaines
de ces spécifications pour augmenter leur portabilité.
Chapitre 5 : Premiers pas avec Angular
Commencer par « Hello, World »
Bon, je vous l'avais promis, et nous y arrivons !
Nous allons réaliser une superbe démonstration de "Hello, World !" avec Angular (enfin !).
Il nous aura fallu quelques chapitres théoriques avant de commencer, mais je pense c'était
nécessaire que vous ayez un aperçu global de cet écosystème.
Avant de se lancer tête baissée dans ce qui nous attend, je vous propose un petit plan de
bataille :
· D'abord, installer un environnement de développement.
· Ecrire le composant racine de notre application : rappelez-vous que notre application
Angular n'est qu'un assemblage de composants, et donc il faut au moins un composant pour
faire fonctionner une application.
· Informer Angular du composant avec lequel nous souhaitons démarrer notre application.
Pour nous ce sera facile, car nous n'aurons qu'un seul composant, le composant racine.
· Ecrire une simple page index.html qui contiendra notre application.
Par défaut, un navigateur affiche toujours le fichier nommé index.html d'un répertoire, c'est
pourquoi nous aurons un fichier index.html à la racine de notre projet.
Je vous propose de ne pas trainer et de commencer tout de suite ! Allez, au boulot !

Choisir un environnement de développement


Alors de quoi allons-nous avoir besoin ? Et bien je vous conseille de lancer votre éditeur de
texte favoris et de commencer à faire chauffer votre cerveau.
Il y a plusieurs IDEs qui supportent TypeScript et qui pourront vous aider lors de vos
développements :
1. Visual Studio Code (Recommandé par Microsoft pour les développements Angular,
évidemment)
2. WebStorm (Payant mais licence de 1 an offerte si vous êtes étudiant).
3. Sublime Text avec ce plugin à installer.
4. Il y en a plein d'autres, d'ailleurs le site officiel de TypeScript a listé sur sa page d’accueil
les IDEs recommandés pour TypeScript.
Retenez que ces outils vous simplifient la vie, mais qu'ils ne sont pas indispensables.
Choisissez l'environnement de développement avec lequel vous êtes le plus à l'aise, et ne vous
embêtez pas avec un nouvel IDE si vous êtes satisfait du vôtre.

Le terme IDE désigne un environnement de développement : il s'agit souvent d'un éditeur de


texte amélioré, spécialisé dans le développement logiciel avec tel ou tel langage.

Pour ma part j'utilise Atom dans ce cours, mais vous êtes libres de travailler avec l'outil
que préférez. Bon, on commence quand, pour de vrai ?
On va commencer par créer un dossier vide, qui sera le dossier racine de notre projet.
Sachez que ce que nous allons développer maintenant, servira de base pour le reste du cours. En
gros, nous commençons notre projet dès maintenant ! Mais rassurez-vous, on va commencer par
la base de la base.
Créons donc un dossier nommé pokemon-app.
Pourquoi un tel nom ? Et bien parce que tout le long du cours je vous propose de
développer une application qui vous permettent de gérer des Pokémons, ni plus ni moins. (Qui y
a vu un lien avec le jeu Pokémon GO ?) Nous partirons d'un dossier vide jusqu'à obtenir une
application finale prête pour la production ! Cela vous permettra d'avoir une vision globale du
processus de réalisation d'une application Angular.
Vous pouvez nommer votre dossier comme vous le voulez, cela n'a pas beaucoup d'importance
pour la suite.

Démarrer un projet Angular


Il y a plusieurs moyens de démarrer un projet Angular, certains sont plus rapides que
d'autres. Par exemple, le projet angular CLI vous permet de mettre en place un nouveau projet
Angular en une seule ligne de commande ! Cependant, je préfère partir d'un dossier vide, pour
que vous puissiez voir le processus de création d’une nouvelle application Angular en partant de
zéro.
Pour commencer nous allons avoir besoin d'ajouter trois fichiers de configuration dans le
dossier racine de notre projet. Je vais détailler le rôle de chacun d'entre eux.

Le fichier package.json
Ce fichier doit vous être familier : il permet de décrire les dépendances d'un projet pour le
Node Package Manager : Ajoutez le fichier package.json à la racine de notre projet, avec le
contenu ci-dessous.
01 {
02 "name": "ng6-pokemon-app",
03 "version": "1.0.0",
04 "description": "An awesome application to handle some pokemons.",
05 "scripts": {
06 "build": "tsc -p src/",
07 "build:watch": "tsc -p src/ -w",
08 "build:e2e": "tsc -p e2e/",
09 "serve": "lite-server -c=bs-config.json",
10 "serve:e2e": "lite-server -c=bs-config.e2e.json",
11 "prestart": "npm run build",
12 "start": "concurrently \"npm run build:watch\" \"npm run serve\"",
13 "pree2e": "npm run build:e2e",
14 "e2e": "concurrently \"npm run serve:e2e\" \"npm run protractor\" --kill-others --success first",
15 "preprotractor": "webdriver-manager update",
16 "protractor": "protractor protractor.config.js",
17 "pretest": "npm run build",
18 "test": "concurrently \"npm run build:watch\" \"karma start karma.conf.js\"",
19 "pretest:once": "npm run build",
20 "test:once": "karma start karma.conf.js --single-run",
21 "lint": "tslint ./src/**/*.ts -t verbose"
22 },
23 "private": true,
24 "dependencies": {
25 "@angular/animations": "^6.0.3",
26 "@angular/common": "^6.0.3",
27 "@angular/compiler": "^6.0.3",
28 "@angular/core": "^6.0.3",
29 "@angular/forms": "^6.0.3",
30 "@angular/http": "^6.0.3",
31 "@angular/platform-browser": "^6.0.3",
32 "@angular/platform-browser-dynamic": "^6.0.3",
33 "@angular/router": "^6.0.3",
34 "core-js": "^2.5.4",
35 "rxjs": "^6.2.0",
36 "rxjs-compat": "^6.2.0",
37 "systemjs": "0.19.40",
38 "zone.js": "^0.8.26"
39 },
40 "devDependencies": {
41 "typescript": "~2.7.2",
42 "@angular/language-service": "^6.0.3",
43 "@types/jasmine": "~2.8.6",
44 "@types/jasminewd2": "~2.0.3",
45 "@types/node": "~8.9.4",
46 "codelyzer": "~4.2.1",
47 "jasmine-core": "~2.99.1",
48 "jasmine-spec-reporter": "~4.2.1",
49 "karma": "~1.7.1",
50 "karma-chrome-launcher": "~2.2.0",
51 "karma-coverage-istanbul-reporter": "~2.0.0",
52 "karma-jasmine": "~1.1.1",
53 "karma-jasmine-html-reporter": "^0.2.2",
54 "protractor": "~5.3.0",
55 "ts-node": "~5.0.1",
56 "tslint": "~5.9.1"
57 }
58 }
Il y a trois parties intéressantes à remarquer dans ce fichier package.json :
1. Les scripts : un certain nombre de scripts prédéfinis sont listés à partir de la ligne 5. Par
exemple, start permet de démarrer notre application et lite permet de démarrer un mini-
serveur sur lequel tournera notre application Angular. Nous verrons comment utiliser
certaines de ces commandes un peu plus tard dans ce chapitre.
2. Les dépendances : A partir de la ligne 24, apparaît une liste de toutes les dépendances de
notre application. Par exemple, @angular/router a pour vocation de gérer les routes de
notre application, et @angular/forms gère les formulaires.
3. Les dépendances relatives au développement : Ces dépendances sont listées à partir de
la ligne 40, et concernent les dépendances dont nous n'aurons plus besoin quand notre
application sera terminée. Par exemple, Typescript (ligne 44) ne sera plus nécessaire une
fois l'application terminée, étant donné que le navigateur ne pourra pas l'interpréter : le
navigateur interprétera uniquement le JavaScript qui a été généré depuis le TypeScript
compilé.
Si vous avez le temps, vous pouvez retrouver toutes les explications détaillées, ligne par
ligne, de ce fichier, dans la documentation officielle sur le sujet.
Angular 6 nécessite au moins la version ~2.7.0 de TypeScript (et au minimum la version 6 de
RxJS, qui est une librairie que je vous présenterai plus tard dans ce cours). C’est bien cette
version de TypeScript que nous avons renseigné dans le fichier package.json.

Le fichier bs-config.json
Ce fichier contient la configuration du mini-serveur lite-server que nous utiliserons dans ce
cours. Le fichier permet de déclarer le dossier qui contiendra le code de notre application, ici le
dossier src, que nous allons créer bientôt, et indique le chemin vers les librairies installées dans
le dossier node_modules. Créez donc le fichier bs-config.json à la racine de votre projet :
01 {
02 ‘server’ : {
03 ‘baseDir’ : ‘src’
04 ‘routes’ : {
05 ‘/node_modules’ : ‘node_modules’
06 }
07 }
08 }

Le fichier systemjs.config.js
System.js est une bibliothèque permettant de charger les modules JavaScript, c'est-à-dire
assembler de manière cohérente tous les fichiers de notre application : les fichiers propre à
Angular, les librairies tierces et les fichiers de notre propre code.
Le plus important à retenir est l'élément map à la ligne 13. Cet objet nous permet de
déclarer deux nouveaux éléments :
· Le dossier de notre application à la ligne 15, ci-dessous le dossier app.
· Le mapping de nos librairies : on lie un alias avec l'emplacement de la librairie. Par
exemple à la ligne 18, on déclare l'alias @angular/core, puis on renseigne son
emplacement dans le dossier node_modules. Cela nous permet dans notre application
d'utiliser cet alias pour l'importation :
01 import { Component } from ‘@angular/core’ ;
Voici le contenu du fichier systemjs.config.js, à placer, dans le dossier src de votre projet
(Il s’agit d’une convention que respectent les projets Angular). Créez donc un dossier src, puis
placez le fichier systemjs.config.js à l’intérieur :
01 /**
02 * La configuration de SystemJS pour notre application.
03 * On peut modifier ce fichier par la suite selon nos besoins.
04 */
05 (function (global) {
06 System.config({
07 paths: {
08 // définition d'un raccourcis, 'npm' pointera vers 'node_modules'
09 'npm:': 'node_modules/'
10 },
11 // L'option map permet d'indiquer à SystemJS l'emplacement
12 // des éléments à charger
13 map: {
14 // notre application se trouve dans le dossier 'app'
15 app: 'app',
16 // packets angular
17 '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
18 '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
18 '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
20 '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
21 '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-
browser-dynamic.umd.js',
22 '@angular/common/http': 'npm:@angular/common/bundles/common-http.umd.js',
23 '@angular/http': 'npm:@angular/common/bundles/common-http.umd.js',
24 '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
25 '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
26 // autres librairies
27 'tslib': 'npm:tslib/tslib.js',
28 'rxjs': 'npm:rxjs',
29 'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
30 },
31 // L'option 'packages' indique à SystemJS comment charger
32 // les paquets qui n'ont pas de fichiers et/ou extensions renseignés
33 packages: {
34 app: {
35 defaultExtension: 'js'
36 },
37 rxjs: {
38 main: 'index.js',
39 defaultExtension: 'js'
40 },
41 'rxjs/operators': {
42 main: 'index.js',
43 defaultExtension: 'js'
44 }
45 }
46 });
47 })(this);

A la ligne 15, nous indiquons que notre application est contenue dans le dossier app.

Le fichier tsconfig.json
Ce document est le fichier de configuration de TypeScript. Vous devez le placer dans le
dossier src également. On peut définir un certain nombre d'éléments de configuration dans ce
fichier, en voici quelques-uns :
· A la ligne 3, target a pour valeur es5, ce qui indique que notre code TypeScript sera
compilé vers du code JavaScript ES5. On pourrait mettre ES6 comme valeur pour générer
du code JavaScript différent.
· A la ligne 12, removeComments indique à TypeScript que l'on ne souhaite pas que les
commentaires de notre code soient retirés lors de la compilation. On pourrait mettre la
valeur true pour obtenir le comportement opposé.
01 {
02 "compilerOptions": {
03 "target": "es5",
04 "module": "commonjs",
05 "moduleResolution": "node",
06 "sourceMap": true,
07 "emitDecoratorMetadata": true,
08 "experimentalDecorators": true,
09 "lib": [ "es2015", "dom" ],
10 "noImplicitAny": true,
11 "suppressImplicitAnyIndexErrors": true,
12 "removeComments": false
13 }
14 }

Les détails de la configuration de TypeScript sont indiqués sur la documentation officielle, si


vous souhaitez y revenir par la suite.

Pour plus de détails sur le fonctionnement des fichiers de configuration, je vous invite à
consulter les annexes "Configuration de TypeScript" et "Dépendances d'un projet Angular ".

Installer les dépendances


Maintenant, nous devons installer les bibliothèques que nous avons déclarées dans le
package.json. Pour cela, utilisons la commande npm install à la racine de votre projet :
npm install
Cette commande devrait créer un dossier à la racine de notre projet nommé node_modules.
Ce dossier contient toutes les dépendances dont nous avons besoin pour faire fonctionner notre
projet.
Vérifiez que vous disposez d’au moins la version 8 de NodeJS, qui est la version minimum
recommandée pour développer avec Angular 6.

Créer notre premier composant


Nous allons créer notre premier composant, depuis le temps que vous en entendez parler !
Cependant, nous allons organiser un peu notre code en mettant nos fichiers sources dans un
dossier app, plutôt qu'avec nos fichiers de configuration à la racine de notre projet. Créer donc un
dossier app, et placez-le dans le dossier src. À l'intérieur du dossier app, créer un fichier
app.component.ts, qui contiendra notre premier composant :
01 import { Component } from '@angular/core';
02
03 @Component({
04 selector: 'pokemon-app',
05 template: `<h1>Hello, {{name}} !</h1>`,
06 })
07 export class AppComponent { name = ‘Angular’; }

Nous allons prendre un peu de temps pour décrire ce fichier, car malgré sa taille réduite, ce
fichier est composé de trois parties différentes.
D'abord, on importe les éléments dont on va avoir besoin dans notre fichier. A la ligne 1,
on importe l'annotation Component depuis le cœur de Angular : @angular/core. Retenez donc
simplement la syntaxe suivante :
01 import { UnElement } from { quelque-part } ;

Un composant doit au minimum importez l'annotation Component, bien sûr.


Ensuite, de la ligne 3 à la ligne 6, on aperçoit l'annotation @Component qui nous permet de
définir un composant. L'annotation d'un composant doit au minimum comprendre deux éléments
: selector et template.
· Selector permet de donner un nom à votre composant afin de l'identifier par la suite. Par
exemple, notre composant se nomme ici pokemon-app, ce qui signifie que dans notre page
web, c'est la balise qui sera insérée. Et ce code sera parfaitement valide. Vous vous
rappelez du chapitre sur les Web Components ?
· Quant à l'instruction template, elle permet de définir le code HTML du component (On
peut bien sûr définir notre template dans un fichier séparé avec l'instruction templateUrl à
la place de template, afin d'avoir un code plus découpé et plus lisible). Ici, vous pouvez
deviner que la syntaxe avec les doubles accolades permet d’afficher la variable déclarée à
la ligne 7. Il s’agit de l’interpolation, que nous verrons dans le chapitre sur les templates.
En attendant, retenez que ce template affiche « Hello, Angular » pour l’utilisateur.
Enfin, à la ligne 7, on retrouve le code de la classe de notre composant. C'est cette classe
qui contiendra la logique de notre composant. Le mot-clef export permet de rendre le composant
accessible pour d'autres fichiers.
Par convention, on suffixe le nom des composants par Component : la classe du composant app
est donc AppComponent.

Créer notre premier module


Pour l'instant nous n'avons qu'un unique composant dans notre application, mais imaginez
que notre application soit composée de 100 pages, avec 100 composants différents, comment
ferions-nous pour nous y retrouvez ?
L'idéal serait de pouvoir regrouper nos composants par fonctionnalité : par exemple
regrouper tous les composants qui servent à l'authentification entre eux, tous ceux qui servent à
construire un blog entre eux, etc.
Eh bien, Angular permet cela, grâce aux modules ! Tous nos composants seront regroupés
au sein de modules.

Mais du coup ? ... Il nous faut au moins un module pour notre composant, non ?
Bravo ! Vous avez tout compris !
Au minimum, votre application doit contenir un module : le module racine. Au fur et à
mesure que votre application se développera, vous rajouterez d'autres modules pour couvrir de
nouvelles fonctionnalités.
Voici le code du module racine de notre application, app.module.ts, à placer dans notre
dossier app :
01 import { NgModule } from '@angular/core';
02 import { BrowserModule } from '@angular/platform-browser';
03 import { AppComponent } from './app.component';
04
05 @NgModule({
06 imports: [ BrowserModule ],
07 declarations: [ AppComponent ],
08 bootstrap: [ AppComponent ]
09 })
10 export class AppModule { }

Je vais présenter ce code rapidement. D'abord, on retrouve des importations en haut du


fichier. Pour déclarer un module, on importe l'annotation NgModule contenue dans le cœur
d'Angular lui-même; à la ligne 1.
Ensuite on importe le BrowserModule, qui est un module qui fournit des éléments
essentiels pour le fonctionnement de l'application, comme les directive ngIf et ngFor dans tous
nos templates, nous reviendrons dessus plus tard.
Ensuite on importe le seul composant AppComponent de notre application, que nous allons
rattacher à ce module.
Mais le plus important ici est l'annotation NgModule, car c'est elle qui permet de déclarer
un module :
· imports : permet de déclarer tous les éléments que l'on a besoin d'importer dans notre
module. Les modules racines ont besoin d'importer le BrowserModule (contrairement aux
autres modules que nous ajouterons par la suite dans notre application).
· declarations : Une liste de tous les composants et directives qui appartiennent à ce
module. Nous avons donc ajouté notre unique composant AppComponent.
· bootstrap : Permet d'identifier le composant racine, qu’Angular appelle au démarrage de
l'application. Comme le module racine est lancé automatiquement par Angular au
démarrage de l'application, et qu’AppComponent est le composant racine du module
racine, c'est donc AppComponent qui apparaîtra au démarrage de l'application. Ça va, rien
de compliqué si on prend le temps de bien comprendre.
Lors de ce cours, nous commencerons à travailler avec une application mono-module, puis
nous ajouterons d'autres modules pour que vous voyez comment se passe le développement d'une
application plus complexe.

Créer un point d'entrée pour notre application


Vous vous rappelez que dans le fichier de configuration de SystemJS, nous avons indiqué à
Angular que le point d'entrée de notre application serait le fichier main.ts ? Eh bien, c'est
maintenant que nous allons le créer !
Créez donc un fichier main.ts avec le contenu suivant, dans le dossier app :
01 import { platformBrowserDynamic } from
02 '@angular/platform-browser-dynamic';
03 import { AppModule } from './app/app.module';
04 platformBrowserDynamic().bootstrapModule(AppModule)
05 .catch(err => console.log(err));

Notre devons préciser dans ce fichier que notre application démarre dans un navigateur
web et pas ailleurs : en effet, nous pourrions choisir d'utiliser Angular pour du développement
mobile native avec NativeScript ou du développement cross-plateform avec Electron.js. Nous
précisons donc que notre application est destinée aux navigateurs web, et que l'on désigne
l’AppModule comme module racine, qui lui-même lancera l’AppComponent.
Sachez que le fichier main.ts sera très peu modifié lorsqu'on développera notre application
par la suite, on peut dire qu'on le développe « une fois pour toute ».

Heu.., pourquoi créer main.ts, app.module.ts et app.component.ts dans 3 fichiers


différents, tout ça pour lancer une application qui affiche un "Hello, Angular" ?

Votre question est légitime. Pour l'instant, ces trois fichiers sont assez simples et
contiennent relativement peu de code.
Sachez pourtant que ces efforts supplémentaires nous ont permis de mettre en place notre
application de la bonne manière. Le démarrage de notre application est indépendant de la
description de notre module, qui est également indépendant des composants qui le constituent.
Ces trois éléments doivent donc être séparés !

Héberger notre application


Il ne nous reste plus qu'un seul fichier pour pouvoir démarrer notre application, promis !
Vous vous rappelez ce que nous sommes en train de développer en terme d'architecture ?
Une SPA (Single Page Application), c'est-à-dire que notre application ne sera composé que d'une
page HTML avec beaucoup de code JavaScript pour dynamiser la page, récupérer des
informations d'un serveur distant, etc... Et bien il nous manque cette fameuse page HTML.
Créons-là tout de suite un fichier index.html, à la racine du projet (et non dans le dossier app !)
:
01 <!DOCTYPE html>
02 <html>
03 <head>
04 <title>Angular QuickStart</title>
05 <meta charset="UTF-8">
06 <meta name="viewport" content="width=device-width, initial-scale=1">
07 <!-- 1. Chargement des librairies -->
08 <!-- Polyfill(s) pour les anciens navigateurs -->
09 <script src="node_modules/core-js/client/shim.min.js"></script>
10 <script src="node_modules/zone.js/dist/zone.js"></script>
11 <script src="node_modules/systemjs/dist/system.src.js"></script>
12 <!-- 2. Configuration de SystemJS -->
13 <script src="systemjs.config.js"></script>
14 <script>
15 System.import('main.js').catch(function(err){ console.error(err); });
16 </script>
17 </head>
18 <!-- 3. Afficher l'application -->
19 <body>
20 <pokemon-app>Loading AppComponent content here ...</pokemon-app>
21 </body>
22 </html>

Ce fichier est une page HTML classique, qui contiendra toute notre application. J'ai essayé
de tout décrire dans les commentaires. Remarquez la ligne 20, c'est à cet endroit que notre
application va vivre, et que les templates de nos composants seront injectés !
Voici l'architecture de notre projet, à ce stade :

Figure 7 - L'architecture de notre projet


Bon, je vous rassure, on a fini ! Il ne nous reste plus qu'à démarrer notre application.

Démarrer l'application
Démarrer l'application va être un jeu d'enfant. Il y a déjà une commande disponible pour
nous permettre de démarrer l'application. Ouvrez donc un terminal qui pointe vers le dossier de
votre projet et taper la commande suivante :
npm start
Vous devriez voir des choses qui s'affichent dans la console, puis après un délai de
quelques secondes, votre navigateur s'ouvre tout seul, et vous voyez affiché le message "Hello,
Angular2 !" dans votre navigateur !
Figure 8 - "Hello, World!" avec Angular !
Ca y est, nous y sommes arrivés !

Pour couper la commande npm start, appuyer sur CTRL + C. En effet cette commande tourne
en continu puisque qu'elle s'occupe de démarrer le serveur qui s'occupe d'envoyer l'application
au navigateur !

Si toutefois vous avez des erreurs et que l'application ne se lance pas, afficher la console dans
laquelle vous avez tapé la commande npm start , et scroller avec votre souris vers le haut
jusqu'à tomber sur des messages d'erreurs. Ils devraient vous dé- crire à quelle ligne et dans
quel fichier l'erreur se trouve.
En cas de pépin pour lancer la commande elle-même, essayer de lancer les deux commandes
suivantes, dans deux consoles différentes :
1. npm run tsc:w
2. npm run lite
Alors je sais, tout ça pour ça !
Mais rassurez-vous, vous venez de faire quelque chose propre à beaucoup de Framework :
installer le socle de notre application. Vous avez fait plus qu'afficher un message à vos
utilisateurs, vous venez de mettre en place l'architecture de votre application, et ça, ça n'a pas de
prix !
Mais revenons à la commande npm start, car sous des airs modestes, cette petite
commande accomplit un travail important :
1. Elle compile tous nos fichiers TypeScript vers du JavaScript.
2. Elle lance l'application dans un navigateur et la met automatiquement à jour si nous
modifions notre code !
Dans les librairies que nous avons installées au début de ce chapitre, BrowserSync s'occupe
de mettre à jour notre application à chaque fois que nous modifions son code source, sans avoir à
appuyer sur F5 !
Testons ça tout de suite, ouvrez le fichier de votre composant app.component.ts, et
modifiez son code comme ceci :
01 import { Component } from '@angular/core';
02
03 @Component({
04 selector: 'pokemon-app',
05 template: '<h1>Bonjour, {{name}} !</h1>'
06 })
07 export class AppComponent { name = ‘Angular’; }
Si maintenant vous retournez dans le navigateur ou tourne votre application, vous verrez
maintenant le message "Bonjour, Angular !" s'afficher, à la place de "Hello, Angular !", le tout
sans avoir à rafraîchir votre navigateur !
Pour que votre application soit mise à jour automatiquement, laissez la commande npm start
tourner en continu pendant vos développements !

Le navigateur Chrome et l’extension AdBlock


Si vous utilisez Chrome et que l’extension AdBlock est activée, il se peut que votre
application affiche une simple page blanche. Cela est du à un conflit entre l’extension AdBlock
et la libraire zone.js, requise pour faire fonctionner Angular. En ouvrant la console de votre
navigateur, voici ce que vous devriez obtenir :

Figure 9 - Il existe un conflit entre l'extension AdBlock et Zone.js


Pour remédier à ce problème, il suffit de désactiver l’extension AdBlock lorsque vous
développez sur votre machine. De toute façon, vous n’avez pas besoin de cette extension pour
vos sites locaux, qui n’affichent pas de publicités.

Figure 10 - Pensez à désactiver AdBlock lors de vos développements !

Nettoyer son dossier de développement


Si vous ouvrez votre IDE (le logiciel que vous utilisé pour effectuer vos développements),
vous constatez qu'un certains nombres de fichiers se sont ajoutés dans le dossier app. Il s'agit de
fichier *.js et *.js.map. Il s'agit des fichiers compilés par TypeScript et ce sont eux qui sont
interprétés par le navigateur. Jusque-là tout va bien.
Figure 11 - Le dossier 'app' n'est très lisible
Le problème est que ces fichiers nous gênent : ils polluent notre dossier de développement
app et le rendent moins lisible. Je vous propose de mettre tous les fichiers destinés au navigateur
dans un dossier à part, le dossier /dist. Les fichiers que nous utilisons pour nos développements
(les fichiers TypeScript) resterons dans notre dossier de développement /app.
Pour cela, nous allons dire à SystemJS de ne pas cherchez à démarrer l'application depuis
le dossier app, mais depuis le dossier dist. Mais avant, nous allons dire à TypeScript de compiler
les fichiers vers ce nouveau répertoire, sinon il restera désespérément vide.
Premièrement, créer un dossier dist à la racine de votre application.
Ensuite, nous allons dire à TypeScript de pointer vers dist. Ouvrez le fichier de
configuration tsconfig.json et ajoutez la ligne de configuration outDir, à la ligne 11 :
01 {
02 "compilerOptions": {
03 "target": "es5",
04 "module": "commonjs",
05 "moduleResolution": "node",
06 "sourceMap": true,
07 "emitDecoratorMetadata": true,
08 "experimentalDecorators": true,
09 "removeComments": false,
10 "noImplicitAny": false,
11 "outDir": "dist"
12 }
13 }

Maintenant, il ne nous reste plus qu'à dire à System.js de démarrer notre application par
rapport aux fichiers JavaScript qui se trouvent dans le nouveau dossier dist. Ouvrez le fichier de
configuration de System.js nommé systemjs.config.js et modifiez la ligne 5 ci-dessous :
01 (function (global) {
02 System.config({
03 // ...
04 map: {
05 app: 'dist/app', // <-- remplacez 'app' par 'dist/app' comme ceci !
Enfin, dans le fichier index.html, ajoutez la bonne inmportation pour SystemJS :
01 <!-- ... -->
02 <script>
03 System.import('dist/main.js').catch(function(err){ console.error(err); });
04 </script>
05 <!-- ... -->

Désormais relancez la commande npm start. Sans surprise, tout fonctionne comme avant
(sinon reprenez les étapes ci-dessus). Si vous ouvrez à nouveau votre IDE favoris, vous
constaterez que le dossier dist contient de nouveaux fichiers générés par TypeScript :

Figure 12 - Le dossier 'dist' contient de nouveaux fichiers


Parfait, les fichiers destinés au navigateur ne polluent plus notre dossier de développement
app. Il ne nous reste plus qu’à supprimer les fichiers suivants dans app, car nous n'en avons plus
besoin :
· app.component.js
· app.component.js.map
· app.module.js
· app.module.js.map
· main.js
· main.js.map
Ne vous inquiétez, l'application fonctionnera toujours !
D'ailleurs si vous ne me croyez pas, aller hop, un petit npm start !
Le nom de dossier 'dist' signifie 'distribution' : le contenu de ce dossier est destiné à la
production. En effet, une fois que nous aurons finis de développer notre application, nous
n'allons pas déployer les fichiers TypeScript, qui seront inutiles. Nous verrons comment
déployer une application Angular en production plus tard dans ce cours.

Pour ceux qui versionnent leur code avec Git, voici le contenu du .gitignore pour notre
application Angular :
/dist
/node_modules

Conclusion
Voilà, vous avez réalisé votre premier "Hello, World !" avec Angular ! Vous pouvez être
fier de vous.
Mine de rien, ce modeste exemple a nécessité d'utiliser les Web Components, ES6 et
TypeScript ! C'est pourquoi il n'était pas inutile de les étudier juste avant.
Je vous propose dans le prochain chapitre de commencer à construire notre application de
Pokémons, par-dessus la base que nous venons de réaliser dans ce chapitre. Mais rassurez-vous,
il y a encore beaucoup de choses à voir !

En résumé
· SystemJS est la bibliothèque par défaut choisie par Angular pour charger les modules.
· On a besoin au minimum d'un module racine et d'un composant racine par application.
· Le module racine se nomme par convention AppModule.
· Le composant racine se nomme par convention AppComponent.
· L'ordre de chargement de l'application est le suivant : index.html > main.ts >
app.module.ts > app.component.ts.
· Le fichier package.json initial est fourni avec des commandes prêtes à l'emploi comme la
commande npm start, qui nous permet de démarrer notre application web.
Questionnaire n°1
L'objectif de ce questionnaire est de valider les acquis théoriques de cette première partie,
ainsi que la mise en place du socle pour une application Angular.
Le questionnaire est noté sur 10 points.
Le questionnaire est accessible en ligne à cette adresse.
Bon courage !
Partie 2

Acquérir les bases d’Angular


Chapitre 6 : Les composants
Dans la section précédente, nous avons eu un aperçu du socle d'une application Angular, et
de son écosystème. Il est temps maintenant de voir plus en profondeur le fonctionnement des
composants.
Je vous propose de mettre en place un simple composant qui affiche une liste de
Pokémons. Nous allons continuer à travailler à partir du socle mis en place lors du chapitre
précédent, et vous allez voir que l'on peut déjà faire pas mal de choses.
Pour le moment, ne touchez pas au code de notre projet. Les débuts de chapitres sont
généralement consacrés à la théorie, et je vous préviendrai quand on passe en mode pratique.

Qu'est-ce qu'un composant ?


Un composant est une classe, qui contrôle une portion de l'écran. Cette portion de l'écran
contrôlée par le composant, on l'appelle une vue : une vue peut être l'ensemble de la page web,
une fenêtre de tchat, une barre de navigation, etc... Cette vue est définie dans le template du
composant.

On peut dire que : 1 composant = 1 classe + 1 vue.

On définit la logique d'un composant dans une classe; c'est-à-dire ce qu'il faut pour faire
fonctionner la vue. La classe d'un composant interagit avec la vue à travers des propriétés et des
méthodes que nous allons voir.
Par exemple, imaginons un composant qui affiche tous les Pokémons présents dans notre
application, sous forme de liste. Ce composant aura donc une propriété pokemons contenant tous
les Pokémons à afficher :
01 import { Component, OnInit } from '@angular/core';
02
03 @Component({
04 selector: 'pokemon-app',
05 template: `<h1>Hello, Angular 2 !</h1>`,
06 })
07 export class AppComponent implements OnInit {
08 // 1. La propriété pokemons est un tableau d'objet de type Pokemon.
09 private pokemons: Pokemon[];
10
11 // 2. Initialisation de la propriété pokemons.
12 ngOnInit() {
13 this.pokemons = [
14 { name: 'Bulbizarre', hp: 6 },
15 { name: 'Salamèche', hp: 6 },
16 { name: 'Carapuce', hp: 6 }
17 ];
18 }
19 }

Il s'agit d'un composant un peu plus complexe que le composant minimum que l'on avait
vu au chapitre précédent, pour deux raisons :
· L'ajout d'une propriété pokemons : à la ligne 9, j'ai déclaré une propriété pokemons, et
j'utilise le typage TypeScript pour indiquer que cette propriété devra contenir un tableau
d'objet de type Pokemon. Cette propriété est privé et son usage est donc interne au
composant AppComponent.
· J'initialise la propriété pokemons : à la ligne 13, dans une méthode nommée ngOnInit,
j'initialise la propriété avec trois Pokémons, qui ont pour propriétés name et hp. ('hp'
désigne le nombre de point de vie du Pokémon : Health Point).

D'accord pour la propriété pokemons, mais c'est quoi cette méthode ngOnInit, et ce
nouvel import OnInit, et pourquoi tu implémentes cette fonction ?

Alors je ne vous ai pas tout dit. En réalité, ngOnInit n'est pas une simple méthode mais une
méthode de cycle de vie du composant. Voyons tout de suite de quoi il s'agit.

Les cycles de vie d'un composant


Chaque composant a un cycle de vie géré par Angular lui-même. Angular crée le
composant, s'occupe de l'afficher, crée et affiche ses composants fils, vérifie quand les données
des propriétés changent, et détruit les composants, avant de les retirer du DOM quand cela est
nécessaire. Pratique, non ?
Angular nous offre la possibilité d'agir sur ces moments clefs quand ils se produisent, en
implémentant une ou plusieurs interfaces, pour chacun des événements disponibles. Ces
interfaces sont disponibles dans la librairie core d'Angular.
Chaque interface permettant d'interagir sur le cycle de vie d'un composant fournit une et
une seule méthode, dont le nom est le nom de l'interface préfixé par ng. Par exemple, l'interface
OnInit fournit la méthode ngOnInit, et permet de définir un comportement lorsque le composant
est initialisé.
Voici ci-dessous la liste des méthodes disponibles pour interagir avec le cycle de vie d'un
composant, dans l'ordre chronologique du moment où elles sont appelées par Angular.

Nom de la méthode Comportement

ngOnChanges C'est la méthode appelée en premier lors de la


création d'un composant, avant même ngOnInit,
et à chaque fois que Angular détecte que les
valeurs d'une propriété du composant sont
modifiées. La méthode reçoit en paramètre un
objet représentant les valeurs actuelles et les
valeurs précédentes disponibles pour ce
composant.

ngOnInit Cette méthode est appelée juste après le premier


appel à ngOnChanges, et elle initialise le
composant après qu’Angular ait initialisé les
propriétés du composant.

ngDoCheck On peut implémenter cette interface pour


étendre le comportement par défaut de la
méthode ngOnChanges, afin de pouvoir détecter
et agir sur des changements qu’Angular ne peut
pas détecter par lui-même.

ngAfterViewInit Cette méthode est appelée juste après la mise en


place de la vue d'un composant (et des vues de
ses composants fils s'il en a).

ngOnDestroy Appelée en dernier, cette méthode est appelée


avant qu’Angular ne détruise et ne retire du
DOM le composant. Cela peut se produire
lorsqu'un utilisateur navigue d'un composant à
un autre par exemple. Afin d'éviter les fuites de
mémoire, c'est dans cette méthode que nous
effectuerons un certain nombre d'opérations afin
de laisser l'application "propre" (nous
détacherons les gestionnaires d'événements par
exemple).

Même si vous ne définissez pas explicitement des méthodes de cycle de vie dans votre
composant, sachez que ces méthodes sont appelées en interne par Angular pour chaque
composant. Lorsqu'on utilise ces méthodes, on vient donc juste surcharger tel ou tel événement
du cycle de vie d'un composant.

Les méthodes que vous utiliserez le plus seront certainement ngOnInit et ngOnDestroy, qui
permettent d'initialiser un composant, et de le nettoyer proprement par la suite lorsqu'il est
détruit.

Interagir sur le cycle de vie d'un composant


Nous allons interagir sur le cycle de vie d'un composant, et en particulier sur l'étape
d'initialisation du composant.

Sachez que la méthode que nous allons voir est valable quelle que soit la méthode de cycle de
vie.

La première chose à faire est d'importer l'interface que nous allons utiliser. Dans notre cas,
il s'agit d’OnInit :
01 import { OnInit } from ‘@angular/core’ ;
Ensuite, il faut implémenter cette interface pour le composant :
01 export class AppComponent implements OnInit {}
Enfin nous devons ajouter la méthode ngOnInit fournie par cette interface, dans notre
composant :
01 export class AppComponent implements OnInit {
02
03 ngOnInit() {
04 // ce message s'affichera dans la console de votre navigateur
05 // dès que le composant est initialisé !
06 console.log('ngOnInit !');
07 }
08 }

Et voilà, vous êtes maintenant capable d'ajouter une étape d'initialisation à vos composants
! Nous utiliserons beaucoup cette méthode pour initialiser les propriétés de nos composants.

Pourquoi est-ce qu'on s'embête à charger les données dans ngOnInit, alors que l'on
pourrait le faire dans le constructeur du composant ?

Surtout pas ! C'est fortement déconseillé !


Il est chaudement recommandé de garder toute la logique de votre application en dehors du
constructeur, surtout quand vous commencerez à récupérer les données d'un serveur distant. Le
corps du constructeur peut éventuellement être utilisé pour les initialisations simples, et encore,
nous allons voir tout de suite une autre manière de le faire.

Heu, j'initialise quoi du coup ? Et où ?

Je vais vous donner un exemple de code avec lequel vous devriez comprendre : vous avez
un composant qui possède une propriété titre, une propriété sous-titre et une liste de tâches à
afficher.
01 import { Component, OnInit } from '@angular/core';
02
03 @component({
04 selector: 'task-component',
05 template: '<h1>{{ title }} <em>{{ subtitle }}</em><h1>'
06 })
07 export class TaskListComponent implements OnInit {
08 // 1. Je choisis de définir mon titre immédiatement lors de
09 // la déclaration de ma propriété. C'est plus concis.
10 title: string = "Liste de tâches";
11 // 2. Je choisis d'assigner une valeur à cette propriété dans
12 // le constructeur de mon composant, plutôt que directement.
13 // J'ai le choix dans ce cas.
14 subtitle: string;
15 // 3. Je récupère les tâches à réaliser depuis un serveur distant.
16 tasks: Task[];
17
18 constructor() {
19 // J'assigne une valeur à ma propriété. Il ne s'agit pas de
20 // code dit 'complexe', je peux le faire dans le constructeur.
21 this.subtitle = "Tâches urgentes";
22 }
23
24 ngOnInit() {
25 // La récupération de données depuis un serveur distant
26 // est une opération complexe
27 // (appel asynchrone, gestion des erreurs...)
28 // Ce genre d'initialisation a sa place dans la méthode ngOnInit.
29 this.tasks = ...;
30 }
31
32 }

Comme vous pouvez le voir, on peut choisir d'assigner des valeurs simples soit :
· Au moment où vous déclarez une propriété à votre composant (ligne 10).
· Soit plus tard dans le constructeur (ligne 18 puis ligne 22).
Vous avez le choix et cela dépend de la manière dont vous préférez vous organiser. La
première méthode est plus compacte et évite d'ajouter des lignes de code supplémentaires à votre
composant. De plus, cela nous permet d'avoir les initialisations des propriétés au même endroit.
Dans la suite du cours c'est l'approche que je retiendrai.
En revanche les opérations dites « complexes », qui correspondent à des opérations propres
à votre application, ne doivent pas se trouver dans le constructeur. Si vous avez compris ça, vos
composants seront « propres » et ce sera un plaisir pour d'autres développeurs de relire votre
code.

Une méthode pour gérer les interactions utilisateurs


Vous vous demandez surement comment gérer les interactions de l'utilisateur. Par exemple,
si l'utilisateur clique sur un Pokémon de notre liste, nous voudrons probablement effectuer une
action comme une redirection ou un affichage.
La capture de l'interaction utilisateur se fait côté template, et nous verrons comment faire
au prochain chapitre. En revanche, lorsque l'utilisateur déclenche l'événement attendu, vous
pouvez définir une méthode qui sera appelée à chaque fois.
01 export class AppComponent {
02 // ...
03 selectPokemon(pokemon: Pokemon) {
04 console.log("Vous avez cliqué sur " + pokemon.name);
05 }
06 // ...
07 }
Comme vous le voyez, nous n'avons rien fait de compliqué ici. La méthode selectPokemon
prend en paramètre un objet de type Pokemon et affiche son nom dans la console du navigateur.
Il ne nous reste plus qu'à développer un template pour ce composant, qui appellera notre
méthode lorsque l'événement 'clic' sera déclenché par l'utilisateur. Mais nous verrons comment
faire cela depuis notre template, au chapitre suivant. Patience !

Un peu de pratique !
Bon passons à la pratique, et essayons d'enrichir quelque peu le composant
app.component.ts de notre application. Je propose d'ajouter deux fonctionnalités que nous venons
de voir :
· Injecter une liste de Pokémons dans le composant.
· Préparer une méthode qui devra gérer l'action lorsque l'utilisateur cliquera sur un
Pokémon de cette liste.
Mais avant de vous laisser chercher tout seul, je me dois de vous donner deux choses :
· Une classe permettant de modéliser un Pokémon.
· Un fichier contenant quelques Pokémons à injecter dans notre composant, à titre
d'exemple.

Le modèle
Créer un fichier pokemon.ts dans le dossier app de notre projet, avec le code suivant :
01 export class Pokemon {
02 id: number;
03 hp: number;
04 cp: number;
05 name: string;
06 picture: string;
07 types: Array<string>;
08 created: Date;
09 }

Cette classe a pour rôle de représenter un Pokémon dans notre application. Chaque
Pokémon aura donc :
· Id : un identifiant unique sous forme de nombre.
· Hp : le nombre de points de vie du Pokémon.
· Cp : le nombre de dégâts d'un Pokémon.
· Name : un nom.
· Picture : l'url d'une image représentant le Pokémon.
· Types : un tableau contenant les types du Pokémon (Eau, Feu, Vol, etc...).
· Created : la date de création du Pokémon.

Les données
Maintenant, créons un fichier mock-pokemons.ts qui contiendra les données de plusieurs
Pokémons. Ce fichier doit être placé dans le dossier src/app (le fichier est un peu long, mais il
s'agit simplement de données mises à disposition pour notre application). Etant donné que nous
n’allons pas modifier ce fichier dans la suite du cours, je vous propose de le copier directement à
partir de ce dépôt.
Comme vous pouvez le constater, ce fichier ne fait qu'exporter la constante POKEMONS,
qui contient des données nécessaires pour notre application.
Bon, maintenant vous avez tous les éléments nécessaires pour améliorer le composant
app.component.ts de notre application. Pour rappel, voici ce que vous devriez pouvoir faire :
· Injecter une liste de Pokémons dans le composant.
· Préparer une méthode qui devra gérer l'action lorsque l'utilisateur cliquera sur un
Pokémon de cette liste.
A vous de jouer maintenant !

Correction
Alors vous avez réussi ? Vous n'avez plus d'erreurs dans la console ?
Je vous propose tout de même une correction, afin que nous ayons la même base pour la
suite. Recopiez donc le code ci-dessous, puis analysons l’ensemble :
01 import { Component, OnInit } from '@angular/core';
02 import { Pokemon } from './pokemon';
03 import { POKEMONS } from './mock-pokemons';
04
05 @Component({
06 selector: 'pokemon-app',
07 template: '<h1>Pokémons</h1>'
08 })
09 export class AppComponent implements OnInit {
10 pokemons: Pokemon[] = null;
11 ngOnInit() {
12 this.pokemons = POKEMONS;
13 }
14 selectPokemon(pokemon: Pokemon) {
15 console.log('Vous avez selectionné ' + pokemon.name);
16 }
17 }
Voici ce qui a été modifié, par rapport au composant qui affichait simplement "Hello,
World !" :
· Trois importations supplémentaires, dans les trois premières lignes : importation de
l'interface OnInit, récupération du modèle représentant un Pokémon, afin de pouvoir typer
nos variables, et ajout de la constante POKEMONS, qui contient les données de différents
Pokémons.
· Modification du template à la ligne 7, en mettant Pokémons comme titre, à la place de
"Hello, World !".
· Déclaration d'une propriété à la ligne 10 : pokemons qui doit contenir la liste des
Pokémons à afficher à l'utilisateur.
· La méthode d'initialisation ngOnInit à la ligne 11, qui a pour fonction d'initialiser la
propriété pokemons du composant avec les valeurs importées depuis mock-pokemons.ts.
· La méthode selectedPokemon à la ligne 14, qui permet d'afficher dans la console le nom
du Pokémon sélectionné par l'utilisateur.
Nous voici avec un composant presque prêt, mais il lui manque encore une chose pour
pouvoir fonctionner. C'est pourquoi nous ajouterons un template à notre composant dans le
prochain chapitre !
Pour l'instant, ce composant ne fait rien d'autre que d'afficher un titre, nous devons le coupler à
un template pour avoir un ensemble fonctionnel !
Conclusion
Nous savons désormais comment mettre en place un composant de base, intervenir sur les
différentes étapes de son cycle de vie et l'initialiser avec des données. Nous avons vu comment
développer la classe d'un composant.
Il ne nous manque plus qu'à associer un template à notre composant, car pour l'instant notre
application se contente d'afficher un simple titre. Je vous propose de voir tout cela au chapitre
suivant, qui sera peut-être un peu plus long que celui-ci !

En résumé
· Un composant fonctionne en associant la logique d'une classe TypeScript avec un
template HTML.
· On utilise l'annotation @Component pour indiquer à Angular qu'une classe est un
composant.
· On peut initialiser une propriété avec une valeur simple directement lors de sa déclaration
ou dans le constructeur d'un composant.
· Des méthodes nous permettent d'interagir avec le cycle de vie d'un composant. Ces
méthodes sont toutes préfixées par ng.
· La méthode ngOnInit permet de définir un comportement spécifique lors de
l'initialisation d'un composant.
· Les méthodes de cycle de vie d'un composant que nous utiliserons le plus sont ngOnInit
et ngOnDestroy.
Chapitre 7 : Les templates
Nous allons voir un chapitre très important, celui des templates. Pour rappel, les templates
sont les vues de nos composants, et ils contiennent le code de notre interface utilisateur. Il est
donc important de bien comprendre ce chapitre pour pouvoir développer une expérience
utilisateur de qualité sur votre site !
Je préfère vous prévenir, ce chapitre peut sembler assez long. Mais rassurez-vous, nous
allons surtout voir une nouvelle syntaxe : la syntaxe des templates avec Angular.

Développer des templates


Pour l'instant, nous écrivons nos templates sous forme de chaîne de caractères. Mais
parfois cette chaîne de caractères peut devenir trop longue pour tenir sur une seule ligne.
Imaginons que vous ayez un template très long, voilà ce que vous seriez obligé de faire :
01 @Component({
02 selector: 'long-template',
03 template: 'Ceci est un template très,'
04 + 'très,'
05 + 'très,'
06 + 'long !'
07 })

Ce n'est vraiment pas pratique ! Je vous rassure, Angular ne nous oblige pas à faire ça. En
fait nous avons même deux possibilités pour gérer les templates trop long. La première
possibilité consiste à utiliser directement ES6 et la deuxième est de profiter d'une autre option
que propose Angular.

Utiliser ES6
En fait, un template peut s'écrire sur plusieurs lignes, à condition d'utiliser le backtick : ` .
Il ne faut pas confondre le backtick ` avec la quote ' !
Ce symbole permet de déclarer une chaîne de caractères sur plusieurs lignes sans avoir à
concaténer des chaînes de caractères ! Nous pouvons donc réécrire notre composant précédent de
la manière suivante :
01 @Component({
02 selector: 'long-template',
03 template: `
04 Ceci est un template de plusieurs lignes
05 écrit sans problème avec le backtick et ES6,
06 nous évitant ainsi des concaténations pénibles.
07 `
08 })

Cette fonctionnalité est disponible avec ES6, mais pas avec ES5, d'où l'intérêt de l’utiliser.

Utiliser Angular
Bien entendu, parfois nos templates sont trop longs pour être écrits dans le même fichier
que notre composant, et nous préférerons décrire notre template dans un fichier à part. Angular
propose l'option templateUrl pour cela :
01 @Component({
02 selector: 'long-template',
03 templateUrl: 'app/long-template.component.html'
04 })

Grâce à cette option, vous devez simplement renseigner le chemin relatif vers votre
template, par rapport à la racine de votre projet. Dans l'exemple ci-dessus, on suppose que le
template se trouve dans le dossier app de notre projet. Maintenant imaginons que vous souhaitiez
regrouper tous vos templates dans un dossier templates. Dans ce cas vous devriez modifier le
chemin relatif :
01 @Component({
02 selector: 'long-template',
03 templateUrl: 'app/templates/long-template.component.html'
04 })

L'interpolation
La manière la plus simple d'afficher la propriété d'un composant dans son template est
d'utiliser le mécanisme de l'interpolation. Avec l'interpolation, nous pouvons afficher la valeur
d'une propriété dans la vue d'un template, entre deux accolades, comme ceci : {{ maPropriete }}.
Par exemple, si vous avez une propriété title dans votre composant, et que vous souhaitez
afficher sa valeur dans le template, comment faites-vous ? Et bien avec l'interpolation. Examinez
l'exemple ci-dessous :
01 import { Component } form '@angular/core';
02
03 @Component({
04 selector: 'title',
05 // La syntaxe pour interpoler une propriété
06 template: '<h1>{{ title }}</h1>'
07 })
08 export class TitleComponent {
09 // La propriété interpolé
10 title: string = "L'interpolation, c'est simple !";
11 }
Angular récupère automatiquement la valeur de la propriété title du composant, et l'injecte
dans le template, pour qu'elle s'affiche dans le navigateur de l'utilisateur. Et ce n'est pas tout,
Angular met à jour l'affichage dans le template si la propriété du composant est modifiée, et le
tout sans que nous n'ayons rien besoin de faire !

Le fait que la propriété interpolée soit mise à jour automatiquement dans le template en cas de
modification s'appelle le binding unidirectionnel. L'interpolation s'occupe de cela, en plus du
simple affichage de la valeur de la propriété.
Syntaxe de liaison des données
L'interpolation est très pratique pour lier notre template et notre composant. Cependant, il
existe d'autres manières de créer des liaisons entre les deux. L'interpolation a une particularité :
elle pousse les données depuis le composant vers le template, mais pas dans le sens inverse.
Nous allons voir comment gérer les autres types de liaison.

Du composant vers le template


Nous pouvons pousser plusieurs données depuis le composant vers le template. Voici les
liaisons que nous pouvons mettre en place :

Propriétés Code Explications


<img [src]=’someImageUrl’>
Propriété On utilise les crochets pour
d'élément lier directement la source de
l'image à la propriété
someImageUrl du
composant.
<label [attr.for]=’someLabelId’>…
Propriété </label> On lie l'attribut for de
d'attribut l'élément label avec la
propriété de notre composant
someLabelId.

Propriété de la <div Fonctionnement similaire,


classe [class.special]=’isSpecial’>Special</div> pour attribuer ou non la
classe special à l'élément
div.

Propriété de <button [style.color]="isSpecial? ‘red’ : On peut également définir un


style ‘green’">Special</div> style pour nos éléments de
manière dynamique : ici on
définit la couleur de notre
bouton en fonction de la
propriété isSpecial, soit
rouge, soit vert. (C'est un
opérateur ternaire que l'on
utilise comme expression)

Du template vers le composant


Maintenant, si on souhaite lier des données dans l'autre sens, comment fait-on ? Voyons
tous de suite comment gérer les interactions de l’utilisateur.
Nous verrons les liaisons bidirectionnelles dans le chapitre dédié sur les Formulaires.

Gérer les interactions de l'utilisateur


Quand un utilisateur clique sur un lien, presse un bouton ou entre un texte, on veut en être
informé. Toutes ces actions lèvent des événements dans le DOM, avec lequel nous voudrions
interagir.

Le DOM est une représentation structurée de votre page HTML, où chaque balise HTML
représente un nœud.

Nous allons apprendre à lier n'importe quel événement du DOM à une méthode de
composants, en utilisant la syntaxe de liaison d’événements d’Angular.

Créer une liaison avec les événements


La syntaxe pour écouter un événement est simple. On entoure le nom de l'événement du
DOM par des parenthèses et on renseigne ensuite une action à réaliser, qui correspond à une
méthode de notre composant. Par exemple, pour écouter un clic sur un bouton, rien de plus
simple :
01 <button (click)=’onClick()’>Cliquez ici !</button>

Ici (click) correspond à l'événement que l'on souhaite intercepter : ici, un événement de
type clic. La méthode onClick du composant associé est alors appelée, à chaque clic de la part de
l'utilisateur.

Récupérer les valeurs entrées par l'utilisateur


Imaginons que l'on veuille récupérer une valeur entrée par un utilisateur, par exemple un
nom qu'il rentre dans un champ texte, puis afficher cette valeur à l'écran, avec un message du
type "Bonjour, Jean" ou "Bonjour, Juliette".
Pour le moment, nous ne savons pas comment récupérer cette valeur.
Eh bien, rassurez-vous, Angular met à disposition un objet représentant un événement du
DOM à travers la variable $event. Regardons tout de suite comment ça marche :
01 import { Component } from '@angular/core';
02
03 @Component({
04 selector: 'evenement',
05 template: `
06 <input (keyup)="onKey($event)">
08 <p>{{ values }}</p>
09 `
10 })
11 export class EventComponent {
12 values: string = '';
13
14 onKey(event: any) {
15 // texte entré par l'utilisateur
16 this.values = "Bonjour " + event.target.value;
17 }
18 }

Regardons les lignes importantes de ce composant :


· A la ligne 6 : dans le template, à chaque fois que l'utilisateur déclenche l'événement
keyup, c'est-à-dire qu'il tape du texte dans le champ texte <input>, alors la méthode
onKey($event) est appelée, avec en paramètre un objet représentant l'événement soulevé.
· A la ligne 16 : Notre méthode traite l'événement soulevé par l'utilisateur, en mettant à
jour la propriété value de notre composant. Ces modifications sont ensuite répercutées
directement sur le template.
any est un type propre à TypeScript, qui signifie simplement "n'importe quel type ».
La structure de l'objet $event est déterminée à l'avance, quel que soit l'événement soulevé,
car cet objet représente un événement standard du DOM. Il contient des propriétés et des
méthodes communes à tout événement. Ainsi, $event.target renvoie un objet de type
HTMLInputElement (un champ texte), qui possède une propriété value, qui contient les données
entrées par l'utilisateur. On peut donc bien accéder aux données entrées par l'utilisateur depuis
nos composants.
Cependant, pour l'instant notre code est minimaliste, nous n'avons typé aucune variable de
notre composant, afin d'en simplifier la lecture. Dans la réalité nous préférons avoir un typage
fort, c'est-à-dire un typage le plus précis possible :
01 export class EventComponent {
02 values: string = '';
03 // avec typage fort
04 onKey(event: KeyboardEvent) {
05 this.values = 'Bonjour ' + (<HTMLInputElement>event.target).value;
06 }
07 }

Le problème, c'est que maintenant on se rend compte que notre code prend en compte les
détails de l'objet $event, alors que cela ne concerne pas directement notre composant. Le code
n'est pas très lisible et nous devons gérer des objets qui ne concernent pas directement notre
application !
Heureusement, nous allons voir une méthode pour résoudre ce problème.

Et mais on n'a vu tout ça pour rien ?

Non, maintenant vous aurez vu comment cela fonctionne de l'intérieur et vous êtes capable
d'interagir avec n'importe quel événement du DOM depuis votre composant !

Les variables référencées dans le template


Il existe une autre manière de récupérer les données entrées par les utilisateurs, sans la
variable $event. Angular propose une fonctionnalité permettant de déclarer des variables locales
dans le template. Ces variables nous garantissent un accès sur l'élément du DOM depuis le
template.
On déclare une variable locale avec l'opérateur # :
01 @Component({
02 selector: 'loop-back',
03 template: `
04 <input #box (keyup)="0">
05 <p>{{box.value}}</p>
06 `
07 })
08 export class LoopbackComponent { }

A la ligne 4, nous avons déclaré une variable box référence dans le template. La variable
box est une référence à l’élément <input> sur lequel elle a été déclarée. Nous pouvons ensuite
récupérer la valeur entrée par l'utilisateur grâce à la propriété value, et l'afficher via à
l'interpolation à la ligne 5. L'interpolation sert à afficher la variable référencée dans le template.

Les variables référencées dans le template sont accessibles pour tous les éléments fils et frères
de l'élément sur lequel elles ont été déclarées.

Heu, à quoi sert le (keyup)="0" à la ligne 4 ?

Ce code est important car Angular prend en compte la liaison de données entre le
composant et le template seulement si on effectue une action en réponse à des événements
asynchrones, comme des frappes sur le clavier.
Et pourquoi zéro ? Et bien le zéro est la plus courte déclaration pour signifier que nous ne
voulons rien exécuter, mais simplement activer la liaison de données entre la variable locale et la
valeur interpolée !
Les variables référencés dans le template peuvent être intrigantes au départ, mais elles
permettent d'écrire un code plus lisible. Reprenons notre exemple précédent avec des variables
référencées dans les templates :
01 @Component({
02 selector: 'loop-back',
03 template: `
04 <input #box (keyup)="onKey(box.value)">
05 <p>{{ values }}</p>
06 `
07 })
08 export class LoopbackComponent {
09 values = '';
10 onKey(value: string) {
11 this.values = 'Bonjour ' + value;
12 }
13 }
C'est quand même plus simple que de manipuler l'objet $event non ?
Je vous propose de voir ensemble deux autres événements qui pourront nous être utiles :
· Détecter lorsque l'utilisateur appuie sur Entrée.
· Lorsqu'il perd le focus sur un champ texte.

Détecter l'appui sur la touche Entrée


Imaginez que vous souhaitez récupérer les données qu'un utilisateur a saisies, mais
seulement lorsqu'il appuie sur Entrée, et non à chaque fois qu'il tape une lettre !
Angular nous permet de filtrer les événements du clavier à travers une syntaxe spécifique.
Nous pouvons écouter uniquement la touche Entrée en utilisant le pseudo-événement keyup.enter
:
01 @Component({
02 selector: 'enter',
03 template: `
04 <input #box (keyup.enter)="values=box.value">
05 <p>{{values}}</p>
06 `
07 })
08 export class EnterComponent {
09 values = '';
10 }

Dans cet exemple, c'est seulement au moment où l'utilisateur appuiera sur la touche Entrée
que la valeur de la propriété values sera prise en compte.
Il aurait était préférable de réaliser l'affectation values=box.value dans le composant plutôt
que directement dans le template, ce qui est recommandé par la documentation officielle, mais je
souhaitais vous montrer que c'est possible.

Détecter la perte du focus sur un élément


Nous pourrions améliorer l'exemple précédent en détectant si l'utilisateur clique ailleurs sur
la page après avoir entré une donnée dans un champ texte.
En effet, si l'utilisateur sélectionne un autre élément de la page, alors qu'il était en train de
remplir un champ texte, cela va provoquer une perte du focus sur le champ texte courant. Cette
perte du focus correspond à un événement nommé blur, et il permet d'écouter la perte du focus
sur un élément. Nous aimerions bien récupérer les données que l'utilisateur a tapées, et dont il a
interrompu la saisie en cliquant ailleurs sur la page :
01 @Component({
02 selector: 'enter',
03 template: `
04 <input #box
05 (keyup.enter)="values=box.value"
06 (blur)="values=box.value">
07 <p>{{values}}</p>
08 `
09 })
10 export class EnterComponent {
11 ...
12 }

Comme vous le voyez, rien de compliqué, on a combiné l'événement blur avec la détection
de la touche Entrée précédente, et ces deux événements déclenchent la même action :
values=box.value.

Les directives NgIf et NgFor


Nous allons voir deux directives dont nous allons avoir souvent besoin dans nos templates :
NgIf et NgFor.

Le chapitre prochain est dédié aux directives, nous pourrons voir de quoi il s'agit plus
exactement.

Ces directives sont disponibles automatiquement dans tous les templates de notre
application, car elles sont ajouté par le BrowserModule au démarrage de l'application. Nous
n'avons donc pas besoin de les importer dans notre composant pour pouvoir les utiliser !

Conditionner un affichage
Parfois on a besoin d'afficher une portion de template seulement dans des circonstances
spécifiques. On a besoin de tester une condition pour décider d'afficher un élément ou non.
La directive ngIf permet d'insérer ou de retirer un élément à partir d'une condition qui peut
être vraie ou fausse.
Essayons d'afficher un message dans notre template, seulement si un utilisateur est majeur :
01 <p *ngIf="utilisateur.age > 18">
02 Ce message est top secret et vous ne pouvez le voir que
03 si vous avez plus de 18 ans.
04 </p>

L'expression entre les doubles guillemets est explicite : l'utilisateur ne verra ce message
que s'il a plus de 18 ans (ce qui implique qu'il y a une propriété utilisateur dans le composant
associé à ce template). Selon le résultat de la condition, Angular ajoute le paragraphe au DOM et
le message apparaît, ou le message n'est pas affiché.

Heu... ? Pourquoi nous n'avons pas utilisé l'interpolation ? Comment Angular sait que
'utilisateur' est lié à une propriété d'un composant ?

La réponse est simple : ngIf. Cette directive vous permet d'appeler les propriétés de vos
composants directement, sans utiliser la syntaxe avec les doubles accolades, lors de la définition
de votre condition.
Avez-vous remarqué * devant le ngIf ? Sachez qu'il s'agit d'une part essentielle de la syntaxe, et
que vous ne devez pas l'oublier sous peine de lever une erreur !

Afficher un tableau
Comment faites-vous si une de vos propriétés est un tableau ? En effet, comment afficher
cette propriété dans votre template ? Et bien en utilisant une directive dédiée : ngFor.
Imaginons que nous souhaitons afficher une propriété d'un composant qui contient une liste
de Pokémons, et que cette propriété se nomme pokemons. Nous devons utiliser la directive ngFor
dans notre template :
01 <ul>
02 <li *ngFor="let pokemon of pokemons">{{ pokemon.name }}</li>
03 </ul>
L'utilisation de cette directive est intuitive, vous l'appliquez sur l'élément HTML que vous
souhaitez afficher pour chaque élément de votre liste. Ici nous souhaitons afficher une liste des
Pokémons, nous devons donc appliquer la directive sur l'élément <li>.
Ensuite, nous passons une expression à notre directive : let pokemon of pokemons.
Cette directive permet de définir une variable locale pokemon dans le code HTML qui est
répété en boucle. Nous pouvons ensuite accéder à cette variable grâce à l'interpolation : {{
pokemon.name }}.
Le terme of permet de faire comprendre à Angular que cette variable locale sera extraite du
tableau pokemons.

Un peu de pratique : Modifions les templates de notre


application
Je vous propose d'améliorer le template qui liste les Pokémons, grâce à la directive NgFor.
Cependant, avant de développer notre template, je vous propose d'ajouter la librairie CSS
Materialize pour avoir une interface utilisateur digne d'un vrai site. Vous n'êtes pas obligé de
l'utiliser, mais cela vous permettra d'avoir la même base de code que celle utilisée dans ce cours.
Pour cela, ajoutez le CSS de Materialize pour styliser notre application. Ajoutez cette ligne
dans le fichier index.html, avant la balise </head> :
01 <!-- Chargement de Materialize pour l'interface utilisateur -->
02 <link
03 rel="stylesheet"
04 href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-
05 beta/css/materialize.min.css">
Ensuite, modifiez la ligne concernant le template, dans app.component.ts, car nous allons
charger notre template depuis un fichier externe désormais :
01 templateUrl: './app/app.component.html'

Vous devez ensuite créer le fichier du template, app.component.html, dans le dossier app.
C'est tout bon ! Attaquons le vif du sujet, notre template !

Je vous encourage fortement à essayer de le faire par vous-même avant de regarder la solution.

Correction
Je vous fournis ci-dessous une correction avec le style fourni par Materialize. Le choix de
l'interface utilisateur est très subjectif, et il est tout à fait probable que vous n'ayez pas le même
code que celui de la correction. Je vous invite à vous en inspirer pour avoir la même base dans la
suite du cours.
Voici le template du composant à ajouter dans le fichier app.component.html :
01 <h1 class='center'>Pokémons</h1>
02 <div class='container'>
03 <div class="row">
04 <div *ngFor='let pokemon of pokemons' class="col s6 m4">
05 <div class="card horizontal" (click)="selectPokemon(pokemon)">
06 <div class="card-image">
07 <img [src]="pokemon.picture">
08 </div>
09 <div class="card-stacked">
10 <div class="card-content">
11 <p>{{ pokemon.name }}</p>
12 <p><small>{{ pokemon.created }}</small></p>
13 </div>
14 </div>
15 </div>
16 </div>
17 </div>
18 </div>

TOUTES les classes utilisées ci-dessus sont des classes issues de la librairie Materialize, et ne
sont pas liées à la syntaxe des templates d’Angular.

Nous allons détailler, comme ça personne n'est perdu.


Ligne 1 : Un titre avec la classe center : c'est une classe de Materialize qui permet de
centrer du texte.
Ligne 2 : Une balise div avec la classe container : c'est une classe de Materialize qui
permet de centrer le contenu de notre page web.
Ligne 3 : Une balise div avec la classe row : c'est encore une classe de Materialize
(décidément !) qui permet de définir une ligne dans la page, ce qui va nous permettre d'afficher
nos Pokémons les uns à la suite des autres.
Ligne 4 : On utilise la directive ngFor, afin de créer un nouveau bloc pour chacun de nos
Pokémons. On précise que sur des petits écrans, chaque Pokémon prendra la moitié de l'écran :
s6, et sur les autres écrans (tablettes, PC) chaque Pokémon prendra un tiers de l'écran : m4. Ces
classes signifient respectivement : s pour small (petits écrans) et m pour medium (écrans
moyens). Le container que nous avons défini précédemment possède une largeur arbitraire de 12,
donc s6 signifie : "Prendre la moitié de l'écran sur les petits écrans", car 6 est bien la moitié de
12 !
Ligne 5 : On utilise la classe card et horizontal pour définir un bloc avec une image à
gauche et un texte à droite au sein de chaque bloc. On détecte le clic sur ce bloc, en appelant la
méthode selectPokemon en passant en paramètre le pokémon sur lequel l'utilisateur a cliqué.
Ligne 6 : On utilise simplement une classe de Materialize pour ajouter un peu de style... je
ne présenterai plus les lignes de ce genre.
Ligne 7 : On utilise la liaison de propriété pour définir une source à notre image. N'oubliez
pas d'ajouter les crochets pour qu’Angular puisse interpréter l'expression passée en paramètre !
Ligne 11 et 12 : On utilise l'interpolation pour afficher les propriétés de nos Pokémons !

Je vous invite à jeter un coup d'œil à la documentation de Materialize pour comprendre ce que
font toutes ces classes. La documentation est très bien faite, et il nous suffit la plupart du temps
de récupérer les classes dont on a besoin pour ajouter du style à notre page web.
Au final, voici ce que vous devriez obtenir, après avoir lancé l'application avec npm start,
bien sûr !

Figure 13 - Notre application de Pokémons s'affiche dans le navigateur

Conclusion
Nous avons vu beaucoup de choses pendant ce chapitre sur les templates Angular. Sachez
que le sujet est vaste, mais que nous avons vu ici les fonctionnalités principales dont nous aurons
besoin pour développer notre application.
Dans le prochain chapitre, je vous propose d'éclaircir un élément que nous n'avons pas
encore vraiment exploré : les directives.

En résumé
· L'interpolation permet d'afficher des propriétés de nos composants dans les templates, via
la syntaxe {{ }}.
· On peut lier une propriété d'élément, d'attribut, de classe ou de style d'un composant vers
le template.
· Si nos templates sont trop longs, on peut utiliser le backtick ` d'ES6, ou définir nos
templates dans des fichiers séparés.
· La directive NgIf permet de conditionner l'affichage d'un template.
· La directive NgFor permet d'afficher une propriété de type tableau dans un template.
· On peut gérer les interactions d'un utilisateur avec un élément de la page grâce à la
syntaxe : ‘(‘ + ‘nom de l'événement’ + ’)’.
· On peut référencer des variables directement dans le template plutôt que de manipuler
l'objet $event.
· Les variables référencées dans le template sont accessibles pour tous les éléments fils et
frères de l'élément dans lequel elles ont été déclarées.
· Essayez d'éviter de mettre la logique de votre application dans vos templates. Gardez-les
le plus simple possible !
Chapitre 8 : Les directives
Nous avons déjà utilisé les directives à plusieurs reprises, sans le savoir. Et pour cause, les
directives se retrouvent partout dans une application Angular. Nous allons rapidement voir à
quelles occasions nous avons déjà eu affaire aux directives, et comment créer ses propres
directives !
Allez hop, au boulot !

Qu'est-ce qu'une directive ?


Commençons par un peu de théorie. Qu'est-ce que peut bien être une Directive ? Et bien
comme je viens de vous le dire, vous avez déjà utilisé des directives, à votre insu !
Une directive est une classe Angular qui ressemble beaucoup à un composant, sauf qu'elle
n'a pas de template (D'ailleurs, au sein du Framework, la classe Component hérite de la classe
Directive). Au lieu d'annoter cette classe avec @Component, vous utiliserez @Directive, logique
!
Une directive permet d'interagir avec des éléments HTML d'une page, en leur attachant un
comportement spécifique.
Nous avons déjà utilisé les directives ngIf et ngFor, qui sont des directives structurelles.

On peut avoir plusieurs directives appliquées à un même élément !

Une directive possède un sélecteur CSS, qui indique au Framework où l'activer dans notre
template.
Lorsqu’Angular trouve une directive dans un template HMTL, il instancie la classe de la
Directive correspondante et donne à cette instance le contrôle sur la portion du DOM qui lui
revient.
Bon, tout ça peut paraître un peu abstrait pour le moment. Avant de nous lancer dans le
concret pour créer notre propre directive, nous allons voir les trois types de directives existantes :
Les composants : oui, vous avez bien lu, tous les composants que nous avons développés
jusqu'à maintenant étaient également des directives ! Ils sont les pierres angulaires d'une
application Angular (sans mauvais jeu de mots) et les développeurs peuvent s'attendre à devoir
en écrire beaucoup.
Les directives d'attributs : Elles peuvent modifier le comportement des éléments HTML,
des attributs, des propriétés et des composants. Elles sont représentées habituellement par des
attributs au sein de balises HTML, d'où leur nom.
Les directives structurelles : ces directives sont responsables de mettre en forme une
certaine disposition d'éléments HTML, en ajoutant, retirant ou manipulant des éléments et leur
fils. Les directives ngIf et ngFor sont des directives structurelles.

Créer une directive d’attribut


Nous allons voir comment créer une directive d'attribut pour notre application. Ce type de
directive va nous permettre de changer l'apparence ou le comportement d'un élément.
Je vous propose de créer la directive BorderCardDirective, qui permettra d'ajouter une
bordure de couleur sur les Pokémons de notre liste, lorsque l'utilisateur les survolera avec son
curseur. Nous fixerons également une hauteur commune à tous les pokémons, afin que les
éléments de notre liste soient toujours alignés. Voici ce que vous devriez obtenir :

Figure 14 - On affiche une bordure lorsque le curseur de l'utilisateur survole un Pokémon.

Ici je survole Salamèche avec mon curseur, mais comme j'ai réalisé l'image avec une capture
d'écran, le curseur n'apparaît pas, c'est normal !

Pour commencer, nous savons qu'une directive d'attribut requiert au minimum une classe
annotée avec @Directive.
Dans cette annotation, nous allons devoir préciser le sélecteur css qui permettra d'appliquer
la directive sur nos éléments HTML. Dans la classe de notre directive nous décrirons le
comportement souhaité. Créons donc une directive globale à notre application dans le dossier
app, nommée border-card.directive.ts :
01 import { Directive, ElementRef } from '@angular/core';
02
03 @Directive({ selector: '[pkmnBorderCard]' })
04 export class BorderCardDirective {
05
06 constructor(private el: ElementRef) {
07 this.setBorder('#f5f5f5');
08 this.setHeight(180);
09 }
10
11 private setBorder(color: string) {
12 let border = 'solid 4px ' + color;
13 this.el.nativeElement.style.border = border;
14 }
15
16 private setHeight(height: number) {
17 this.el.nativeElement.style.height = height + 'px';
18 }
19 }
Tout d'abord, nous avons importé deux d'éléments depuis la librairie core d'Angular.
Détaillons chacun de ces éléments :
· Directives : Nous en avons besoin pour pouvoir utiliser l'annotation @Directive.
· ElementRef : Cet objet représente l'élément du DOM sur lequel nous appliquons notre
directive, nous y avons directement accès en paramètre du constructeur.
Ensuite nous passons un sélecteur css en paramètre de l'annotation @Directive, à la ligne 3.
Notre directive s'appliquera donc à tous les éléments du DOM qui ont un attribut
pkmnBorderCard, conformément au sélecteur que nous avons indiqué. Vous avez remarqué que
nous avons bien mis le nom de l'attribut entre crochets dans le sélecteur. En effet, si nous avions
simplement mis ceci …
01 @Directive({ selector: ‘pkmnBorderCard'}) // à ne pas faire !
02 export class BorderCardDirective { }

... alors notre directive se serait appliquée sur toutes les balises <pkmnBorderCard>, ce qui
n'a pas de sens, puisque dans ce cas ce ne serait pas une directive d'attribut mais un composant !
Lorsque nous avons choisi le nom de notre directive, nous avons préfixé border-card par
pkmn. Il est recommandé de préfixer leur nom, afin d'éviter le risque de collisions avec des
librairies tierces, ou avec des attributs HTML standard.
Par exemple, si votre application s'appelle AwesomeApp, utilisez le préfixe aa<Nom-de-
votre-directive>.
N'utilisez pas ng comme préfixe pour vos directives ! Ce préfixe est réservé par Angular. De
plus, utilisé la syntaxe camelCase pour nommer vos directives.
Enfin on exporte la classe BorderCardDirective pour pouvoir utiliser cette directive dans
nos composants !
Bref, récapitulons : Angular crée une nouvelle instance de notre directive à chaque fois
qu'il détecte un élément HTML avec l'attribut pkmnBorderCard, et injecte l'élément du DOM et
de quoi le modifier dans le constructeur de la directive. A partir de là, notre directive impose une
bordure grise et une hauteur de 180px aux éléments HTML sur lesquels elle est rattachée, grâce
aux méthodes setBorder et setHeight que nous avons définies.

Prendre en compte les actions de l'utilisateur


Ce que nous aimerions maintenant, c'est que la bordure change de couleur lorsqu'un
utilisateur survole un élément. Pour cela nous avons besoin de :
· Détecter lorsque le curseur entre ou sort d'un élément de la liste.
· Définir une action pour chacun de ces événements.
On va utiliser une nouvelle annotation @HostListener. Cette annotation permet de lier une
méthode de notre directive à un événement donné. Nous allons donc créer deux nouvelles
méthodes dans notre directive, qui seront appelées respectivement quand le curseur de
l'utilisateur entre sur un élément, et quand il en ressort : ce sont les événements mouseenter et
mouseleave.
Voici le code final de notre directive, qui utilise l'annotation @HostListener :
01 import { Directive, ElementRef, HostListener } from '@angular/core';
02
03 @Directive({ selector: '[pkmnBorderCard]' })
04 export class BorderCardDirective {
05
06 constructor(private el: ElementRef) {
07 this.setBorder('#f5f5f5');
08 this.setHeight(180);
09 }
10
11 @HostListener('mouseenter') onMouseEnter() {
12 this.setBorder('#009688');
13 }
14
15 @HostListener('mouseleave') onMouseLeave() {
16 this.setBorder('#f5f5f5');
17 }
18
19 private setBorder(color: string) {
20 let style = 'solid 4px ' + color;
21 this.el.nativeElement.style.border = border;
22 }
23
24 private setHeight(height: number) {
25 this.el.nativeElement.style.height = height + 'px';
26 }
27 }
Voici ce que nous avons ajouté par rapport à la version précédente de notre directive :
· Nous avons importé HostListener depuis la librairie core d'Angular à la ligne 1.
· Nous avons défini la propriété el grâce à la syntaxe des constructeurs de TypeScript. C'est
pourquoi nous pouvons utiliser this.el ailleurs dans notre directive.
· Nous utilisons l'annotation @HostListener pour détecter deux événements : mouseenter
et mouseleave.
Il est fortement recommandé d'utiliser @HostListener plutôt que de définir nous-même des
écouteurs d'événements. Vous y gagnerez à tous les niveaux : cela vous évite d'interagir
directement avec l'API du DOM, vous n'avez pas à détacher vous-même les écouteurs
d'événements lorsque la directive est détruite, etc.
Voilà, nous avons désormais une directive qui va rajouter une bordure de couleur lorsqu'un
utilisateur passera son curseur au-dessus d'un Pokémon de la liste. Il ne reste plus qu'à l'intégrer à
notre application.

Utiliser notre directive


Il faut maintenant déclarer notre directive pour pouvoir l'utiliser dans les templates de nos
composants. Ouvrez le fichier app.module.ts et ajoutez-y les lignes suivantes :
01 // ...
02 // importez la directive
03 import { BorderCardDirective } from './border-card.directive';
04
05 @NgModule({
06 imports: [ BrowserModule ],
07 // déclarez BorderCardDirective dans le module racine de l'application
08 declarations: [ AppComponent, BorderCardDirective ],
09 bootstrap: [ AppComponent ]
10 })
11 export class AppModule { }

Maintenant, il ne nous reste plus qu'à appliquer cette directive dans le template de notre
composant app.component.ts, comme ceci :
01 <div *ngFor='let pokemon of pokemons' class="col s6 m4">
02 <div class="card horizontal" (click)="selectPokemon(pokemon)"
03 pkmnBorderCard> <!-- on applique notre directive ici ! -->
04 ...
05 </div>
06 </div>
Maintenant, lancez l'application avec npm start si vous l'aviez fermée, chaque Pokémon
devrait être entouré d'une bordure de couleur lorsque vous le survolez !

Ajouter un paramètre à notre directive


Pour le moment, notre directive pkmnBorderCard n’est pas personnalisable. A chaque
utilisation, cette directive impose une couleur unique aux bordures. Je vous propose donc de
paramétrer la couleur des bordures. De cette manière il sera possible de préciser une couleur lors
de l’appel de la directive dans un template.
Pour cela, nous devons préciser une propriété d’entrée pour notre directive, avec l’annotation
@Input. Nous allons ajouter une propriété borderColor pour paramétrer la couleur des bordures :
01 // 1. On ajoute ‘Input’ à nos importations
02 import { Directive, ElementRef, HostListener, Input } from '@angular/core';
03
04 @Directive({ selector: '[pkmnBorderCard]' })
05 export class BorderCardDirective {
06
07 constructor(private el: ElementRef) {
08 this.setBorder('#f5f5f5');
09 this.setHeight(180);
10 }
11
12 // 2. On declare notre propriété borderColor
13 @Input('pkmnBorderCard') borderColor: string;
14
15 @HostListener('mouseenter') onMouseEnter() {
16 // 3. On attribute la couleur souhaitée,
17 // sinon on attribue une valeur par défaut
18 this.setBorder(this.borderColor || '#009688');
19 }
20
21 @HostListener('mouseleave') onMouseLeave() {
22 this.setBorder('#f5f5f5');
23 }
24
25 private setBorder(color: string) {
26 let style = 'solid 4px ' + color;
27 this.el.nativeElement.style.border = border;
28 }
29
30 private setHeight(height: number) {
31 this.el.nativeElement.style.height = height + 'px';
32 }
33 }

Nous avons fait trois choses dans le code ci-dessous :


1. Importer l’annotation @Input depuis le paquet @angular/core, à la ligne 2.
2. Déclarer la propriété borderColor avec un alias, à la ligne 13. Les explications sur ce
qu’est un alias se trouve un peu plus bas.
3. Utiliser cette propriété dans la méthode setBorder, à la ligne 18. On utilise l’opérateur
logique OU (||) pour définir une valeur par défaut s’il n’y aucune couleur qui a été définie
dans le template.
Ensuite, depuis votre template app.component.html, vous pouvez paramétrer votre directive
comme ceci :
<div class="card horizontal" (click)="selectPokemon(pokemon)" [pkmnBorderCard]=”red”>
Vous obtiendrez une bordure rouge au survol. C’est quand même plus sympathique de
pouvoir choisir la couleur des bordures !

C’est quoi, un « alias » ?


En fait, il y a deux manières de déclarer une propriété d’entrée : avec ou sans alias.
01 @Input() pkmnBorderCard: string; // sans alias
02 @Input('pkmnBorderCard') borderColor: string; // avec alias
Sans alias, nous sommes obligé d’utiliser le nom de la directive pour nommer la propriéte.
Or ce nom de propriété, pkmnBorderCard, n’est pas un nom adapté pour la couleur de nos
bordures.
Heureusement, grâce à l’alias, vous pouvez nommer la propriété de la directive comme
vous voulez, et utilisez ce nom ailleurs dans votre directive. Spécifiez simplement le nom de la
directive dans l'argument @Input.

Réorganiser notre code


Pour terminer le développement de notre directive, je vous propose de remplacer les
valeurs codées « en dur », par des propriétés :
- initialColor : la couleur initiale affiché au chargement de la page.
- defaultColor : la couleur par défaut si aucune couleur de bordure n’est précisée.
- defaultHeight : la hauteur par défaut du cadre pour la bordure.
Le code ce que j’obtiens de mon côté est dans la correction de ce chapitre.
Vous pouvez bien sûr continuer à vous entrainer en améliorant cette directive. Par
exemple, en ajoutant des propriétés d’entrées pour paramétrer la largeur ou le type de la bordure :
pointillé, double trait… ou tout ce qui vous passe par la tête ! Faites preuve d’imagination.

Conclusion
Nous avons vu comment créer et utiliser des directives dans notre application. Même si la
plupart du temps nous utiliserons les directives natives fournis par Angular, ou des directives
issues de librairies, il est parfois intéressant de définir ses propres directives, pour définir un
comportement spécifique aux besoins de notre application. Dans tous les cas, vous connaissez
maintenant le fonctionnement !

En résumé
· On utilise l'annotation @Directive pour déclarer une directive dans notre application.
· Il existe trois types de directives différentes : les composants, les directives d'attributs et
les directives structurelles (ngFor et ngIf par exemple).
· Une directive d'attribut permet d'agir avec les éléments HTML d'une page, en leur
attachant un comportement spécifique.
· Une directive utilise un sélecteur CSS pour cibler les éléments HTML sur lesquels elle
s’applique.
· Il est recommandé de préfixer le nom de ses directives pour éviter les problèmes de
collisions.
· Angular crée une nouvelle instance de notre directive à chaque fois qu'il détecte un
élément HTML avec l'attribut correspondant. Il injecte alors dans le constructeur de la
directive l'élément du DOM ElementRef.
· Il faut déclarer notre directive pour pouvoir l’utiliser.
· On utilise l'annotation @HostListener pour gérer les interactions de l'utilisateur au sein
d'une directive.
Chapitre 9 : Les pipes
Grâce à l'interpolation, nous avons vu que nous pouvons afficher des données dans les
templates. Mais parfois les données que l'on récupère ne peuvent pas être affichées directement à
l'utilisateur. L'exemple le plus courant est le cas des dates. Imaginez que vous récupérez une date
depuis un serveur distant, qui représente la date de la dernière connexion de l'utilisateur : Mon
Apr 18 2017 12:45:26. Vous ne souhaitez pas afficher ce texte directement à l'utilisateur, n'est-ce
pas ? Vous avez donc besoin de le formater un peu, afin d'afficher quelque chose de plus lisible à
l'utilisateur.
De plus, au fur et à mesure que vous développerez, vous vous rendrez compte que vous
avez souvent besoin d'effectuer les mêmes transformations, dans plusieurs templates différents, à
travers vos projets.
C'est pourquoi Angular dispose de pipes, pour vous permettre d'effectuer plus simplement
ces transformations dans vos templates.

Utiliser un Pipe
Rien ne vaut un exemple pour comprendre. Je vous propose d'afficher des dates formatées
à nos utilisateurs. Voici à quoi ressemble les dates affichées dans nos templates pour le moment :
Mon Aug 29 2016 17:28:45 GMT+0200 (CEST). On ne peut pas dire que ça soit très sexy !
Je vous propose d'appliquer un pipe présent nativement dans toutes les applications
Angular : le pipe Date.
Prenons le composant suivant, qui affiche simplement une date d'anniversaire :
01 import { Component } from '@angular/core';
02
03 @Component({
04 selector: 'birthday',
05 template: `<p>Date d'anniversaire : {{ birthday | date }}</p>`
06 })
07 export class BirthdayComponent {
08 birthday = new Date(1992, 12, 19);
09 }
Ce composant affichera comme date d'anniversaire Dec 19, 1992. C'est déjà plus sympa
que ce que nous avions.
A la ligne 8, nous définissons une propriété birthday dans le composant, et nous lui
attribuons comme valeur la date du 19 Décembre 1992 (Pourquoi pas ?). Ensuite regardez la
ligne 5, nous utilisons l'interpolation pour afficher cette propriété, jusque-là rien d'anormal, mais
ensuite, vous apercevez ce code : | date.
Qu'est-ce que cela, me direz-vous ? Et bien il s'agit ni plus ni moins de la syntaxe des pipes
!
D'abord on place l'opérateur de pipe | à droite de la propriété sur laquelle on veut appliquer
la transformation, et ensuite on applique le nom du pipe que l'on veut utiliser, à la suite de cet
opérateur. Tous les pipes fonctionnent de cette manière.

Les pipes disponibles de base


Angular est fourni avec un certain nombre de pipes qui sont immédiatement disponibles
dans tous nos templates. Pourquoi s'en priver ? Voici une liste de pipes directement disponibles :
DatePipe pour afficher les dates au bon format, UpperCasePipe et LowerCasePipe pour mettre
un texte en minuscule ou en majuscule, CurrencyPipe pour l'affichage des devises ($, €), …
Si vous utilisez les pipes Date et Currency, sachez qu’ils utilisent l'API d'internationalisation
d'ECMAScript, qui n'est pas supportée par Safari et d'autres anciens navigateurs. Vous devez
ajouter ce polyfill dans votre fichier index.html :
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en"></script>

Combiner les pipes


Il est possible d'utiliser plusieurs pipes différents pour une même propriété, et de les
combiner pour obtenir l'affichage souhaité. Par exemple, imaginons que l'on souhaite afficher
une date en majuscule, et bien il suffit d'utiliser le pipe date et le pipe uppercase,
successivement, en utilisant l'opérateur de pipes :
01 <p>{{ birthday | date | uppercase }}</p>

Les transformations s'appliquent de la gauche vers la droite : d'abord date puis uppercase.

Paramétrer nos pipes


Les pipes peuvent faire l'objet d'une utilisation plus poussée. Par exemple pour date, on ne
choisit pas vraiment le format que l'on souhaite afficher pour nos dates, on se contente du résultat
que nous fournit le pipe. Heureusement, il est possible de passer des paramètres aux pipes, afin
de mieux définir l'affichage souhaité.
Un pipe peut accepter n'importe quel nombre de paramètres facultatifs. On ajoute des
paramètres à un pipe avec des « : » et les valeurs des paramètres. Par exemple, pour afficher un
prix en euros :
01 <p>{{ 15 | currency:’EUR’ }}</p>
Et on peut faire la même chose pour nos dates, en passant en paramètre un format pour
l'affichage :
01 <p>{{ birthday | date:’dd/MM/yyyy’ }}</p>

Améliorons l'affichage des dates de notre application


Essayons tout de suite d'ajouter un pipe dans notre application, afin de proposer le format
de date suivant à nos utilisateurs : 19/12/1992 pour le 19 décembre 1992. Je vous laisse ajouter le
pipe vous-même, et je vous donne la solution à la fin du chapitre.

Créer un pipe personnalisé


Angular nous permet bien sûr de créer nos propres pipes, pour les besoins de notre
application.
Je vous propose d'ajouter le pipe PokemonTypeColorPipe dans notre application. Il s'agit
d'un simple pipe qui est censé renvoyer une couleur correspondant au type d'un Pokémon. Par
exemple, si notre Pokémon est de type eau, alors le pipe renverra la couleur bleue, et si le
Pokémon est de type feu, alors le pipe renverra la couleur rouge. Voilà ce que nous devrions
obtenir :

Figure 15 - Notre application avec les pipes !


Dans notre dossier /app, ajoutez un fichier nommé pokemon-type-color.pipe.ts :
01 import { Pipe, PipeTransform } from '@angular/core';
02
03 /*
04 * Affiche la couleur correspondant au type du pokémon.
05 * Prend en argument le type du pokémon.
06 * Exemple d'utilisation:
07 * {{ pokemon.type | pokemonTypeColor }}
08 */
09 @Pipe({name: 'pokemonTypeColor'})
10 export class PokemonTypeColorPipe implements PipeTransform {
11 transform(type: string): string {
12 let color: string;
13
14 switch (type) {
15 case 'Feu':
16 color = 'red lighten-1';
17 break;
18 case 'Eau':
19 color = 'blue lighten-1';
20 break;
21 case 'Plante':
22 color = 'green lighten-1';
23 break;
24 case 'Insecte':
25 color = 'brown lighten-1';
26 break;
27 case 'Normal':
28 color = 'grey lighten-3';
29 break;
30 case 'Vol':
31 color = 'blue lighten-3';
32 break;
33 case 'Poison':
34 color = 'deep-purple accent-1';
35 break;
36 case 'Fée':
37 color = 'pink lighten-4';
38 break;
39 case 'Psy':
40 color = 'deep-purple darken-2';
41 break;
42 case 'Electrik':
43 color = 'lime accent-1';
44 break;
45 case 'Combat':
46 color = 'deep-orange';
47 break;
48 default:
49 color = 'grey';
50 break;
51 }
52
53 return 'chip ' + color;
54 }
55 }
Examinons un peu ce code :
D'abord, on déclare un pipe en appliquant sur une classe l'annotation @Pipe, que l'on
importe depuis la librairie core d'Angular.
Ensuite, la classe d'un pipe implémente l'interface PipeTransform. Cette interface possède
une méthode transform qui accepte une valeur (qui correspond à la valeur de la propriété sur
laquelle s'applique notre pipe, dans notre cas ce sera le type d'un Pokémon), suivie par plusieurs
paramètres facultatifs (notre pipe n'aura pas besoin de paramètres, nous n'avons donc qu'un
paramètre dans notre méthode transform).
Ensuite dans la fonction transform de notre pipe, on déduit la couleur à partir du type du
Pokémon. Par exemple, si le Pokémon est de type Feu, alors le pipe renverra la couleur rouge.
Les valeurs renvoyées par le pipe sont des classes de la librairie Materialize, qui permettent
d'attribuer une couleur aux éléments.
Pour pouvoir utiliser notre pipe, il ne faut pas oublier de le déclarer dans le fichier
app.module.ts :
01 // ...
02 import { PokemonTypeColorPipe } from './pokemon-type-color.pipe';
03
04 @NgModule({
05 imports: [ BrowserModule ],
06 // on déclare notre pipe PokemonTypeColorPipe dans le module
07 declarations: [
08 AppComponent,
09 BorderCardDirective,
10 PokemonTypeColorPipe
11 ],
12 bootstrap: [ AppComponent ]
13 })
14 export class AppModule { }

C'est bon, il ne nous reste plus qu'à appliquer notre pipe dans le template
app.component.html :
01 <h1 class='center'>Pokémons</h1>
02 <div class='container'>
03 <div class="row">
04 <div *ngFor='let pokemon of pokemons' class="col s6 m4">
05 <div class="card horizontal" (click)="selectPokemon(pokemon)"
06 pkmnBorderCard >
07 <div class="card-image">
08 <img [src]="pokemon.picture">
09 </div>
10 <div class="card-stacked">
11 <div class="card-content">
12 <p>{{ pokemon.name }}</p>
13 <!-- on ajoute le pipe pour les dates, avec le bon format : -->
14 <p><small>{{ pokemon.created | date:"dd/MM/yyyy" }}</small></p>
15 <!-- on utilise la directive ngFor pour afficher
16 tous les types d'un pokémon donnée -->
17 <span *ngFor='let type of pokemon.types'
18 class="{{ type | pokemonTypeColor }}">{{ type }}</span>
19 </div>
20 </div>
21 </div>
22 </div>
23 </div>
24 </div>
A la ligne 14, nous utilisons le pipe natif date, afin de formater les dates de notre template.
Il s'agit de la correction que je vous avais promise à la section précédente.
Enfin à la ligne 18, nous utilisons notre propre pipe pokemonTypeColor, afin d'afficher le
type du Pokémon avec la couleur correspondante à son type !

Conclusion
Les pipes sont pratiques pour formater des données jugées trop "brutes", au sein de vos
templates. Plutôt que de définir les mêmes transformations à travers plusieurs composants, on
peut développer des pipes personnalisées afin de centraliser la définition de ces transformations.
C'est très pratique et il ne faut pas hésiter à utiliser les pipes dès que vous jugez que cela est
nécessaire !

En résumé
· Les pipes permettent de formater les données affichées dans nos templates.
· L'opérateur des pipes est « | ».
· Angular fournit des pipes prêts à l'emploi, disponibles dans tous les templates de notre
application : DatePipe, UpperCasePipe, LowerCasePipe, etc.
· Les pipes peuvent avoir des paramètres, mais tous les paramètres sont facultatifs.
· On peut créer des pipes personnalisés pour les besoins de notre application avec
l'annotation @Pipe.
· Les pipes personnalisés doivent être déclarés avant de pouvoir être utilisés dans les
templates de composants.
Chapitre 10 : Les routes
Pour l'instant notre application est assez limitée, elle est composée d'un unique composant,
accessible par défaut au démarrage de l'application. Nous ne pouvons donc pas développer une
application plus complexe, avec plusieurs composants, et une navigation entre des urls
différentes.
Je vous propose de remédier à ce problème, en dotant notre site web d'un système de
navigation digne de ce nom. La question "Comment mettre en place plusieurs pages dans mon
application ?" n'aura plus de secret pour vous.

Le fonctionnement de la navigation dans une


application Angular
Tout d'abord, vous devez savoir que le système de navigation fourni par Angular simule
parfaitement la navigation auprès de votre navigateur. Lorsque vous naviguez dans votre
application, vous pouvez revenir en arrière, rentrer directement une url dans la barre du
navigateur pour rejoindre une autre page de votre application, et l'historique du navigateur sera
automatiquement mis à jour ! Angular s'occupe pour nous de simuler la navigation auprès du
navigateur.

... et comment on le met en place ce merveilleux système de navigation ?

Avant de voir l'aspect technique de la chose, nous allons voir comment sont organisées les
routes dans une application.
Les routes doivent être regroupées par grande fonctionnalité au sein de votre application :
on parle de modules.
Par exemple, toutes les routes concernant les Pokémons devront être centralisées au même
endroit, c'est-à-dire dans le même module, et toutes les routes concernant les autres aspects de
notre application (Connexion, Inscription, Profil) dans un autre module. Je vous propose de voir
le système de routes d'Angular avec un système de routing basique entre deux pages, au sein de
notre module racine existant.

Deux nouveaux composants


Pour pouvoir mettre en place la navigation dans notre application, nous allons avoir besoin
d'au moins deux composants. Créez donc deux nouveaux composants dans le dossier src/app de
notre projet :

Url Composant Rôle

/pokemons list-pokemon.component.ts Lister tous les Pokémons de


l'application.

/pokemon/:id detail- Afficher une page


pokemon.component.ts d'information sur un pokémon.

Le composant qui affichera des détails devra être accessible à l’url /pokemon/:id, où :id
désigne un nombre entier représentant l'identifiant du Pokémon. Par exemple, la route
/pokemon/3 affichera le Pokémon avec l'identifiant n° 3 (qui est Carapuce dans notre cas).

Le code complet des deux composants est disponible plus loin dans le chapitre si vous êtes
bloqués. De plus, vous pouvez tout de suite créer les fichiers de templates pour ces deux
nouveaux composants.

Le composant DetailPokemonComponent
Avant de construire notre système de navigation, nous avons besoin de créer deux
nouveaux composants : commençons par le composant detail-pokemon.component.ts. Créez ce
fichier dans le dossier app de notre projet. Nous allons voir étape par étape comment construire
ce fichier.
Les importations
Tout d'abord, nous devons importer un certain nombre d'éléments dont nous allons avoir
besoin :
01 import { Component, OnInit } from '@angular/core';
02 import { ActivatedRoute, Router, Params } from '@angular/router';
03 import { Pokemon } from './pokemon';
04 import { POKEMONS } from './mock-pokemons';

Il n'y a rien de nouveau ici, excepté à la ligne 2. Nous importons les éléments
ActivatedRoute, Router et Params. Ces éléments vont nous être utiles pour extraire l'identifiant
du pokémon à afficher, qui est contenu dans l'url du composant. Et l'élément Router va nous
permettre de rediriger l’utilisateur.
L’annotation du composant
L’annotation du composant est standard. (On utilisera un autre fichier pour le tempate.)
01 @Component({
02 selector : ‘detail-pokemon’,
03 templateUrl : ‘./app/detail-pokemon.component.html’
04 })

La classe du composant
01 export class DetailPokemonComponent implements OnInit {
02 pokemons: Pokemon[] = null; // liste des pokémons de notre application
03 pokemon: Pokemon = null; // pokémon à afficher dans le template
04
05 constructor(private route: ActivatedRoute, private router: Router) {}
06 // on injecte 'route' pour récupérer les paramètres de l'url,
07 // et 'router' pour rediriger l'utilisateur.
08 ngOnInit(): void {
09 // on initialise la liste de nos pokémons
10 this.pokemons = POKEMONS;
11 // on récupère le paramère 'id' contenu dans l'url
12 let id = +this.route.snapshot.paramMap.get('id');
13 // on itère sur le tableau de pokémon ensuite pour trouver
14 // le pokémon ayant le bon identifiant
15 for (let i = 0; i < this.pokemons.length; i++) {
16 // si on trouve un pokémon avec le bon identifiant,
17 // on affecte ce pokémon à la propriété du composant
18 if (this.pokemons[i].id == id) {
19 this.pokemon = this.pokemons[i];
20 }
21 }
22 }
23 // Méthode permettant de rediriger l'utilisateur
24 // vers la page principale de l'application.
25 goBack(): void {
26 this.router.navigate(['/pokemons']);
27 }
28 }

void permet d'indiquer à TypeScript qu'une fonction n'a pas de valeur de retour.

Dans ce composant, nous avons besoin de faire quelque chose de particulier lors de son
initialisation : nous devons récupérer l'identifiant qui se trouve dans l'url (exemple : /pokemon/3)
et ensuite récupérer le Pokémon qui correspond à cet identifiant. Regardons ce code, morceaux
par morceaux :
De la ligne 2 à 3, nous déclarons deux propriétés pour notre composant :
· pokemons : Cette propriété contiendra la liste de tous les pokémons de notre application,
soit celle que nous afficherons également dans le composant list-pokemon.component.ts.
· pokemon : Cette propriété contiendra le Pokémon à afficher à l’utilisateur.
À la ligne 5, nous déclarons un constructeur pour notre composant. Ce composant contient
un paramètre route et router. Je vous présenterai dans le chapitre dédié à l'injection de
dépendance, ce que signifie réellement cette syntaxe. Pour l'instant, retenez que nous venons
simplement de déclarer à Angular : "Dans ce composant, je vais avoir besoin de récupérer des
informations depuis l'url du composant, et aussi de rediriger l'utilisateur grâce à ces deux
paramètres".
À partir de la ligne 8, nous effectuons un certain nombre d'opérations pour initialiser le
composant.
D'abord, nous récupérons tous les Pokémons de notre application et nous les affectons à la
propriété pokemons :
this.pokemons = POKEMONS;

Ensuite nous récupérons l'identifiant du Pokémon contenu dans l'url :


01 let id = +this.route.snapshot.paramMap.get('id');
02
03 for (let i = 0; i < this.pokemons.length; i++) {
04 if (this.pokemons[i].id == id) {
05 this.pokemon = this.pokemons[i];
06 }
07 }

· Ligne 1 : La première ligne permet de récupérer le paramètre id de la route. La propriété


snapshot indique que nous récupérons ce paramètre de manière synchrone. C'est-à-dire que
nous bloquons l'exécution du programme tant que nous n'avons pas récupéré l'identifiant
du Pokémon à afficher. Ensuite nous accédons à l’objet paramMap contenant tous les
paramètres de la route sous forme d’un dictionnaire de valeur, et on extrait le paramètre id.
(On utilise l’opérateur JavaScript « + » pour convertir une chaîne de caractère en nombre,
car paramMap ne contient que des chaînes de caractère par défaut).
· Ligne 3 : On itère sur les Pokémons de l'application pour trouver celui à afficher.
· De la ligne 4 à la ligne 6 : On cherche le Pokémon qui a un identifiant qui correspond à
l'identifiant de la route. Une fois ce Pokémon trouvé, nous l'affectons à la propriété
pokemon de notre composant (ligne 5 ci-dessus).
Si aucun Pokémon n'est trouvé, alors la valeur de la propriété pokemon restera null. Nous
pourrons traiter ce cas dans le template par la suite.
Enfin, nous avons ajouté une méthode permettant à l'utilisateur de revenir à la liste des
Pokémons, après avoir consulté un Pokémon particulier :
01 // Méthode recommandée
02 goBack(): void {
03 this.router.navigate(['/pokemons']);
04 }

Nous utilisons ici le routeur d’Angular (que nous avons injecté depuis le constructeur) pour
rediriger l'utilisateur vers la liste des Pokémons. La méthode navigate du router permet de faire
cela, et elle prend l'url de redirection en paramètre, dans un tableau (car on peut vouloir passer
des paramètres à la route).

Pourquoi il y a écrit "Méthode recommandée" en commentaire (ligne 1) ?

Et bien, nous pouvons utiliser une autre façon de faire pour rediriger l'utilisateur. On peut
utiliser Location, qui est un service mis à disposition par Angular, et que nous pouvons utiliser
pour interagir avec l'URL du navigateur.
01 // Autre méthode
02 import { Location } from ‘@angular/common’;
03 // …
04 constructor(private location:Location) {}
05
06 goBack(): void {
07 this.location.back();
08 }
Le code parle de lui-même, on accède au service Location, et on demande un retour en
arrière avec la méthode back, et hop, le tour est joué : notre utilisateur peut revenir à liste des
Pokémons !
Si l'utilisateur est arrivé depuis une autre page que la liste des pokémons, il sera renvoyé vers
cette autre page ! C'est pour cela que la première méthode est plus fiable, car on sait
exactement où sera redirigé l’utilisateur.
Le template du composant
Le code du template detail-pokemon.component.html est :
01 <div *ngIf="pokemon" class="row">
02 <div class="col s12 m8 offset-m2">
03 <h2 class="header center">{{ pokemon.name }}</h2>
04 <div class="card horizontal hoverable">
05 <div class="card-image">
06 <img [src]="pokemon.picture">
07 </div>
08 <div class="card-stacked">
09 <div class="card-content">
10 <table class="bordered striped">
11 <tbody>
12 <tr>
13 <td>Nom</td>
14 <td><strong>{{ pokemon.name }}</strong></td>
15 </tr>
16 <tr>
17 <td>Points de vie</td>
18 <td><strong>{{ pokemon.hp }}</strong></td>
19 </tr>
20 <tr>
21 <td>Dégâts</td>
22 <td><strong>{{ pokemon.cp }}</strong></td>
23 </tr>
24 <tr>
25 <td>Types</td>
26 <td>
27 <span *ngFor="let type of pokemon.types"
28 class="{{ type | pokemonTypeColor }}">{{ type }}</span>
29 </td>
30 </tr>
31 <tr>
32 <td>Date de création</td>
33 <td><em>{{ pokemon.created | date:"dd/MM/yyyy" }}</em></td>
34 </tr>
35 </tbody>
36 </table>
37 </div>
38 <div class="card-action">
39 <a href="#" (click)="goBack()">Retour</a>
40 </div>
41 </div>
42 </div>
43 </div>
44 </div>
45 <h4 *ngIf='!pokemon' class="center">Aucun pokémon à afficher !</h4>

Je ne vais pas vous détailler ligne par ligne ce composant, puisqu'il s'agit essentiellement
de code HTML et de classes nécessaires à la librairie Materialize. En revanche, il y a certains
éléments à remarquer :
· Ligne 1 : La directive ngIf nous permet de vérifier qu'un Pokémon est disponible. Si
aucun Pokémon n'est disponible, on affiche un message d'erreur, à la ligne 45.
· Ligne 6 : On utilise la liaison de donnée pour déterminer la valeur de l'attribut src de
l'image à afficher.
· Ligne 27 : La directive ngFor nous permet d'afficher tous les types de notre Pokémon
avec une couleur déterminée, mais nous avons déjà effectué cette opération précédemment
dans ce cours.
· Ligne 39 : Nous écoutons l'événement click sur le lien, qui déclenche la méthode goBack.

Et que ce passe-t-il si aucun Pokémon n'est trouvé à partir de l'identifiant trouvé dans
l'url ?

A la ligne 45, nous détectons ce cas grâce à la directive *ngIf='!pokemon'. Le template du


composant affichera alors simplement le message : « Aucun Pokémon à afficher ! ».

Le composant ListPokemonComponent
Nous devons ensuite créer notre list-pokemon.component.ts, qui contient pour une grande
part le code qu'il y avait dans app.component.ts. Créer donc deux nouveaux fichiers list-
pokemon.component.ts et list-pokemon.component.html et copiez respectivement le code de
app.component.ts et app.component.html à l'intérieur.
Effectuez ensuite les quatres modifications ci-dessous dans vos nouveaux fichiers :
· Renommer la classe du composant en ListPokemonComponent.
· Modifier l'option selector et templateUrl de l'annotation @Component :
o selector vaut : ‘list-pokemon’.
o templateUrl vaut : ‘./app/list-pokemon.component.html’.
· Injecter un router dans le composant : constructor(private router: Router) {}.
· Utiliser le router dans la méthode selectPokemon pour rediriger l'utilisateur vers la page
qui détaille un Pokémon :
01 selectPokemon(pokemon: Pokemon): void {
02 let link = ['/pokemon', pokemon.id];
03 this.router.navigate(link);
04 }

On utilise la méthode navigate du router pour rediriger l'utilisateur, en passant en


paramètre de cette méthode un tableau contenant deux éléments : l'url de redirection et les
paramètres éventuels pour la route.

On peut également effectuer une redirection directement depuis le template grâce à la directive
routerLink. Par exemple : <a routerLink="/pokemons">Tous les Pokémons</a>.

Code source des deux composants


Le code source des deux composants detail-pokemon.component.ts et list-
pokemon.component.ts, est disponible à cette adresse.
Il s’agit d’une sorte de checkpoint, pour vous assurer d’avoir votre code à jour pour suivre
la suite du cours !

Mise en place du système de navigation


Pour l'instant, le fonctionnement de notre application n'a pas été modifié du point de vue de
l'utilisateur. Nous allons donc mettre en place un système de route avec les deux composants
précédents :
· Lorsque l'application démarrera, c'est la liste des Pokémons qui sera affichée par défaut.
· Quand l'utilisateur sélectionnera un Pokémon, il sera redirigé vers la fiche d'information
de ce Pokémon.
Pour commencer, nous devons reprendre le template de notre composant racine et le
modifier. Etant donné que ce composant n'a plus comme rôle d'afficher nos Pokémons, voilà à
quoi il ressemble désormais :
01 <router-outlet></router-outlet> // c’est tout !
Il n'y a qu'une chose à remarquer ici, la balise <router-outlet>. Lorsque nous naviguerons
dans notre application, le template des composants parcourus sera injecté immédiatement au sein
de cette balise. Par exemple, si je me rends sur l'url /pokemons, alors le template du composant
list-pokemon.component.ts sera injecté dans la balise <router-outlet> du template de
AppComponent.
AppComponent est le composant père des deux autres composants de notre application.

Mais alors, on peut mettre le code commun à nos deux composants dans ce composant
père ?

Et oui ! Et pour illustrer ce propos, je vous propose d'ajouter une barre de navigation à
notre projet. Modifier le template app.component.html :
01 <!-- Barre de navigation -->
02 <nav>
03 <div class="nav-wrapper teal">
04 <a href="#" class="brand-logo center">Application de Pokémons</a>
05 </div>
06 </nav>
07
08 <!-- Contenu des composants -->
09 <router-outlet></router-outlet>
Nous avons juste ajouté une barre de navigation avec des balises et des classes propre à
Materialize. La partie que vous devez retenir est la balise <router-outlet> à la ligne 9.
Nous avons maintenant trois composants prêts à l'emploi.
Cependant, il nous reste encore trois choses à faire pour mettre en place notre système de
navigation :
· Ajouter une balise HTML <base> dans le fichier index.html.
· Déclarer les routes de notre application dans le fichier app.routing.ts.
· Importer ces routes dans le module de notre application app.module.ts.
Allez, vous allez bientôt voir les résultats de votre travail !

La balise <base>
L'élément HTML <base> définit l'URL de base à utiliser pour composer toutes les urls
relatives contenues dans un document. Vérifier que cette balise est bien présente, juste après la
première balise <head> :
01 <head>
02 <base href="/">
03 <!-- ... -->
04 </head>

Il ne peut y avoir qu'une seule balise <base>.

Créer les routes


Afin de déclarer les routes de notre application, nous allons créer un module dédié app-
routing.module.ts dans le dossier app :
01 import { NgModule } from '@angular/core';
02 import { RouterModule, Routes } from '@angular/router';
03 import { ListPokemonComponent } from './list-pokemon.component';
04 import { DetailPokemonComponent } from './detail-pokemon.component';
05
06 // routes
07 const appRoutes: Routes = [
08 { path: 'pokemons', component: ListPokemonComponent },
09 { path: 'pokemon/:id', component: DetailPokemonComponent },
10 { path: '', redirectTo: 'pokemons', pathMatch: 'full' }
11 ];
12
13 @NgModule({
14 imports: [
15 RouterModule.forRoot(appRoutes)
16 ],
17 exports: [
18 RouterModule
19 ]
20 })
21 export class AppRoutingModule { }
D'abord, on importe deux nouvelles classes : Routes et RouterModule. Ces deux classes
nous aident à déclarer les routes de notre application. On doit donc lancer l'application avec un
tableau de routes appRoutes que l'on fournit en paramètre de la fonction RouterModule.forRoot,
à la ligne 15 ci-dessus.
Par défaut, le Router d'une application n'a pas de routes jusqu'à ce qu'on les configure !
On importe également les composants :
· ListPokemonComponent,
· et DetailPokemonComponent …
… qui sont utilisés pour construire la navigation.
Ensuite, on déclare une constante appRoutes à la ligne 7, qui contient la liste de nos routes
sous forme de tableau.
Enfin nous exportons nos routes sous la forme d'un module, que nous pouvons maintenant
déclarer dans le module de l'application !

Heu, ... je n'ai pas tout compris. Pourquoi nous déclarons nos routes dans un nouveau
module ?

Excellente question. En fait, il y a deux écoles.


· Vous pouvez définir vos routes sous forme d'une constante de type tableau, et les
déclarer directement dans le fichier app.module.ts ;
· Appliquer un découpage plus systématique et déclarer les routes dans un module
dédié, comme nous l’avons fait.
La seule règle est de ne jamais mélanger les deux manières de faire dans le même projet.
Choisissez une seule façon de faire.
Certains développeurs ignorent le module de lorsque la configuration est simple et
fusionnent la déclaration des routes directement dans le module principal (par exemple,
AppModule). C’est vrai que cela a pu vous sembler exagéré de déclarer deux modules pour
seulement quelques routes.
Cependant, la deuxième méthode est tout de même la plus recommandée (c’est celle que
nous appliquons dans ce cours), car elle permet de mieux découper la structure de votre
application, et vous pouvez configurer plus facilement des modules de routes pour chaque
fonctionnalité dans votre application.

Vous remarqué également que nous avons trois routes déclarées, alors que nous n'avons
prévu que deux ?

La raison est simple, au démarrage de notre application; l'url appelée est "l'url vide", c'est-
à-dire celle qui correspond au simple nom de domaine de votre application : dans mon cas il
s'agit de http://localhost:3000/, mais le numéro du port peut être différent selon votre machine :
3002, 8080, … Comme cette url ne correspond à aucune url de mes composants, nous faisons
une redirection vers la route /pokemons. Ainsi au démarrage de notre application, nous affichons
bien notre liste de Pokémons.

Déclarer nos routes auprès du module racine


Nous devons déclarer les routes de notre application dans le module racine app.module.ts :
01 import { NgModule } from '@angular/core';
02 import { BrowserModule } from '@angular/platform-browser';
03 import { AppRoutingModule } from './app-routing.module';
04
05 import { AppComponent } from './app.component';
06 import { ListPokemonComponent } from './list-pokemon.component';
07 import { DetailPokemonComponent } from './detail-pokemon.component';
08 import { BorderCardDirective } from './shadow-card.directive';
09 import { PokemonTypeColorPipe } from './pokemon-type-color.pipe';
10
11 @NgModule({
12 imports: [
13 BrowserModule,
14 AppRoutingModule
15 ],
16 declarations: [
17 AppComponent,
18 BorderCardDirective,
19 PokemonTypeColorPipe,
20 ListPokemonComponent,
21 DetailPokemonComponent
22 ],
23 bootstrap: [ AppComponent ]
24 })
25 export class AppModule { }
Nous avons rajouté deux choses dans ce module :
· D'abord nous avons intégré nos routes. À la ligne 3, on importe le module contenant nos
routes, et nous les intégrons dans le module à la ligne 14, avec les éléments importés.
· Ensuite nous déclarons nos deux nouveaux composants auprès de notre module, à la ligne
20 et 21. Il faut bien sûr ne pas oublier de les importer avant (ligne 6 et 7).
C'est bon, vous pouvez lancer l'application avec la commande npm start, et admirer le
résultat !

Gérer les erreurs 404


Notre application doit être en mesure de gérer le cas où la page demandée par l'utilisateur
n'existe pas.
Par exemple, si votre internaute rentre une url différente de celles traitées par votre
application, comme http://localhost:3000/poke, vous aurez l'erreur suivante dans la console du
navigateur : Error: Cannot match any routes: 'poke'.
En fait, vous aurez cette erreur chaque fois que l'utilisateur se rend sur une url différente de
/pokemons ou /pokemon/:id. Et c'est normal, puisque notre système de navigation n'est pas fait
pour intercepter les autres routes. Ce que nous voudrions, c'est intercepter toutes les routes qui ne
sont pas gérées par notre application et afficher une page d'erreur à l'utilisateur.
Pour cela, nous allons créer un composant PageNotFoundComponent qui aura pour rôle
d'afficher un message d'erreur à l'utilisateur. Créez donc ce composant page-not-
found.component.ts dans le dossier app :
01 import { Component } from '@angular/core';
02
03 @Component({
04 selector: 'page-404',
05 template: `
06 <div class='center'>
07 <img src=
08 "http://assets.pokemon.com/assets/cms2/img/pokedex/full/035.png"/>
09 <h1>Hey, cette page n'existe pas !</h1>
10 <a routerLink="/pokemons" class="waves-effect waves-teal btn-flat">
11 Retourner à l' accueil
12 </a>
13 </div>
14 `
15 })
16 export class PageNotFoundComponent { }

Ce composant n'a pas de logique propre, il s'agit juste d'un template qui sera affiché à
l'utilisateur.

On utilise la directive routerLink à la ligne 10 pour rediriger l'utilisateur vers la page d’accueil.

Il ne nous reste plus qu'à intercepter les routes qui ne sont pas prises en charge par notre
application et de les rediriger vers ce composant. Rien n'est plus simple grâce à l'opérateur '**',
qui permet d'intercepter toutes les routes au sein de votre application :
01 // app-module.routing.ts
02 // ...
03 import { PageNotFoundComponent } from './page-not-found.component';
04
05 const appRoutes: Routes = [
06 // ...
07 { path: '**', component: PageNotFoundComponent }
08 ];

Ainsi, toutes les routes qui ne sont pas interceptées par notre système de navigation seront
redirigées vers le composant PageNotFoundComponent. Pensez donc à déclarer cette route en
dernier dans votre tableaux de routes, car si vous la mettez en premier, alors toutes les routes de
votre application pointeront vers votre page d'erreur ! Ce n'est pas vraiment ce que souhaitent vos
utilisateurs !
L'ordre dans lequel vous déclarez vos routes a une importance, les routes déclarées en premier
doivent être les plus "précises", et les routes les plus générales à la fin !
Allez, il ne nous reste plus qu'à déclarer ce composant dans notre module racine
app.module.ts :
01 // ...
02 import { PageNotFoundComponent } from './page-not-found.component';
03
04 @NgModule({
05 // ...
06 declarations: [
07 // ...
08 PageNotFoundComponent,
09 // ...
10 ],
11 // ...
12 })
13 export class AppModule { }
Essayer maintenant de lancer l'application, et entrez une url pouvant provoquer une erreur,
comme /404 par exemple. Vous devriez obtenir ceci :

Figure 16 - La page d'erreur 404 de notre application


Je tiens à m'excuser auprès de tous ceux qui déteste les Pokémons, et qui sont obligés de les
supporter partout dans ce cours !

Débugger vos routes


Lorsque vous mettez en place vos routes, il arrive souvent d’avoir quelques erreurs dans
votre application. Afin de faciliter le débuggage, Angular vous permet de débugger simplement
le système de route de votre application :
01 // app-routing.module.ts
02 @NgModule({
03 imports: [
04 RouterModule.forRoot(appRoutes, {enableTracing : true})
05 ],
06 // …
Cela vous évitera d’ajouter des console.log dans toute votre application, pour voir d’où
peut venir l’erreur !

Conclusion
Nous avons vu dans ce chapitre comment créer un système de routing dans notre
application, en liant deux composants simples. Nous avons également vu comment gérer les
erreurs 404, le cas où l'utilisateur souhaite afficher un Pokémon qui n'existe pas, et comment
centraliser du code commun entre deux composants fils dans un composant parent.
Mais pour aller vers des applications plus poussées, je vous propose de voir dans le
prochain chapitre comment développer une application à plusieurs modules, ce qui nous
permettra de mieux organiser le futur code de notre application au fur et à mesure qu'elle se
développera.
Allez, courage !

En résumé
· Angular simule la navigation de l'utilisateur auprès du navigateur, sans que nous n'ayons
rien à faire.
· On construit un système de navigation en associant une url et un composant dans un
fichier à part.
· Le système de routes d'Angular interprète les routes qui sont déclarées du haut vers le
bas.
· La balise <router-outlet> permet de définir où le template des composants fils sera
injecté. Cette balise est disponible dans tous les templates des composants du module
racine.
· L'opérateur permettant d'intercepter toutes les routes est **.
· Les routes doivent être regroupées par fonctionnalité au sein de modules.
Chapitre 11 : Les modules
Nous allons voir dans ce chapitre comment créer un nouveau module pour notre
application, afin de mieux organiser le code de notre application. En effet, au fur et à mesure que
notre application grandit, si nous rajoutons tous nos fichiers dans le dossier app, on obtiendra
rapidement un sacré bazar !
Nous allons donc créer un module consacré uniquement à la gestion des Pokémons dans
notre application. En sachant comment ajouter un nouveau module dans votre application, vous
serez capable de créer des applications de n'importe quelle taille, vos ambitions n'auront plus de
limites.
Ce chapitre prolonge donc le chapitre précédent, en créant un système de navigation plus
complexe, avec plusieurs modules.
Du point de vue utilisateur, notre application restera la même que celle du chapitre précédent.
Nous allons modifier l'architecture interne de notre application, ce qui fera une grande
différence pour vous en tant que développeur.

Le module racine et les autres


Au risque de me répéter, nous allons revoir rapidement les fondamentaux sur les modules !
Les applications Angular sont modulaires, et elles possèdent leur propre système de
modules. Chaque application possède au minimum un module : le module racine, qui est nommé
AppModule par convention. Alors que ce module peut suffire pour les petites applications, la
plupart des applications ont besoin des modules de fonctionnalités.
Les modules de fonctionnalité sont un ensemble de classes et d'éléments, dédiés à un
domaine spécifique de votre application.
Quel que soit la nature du module, un module est toujours une classe avec le décorateur
@NgModule. Ce décorateur prend en paramètre un objet avec des propriétés qui décrivent le
module. Les propriétés les plus importantes sont :
· declarations : Les classes de vues qui appartiennent à ce module. Angular a trois types
de classes de vues : les composants, les directives, et les pipes. Il faut toutes les renseigner
dans ce tableau.
· exports : Il 'agit d'un sous-ensemble des déclarations, qui doivent être visibles et
utilisables dans les templates de composants d'autres modules.
· imports : Les classes exportées depuis d'autres modules, dont on a besoin dans le
module.
· providers : Cette propriété concerne les services et l'injection de dépendances que nous
traiterons au prochain chapitre.
· bootstrap : Cette propriété ne concerne que le module racine. Il faut y renseigner le
composant racine, c'est-à-dire le composant qui sera affiché au lancement de l’application.
JavaScript a son propre système de module, qui est complétement différent et sans rapport avec
le système de module d’Angular.
En JavaScript, chaque fichier est un module, et tous les objets définis dans ce fichier
appartiennent au module. Le module déclare certains objets comme publics en les déclarants
avec le mot-clé export. Ensuite d'autres modules JavaScript utilisent le mot-clé import pour
accéder à ces objets. C'est ce mécanisme que l'on utilise lorsque l'on fait :
01 export class AppComponent {}

Et par la suite :
01 import { AppComponent } from ‘./app.component’;

Les systèmes de modules de JavaScript et d’Angular sont différents mais complémentaires.


Nous utilisons les deux pour écrire nos applications. Cependant, dans ce chapitre nous étudions
bien les modules d’Angular.

Créer un module
Nous allons passer tout de suite à la pratique, en créant un nouveau module permettant de
centraliser les éléments concernant la gestion des Pokémons de notre application.
La première chose à faire est de créer un dossier pokemons pour notre module, dans le
dossier app.

C'est dans ce dossier que nous placerons tous les éléments relatifs au module : templates,
composants, routes, directives, pipes, etc ...

Nous devons maintenant déplacer les éléments que nous avons déjà développés dans le
dossier du module pokemons, à savoir :
· Le composant qui affiche la liste des Pokémons list-pokemon.component.ts et list-
pokemon.component.html.
· Le composant qui affiche un Pokémon spécifique detail-pokemon.component.ts et detail-
pokemon.component.html.
· Le pipe pokemon-type-color-type.pipe.ts que nous avons développé précédemment.
· La directive border-card.directive.ts.
· Notre modèle pokemon.ts qui représente nos Pokémons dans l'application.
· Les données de l'application issue de mock-pokemon.ts.
Mettez donc tous ces fichiers dans le dossier pokemons.
Ensuite, nous devons créer le module proprement dit dans un fichier dédié
pokemons.module.ts, dans le dossier src/app/pokemons. C'est ce fichier qui servira à articuler les
autres éléments du module :
01 import { NgModule } from '@angular/core';
02 import { CommonModule } from '@angular/common';
03
04 import { ListPokemonComponent } from './list-pokemon.component';
05 import { DetailPokemonComponent } from './detail-pokemon.component';
06 import { BorderCardDirective } from './shadow-card.directive';
07 import { PokemonTypeColorPipe } from './pokemon-type-color.pipe';
08
09 @NgModule({
10 imports: [
11 CommonModule
12 ],
13 declarations: [
14 ListPokemonComponent,
15 DetailPokemonComponent,
16 BorderCardDirective,
17 PokemonTypeColorPipe
18 ],
19 providers: []
20 })
21 export class PokemonsModule {}

Nous devons remarquer plusieurs choses dans ce module :


· Nous n'importons pas le BrowserModule mais le CommonModule dans le tableau
imports. En fait, le BrowserModule incluait déjà le CommunModule, ce qui nous
fournissait un certain nombre d'éléments dans les templates de nos composants, comme les
directives *ngIf et *ngFor, que n'avons jamais eu besoin d'importer au cas par cas. Le
BrowserModule permet simplement de démarrer l'application dans le navigateur, ce que
nous n'avons plus besoin de faire dans nos sous-modules : le CommonModule est suffisant.
· Nous déclarons quatre éléments dans le tableau declarations. Ce sont des éléments qui
n'appartiennent plus au module racine, mais au module spécifique pokemons. Nous
déclarons les composants, les directives et les pipes propre à ce module.
· Le tableau providers est vide. C'est dans ce tableau que l'on peut déclarer des Services
propres à ce module. Nous verrons dans le chapitre suivant ce que sont les Services.

Les routes du module


Il faut maintenant que notre module pokemon dispose de ses propres routes,
indépendamment des routes définies au niveau global de l'application dans app-
routing.module.ts. En effet, les modules sont destinés à regrouper des fonctionnalités
indépendantes les unes des autres le plus possible, et donc chaque module doit posséder les
routes qui lui sont propres, en interne. Créons donc un fichier pokemons-routing.module.ts dans
le dossier pokemons :
01 import { NgModule } from '@angular/core';
02 import { RouterModule, Routes } from '@angular/router';
03
04 import { ListPokemonComponent } from './list-pokemon.component';
05 import { DetailPokemonComponent } from './detail-pokemon.component';
06
07 // routes definition
08 const pokemonsRoutes: Routes = [
09 { path: 'pokemons', component: ListPokemonComponent },
10 { path: 'pokemon/:id', component: DetailPokemonComponent }
11 ];
12
13 @NgModule({
14 imports: [
15 RouterModule.forChild(pokemonsRoutes)
16 ],
17 exports: [
18 RouterModule
19 ]
20 })
21 export class PokemonRoutingModule { }

Il y a une différence par rapport au fichier des routes globales de notre application. On
utilisait jusqu'à maintenant la méthode statique forRoot pour enregistrer nos routes auprès du
Router. Mais là nous définissons les routes du sous-module pokemons : on doit donc utiliser la
méthode forChild pour enregistrer les routes additionnelles (ligne 15).
N'utilisez pas la méthode forRoot dans les fichiers de définitions des routes de vos sous-
modules, sous peine de lever une erreur !
Le router se chargera de combiner et d'assembler TOUTES les routes de notre application !
Cela nous permet de définir les routes de nos sous-modules sans avoir à modifier les routes
principales de notre configuration. Il ne nous reste donc plus qu'à importer ces routes dans notre
module pokemons.module.ts :
01 // ...
02 import { PokemonRoutingModule } from './pokemons-routing.module';
03
04 @NgModule({
05 imports: [
06 CommonModule,
07 PokemonRoutingModule
08 ],
09 // ...
10 })

Voilà, notre module pokemons est théoriquement prêt !

Comment ça 'théoriquement' ? Notre application ne fonctionne pas ?

Non, pas vraiment ! Rappelez-vous que notre application est composée maintenant de deux
modules : app.module.ts et pokemons.module.ts. Nous venons de développer un module dédié à
la gestion de nos Pokémons, mais il nous reste maintenant à mettre à jour le module racine de
notre application.

Mettre à jour le reste de l'application


Nous devons à présent mettre à jour le module racine de notre application. Voici à quoi
ressemble actuellement notre module racine app.module.ts :
01 import { NgModule } from '@angular/core';
02 import { BrowserModule } from '@angular/platform-browser';
03 import { AppComponent } from './app.component';
04 import { AppRoutingModule } from './app-routing.module';
05
06 // Les quatres importations suivantes sont inutiles maintenant !
07 import { BorderCardDirective } from './shadow-card.directive';
08 import { PokemonTypeColorPipe } from './pokemon-type-color.pipe';
09 import { ListPokemonComponent } from './list-pokemon.component';
10 import { DetailPokemonComponent } from './detail-pokemon.component';
11 import { PageNotFoundComponent } from './pagenotfound.component';
12
13 @NgModule({
14 imports: [
15 BrowserModule,
16 AppRoutingModule
17 ],
18 declarations: [
19 AppComponent,
20 BorderCardDirective, // on a plus besoin de ça ici...
21 PokemonTypeColorPipe, // ... de ça non plus ...
22 ListPokemonComponent, // ... ni de ça ...
23 DetailPokemonComponent, // ... et pas de ça non plus !
24 PageNotFoundComponent
25 ],
26 bootstrap: [ AppComponent ]
27 })
28 export class AppModule { }
Il y a plusieurs éléments dont nous n'avons plus besoin au niveau de ce module, car nous
les avons transférés au niveau du module pokemon.
En revanche, nous avons besoin de laisser certains éléments au niveau global de notre
application. Par exemple le composant PageNotFoundComponent, qui est utile dans toute
l'application.
Je vous invite donc à modifier notre module racine de la manière suivante :
· Supprimer les lignes commentées ci-dessus qui ne sont plus utiles.
· Importer le module PokemonsModule dans le module racine.
Vous devriez obtenir le code suivant pour app.module.ts :
01 import { NgModule } from '@angular/core';
02 import { BrowserModule } from '@angular/platform-browser';
03 import { AppRoutingModule } from './app-routing.module';
04 import { PokemonsModule } from './pokemons/pokemons.module';
05
06 import { AppComponent } from './app.component';
07 import { PageNotFoundComponent } from './page-not-found.component';
08
09 @NgModule({
10 // L'odre de chargement des modules est très important
11 // par rapport à l'ordre de déclaration des routes !
12 imports: [
13 BrowserModule,
14 PokemonsModule,
15 AppRoutingModule // pour l'ordre de déclaration des routes !
16 ],
17 declarations: [
18 AppComponent,
19 PageNotFoundComponent
20 ],
21 bootstrap: [AppComponent]
22 })
23 export class AppModule { }

Remarquez l'ordre de déclaration des modules à la ligne 14 et 15. On charge d'abord le sous-
module avant le module racine. En effet, l'ordre d'importation des modules ci-dessus détermine
directement l'ordre de déclaration des routes. Rappelez-vous que notre module racine contient
une route qui intercepte toutes les urls qui ne font pas partie de votre application, afin de
rediriger l'utilisateur vers une page 404 personnalisée. Si vous chargez d'abord le module
racine, votre application se contentera d'afficher cette page d'erreur à l'utilisateur, quelle que
soit la page que ce dernier demande !
Maintenant nous devons mettre à jour le fichier des routes globales app-routing.module.ts,
qui ne contient plus que deux routes : la route de redirection au démarrage de l'application et la
route de gestion des erreurs :
01 import { NgModule } from '@angular/core';
02 import { RouterModule, Routes } from '@angular/router';
03 import { PageNotFoundComponent } from './page-not-found.component';
04
05 const appRoutes: Routes = [
06 { path: '', redirectTo: 'pokemons', pathMatch: 'full' },
07 { path: '**', component: PageNotFoundComponent }
08 ];
09
10 @NgModule({
11 imports: [
12 RouterModule.forRoot(appRoutes)
13 ],
14 exports: [
15 RouterModule
16 ]
17 })
18 export class AppRoutingModule { }
Pour terminer, nous devons mettre à jour le chemin des templates de nos composants, car
nous avons déplacé ces fichiers de place dans le dossier pokemons. Pour
ListPokemonComponent :
01 // …
02 templateUrl : ‘./app/pokemons/list-pokemon.component.html’

Et aussi pour DetailPokemonComponent :


01 // …
02 templateUrl : ‘./app/pokemons/detail-pokemon.component.html’
Voilà, votre application fonctionne à nouveau, faites le test avec npm start !
L'application fait exactement la même chose qu'auparavant, mais notre code est mieux
organisé. La réorganisation de notre code a nécessité un certain nombre d'opérations : nous avons
dû changer le contenu de certains fichiers, et en créer d'autres. Il aurait été plus simple de définir
à l'avance l'architecture de notre application, non ?
C'est ce que nous allons faire maintenant : voir tout de suite comment seras structurer notre
application finale, avant de nous lancer dans la suite des développements !

Structurer l'architecture de notre application


Pour l'instant notre application comprend deux modules : AppModule (le module racine) et
PokemonModule (le sous-module qui gère les Pokémons).
Je vous propose de réfléchir à l'architecture finale de notre application. Notre application
sera un simple gestionnaire de Pokémons. L'utilisateur se connectera à son espace privé, et
pourra ensuite modifier les informations des Pokémons, ou les supprimer.
L'espace privée de l'utilisateur sera le module PokemonModule, dans lequel nous pourront
ajouter les éléments nécessaires à nos développements au fur et à mesure. Nous ajouterons un
composant LoginComponent dans le module racine pour permettre à l'utilisateur de se connecter.

Hé, mais l'architecture de notre application est déjà en place du coup, non ? On a juste
besoin de rajouter les composants nécessaires dans le bon module, au fur et à mesure ?

Bien vu !
Je souhaitais juste que vous en preniez bien conscience ! Notre application est prête à
encaisser les développements futurs sans problème !

Des modules pour factoriser votre code


Angular propose deux modules pour factoriser votre code sur sa documentation officielle,
le SharedModule et le CoreModule. Voyons ensemble chacun de ces deux modules :
Le Shared Module
L’équipe Angular recommande de créer un module nommé SharedModule pour centraliser
les composants, les directives et les pipes qui seront utilisés dans des composants appartenants à
des modules de fonctionnalités. Vous pouvez aussi partager des services statiques, dans le sens
où ils ne dépendent pas de l’état de votre application. Par exemple, un service nommé
TextFilterService à sa place dans le SharedModule, mais pas un service nommé
SharedUserData.
En pratique, à chaque fois que vous vous servez d’un élément mentionné ci-dessus dans au
moins deux composants différents,appartenant à des modules de fonctionnalités, factoriser ces
éléments dans le SharedModule. Par convention, ce module se trouve dans le dossier
app/shared/shared.module.ts.
Il peut être importé dans tous les modules qui nécessitent des éléments partagés du
SharedModule.
Le core Module
Le rôle du CoreModule est différent du SharedModule. Il doit seulement importer les
services et les modules de l’application qui peuvent être factorisé. Son rôle est d’alléger le
module racine.
Ce module ne doit être importé qu’une seule fois dans le AppModule.
Pendant ce temps, dans la vraie vie…
Heu, ce n’est pas un peu lourd d’avoir deux modules de factorisation différents ?

Je suis tout à fait d’accord avec vous, pour notre application, cela ajouterai beaucoup de
lourdeur, pour pas grand-chose ! De plus, la plupart des applications professionnelles n’utilisent
pas ces deux modules différents, souvent un seul module nommé CommonModule suffit.
Rappelez-vous, il s’agit de recommandations de la part de l’équipe d’Angular, pas de
règles. De plus, Angular ne verra aucune différence si vous appelez ce module TotoModule ou
MachinModule. Par contre, les autres développeurs qui travaillent avec vous risquent de ne pas
être contents ! J
En tous cas dans la suite de ce cours, je n’utiliserai pas ces modules, car notre application
de démonstration est trop petite pour justifier leur utilisation. La documentation officielle est très
claire à ce sujet, si votre application est de petite taille, le module racine suffit pour factoriser les
éléments globaux de votre application.
Cependant, si vous voulez vous entrainer à la fin de ce cours, en rajoutant de nouvelles
fonctionnalités, vous pourrez alors factoriser certains éléments, pensez-y !

Conclusion
Nous sommes maintenant capables de créer des applications bien organisées en regroupant
nos fonctionnalités en module. Cela nous permet de créer des applications plus importantes
qu'auparavant, sans nous emmêler les pinceaux dans notre code. Il y a cependant une chose que
nous n'avons pas encore vue : avec tous ces composants différents que nous allons devoir créer,
comment allons-nous factoriser le code présent dans plusieurs composants différents ? C'est ce
que nous allons voir dans le chapitre suivant sur les Services !

En résumé
· Il existe deux types de modules : le module racine et les modules de fonctionnalité,
appelés également sous-modules.
· On déclare un module avec l'annotation @NgModule, quel que soit le type du module.
· On peut créer des applications complexes en ajoutant des modules de fonctionnalité au
module racine.
· Chaque module regroupe tous les composants, directives, pipes, services, ... liés au
développement d'une fonctionnalité donnée, dans un dossier à part.
· Chaque module peut disposer de ses propres routes également.
· On définit les routes de nos sous-modules avec forChild, et forRoot pour le module
racine.
· Modifier l'architecture d'une application existante peut être pénible, il est préférable de
prévoir l'architecture des modules de votre application à l'avance.
Chapitre 12 : Les services et l'injection de
dépendances
Maintenant que nous avons une architecture solide pour notre application, je vous propose
de commencer à l'enrichir avec des Services. Plusieurs de nos composants vont avoir besoin
d'accéder et d'effectuer des opérations sur les Pokémons de l'application : nous allons centraliser
ces opérations, ainsi que les données concernant les Pokémons.
On va donc créer un service, qui sera utilisable pour tous les composants du module
pokemons, afin de leur fournir un accès et des méthodes prêtes à l'emploi pour gérer les
Pokémons !

Créer un service simple


Nous allons créer un service qui s'occupe de fournir des données et des méthodes pour
gérer nos Pokémons. L'objectif est de masquer au reste de l'application la façon dont nous
récupérons ces données, et le fonctionnement interne des méthodes.
Créons donc un fichier pokemons.service.ts dans le dossier de notre module
/app/pokemons.
01 import { Injectable } from '@angular/core';
02
03 @Injectable()
03 export class PokemonsService { }

Rassurez-vous, ce service ne fait rien pour l'instant ! Cependant, remarquez le décorateur


@Injectable appliqué à notre service. Il permet d'indiquer à Angular que ce service peut lui-
même avoir d'autres dépendances. Pour l'instant, notre service n'a pas de dépendances, mais il
fortement recommandé d'ajouter le décorateur @Injectable systématiquement (en fait, ne vous
posez même pas la question).
Les parenthèses sont très importantes, il ne faut les oublier sous peine de lever une erreur !

Heu, pourquoi nous n'avons pas eu besoin d'ajouter @Injectable sur nos composants ?
Ils ont bien des dépendances eux aussi, non?

C'est une excellente question. En fait la réponse se trouve dans l'annotation @Component
elle-même, qui s'occupe déjà de ça pour nous ! (de même pour @Pipe et @Directive).
Je vous propose de créer plusieurs méthodes au sein de ce service :
· Une méthode getPokemons, qui renvoie la liste de tous les Pokémons de notre
application.
· Une autre méthode getPokemon, qui renvoie un Pokémon en fonction de son identifiant.
· Enfin, une dernière méthode getPokemonTypes, qui renvoie la liste de tous les types
possibles pour un pokémon. (Nous nous servirons de cette méthode dans le chapitre sur les
Formulaires, pour permettre à l’utilisateur de sélectionner les types qu’il souhaite attribuer
à un pokémon.)
01 import { Injectable } from '@angular/core';
02 import { Pokemon } from './pokemon';
03 import { POKEMONS } from './mock-pokemons';
04
05 @Injectable()
06 export class PokemonsService {
07
08 // Retourne tous les pokémons
09 getPokemons(): Pokemon[] {
10 return POKEMONS;
11 }
12
13 // Retourne le pokémon avec l'identifiant passé en paramètre
14 getPokemon(id: number) {
15 let pokemons = this.getPokemons();
16 for(let index = 0; index < pokemons.length; index++) {
17 if(id === pokemons[index].id) {
18 return pokemons[index];
19 }
20 }
21 }
22
23 getPokemonTypes(): Array<string> {
24 return [
25 'Plante', 'Feu', 'Eau', 'Insecte', 'Normal', 'Electrik',
26 'Poison', 'Fée', 'Vol', 'Combat', 'Psy'
27 ];
28 }
29
30 }

Le code des trois nouvelles méthodes est assez explicite, je souligne juste le fait que j'ai dû
importer le modèle d'un Pokémon et les données de base, à la ligne 2 et 3.
Pour l'instant, nos composants n'utilisent pas encore ce nouveau service. Regardons tout de
suite comment injecter ce service dans les composants qui en ont besoin !

Utiliser un service
Je vous propose de voir comment utiliser un service en essayant d'injecter notre
PokemonsService dans le composant ListPokemonComponent.
Pour utiliser un service dans un composant, la première chose à faire est de l'importer :
01 import { PokemonsService } from ‘./pokemons.service’;

Jusque-là, rien d'anormal. Maintenant, comment récupérer une instance de notre service
dans nos composants ? En utilisant le constructeur ?
01 let pokemonsService = new PokemonsService();
Ne faite J-A-M-A-I-S ça !
Mais pourquoi ? Qu'est-ce qu'il y a de si terrible, c'est bien comme ça qu'on instancie une
classe, non ?
Alors, il y a trois problèmes majeurs :
· D'abord, notre composant devra savoir comment créer le pokemonsService. Si nous
modifions le constructeur de notre service par la suite, nous devrons retrouver chaque
composant où nous avons utilisé le service : ce n'est vraiment pas terrible.
· Ensuite, on crée une nouvelle instance du service à chaque fois que l'on utilise le mot-clé
new. Que se passe-t-il si le service doit mettre en cache des pokémons, et les partager avec
tous les composants ? On ne pourrait pas le faire avec ce système, car nous aurions besoin
de la même instance de notre service à travers toute l’application.
· Enfin, lorsque vous développez un service, le consommateur de votre service ne doit pas
se demander comment fonctionne le service de l'intérieur. En quelque sorte, il doit s'agir
d'une boîte noire, prête à l’emploi.

Mais comment on fait alors, pour utiliser un service dans un composant ?

Rassurez-vous, Angular nous propose d'injecter les services plutôt que de se charger de les
instancier nous-mêmes !

Injecter un service
L'injection d'un service dans un composant est très simple car Angular nous propose de le
faire en deux lignes seulement :
· Ajouter un constructeur qui définit une propriété privée.
· Ajouter un fournisseur (le terme anglais est Provider) pour le service grâce aux
annotations.
Reprenons le fichier list-pokemon.component.ts et modifier le constructeur pour injecter
notre service :
01 constructor(
02 private router: Router,
03 private pokemonsService: PokemonsService) {}

Pensez à importer ce service, pour pouvoir l'utiliser : import { PokemonsService } from


'./pokemons.service';
Avec cette simple ligne, nous avons fait beaucoup de choses. Nous avons injecté une
instance de PokemonsService dans notre composant. Cette instance est disponible sous forme de
propriété privée dans notre composant :
01 let pokemonsService: PokemonsService = this.pokemonsService;
De plus, étant donné que nous avons utilisé l'injection de dépendance, Angular nous
garantit que cette instance est unique à travers toute notre application. Ainsi, si j'injecte ce
service dans un autre composant, il s'agira bien de la même instance de ce service. Le fait que
l'instance du service soit unique nous permettra d'utiliser les Services comme stockage provisoire
des données.

Le constructeur lui-même ne fait rien comme vous pouvez le constatez. Il s'occupe simplement
de définir les bons paramètres afin d'injecter le service PokemonsService dans notre composant.
Souvent, vous aurez besoin d'injecter un service dans un service. Le fonctionnement est le
même que pour les composants, il faut utiliser également le constructeur pour injecter le service
: on parle du constructor injection pattern.

Angular sait désormais fournir l'instance de pokemonsService lorsque l'on crée un nouveau
ListPokemonComponent. Cependant, nous aurons une erreur si nous lançons notre application :
EXCEPTION : No provider for PokemonsService ! (ListPokemonComponent ->
PokemonsService)
Le message est clair : nous devons enregistrer un fournisseur (un Provider en anglais) pour
notre service. Pour cela nous devons ajouter un élément supplémentaire dans les annotations du
composant :
01 @Component({
02 selector: 'list-pokemon',
03 templateUrl: ‘./app/pokemons/list-pokemon.component.html’,
04 providers: [PokemonsService]
05 })
06 export class ListPokemonComponent { // ... }
Le provider fournis à la ligne 4 permet à Angular de créer une nouvelle instance de
pokemonsService quand il instancie ListPokemonComponent. Ce ListPokemonComponent peut
maintenant utiliser ce service pour récupérer des Pokémons, et chacun de ses composants fils
également.

Consommer le service
Nous n'avons plus qu'à utiliser ce service pour injecter des Pokémons dans notre
composant ! Modifions la méthode d'initialisation de list-pokemons.component.ts :
01 ngOnInit(): void {
02 //this.pokemons = POKEMONS;
03 this.pokemons = this.pokemonsService.getPokemons();
04 }

On pourrait même découper notre code d'une meilleure façon, de manière à ne pas avoir de
'logique métier' au sein de ngOnInit, mais dans une méthode dédiée :
01 ngOnInit(): void {
02 this.getPokemons();
03 }
04
05 getPokemons(): void {
06 this.pokemons = this.pokemonsService.getPokemons();
07 }

De cette manière, nous avons réduit le contenu de ngOnInit à l'essentiel. Le code parle de
lui-même : lorsque le composant est initialisé, on récupère tous les Pokémons.
Et notre composant detail-pokemon.component.ts ? Nous allons utiliser le même service,
bien sûr. Voici ce que nous devons faire :
· Importer la classe du service dans DetailPokemonComponent.
· Définir le fournisseur de notre service.
· Ajouter le service dans le composant grâce au constructeur.
· Ensuite, nous n'avons plus qu'à adapter le code de notre composant avec la méthode du
service.
Voici le code que j'obtiens de mon côté, j'ai commenté les lignes qui ont été modifiées :
01 import { Component, OnInit } from '@angular/core';
02 import { ActivatedRoute, Params } from '@angular/router';
03 import { Pokemon } from './pokemon';
04 // on peut supprimer cette ligne, nous n'en avons plus besoin :
05 import { POKEMONS } from './mock-pokemons';
06 // on importe le service PokemonsService.
07 import { PokemonsService } from './pokemons.service';
08
09 @Component({
10 selector: 'detail-pokemon',
11 template: `...`,
12 // on déclare un fournisseur pour le service.
13 providers: [PokemonsService]
14 })
15 export class DetailPokemonComponent implements OnInit {
16 pokemon: Pokemon = null;
17
18 // on injecte ce service pour pouvoir l'utiliser dans le composant.
19 constructor(
20 private route: ActivatedRoute,
21 private router: Router,
22 private pokemonsService: PokemonsService) {}
23
24 ngOnInit(): void {
25 let id = +this.route.snapshot.paramMap.get('id');
26 this.pokemon = this.pokemonsService.getPokemon(id);
27 }
28
29 goBack(): void {
30 window.history.back();
31 }
32 }

Vous remarquerez que la propriété pokemons a été retirée du composant, nous n'en avons
plus besoin car nous utilisons directement la méthode getPokemon du service !
Nous avons deux composants différents, qui récupèrent les données depuis le même
service, ainsi notre accès aux données est bien centralisé. Qu'est-ce qu'il nous faudrait de plus ?
Et bien justement, vous avez remarquez que nous avons défini un fournisseur pour notre
service à deux endroits différents : un dans ListPokemonComponent et un autre dans
DetailPokemonComponent ? Ne vaudrait-il pas fournir notre service directement au niveau du
module PokemonsModule, ainsi notre service serait disponible pour tous les composants
directement ?
Et bien je suis parfaitement d'accord avec vous ! Mais il faut pour cela que nous fassions
un peu de théorie concernant l'injection des dépendances avec Angular, c'est indispensable pour
comprendre ce que nous allons faire. Ce ne sera pas trop long, promis !

L'injection de dépendances
Nous allons voir comment fonctionne l'injection de dépendance, c'est-à-dire comment
injecter nos services dans nos composants, et comment délimiter un espace dans notre
application depuis lequel le service est disponible.
Angular dispose de son propre Framework d'injection, et on ne peut pas vraiment
développer une application sans cet outil.

On parle souvent de DI (Dependency Injection) pour désigner l'injection de dépendances, et je


vais souvent utiliser cet acronyme, ne soyez pas surpris.

Comme nous l'avons vu, il est nécessaire d'utiliser la DI plutôt que des constructeurs pour
gérer nos dépendances.

Qu'est-ce que la DI exactement ?

L'injection de dépendances est un modèle de développement (ou design pattern pour nos
amis anglo-saxons), dans lequel chaque classe reçoit ses dépendances d'une source externe plutôt
qu'en les créant elle-même.
Imaginez que le Framework d'injection possède quelque chose appelé un injecteur. Dans ce
cas on utilise l'injecteur pour gérer les dépendances de nos classes, sans s'occuper nous-même de
les gérer.
Angular crée un injecteur à l'échelle de l'application durant le processus de démarrage de
l'application :
01 platformBrowserDynamic().bootstrapModule(AppModule) ;
Nous n'avons pas à nous en occuper. En revanche, nous devons enregistrer des fournisseurs
pour rendre disponible le service là où nous en avons besoin : au niveau d'un module, de toute
l'application ou d'un composant.

Fournir un service à toute l'application


Parfois, nous avons besoin de rendre un service disponible au niveau de l'ensemble de
l'application. Pour cela, placez le fichier de votre service directement au niveau du dossier /app,
et ensuite vous devez fournir votre service au niveau du module racine de votre application :
01 @NgModule({
02 imports: [ ... ],
03 declarations: [ ... ],
04 providers: [AwesomeService] // <-- ici !
05 })
06 export class AppModule { }
Immédiatement, le service AwesomeService est disponible dans toute l'application !

Fournir un service au niveau d'un module


Dans le cas où votre service ne doit être disponible que dans un module, il est inutile de le
fournir au niveau de toute l'application. Par exemple, notre service PokemonService ne doit être
disponible qu'au niveau du PokemonsModule, et non ailleurs dans l'application. Nous allons
modifier notre code afin de faire cela. Supprimez-donc les annotations Providers dans
ListPokemonComponent et DetailPokemonComponent.
Conservez le fichier pokemons.service.ts dans le dossier app/pokemons, puisque c'est là
qu'il doit logiquement se trouver. Modifiez simplement le pokemons.module.ts pour que notre
service soit disponible au niveau de ce module :
01 // ...
02 import { PokemonsService } from './pokemons.service';
03
04 @NgModule({
05 imports: [ ... ],
06 declarations: [ ... ],
07 providers: [PokemonsService] // <-- ici !
08 })
09 export class PokemonsModule {}

C'est tout ! Maintenant, nous ne sommes plus obligés de fournir le service pour chaque
composant du module qui en aurait besoin, il sera disponible directement. Il ne restera plus qu'à
l'injecter via le constructeur du composant comme nous l'avons déjà vu !
Essayer toujours d'être le plus précis possible dans votre stratégie de providers. Ne fournissez
jamais tous vos services au niveau de l'application si cela n'est pas nécessaire.

Fournir un service au niveau d'un composant


Si vous avez besoin de fournir un service à un seul endroit dans un composant, vous
pouvez simplement utiliser l'annotation @Component comme nous l'avons déjà vu :
01 @Component({
02 selector: 'some-component',
03 template: '...',
04 providers: [AloneService]
05 })
06 export class SomeComponent { }

Bien, vous savez maintenant comment créer un service et l'injecter dans vos composants !

Conclusion
Nous avons pu voir comment centraliser le code commun à plusieurs composants dans des
services, et les rendre disponibles dans telle ou telle partie de notre application, grâce aux
fournisseurs. Sachez cependant que j'ai été volontairement simpliste et que le système d'injection
de dépendance dans Angular est plus complexe que ce que l'on a vu dans ce chapitre (système
hiérarchique des injections, fournir d'autres éléments que des classes : strings, objets, fonctions
...), mais nous avons vu l'essentiel pour construire notre application.

En résumé
· Il faut ajouter l'annotation @Injectable sur tous nos services.
· Un service permet de factoriser et de centraliser du code qui peut être utile ailleurs dans
l'application.
· On utilise l'injection de dépendances pour rendre un service disponible dans un
composant.
· On ne gère jamais nous-mêmes les dépendances sur un composant ou un service, on
passe toujours par l'injection de dépendances.
· L'injection de dépendance permet de garantir que l'instance de notre service est unique à
travers toute l'application.
· On définit un fournisseur de service pour déterminer dans quelles zones de notre
application notre service sera disponible.
· On peut fournir un service pour toute l'application, pour un module particulier ou pour un
composant.
Questionnaire n°2
L'objectif de ce questionnaire est de valider les acquis théoriques et pratiques de la
deuxième partie du cours : composants, directives, templates, modules, etc.
Le questionnaire est noté sur 10 points.
Le questionnaire est accessible en ligne à cette adresse. (https://goo.gl/XRRYzY)
Bon courage !
Partie 3

Aller plus loin avec Angular


Chapitre 13 : Formulaires pilotés par le
template
Les formulaires sont omniprésents dans les applications : il y a un formulaire pour la
connexion, un formulaire de contact, un formulaire pour modifier telle ou telle donnée, pour
mettre à jour son profil, etc.
Cependant, la gestion des formulaires a toujours été complexe. Entre le traitement des
données utilisateurs, la validation, l'affichage de messages d'erreurs ou de succès, etc. il faut
souvent beaucoup de travail aux développeurs pour offrir une expérience complète et agréable
aux utilisateurs. Heureusement, Angular peut nous aider à créer des formulaires, et il va nous
faciliter le travail !

Introduction
Lorsque vous souhaitez créer un formulaire avec Angular, vous avez la possibilité d'utiliser
deux modules différents : FormsModules et ReactiveFormsModule.
Ces deux modules répondent au même besoin, mais avec une approche différente. Le
premier, FormsModules, développe une partie importante du formulaire dans le template (on
parle de Template-driven form), alors que ReactiveFormsModule est plus centré sur le
développement du formulaire côté composant. Il n'y a pas une méthode mieux qu'une autre dans
l'absolu. Cependant, le module FormsModule est plus adapté :
· Pour les petits formulaires
· Pour se faire la main en tant que débutant.
Je crois que vous avez compris la suite, je vous présenterai dans ce chapitre la première
façon de faire, celle avec FormsModule !

Ces deux modules proviennent de la même librairie @angular/forms.

Avant d'aborder le vif du sujet, il y a plusieurs éléments que je dois vous présenter :
· Le module FormsModule.
· La directive NgForm.
· La directive NgModel.

Le module FormsModule
Ce module va nous aider à développer des formulaires : il met notamment à notre
disposition les directives NgForm et NgModel que nous allons utiliser. Nous devrons le déclarer
dans le module pokemons de notre application.

La directive NgForm
A partir du moment où FormsModule a été importé dans un module, la directive NgForm
devient active sur toutes les balises <form>
de ce module. Nous n'avons pas besoin d'ajouter des sélecteurs supplémentaires dans les
templates !
Pour chaque formulaire où elle est appliquée, la directive NgForm crée une instance de
FormGroup au niveau global du formulaire, nous reviendrons plus tard sur le rôle de cet objet.
En tout cas, sachez qu'une référence à cette directive nous permet de savoir si le formulaire que
remplit l'utilisateur est valide ou non, au fur et à mesure que l'utilisateur complète ce formulaire.
De plus, on peut être notifié lorsque l'utilisateur déclenchera la soumission de formulaire !

La directive NgModel
Cette directive doit s'appliquer sur chacun des champs du formulaire, et ce pour plusieurs
raisons :
· Cette directive créera une instance de FormControl pour chaque champ de votre
formulaire, comme un input ou un select par exemple.
· Chaque FormControl est une brique élémentaire du formulaire, qui encapsule l'état donné
d'un champ. Il a pour rôle de traquer la valeur du champ, les interactions avec l'utilisateur,
la validité des données saisies et de garder la vue synchronisée avec les données.
· Chaque FormControl doit être défini avec un nom. Pour cela, il suffit d'ajouter l'attribut
name à la balise HTML associée.
· Lorsque cette directive est utilisée dans au sein d'une balise <form>, la directive s'occupe
pour nous de l'enregistrer auprès du formulaire comme un élément fils de ce formulaire. En
combinant cette directive avec NgForm, on peut donc savoir en temps réel si le formulaire
est valide ou non !
· On peut aussi utiliser la directive NgModelGroup pour créer des sous-groupes de champs,
à l'intérieur d'un formulaire.
En plus, la directive NgModel s'occupe de mettre en place une liaison de données
bidirectionnelle pour chacun des champs du formulaire ! Nous aurons souvent besoin de cette
liaison bidirectionnelle pour les formulaires : gérer les interactions de l'utilisateur côté template,
et traiter les données saisies côté composant.
Et ce n'est pas tout !
La directive s'occupe également d'ajouter et de retirer des classes spécifiques sur chaque
champ. Nous pouvons ainsi savoir si l'utilisateur a cliqué ou non sur un champ, si la valeur du
champ a changée, ou s'il est devenu invalide. En fonction de ces informations, nous pourrons
changer l'apparence d'un champ, et faire apparaitre un message d'erreur, ou de confirmation.

Et quelles sont les classes ajoutées par la directive exactement ?

Bonne question.
Le tableau ci-dessous montre quelles classes sont ajoutées, et à quelles conditions :

Etat du champ 1 0

La valeur du champ a été visitée ng-touched ng-untouched


au moins une fois, c'est-à-dire
que l'utilisateur a cliqué ou
effectué une tabulation sur ce
champ.

La valeur du champ a changée ng-dirty ng-pristine


par rapport à sa valeur initiale.

La valeur est considérée comme ng-valid ng-invalid


correcte par rapport aux règles
de validation de ce champ.

Créer un formulaire
Avant de nous lancer "tête baissée" dans la création d'un formulaire, essayons de spécifier
un peu nos besoins. De quoi avons-nous besoin exactement ? Notre futur formulaire doit
permettre d'éditer certaines propriétés d'un Pokémon. En effet, nous n'allons pas modifier
l'identifiant ou la date de création d'un Pokémon, puisque par définition ces données ne sont pas
appelées à être modifiées. Il nous reste donc les propriétés suivantes :
· Les points de vie du Pokémon.
· Les dégâts du Pokémon.
· Le nom du Pokémon.
· Les types du Pokémon.
Ainsi, notre formulaire comptera quatre champs.
Il sera un composant à part entière, chargé de gérer les données saisies par l'utilisateur, et
qui permettra d'éditer un Pokémon. Cependant comme le code du template sera assez complexe,
nous allons découper le template et le composant du formulaire dans deux fichiers séparés :
· La classe du composant : pokemon-form.component.ts.
· Le template du composant : pokemon-form.component.html.
Placez ces deux fichiers dans le dossier du module dédié à la gestion des pokémons :
/app/pokemons/.

Par convention, il est recommandé d'avoir un fichier dédié pour chaque tâche dans votre
application : un fichier par module, un fichier par template, un fichier par formulaire, etc…

Le composant du formulaire
Voici donc le code de la classe de notre formulaire, que je vous donne ci-dessous et que je
détaillerai ensuite :
01 import { Component, Input, OnInit } from '@angular/core';
02 import { Router } from '@angular/router';
03 import { PokemonsService} from './pokemons.service';
04 import { Pokemon } from './pokemon';
05
06 @Component({
07 selector: 'pokemon-form',
08 templateUrl: './app/pokemons/pokemon-form.component.html'
09 })
10 export class PokemonFormComponent implements OnInit {
11 // propriété d'entrée du composant
12 @Input() pokemon: Pokemon;
13 // types disponibles pour un pokémon : 'Eau', 'Feu', etc.
14 types: Array<string>;
15
16 constructor(
17 private pokemonsService: PokemonsService,
18 private router: Router) { }
19
20 ngOnInit() {
21 // Initialisation de la propriété types
22 this.types = this.pokemonsService.getPokemonTypes();
23 }
24
25 // Détermine si le type passé en paramètres appartient ou non
26 // au pokémon en cours d'édition.
27 hasType(type: string): boolean {
28 let index = this.pokemon.types.indexOf(type);
29 if (~index) return true;
30 return false;
31 }
32
33 // Méthode appelée lorsque l'utilisateur ajoute ou retire
34 // un type au pokémon en cours d'édition.
35 selectType($event: any, type: string): void {
36 let checked = $event.target.checked;
37 if ( checked ) {
38 this.pokemon.types.push(type);
39 } else {
40 let index = this.pokemon.types.indexOf(type);
41 if (~index) {
42 this.pokemon.types.splice(index, 1);
43 }
44 }
45 }
46
47 // La méthode appelée lorsque le formulaire est soumis.
48 onSubmit(): void {
49 console.log("Submit form !");
50 let link = ['/pokemon', this.pokemon.id];
51 this.router.navigate(link);
52 }
53
54 }
L'opérateur ~ est un raccourci de JavaScript qui permet de vérifier qu'une variable est
supérieure à -1.
Alors, je vous dois quelques explications. Prenons les choses les unes après les autres.
D'abord, les importations :
01 import { Component, Input, OnInit } from '@angular/core';
02 import { Router } from '@angular/router';
03 import { PokemonsService} from './pokemons.service';
04 import { Pokemon } from './pokemon';

A la ligne 1, on importe les éléments Component, Input et OnInit depuis la librairie


@angular/core. Les éléments Component et OnInit, nous les connaissons déjà. En revanche,
l'élément Input est nouveau. Il permet d'écrire une propriété d'entrée pour un composant. C'est-à-
dire que si nous définissons ce composant comme fils d'un autre composant, il faudra
nécessairement renseigner les valeurs des propriétés d'entrée de ce composant. Regardez, à la
ligne 12, nous utilisons l’annotation @Input() pour indiquer à Angular que notre composant a
une propriété d'entrée nommée pokemon. Cela signifie que notre composant ne peut pas
fonctionner si nous ne lui avons pas passé un Pokémon comme valeur d'entrée. En effet, c'est
indispensable pour un formulaire chargé de créer et éditer des Pokémons !

Les composants avec une propriété d'entrée sont souvent les fils d'autres composants, puisqu'ils
dépendent d'eux pour récupérer cette valeur d'entrée. Par exemple, notre formulaire sera le fils
du composant EditPokemonComponent que nous créerons plus tard, qui se chargera de lui
transmettre le Pokémon à modifier.

A la ligne 2, on importe Router, qui nous permettra de rediriger l'utilisateur après avoir
soumis le formulaire. Ensuite on retrouve notre service PokemonsService et notre modèle
Pokemon, que nous connaissons déjà.
Nous définissons ensuite deux propriétés pour ce composant :
01 @Input() pokemon: Pokemon;
02 types: Array<string>;
La première propriété est la propriété d'entrée du composant, et la deuxième est le tableau
types, qui doit contenir tous les types de Pokémons disponibles, afin de les afficher dans le
formulaire.
Ensuite, nous implémentons quatre méthodes dans ce composant :
· La méthode ngOnInit : On utilise cette méthode pour initialiser la propriété types avec
les types de Pokémons récupérées depuis le service pokemons.service.ts. Cela nous
permettra de construire un champ spécifique où l'utilisateur pourra sélectionner les types
du Pokémon qu'il souhaite ajouter ou retirer.
· La méthode hasType : Cette méthode permet de savoir si le pokémon en cours d'édition
possède ou non le type passé en paramètre. Au chargement du formulaire, on peut ainsi
pré-cocher les checkbox correspondant aux types que possède déjà le Pokémon courant.
· La méthode selectType : A chaque fois que l'utilisateur sélectionne ou désélectionne un
type, le modèle du Pokémon est mis à jour en ajoutant ou en supprimant le type
correspondant.
· La méthode onSubmit : C'est la méthode qui est appelée lorsque le formulaire est
soumis, une fois que tous les champs sont considérés comme valides. Concrètement, cette
méthode effectue simplement une redirection vers la page de détail du Pokémon. Les
changements ont déjà été pris en compte par la directive NgModel !
Le template du formulaire
Maintenant, passons au code du template :
01 <form *ngIf="pokemon" (ngSubmit)="onSubmit()" #pokemonForm="ngForm">
02 <div class="row">
03 <div class="col s8 offset-s2">
04 <div class="card-panel">
05
06 <!-- Pokemon name -->
07 <div class="form-group">
08 <label for="name">Nom</label>
09 <input type="text" class="form-control" id="name"
10 [(ngModel)]="pokemon.name" name="name"
11 #name="ngModel">
12 </div>
13
14 <!-- Pokemon hp -->
15 <div class="form-group">
16 <label for="hp">Point de vie</label>
17 <input type="number" class="form-control" id="hp"
18 [(ngModel)]="pokemon.hp" name="hp"
19 #hp="ngModel">
20 </div>
21
22 <!-- Pokemon cp -->
23 <div class="form-group">
24 <label for="cp">Dégâts</label>
25 <input type="number" class="form-control" id="cp"
26 [(ngModel)]="pokemon.cp" name="cp"
27 #cp="ngModel">
28 </div>
29
30 <!-- Pokemon types -->
31 <form class="form-group">
32 <label for="types">Types</label>
33 <p *ngFor="let type of types">
34 <label>
35 <input type="checkbox"
36 class="filled-in"
37 id="{{ type }}"
38 [value]="type"
39 [checked]="hasType(type)"
40 (change)="selectType($event, type)">
41
42 <span [attr.for]="type">
43 <div class="{{ type | pokemonTypeColor }}">{{ type }}</div>
44 </span>
45 </label>
46 </p>
47 </form>
48
49 <!-- Submit button -->
50 <div class="divider"></div>
51 <div class="section center">
52 <button type="submit"
53 class="waves-effect waves-light btn"
54 [disabled]="!pokemonForm.form.valid">Valider</button>
55 </div>
56
57 </div>
58 </div>
59 </div>
60 </form>
61 <h3 *ngIf="!pokemon" class="center">Aucun pokémon à éditer...</h3>

Ne vous laissez pas impressionner par la quantité importante de code !


Il s'agit majoritairement de code HTML, agrémenté de certaines classes de la librairie
Materialize, afin de donner un aspect plus sympathique au formulaire. Prenons les éléments les
uns après les autres. D'abord à la première ligne :
01 <form *ngIf=”pokemon” (ngSubmit)=”onSubmit()” #pokemonForm=”ngForm”>
Vous vous demandez peut-être ce que ngSubmit apporte ? Eh bien, cela permet de lier
l'événement de validation du formulaire à la méthode onSubmit du composant, chargé de gérer la
soumission du formulaire.
Ensuite, nous utilisons la directive NgForm, en l'utilisant pour déclarer une variable de
template nommée pokemonForm. Cela nous permettra d'avoir accès à l'état de validité du
formulaire, ailleurs dans le template. Nous verrons comment faire cela.
Enfin, la directive NgIf nous assure qu'un Pokémon a bien été transmis au composant du
formulaire. Dans le cas contraire, nous affichons un message d'erreur plutôt que le formulaire !

Les champs du formulaire


Chaque champ du formulaire est représenté par une ou plusieurs lignes de code :
· Le nom : de la ligne 6 à 12.
· Les points de vie : de la ligne 14 à 20.
· Les dégâts : de la ligne 22 à 28.
· Les types : de la ligne 30 à 47.
Les quatre premiers champs sont assez similaires, je vais donc vous présenter seulement un
champ sur les quatre : le champ name.

En revanche, la gestion des types est plus complexe et fera l'objet d'une explication à part.

Voici donc le champ du formulaire permettant d'éditer le nom d'un Pokémon :


01 <!-- Pokemon name -->
02 <div class="form-group">
03 <label for="name">Nom</label>
04 <input type="text" class="form-control" id="name"
05 [(ngModel)]="pokemon.name" name="name"
06 #name="ngModel">
07 </div>
Hormis les balises et les attributs que nous connaissons, il y a deux éléments nouveaux.
L'utilisation de la directive NgModel. Cette directive permet la liaison bidirectionnelle
entre le composant et le template. La syntaxe [()] est donc la combinaison du property-binding
(liaison de propriétés pour pousser une valeur du composant vers le template, avec les crochets)
et du event-binding (liaison d'événement du template vers le composant, avec les parenthèses).
La déclaration de la variable de template name à la ligne 6. En lui associant la directive
NgModel, cette variable nous permet de déclarer le champ sur lequel elle est rattachée comme
étant un FormControl. Le champ est ainsi rattaché au formulaire global, et nous pouvons obtenir
des informations dessus : est-il valide ? A-t-il déjà été modifié ?

Pour se souvenir dans quel ordre mettre les crochets et les parenthèses pour NgModel, imaginer
simplement que c'est une boîte qui contient une banane : [()].

Le champ qui gère les types d'un Pokémon est un peu plus complexe :
01 <!-- Pokemon types -->
02 <form class="form-group">
03 <label for="types">Types</label>
04 <p *ngFor="let type of types">
05 <label>
06 <input type="checkbox"
07 class="filled-in"
08 id="{{ type }}"
09 [value]="type"
10 [checked]="hasType(type)"
11 (change)="selectType($event, type)"/>
12
13 <span [attr.for]="type">
14 <div class="{{ type | pokemonTypeColor }}">{{ type }}</div>
15 </span>
16 </label>
17 </p>
18 </form>

Il y a plusieurs choses à relever dans cet extrait de code :


· Ligne 4 : On utilise la directive NgFor pour créer une liste de cases à cocher, avec un
type de Pokémon associé à chacune.
· Ligne 8 et 13 : On utilise le nom du type (du Pokémon), pour lier la balise <input> et
<label>, grâce à l’interpolation et à la liaison de l’attribut for.
· Ligne 9 : On définit le nom du type comme valeur du champ.
· Ligne 10 : On coche automatiquement la case si le Pokémon possède le type associé à
cette case.
· Ligne 10 : A chaque fois que l'utilisateur coche ou décoche une case, on déclenche un
appel à la méthode selectType, avec le type qui a été ajouté ou retiré, afin de réajuster le
modèle côté composant.
· Ligne 14 : On applique le pipe pokemonTypeColor, pour afficher un petit label de
couleur avec le nom du type, à côté de chaque case à cocher.
Le bouton submit
La dernière chose qui manque à notre formulaire, c'est un bouton pour la validation :
01 <button type="submit"
02 class="waves-effect waves-light btn"
03 [disabled]="!pokemonForm.form.valid">
04 Valider</button>

Hormis les attributs type et class que vous connaissez déjà, nous utilisons la liaison de
propriété disabled pour désactiver le bouton de validation si le formulaire est invalide, ce qui
oblige l'utilisateur à avoir correctement rempli tous les champs auparavant !

Mais comment on détermine si le formulaire est valide ou non ?

Très bonne question, c'est ce que nous allons voir tout suite, en ajoutant des règles de
validation sur nos champs !

Ajouter des règles de validation


Avant de voir comment ajouter des règles de validation, je vous propose de définir quelles
restrictions nous souhaitons implémenter, champ par champ :

Nom du champ Condition(s) de validité Implémentation


proposée

Name Champ requis, chaîne de attribut required et


caractères de 1 à 25 pattern="[AZa-z]{1,25}"
lettres.

HP Champ requis, nombre attribut required et


entre 0 et 999. pattern="[0-9] {1,3}"

CP Champ requis, nombre attribut required et


entre 0 et 99. pattern="[0-9] {1,2}"

Types Champ requis, Validation personnalisé


l'utilisateur doit avec la méthode
sélectionner entre 1 et 3 isTypesValid du
types différents dans une composant.
liste donnée.

Les quatre premières règles de validation peuvent être implémentées facilement grâce aux
nouveaux attributs apportés par HTML 5 : required pour rendre un champ obligatoire, et pattern
qui permet de définir une expression régulière pour valider un champ.
Si vous ne connaissez pas le fonctionnement des expressions régulières, ou que vous souhaitez
en apprendre plus sur l'attribut pattern, je vous recommande de cliquer ici.
Voici comment implémenter ces règles de validation pour le champ name, à la ligne 5 :
01 <!-- Pokemon name -->
02 <div class="form-group">
03 <label for="name">Nom</label>
04 <input type="text" class="form-control" id="name"
05 pattern="^[a-zA-Z0-9áàâäãåçéèêëíìîïñóòôöõúùûüýÿæœ]{1,25}$"
06 required
07 [(ngModel)]="pokemon.name" name="name"
08 #name="ngModel">
09 </div>

Nous avons simplement ajouté l'attribut required et pattern, avec la bonne expression
régulière.
Vous devez faire la même chose pour les trois autres champs : pictures, hp et cp. Il suffit
d'ajouter la bonne expression régulière dans le champ correspondant. Reprenez le tableau ci-
dessus et ajoutez les règles de validation, je vous laisse le faire tout(e) seul(e), ce n’est pas bien
méchant !
Maintenant, regardons comment implémenter un validateur personnalisé pour les types des
pokémons. Nous devons limiter leur nombre entre un et trois d'après les règles de validation que
nous avons étabi. Commençons par développer cette méthode côté composant :
01 // valide le nombre de types pour chaque pokémon (entre 1 et 3)
02 isTypesValid(type: string): boolean {
03 if (this.pokemon.types.length === 1 && this.hasType(type)) {
04 return false;
05 }
06 if (this.pokemon.types.length >= 3 && !this.hasType(type)) {
07 return false;
08 }
09 return true;
10 }

Cette méthode s'occupe de renvoyer un booléen, pour savoir si une case à cocher doit être
verrouillée ou non :
· Si l’utilisateur a sélectionné une seule case, il faut l’empécher de pouvoir déselectionner
cette case. Sinon il pourrait choisir de ne renseigner aucun type pour un pokémon, et ce
n’est pas ce que nous voulons.
· Si l'utilisateur a déjà sélectionné trois cases, alors il faut l'empêcher de pouvoir
sélectionner d'autres cases, mais il doit pouvoir déselectionner les types déjà présents s’il
souhaite modifier son pokémon !

On fait aussi appel à la méthode hasType pour vérifier que nous ne verrouillons pas des cases
que l'utilisateur a déjà coché, pour lui permettre de les désélectionner.

Ensuite pour verrouiller les cases à cocher de la liste, il faut lier le résultat de la méthode
isTypesValid à la propriété disabled, grâce à la liaison de propriété (ligne 10 ci-dessous) :
01 <!-- Pokemon types -->
02 <div class="form-group">
03 <label for="types">Types</label>
04 <div *ngFor="let type of types" class="row">
05 <input type="checkbox"
06 class="filled-in"
07 id="{{ type }}"
08 [value]="type"
09 [checked]="hasType(type)"
10 [disabled]="!isTypesValid(type)"
11 (change)="selectType($event, type)"
12 />
13 <label [attr.for]="type">
14 <span class="{{ type | pokemonTypeColor }}">{{ type }}</span>
15 </label>
16 </div>
17 </div>

Lorsque la méthode isTypesValid renvoie false, alors la case à cocher est verrouillée et ne
peut plus être sélectionnée ! C'est exactement le comportement souhaité !
Bien sûr, la validation côté client n'est jamais suffisante si vous sauvegardez vos données sur un
serveur distant. Pensez à valider les données aussi côté serveur, sous peine d'introduire
d'importantes failles de sécurité dans votre application !

Prévenir l'utilisateur en cas d'erreurs


Pour l'instant, si le formulaire contient des erreurs, le bouton submit restera vérouillé et
inutilisable : l'utilisateur verra bien que quelque chose ne va pas dans le formulaire, mais il ne
saura pas pourquoi !
Nous devons prévenir l'utilisateur sur ce qui ne fonctionne pas. Pour cela, nous allons
utiliser les classes ajoutées automatiquement par la directive NgModel, pour faire deux
choses :
· Utiliser ces classes avec le CSS, en ajoutant une bordure verte ou rouge en fonction de la
validité des champs du formulaire : cela permettra à l'utilisateur d'avoir un retour visuel sur
l'état de validité d'un champ.
· Utiliser ces classes avec les variables de templates et la directive NgIf, pour afficher un
message en cas d'erreur sur un champ.

Ajouter des indicateurs visuels


Nous devons simplement ajouter une feuille style qui s'appliquera uniquement sur le
composant du formulaire, ce qui est est le comportement par défaut d'un composant Angular
grâce au Shadow DOM.
Afin de respecter le principe de 1 tâche = 1 fichier, nous allons créer une feuille de style à
part. Créez un fichier nommé pokemon-form.component.css (à placer dans le dossier du module
des Pokémons) :
01 .ng-valid[required], .ng-valid.required {
02 border-left: 5px solid #42A948; /* bordure verte */
03 }
04
05 .ng-invalid:not(form) {
06 border-left: 5px solid #a94442; /* bordure rouge */
07 }

Que fait ce code ? Il s'occupe de deux choses :


· Il affiche une bordure verte à gauche, si un champ requis est valide.
· Sinon, il affiche une bordure rouge pour les éléments invalides qui ne sont pas des
éléments de type form : c'est-à-dire tous les champs inputs, select, etc.
Ensuite, nous devons lier le composant de notre formulaire à cette nouvelle feuille de style
:
01 @Component({
02 selector: 'pokemon-form',
03 templateUrl: 'app/pokemons/pokemon-form.component.html',
04 styleUrls: ['app/pokemons/pokemon-form.component.css']
05 })
06 export class PokemonFormComponent implements OnInit { ... }

Comme vous le constatez, nous utilisons l’attribut styleUrls, afin de renseigner un tableau
de chemins relatifs vers des feuilles de styles, qui seront appliquées sur le template du
composant.
Il y a bien un 's' à l'option styleUrls, contrairement à templateUrl. En effet, il est possible de
passer plusieurs feuilles de styles à un composant, mais un seul template. Logique !
On aurait pu également utiliser la propriété styles qui permet d'écrire le CSS directement
dans le fichier qui contient le composant :
01 @Component({
02 selector: 'pokemon-form',
03 templateUrl: 'app/pokemons/pokemon-form.component.html',
04 styles: [`
05 .ng-valid[required], .ng-valid.required {
06 border-left: 5px solid #42A948;
07 }
08 .ng-invalid:not(form) {
09 border-left: 5px solid #a94442;
10 }
11 `]
12 })
13 export class PokemonFormComponent implements OnInit { ... }

Cependant, comme pour les templates, si votre code est trop long, cela n'est pas très
pratique. De plus, vos feuilles de styles sont souvent communes à plusieurs composants, il est
donc préférable de les centraliser dans des fichiers à part.

Afficher des messages d'erreurs


L'affichage des messages d'erreurs pour les champs <input> peut se faire de la manière
suivante : nous combinons la liaison de la propriété hidden avec la variable de template name :
01 <!-- Pokemon name -->
02 <div class="form-group">
03 <label for="name">Nom</label>
04 <input type="text" class="form-control" id="name"
05 pattern="^[a-zA-Z0-9áàâäãåçéèêëíìîïñóòôöõúùûüýÿæœ]{1,25}$"
06 required
07 [(ngModel)]="pokemon.name" name="name"
08 #name="ngModel">
09
10 <!-- error -->
11 <div [hidden]="name.valid || name.pristine"
12 class="card-panel red accent-1">
13 Le nom du pokémon est requis (1-25).
14 </div>
15 </div>

Regardez à la ligne 11, le message d'erreur est masqué si la propriété name est valide, ou
qu'elle n'a jamais été modifiée. En effet, on ne va pas afficher un message d'erreur à l'utilisateur
lorsqu'on affiche le formulaire pour la première fois, sous prétexte qu'il y a encore des champs
vides !
Le principe est le même pour les champs Picture, Cp et Hp. Vous êtes tout à fait capable
de les ajouter vous-même à ce niveau. Il vous suffit d’ajouter les attributs avec les valeurs
énumérées dans le tableau un peu plus haut.
L'interface que nous avons déjà développé pour les types de Pokémons (avec les cases à cocher
qui se verrouillent et déverrouillent automatiquement en fonction d'un certain état de validité)
est assez ergonomique pour que l'utilisateur comprenne tout seul les règles de validation, nous
n'avons pas besoin d'afficher un message spécifique en plus, cela surchargerai inutilement
l’interface utilisateur.

Intégration du formulaire
Avant de pouvoir utiliser notre formulaire, il faut l'intégrer dans l'application existante. La
première chose à faire est de développer un composant parent pour le formulaire, que nous
appellerons edit-pokemon.component.ts :
01 import { Component, OnInit } from '@angular/core';
02 import { ActivatedRoute, Params } from '@angular/router';
03 import { Pokemon } from './pokemon';
04 import { PokemonsService } from './pokemons.service';
05
06 @Component({
07 selector: 'edit-pokemon',
08 template: `
09 <h2 class="header center">Editer {{ pokemon?.name }}</h2>
10 <p>
11 <img *ngIf="pokemon" [src]="pokemon.picture">
12 <p>
13 <pokemon-form [pokemon]="pokemon"></pokemon-form>
14 `,
15 })
16 export class EditPokemonComponent implements OnInit {
17 pokemon: Pokemon = null;
18
19 constructor(
20 private route: ActivatedRoute,
21 private pokemonsService: PokemonsService) {}
22
23 ngOnInit(): void {
24 let id = +this.route.snapshot.params['id'];
25 this.pokemon = this.pokemonsService.getPokemon(id);
26 }
27 }

Ce composant ne possède rien de spécial, à part à la ligne 13, dans le template :


01 <pokemon-form [pokemon]=’pokemon’></pokemon-form>

Il s'agit du composant de notre formulaire, que nous injectons dans ce template ! La


syntaxe entre crochets indique une liaison de propriété sur la propriété d'entrée du
composant. A droite, on passe le Pokémon de la propriété d’EditPokemonComponent. Il s'agit
du Pokémon qui doit être édité, et que l'on récupère depuis la méthode ngOnInit.
Nous pouvons maintenant utiliser ce composant EditPokemonComponent pour afficher le
formulaire, avec comme url : /pokemon/edit/:id.
Le point d’interrogation dans {{ pokemon?.name }} signifie "Affiche le nom du pokémon,
seulement si le pokémon existe". Cela évite de lever une erreur inutilement si l'identifiant passé
en paramètre de l'url ne correspond à aucun pokémon. De plus, on a appliqué la directive NgIf
sur l’image d’un pokémon, afin d’éviter de faire une requête « dans le vide », si le pokémon n’a
pas encore été chargé.

Pourquoi ne pas avoir développé le formulaire directement dans le composant


EditPokemonComponent ?

Il aurait été possible de le faire, mais laissez-moi vous rappeler deux choses :
· Nous pourrons par la suite réutiliser ce formulaire pour gérer le cas d'ajout de Pokémons
dans l'application, par exemple. Il est donc logique d'avoir développé le formulaire à part.
· Essayez toujours de respecter le principe de 1 tâche = 1 fichier. Cela s'applique aussi
pour les formulaires !
Nous voilà avec un composant prêt à l'emploi que nous pouvons ajouter n'importe où dans
l'application, pour permettre à l'utilisateur d'éditer ses Pokémons !
Nous devons désormais déclarer plusieurs éléments dans le module Pokémons :
· FormsModule, le module permettant de créer des formulaires ;
· Le composant de notre formulaire PokemonFormComponent ;
· Le composant EditPokemonComponent.
01 // ...
02 import { FormsModule } from '@angular/forms';
03 import { EditPokemonComponent } from './edit-pokemon.component';
04 import { PokemonFormComponent } from './pokemon-form.component';
05 // ...
06
07 @NgModule({
08 imports: [
09 CommonModule,
10 FormsModule, // nouvelle déclaration
11 pokemonsRouting
12 ],
13 declarations: [
14 ListPokemonComponent,
15 DetailPokemonComponent,
16 EditPokemonComponent, // nouvelle déclaration
17 PokemonFormComponent, // nouvelle déclaration
18 BorderCardDirective,
19 PokemonTypeColorPipe
20 ],
21 providers: [PokemonsService]
22 })
23 export class PokemonsModule {}

Nous devons ensuite déclarer une nouvelle route pour l'édition d'un pokémon. Modifier
donc le fichier pokemons-routing.module.ts en ajoutant la route suivante :
01 import { EditPokemonComponent } from './edit-pokemon.component';
02 // ...
03
04 const pokemonsRoutes: Routes = [
05 { path: 'pokemons', component: ListPokemonComponent },
06 // ajouter la route d'édition
07 { path: 'pokemon/edit/:id', component: EditPokemonComponent },
08 { path: 'pokemon/:id', component: DetailPokemonComponent }
09 ];
10 // …

Rappelez-vous que l'ordre de déclaration des routes à une importance !


Il ne nous reste plus qu'une seule chose à faire : ajouter un lien vers la page d'édition d'un
Pokémon. L'endroit le plus approprié pour ce lien semble être la page de détail d'un Pokémon.
Ouvrez donc le template detail-pokemon.component.html et ajoutez la ligne suivante :
01 <!-- ... -->
02 <div class="card-action">
03 <a (click)="goBack()">Retour</a>
04 <!-- On rajoute un lien vers la page d'édition de ce pokémon -->
05 <a (click)="goEdit(pokemon)">Editer</a>
06 </div>
07 <!-- ... -->

On ajoute ensuite la méthode goEdit dans la classe du composant detail-


pokemon.component.ts :
01 // On crée une méthode qui s'occupe de la redirection
02 goEdit(pokemon: Pokemon): void {
03 let link = ['/pokemon/edit', pokemon.id];
04 this.router.navigate(link);
05 }

Et voilà ! Maintenant, lorsque l'utilisateur cliquera sur le lien "Editer", il sera redirigé vers
le formulaire d'édition, déjà pré-rempli avec les valeurs du Pokémon sur lequel il a cliqué !
Figure 17 - Le formulaire d'édition des Pokémons est bien intégré à l'application !

Conclusion
Les formulaires ne sont jamais simples à développer, il faut mettre en place des processus
de traitements et de validation des données. Même si Angular nous simplifie la tâche, il nous a
fallu quand même un peu de travail !
Avez-vous remarqué qu'à chaque fois que l'on recharge notre application dans le
navigateur, les données concernant le pokémons sont réinialisées, et que nos changements ne
sont pas pris en compte ? Nous verrons d'ici la fin de la partie 3 comment persister nos
modifications sur un serveur distant, afin de donner une "mémoire" à notre application.
Mais avant, dans le prochain chapitre, nous allons voir comment développer des
formulaires pilotés par le modèle, pour que vous puissiez choisir la méthode qui vous convient le
mieux.

En résumé
· Il y a deux modules différents pour développer des formulaires avec Angular:
FormsModule et ReactiveFormsModule.
· Le FormsModule est pratique pour développer des formulaires de petites tailles, et met à
disposition les directives NgForm et NgModel.
· La directive NgModel ajoute et retire certaines classes au champ sur lequel elle
s'applique. Ces classes peuvent être utilisées pour afficher des messages d'erreurs ou de
succès, et des indicateurs visuels.
· La syntaxe à retenir pour utiliser NgModel est [()].
· On peut utiliser les attributs HMTL5 pour gérer la validation côté client, comme required
ou pattern.
· On peut utiliser des validateurs personnalisés en développant ses propres méthodes de
validation.
· Il faut toujours effectuer une validation côté serveur en complément de la validation côté
client, si vous avez prévu de stocker des données depuis votre application.
Chapitre 14 : Formulaires pilotés par le
modèle
Le chapitre précédent nous a donné un aperçu du processus de création des formulaires.
Les formulaires pilotés par le template sont très pratiques pour développer des formulaires de
petite taille, dans le cadre d'applications qui ne sont pas appelées à évoluer souvent.
Cependant le code coté template était plutôt verbeux, et il aurait été compliqué de modifier
le formulaire sans y passer beaucoup de temps. De plus, le formulaire n'était pas vraiment
testable.
On ne peut pas toujours justifier le temps de développer ces formulaires fait à la main.
Surtout si vous avez besoin de développer beaucoup de formulaires, et qu'il faut les modifier
régulièrement car les besoins de vos utilisateurs changent !
Il peut être plus économique de créer les formulaires dynamiquement, basés sur des
modèles d'objets métiers de votre application. C'est ce que nous allons voir dans ce chapitre !
Ce chapitre est théorique et les extraits de code sont donnés à titre d'exemple. Nous ne
modifierons pas notre application de Pokémons dans ce chapitre !

Les Formulaires pilotés par le modèle


Les formulaires pilotés par le modèle sont différents de ceux que nous avons étudiés au
chapitre précédent, sous plusieurs aspects :
· Ce type de formulaire est principalement développé dans la classe du composant plutôt
que dans le template, comme les règles de validations par exemple.
· On n'utilise plus la directive NgModel ou NgForm !
· On n'utilise plus le module FormsModule mais le module ReactiveFormsModule.
Nous allons d'abord voir comment mettre en place ces formulaires, et pour finir nous
ferons une petite rétrospectives de quel type de formulaire est le plus adapté, dans telle ou telle
situation.
Nous n'allons pas continuer le développement de notre application de Pokémons dans ce
chapitre. Les extraits de code source sont donnés à titre d'exemples, ne les ajoutez pas au projet
!

Un peu de théorie
La première chose à faire, c'est d'importer le module ReactiveFormsModule dans votre
application :
01 import { ReactiveFormsModule } from ‘@angular/forms’ ;

Le package @angular/forms permet de développer des formulaires pilotés par le template et


par le modèle.

Une fois que vous avez importé le module, vous pouvez utiliser les éléments suivants.
Les contrôles avec FormControl
Nous allons associer chaque champ de notre formulaire (par exemple un champ input ou
select) à une certaine représentation que nous nommerons un contrôle. Cette représentation est
modélisée par un objet de type FormControl, qui traque la valeur et le statut de validation du
champ auquel il est associé.
Lorsque l'on développait des formulaires pilotés par le template, on appliquait la directive
NgModel sur un champ HTML pour déclarer un FormControl. Maintenant, on peut instancier
directement cet objet dans la classe du composant :
01 const control = new FormControl();

Voilà ! Nous avons créé un contrôle ! Rien de très compliqué. Et nous pouvons faire mieux
que ça, en passant une valeur initiale au contrôle comme premier argument :
01 const control = new FormControl('une valeur');
02 console.log(control.value); // 'une valeur'

On peut aussi initialiser le contrôle avec un objet qui comprend une valeur, et l'état
d'activation du champ. Ces deux informations sont nécessaires pour utiliser cette manière
d'initialiser un contrôle :
01 const ctrl = new FormControl({value: 'une valeur', disabled: true});
02 console.log(ctrl.value); // 'une valeur'
03 console.log(ctrl.status); // 'DISABLED'

Les contrôles possèdent également une méthode reset, qui permet de réinitialiser un
champ. Quand elle est appelée, cette méthode effectue trois choses :
· La valeur du champ est définie à null.
· Le contrôle est marqué comme "pristine", c'est-à-dire que sa valeur est considérée comme
n'ayant jamais été modifiée.
· Le contrôle est marqué comme "untouched", c'est-dire que l'utilisateur n'aurait jamais
interagit avec le champ.
01 let ctrl = new FormControl('Toto');
02 console.log(ctrl.value); // 'Toto
03 ctrl.reset();
04 console.log(ctrl.value); // null

La méthode reset est disponible pour les contrôles et les groupes de contrôles, que nous allons
voir tout de suite.

Regrouper les contrôles avec FormGroup


Qu'est-ce qu'un formulaire, si ce n'est un assemblage de différents champs ?
C'est le rôle du FormGroup : regrouper les contrôles du formulaire au sein du même objet.
Le FormGroup est l'un des deux piliers des formulaires piloté par le modèle, avec l'objet
FormControl que nous venons de voir. Il suffit de déclarer tous les contrôles qui constituent
notre formulaire, et de les regrouper au sein du même objet FormGroup pour constituer un
formulaire !
Un FormGroup s'occupe de traquer la valeur et la validité d'un groupe d'instances de
FormControl. Un FormGroup agrège les valeurs de chaque FormControl en un seul objet, avec
le nom du contrôle comme clef.
Quand on instancie un FormGroup, il faut lui passer une collection de contrôles comme
premier argument. La clef utilisée pour chaque contrôle servira de nom à ce contrôle :
01 const form = new FormGroup({
02 first: new FormControl('Nancy'),
03 last: new FormControl('Drew'),
04 });
05 console.log(form.value); // {first: 'Nancy', last; 'Drew'}

Ainsi, chaque contrôle devient accessible depuis le FormGroup, via sa clef :


· form.first : permet d'accéder au premier contrôle.
· form.last : permet d'accéder au deuxième contrôle.

Un formulaire piloté par le modèle est simplement une instance de FormGroup !

Un comportement commun avec AbstractControl


Les classes FormControl et FormGroup héritent toutes les deux de la classe
AbstractControl. Cette classe fournit des propriétés et des méthodes qui sont partagées par les
contrôles et les groupes de contrôles.
Définir des règles de validations, déterminer la validité d'un champ, ou réinitialiser l'état
d'un groupe de contrôle, sont des comportements qui sont définis dans la superclasse
AbstractControl. En voici quelques exemples :

Propriété ou méthode Description

value Renvoie la valeur courante.

valid Permet de connaître l'état de validité.

setValue Définit une nouvelle valeur

reset Réinitialise la valeur.

valueChanges Emet un événement à chaque fois que


la valeur change.

statusChanges Emet un événement à chaque fois que


l'état de validité est recalculé.

Les propriétés valueChanges et statusChanges sont des Observables, c'est-à-dire des flux
d'événements auxquels on peut s'abonner (nous verrons exactement ce que sont les Observables
dans le chapitre prochain).
En attendant, voici ce que vous pourriez développer pour afficher dans la console du
navigateur tous les changements de valeurs issues de votre formulaire :
01 // on récupère un Observable grâce à la propriété 'valueChanges'
02 let observableDuFormulaire = this.form.valueChanges;
03
04 // on écoute les changements de valeurs du formulaire
05 // avec la méthode 'subscribe'
06 observableDuFormulaire.subscribe(nouvelleValeur =>
07 console.log(nouvelleValeur));

Ainsi, à chaque fois que les valeurs saisies dans le formulaire sont modifiés, elles sont
affichées dans la console. Evidemment, cette exemple n'est pas très intéressant en soi, mais il
permet de se rendre compte de tout ce qu'il est possible de faire avec les formulaires pilotés par
le modèle !

Un AbstractControl ne peut pas être créé, d'où son nom. Vous ne pouvez instancier que ses
classes filles.

Les validateurs
Les validateurs permettent d'appliquer des règles de validation sur des contrôles ou des
groupes de contrôles.
Chaque validateur retourne un dictionnaire d'erreurs. Si ce dictionnaire est vide, cela
signifie que la validation a réussi, sinon c'est que le champ n'est pas valide :
01 const ctrl = new FormControl('', Validators.required);
02 console.log(ctrl.value); // ''
03 console.log(ctrl.status); // 'INVALID'

Il y a deux choses à remarquer dans l'exemple ci-dessus :


· A la ligne 1 : nous avons passé un validateur en deuxième argument du constructeur de
FormControl. Un validateur se présente comme une propriété statique de la classe
Validators. Dans notre cas, ce validateur indique qu'une valeur est requise pour ce
contrôle.
· A la ligne 3 : La propriété status du contrôle permet de vérifier si le contrôle est dans un
état valide ou invalide, en fonction des règles de validation qui sont appliquées. Comme
nous avons défini le contrôle comme requis, mais que nous lui avons transmis une chaîne
vide, il est normal qu'il soit marqué comme invalide !

On importe la classe Validators depuis la librairie @angular/forms.

Les règles de validation présentes dans la classe Validators sont indiquées dans le tableau
ci-dessous :

Nom de la Paramètre Description


propriété

required Aucun Oblige un champ à ne pas


rester vide.

minLength Un nombre Définit une longueur


minimum pour la valeur à
saisir dans un champ.

maxLength Un nombre Définit une longueur


maximum pour la valeur à
saisir dans un champ.

pattern Une chaîne de Compare l'expression régulière


caractères du validateur et la valeur saisie
par l'utilisateur.

Combiner les validateurs


On peut aussi combiner plusieurs validateurs, avec la méthode compose. Par exemple si un
champ est requis et qu'il doit faire plus de cinq caractères, on passe un validateur composé en
second argument du contrôle :
01 let ctrl = new FormControl('',
02 Validators.compose([Validators.required, Validators.minLength(5)]));
La méthode compose prend en paramètre un tableau de validateurs, qu'elle assemble pour
former un nouveau validateur.

Valider un groupe de contrôle


On peut aussi inclure un validateur au niveau d'un FormGroup, en tant que second
argument. C'est très utile si vous souhaitez contrôler la valeur de plus d'un contrôle à la fois.
Dans l'exemple ci-dessous, on utilise le validateur fictif passwordMatchValidator, pour vérifier
que l'utilisateur saisit le même mot de passe lors de la confirmation :
01 const form = new FormGroup({
02 password: new FormControl('', Validators.minLength(2)),
03 passwordConfirm: new FormControl('', Validators.minLength(2)),
04 }, passwordMatchValidator);
Un FormGroup dispose de son propre statut global en "assemblant" les statuts de ses
contrôles fils. Par exemple, si un des contrôles du groupe est invalide, alors le groupe entier
devient invalide. Etant donné qu'un formulaire n'est ni plus ni moins qu'un FormGroup
spécifique, on peut connaître le statut d'un formulaire simplement avec myForm.status !
Le validateur passwordMatchValidator n'existe pas vraiment, cela ne donnera rien si vous
l'utilisez tel quel dans votre application ! Mais nous allons voir comment créer des validateurs
personnalisés, ne vous en faites pas.

Déclarer vos formulaires sous forme de tableaux


Il existe une autre manière de déclarer vos formulaires qu'avec des groupes de contrôles.
Vous pouvez aussi les déclarer sous forme de tableau avec l'objet FormArray. Concrètement,
vous obtiendrez presque la même chose, à la différence que vous ne disposerez pas de clef pour
nommer vos contrôles.
Quel est l’intérêt du coup ?

Parfois, vous devez présenter un nombre arbitraire de contrôles ou de groupes (par


exemple, une liste d’amis, qui n’a pas de limite pré-définie). Voici un exemple d’utilisation du
FormArray :
01 const friends = new FormArray([
02 new FormControl('Jean'),
03 new FormControl('Pierre'),
04 ]);
05 console.log(arr.value); // ['Jean', 'Pierre']
06 console.log(arr.status); // 'VALID'

Comme vous pouvez le voir, le comportement est similaire au FormGroup. Pour accéder
au premier amis dans le formulaire, il faudra faire form[0]. Pareille, pour récupérer le nom :
form[1].

La classe FormArray hérite de la classe AbstractControl : n'oubliez pas que vous avez accès à
tous les propriétés de cette classe depuis vos FormArray.

A titre personnel, je vous recommande fortement d'utiliser les FormGroup plutôt que les
FormArray, vous gagnerez en précision et en clarté dans votre code.

Construire un formulaire
Bon, jusque-là nous avons vu plusieurs petites briques, mais nous ne savons pas comment
construire un formulaire en entier, au sein d'un composant. Je vous propose l'exemple de code ci-
dessous, qui est un simple formulaire de connexion. Ce formulaire possède les champs suivants :
· Un nom : composé du prénom et du nom de la personne.
· Une adresse email.
01 import {Component, OnInit} from '@angular/core';
02 import {FormControl, FormGroup, Validators} from '@angular/forms';
03
04 @Component({
05 selector: 'my-app',
06 template: `
07 <form [formGroup]="form" novalidate
08 (ngSubmit)="save(myForm.value, myForm.valid)">
09 <div formGroupName="name">
10 <input formControlName="first" placeholder="First">
11 <input formControlName="last" placeholder="Last">
12 </div>
13 <input formControlName="email" placeholder="Email">
14 <button>Submit</button>
15 </form>
16 <p>Value: {{ form.value | json }}</p>
17 <p>Validation status: {{ form.status }}</p>
18 `
19 })
20 export class AppComponent implements OnInit {
21 form: FormGroup;
22
23 ngOnInit():void {
24 // On initialise notre formulaire piloté par le modèle
25 this.form = new FormGroup({
26 'name': new FormGroup({
27 first: new FormControl('',
28 Validators.compose([Validators.required,
29 Validators.minLength(3)])),
30 last: new FormControl('',
31 Validators.compose([Validators.required,
32 Validators.minLength(3)]))
33 }),
34 email: new FormControl('',
35 Validators.pattern('^[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}$'))
36 });
37 }
38 }
Commençons par regarder la partie que nous connaissons déjà : le composant lui-même.
Nous déclarons une propriété form, de type FormGroup, à la ligne 21. Il s'agit du formulaire lui-
même.
Nous l'initialisons ensuite dans la méthode de cycle de vie ngOnInit. Analysez bien le
code, nous déclarons deux groupes de contrôles si vous regardez bien :
· Un premier pour représenter le formulaire lui-même, à la ligne 25.
· Un deuxième FormGroup, pour rassembler le prénom et le nom sous un seul groupe de
contrôle, nommé name, à la ligne 26.
C'est un choix délibéré de ma part, afin de vous montrer qu'un FormGroup peut contenir
d'autres FormGroup, et que vous pouvez développer des formulaires aussi complexes que vous
le souhaitez !
Ensuite, nous avons trois FormControl : ce qui est normal puisque notre formulaire
possède bien trois champs (prénom, nom et email). Nous les avons simplement regroupés
différemment en fonction de nos besoins.
Concernant les règles de validation, nous pouvons constater plusieurs choses :
· Il n'y a pas de règle(s) définie(s) au niveau des groupes de contrôles.
· Nous avons défini plusieurs règles de validation pour les champs first et last : ils sont
requis et doivent faire au moins trois caractères de long (ligne 28/29 et 31/32).
· Pour valider l'email, nous avons spécifié une expression régulière grâce à la propriété
pattern. Si l'utilisateur saisit une chaîne de caractères qui ne correspond pas à l'expression
régulière, le validateur renverra une erreur.
Ensuite, dans le template, nous utilisons trois éléments pour construire la vue :
· La liaison de propriété formGroup, avec crochets, pour relier la balise <from> à notre
formulaire côté composant.
· La directive formGroupName, qui permet d'associer une partie du template à une instance
de FromGroup, grâce au nom de sa clef.
· La directive formControlName, qui permet de faire la même chose, mais avec une
instance de FormControl. Ainsi vous pouvez associer chaque champ de votre formulaire
avec un contrôle côté composant.
Enfin, concernant la soumission du formulaire, nous avons lié l'événement de soumission
d'un formulaire avec la méthode save de notre composant. Cette méthode prend en paramètre les
données entrées dans le formulaire, et l'état de validité de ce dernier. La méthode save n'a pas été
implémentée dans l'exemple ci-dessus, mais vous pouvez ensuite traiter les données entrées par
l'utilisateur.

C'est quoi exactement l'attribut novalidate à la ligne 7 ? C'est une directive spéciale
d'Angular ?

Non, pas du tout ! C'est un attribut propre à HTML5, qui permet de désactiver la validation
par défaut du navigateur. En l'appliquant sur vos formulaires, cela vous permet de partir d'une
base commune pour définir vos propres comportements, sans interférences avec le comportement
par défaut du navigateur. Vous trouverez la description de cet attribut sur le site du W3C.
Il n'y a que Safari qui ne supporte pas l'attribut novalidate.

Le FormBuilder
Le FormBuilder, ou constructeur de formulaire, est une classe utilitaire proposée par
Angular pour simplifier la déclaration des formulaires pilotés par le modèle. Cette classe dispose
des méthodes group, control et array qui permettent de définir respectivement un FormGroup,
un FormControl ou un FormArray. L'utilisation de ces méthodes vous évitera de devoir
instancier tous vos éléments lors de la déclaration du formulaire, ce qui rendra votre code moins
verbeux et donc plus lisible.
Pour utiliser le FormBuilder, vous devez importer la classe FormBuilder depuis la librairie
@angular/forms, puis l'injecter dans la classe de votre composant.
Je vous propose de récrire la méthode ngOnInit de notre exemple de formulaire précédent,
en utilisant le FormBuilder :
01 import {FormBuilder} from '@angular/forms';
02 // ...
03
04 constructor(private fb: FormBuilder) {}
05 // ...
06 this.form = fb.group({
07 name: fb.group({
08 first: ['', Validators.compose([Validators.required,
09 Validators.minLength(3)])],
10 last: ['', Validators.compose([Validators.required,
11 Validators.minLength(3)])]
12 }),
13 email: ['',
14 Validators.pattern('^[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}$')]
15 });

Comme vous pouvez le constater, l'utilisation du FormBuilder rend votre code plus lisible !
Nous avons même utilisé un raccourci nous permettant de déclarer un contrôle directement
via un tableau, aux lignes 8, 10 et 13.

Bien que l'utilisation du constructeur de formulaire ne soit pas obligatoire, je vous le


recommande : c'est un outil développé spécialement pour faciliter le développement de vos
formulaires !

Au fait, pourquoi cela s'appelle formulaire pilotée par le modèle ? Ce n'est pas plutôt un
formulaire piloté par le composant ?

C'est une excellente question ! Nous avons failli passer à côté de l'essentiel.
En fait, le rôle de ces formulaires, c'est de se calquer sur l'un de vos modèles, et ensuite de
développer votre formulaire par-dessus, avec vos contraintes métiers. Un exemple sera plus
efficace qu'un long discours.
Reprenons notre formulaire précédent avec ses trois champs : prénom, nom et email.
En réalité, ce formulaire modélise une personne dans notre application. Il est piloté par le
modèle, puisque c'est à partir d'un modèle Person que nous aurions dû construire notre
formulaire. Pour vous prouver que c'est bien le modèle qui pilote le formulaire, je vous propose
de créer un objet Person, et de pré-remplir le formulaire avec les données de cet objet. Pour cela,
nous utiliserons la méthode setValue propre aux objets de type FormControl et FormGroup :
01 // Notre formulaire actuel ...
02 this.form = fb.group({
03 name: fb.group({
04 first: ['', Validators.compose([Validators.required,
05 Validators.minLength(3)])],
06 last: ['', Validators.compose([Validators.required,
07 Validators.minLength(3)])]
08 }),
09 email: ['',
10 Validators.pattern('^[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}$')]
11 });
12
13 // On déclare une variable 'person',
14 // avec la même structure que le formulaire.
15 let person = {
16 name: {
17 first: 'Toto',
18 last: 'Dupond'
19 },
20 email: 'toto@gmail.com'
21 };
22
23 // On pré-remplis le formulaire avec la variable 'person' !
24 // Les champs seront initialisés avec la valeur
25 // de la propriété correspondante.
26 this.form.setValue(person);
Oui, vous avez bien vu, nous avons pré-rempli notre formulaire avec les données de l'objet
Person. C'est le comportement parfait pour un formulaire de modification ! On peut récupérer un
objet dans notre application pour le transmettre au formulaire, et ensuite prendre en compte les
modifications de l'utilisateur !

Nous aurions tout à fait pu utiliser cette façon de faire pour développer le formulaire d'édition
des Pokémons du chapitre précédent.

Développer une règle de validation personnalisée


Je vous propose d'ajouter une règle de validation qui se charge de vérifier qu'un mot de
passe est robuste. Par exemple si un utilisateur choisit comme mot de passe "123" ou "password"
alors nous devrons le considérer comme invalide car facilement corruptible. Voici les règles
auxquelles devra être soumis le mot de passe :
· Contenir respectivement au moins une majuscule et une minuscule.
· Contenir au moins un chiffre.
· Contenir au moins un symbole spécial (@, #, $, %, ^, & ou *).
· Contenir au moins 8 caractères.
Pour implémenter notre règle de validation, nous allons devoir développer un validateur,
que nous nommerons strongPasswordValidator.
Un validateur est un objet qui implémente l'interface ValidatorFn, qui représente une règle
de validation sous la forme d'une fonction. Cette fonction utilisée pour définir le validateur devra
prendre en paramètre un objet de type AbstractControl, et devra renvoyer null si aucune erreur
n'a été levée, et un dictionnaire d'erreurs sinon.
Pour rappel, un AbstractControl peut être un contrôle (FormControl), mais aussi un groupe de
contrôles (FormGroup).
Bon, voyons concrètement ce que cela donne. Créer un fichier strong-
password.validator.ts dans le dossier app :
01 import { FormGroup, ValidatorFn } from '@angular/forms';
02
03 export function strongPasswordValidator(): ValidatorFn {
04 return (control: AbstractControl): { [key: string]: any } => {
05 // on utilise une expression régulière pour la validation
06 var strongRegex = new RegExp("^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-
07 9])(?=.*[!@#\$%\^&\*])(?=.{8,})");
08 // on récupère le mot de passe du champ 'control' passé en paramètre
09 const password = control.value;
10 // on teste le mot de passe avec l'expression régulière
11 const valid = strongRegex.test(password);
12 // on retourne null si le mot de passe est assez fort,
13 // et un dictionnaire d'erreur sinon.
14
15 return valid ? null : { 'strongPasswordError': { password } };
16 };
17 }

Ce validateur est assez simple, il vérifie simplement que la valeur du champ passés en
paramètre correspondent aux besoins de la regex définit à la ligne 6/7. La vérification se fait à la
ligne 11, grâce à la méthode test de l'objet RegExp, qui représente une expression régulière en
JavaScript.

Pourquoi ne pas avoir simplement utilisé un validateur de type pattern, plutôt que de
définir un validateur personnalisé ?

Oui, cela aurait été possible, ... si vous n'avez pas besoin de réutiliser ce validateur ailleurs
! Comment feriez-vous si vous devez changer la règle de validation de tous les mots de passe de
votre application, en passant le nombre de caractère minimum de 8 à 10, par exemple ? Vous
seriez obligé de retrouver et modifier tous les validateurs de type pattern de votre application, un
à un. Mais grâce à notre validateur personnalisé, nous avons défini une règle une bonne fois pour
toutes, et nous pouvons la réutiliser autant de fois que nous le voulons.
Voilà, vous disposez maintenant d'un validateur que vous pouvez utiliser dans les
formulaires qui en auront besoin ! Voici un exemple d'utilisation :
01 import {strongPasswordValidator} from './strong-password.validator';
02 // ...
03
04 this.form = fb.group({
05 login: ['', Validators.required],
06 password: ['', strongPasswordValidator()]
07 // Remarque : on applique notre validateur de la même manière
08 // qu'un validateur 'classique'.
09 });

Exercice
Bon, et si nous faisions un petit exercice ? Un peu de pratique, pourquoi pas ?
Je vous propose l'exercice suivant : développer un petit formulaire d'inscription !
Ne vous inquiétez pas, il s'agit simplement d'un exercice informel, qui ne sera pas pris en
compte dans l'évaluation de ce cours.
Voici les instructions, le formulaire sera composé des cinq champs suivants :
· Un champ firstname, qui fera partie du groupe de contrôle 'name'.
· Un champ lastname, qui fera partie du groupe de contrôle 'name'.
· Un champ email.
· un champ password, qui fera partie du groupe de contrôle 'verification'.
· Un champ passwordConfirm, qui fera partie du groupe de contrôle 'verification'.
Les règles de validation que vous devez implémenter sont les suivantes :

Nom du contrôle ou du groupe Règles de validation

firstname champ requis, entre 3 et 25 caractères.

lastname champ requis, entre 3 et 25 caractères.

email La valeur saisie par l'utilisateur doit


être conforme à l'expression régulière
suivante : ^[a-z0-9._-] +@[a-z0-9._-]
{2,}\.[a-z]{2,4}$.

password champ requis, au minimum 8


caractères.

passwordConfirm champ requis, au minimum 8


caractères.

verification (groupe de contrôle) Utiliser notre validateur personnalisé


passwordMatchValidator. Considérez
qu'il est directement disponible dans
votre composant.

Le cadre de cet exercice est précis, vous devez « simplement » complétez le code suivant :
01 ngOnInit(): void {
02 // complétez le code suivant avec les instructions données ci-dessus.
03 this.form = ...
04 }
Une fois que vous aurez fini de compléter cette méthode ngOnInit, vous pourrez regarder
la correction.
Voilà, vous avez tout ce qu'il vous faut pour commencer à travailler. Essayez de vraiment
faire l'effort de faire l'exercice par vous-même, c'est comme ça que l'on apprend ! En plus, le
résultat n'est pas pris en compte dans votre évaluation de cours, profitez-en pour faire un
maximum d'erreurs maintenant !
Vous pouvez choisir d'utiliser le constructeur de formulaire ou non. Cependant, sachez que je
l'utilise pour la correction !

Correction
Alors, vous avez réussi ? Bravo pour ceux qui ont fait l'effort, et tant pis pour les autres,
vous avez raté une belle occasion de progresser !
Alors, voici la correction que je vous propose :
01 this.form = fb.group({
02 name: fb.group({
03 firstname: ['', Validators.compose([
04 Validators.required,
05 Validators.minLength(3),
06 Validators.maxLength(25)])],
07 lastname: ['', Validators.compose([
08 Validators.required,
09 Validators.minLength(3),
10 Validators.maxLength(25)])]
11 }),
12 email: ['',
13 Validators.pattern('^[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}$')],
14 verification: fb.group({
15 password: ['', Validators.compose([Validators.required,
16 Validators.minLength(8)])],
17 passwordConfirm: ['', Validators.compose([Validators.required,
18 Validators.minLength(8)])]
19 }, passwordMatchValidator)
20 });

Il n'y a vraiment aucun piège, il fallait juste reprendre les éléments que nous avions déjà
vus, et les mettre dans le bon ordre. Si le code ci-dessus vous pose problème, reprenez les
explications théoriques au début du chapitre.
Vous n'avez pas tout à fait la même chose ? Ce n'est pas grave, le plus important est que
vous ayez tenté de faire l'exercice par vous-même. Profitez de cette correction pour rectifier vos
erreurs, et voir ce qui va et ce qui ne va pas.
Et si maintenant nous faisions un bilan de ces deux derniers chapitres sur les formulaires ?

Choisir entre formulaires pilotés par le template ou


par le modèle
Alors, êtes- vous plutôt formulaires pilotés par le template ou par le modèle ?
En fait, chaque type de formulaire a ses avantages et ses inconvénients. Les formulaires
pilotés par le modèle permettent :
· d'écouter et de contrôler les champs très facilement.
· de tester unitairement nos formulaires, car nous les avons développés dans la classe d'un
composant (par contre nous n’aborderons pas les tests dans ce cours destiné aux débutants,
mais gardez-le dans un coin de votre tête).
De leur côté, les formulaires pilotés par le template ne sont pas simplement une roue de
secours, ils ont aussi leurs avantages :
· Les formulaires pilotés par le template dispose de la propriété form.submitted issue de la
directive NgForm, ce dont ne disposent pas les formulaires pilotés par le modèle.
· La lecture des contrôles n'est pas très conviviale avec la syntaxe des formulaires pilotés
par le modèle. Par exemple, pour valider un champ nom, nous devons faire :
myForm.name.valid, alors que name.valid suffit pour les formulaires pilotés par le
template, grâce aux variables référencées dans le template !
· La syntaxe est plus familière si vous venez d'AngularJS.
Alors, comment choisir ?
En fait c'est simple, si vous n'avez pas prévu d'effectuer des tests sur votre application, et
que le comportement de vos formulaires est simple, alors utilisez simplement les formulaires
pilotés par le template.
Si vous avez des cas d'utilisation plus avancés, je vous recommande les formulaires pilotés
par le modèle. Ils sont conçus pour construire des formulaires plus complexes.
N’hésitez pas à utiliser les deux façons de faire dans votre application, au cas par cas.
Voilà, les formulaires avec Angular n'ont plus de secrets pour vous !
Conclusion
Les deux derniers chapitres étaient assez denses : nous avons vu le fonctionnement des
formulaires avec Angular, et ce n'est pas rien ! Nous sommes quasiment capables de développer
des applications complètes.
Cependant, il nous manque encore une chose, c'est de pouvoir échanger les données de
notre application avec celles d'un serveur distant. Car pour le moment, nous ne savons pas
comment sauvegarder les données de notre application !
Mais pas d'inquiétudes, nous allons voir comment faire cela dans les deux prochains
chapitres !

En résumé
· Un formulaire piloté par le modèle est composé au moins d'un FormControl et d'un
FormGroup (ou d'un FormArray).
· Un formulaire piloté par le modèle n'est ni plus ni moins qu'une instance particulière de
FormGroup.
· Les classes FormControl et FormGroup sous des sous-classes de la classe
AbstractControl, qui définit un ensemble de propriétés et de méthodes communes.
· Angular fournit une classe utilitaire nommé FormBuilder pour faciliter le développement
des formulaires pilotés par le modèle.
· On utilise des validateurs de la classe Validators pour construire les règles de validation
de nos champs.
· On peut combiner plusieurs validateurs entre eux grâce à la méthode compose.
· Il est possible de créer des validateurs personnalisés de type ValidatorFn, si les
validateurs présents par défaut ne sont suffisants.
· Il n'y a pas un type de formulaire meilleur qu'un autre en soi, choisissez à chaque fois le
type le plus adapté pour les besoins de votre application. Entrainez-vous, c'est important
d'être à l'aise avec les deux types de formulaires !
Chapitre 15 : La programmation réactive
Il y a un élément indispensable à la plupart des applications, que nous n'avons pas encore
abordé : comment communiquer avec un serveur distant ?
Comme pour les formulaires, il y a plusieurs manières de faire des appels sur le réseau :
avec les Promesses ou avec les Observables et la programmation réactive.
Nous verrons quels sont les outils existants pour interagir avec un serveur distant, et
comment ils fonctionnent, en attendant de le faire pour de "vrai" dans le chapitre prochain. Allez,
au boulot !
Ce chapitre est théorique et les extraits de code sont donnés à titre d'exemple. Nous ne
modifierons pas notre application de Pokémons dans ce chapitre !

Tenir ses promesses


Les promesses sont natives en JavaScript depuis l'arrivée d’ES6. Ce n'est plus un objet
interne à Angular comme c'était le cas dans la version 1.x, et donc nous pouvons les utiliser sans
avoir besoin de nouvelles importations.
Nous avons déjà traité des promesses dans le chapitre sur ES6, mais nous allons quand même
faire un petit rappel.
Les promesses sont là pour essayer de simplifier la programmation asynchrone. La
programmation asynchrone désigne un mode de fonctionnement dans lequel les opérations
sont non-bloquantes. Concrètement, cela signifie que votre utilisateur peut continuer à utiliser
votre application web (navigation, formulaires, ...) sans que le site soit bloqué dès que vous faites
un appel au serveur.

On peut utiliser des fonctions de callbacks pour gérer les appels asynchrones, mais les
promesses sont plus pratiques.

En fait, lorsque vous créer une promesse (avec la classe Promise), vous lui associez
implicitement une méthode then qui prend deux arguments : une callback de succès et une
callback d'erreur. Ainsi, lorsque la promesse a réussi, c'est la callback de succès qui est appelée,
et en cas d'erreur, c'est la callback d'erreur qui est invoquée.
Voici un exemple d'une promesse qui récupère un utilisateur depuis un serveur distant, à
partir de son identifiant :
01 let recupererUtilisateur = function(idUtilisateur) {
02 return new Promise(function(resolve, reject) {
03 // appel asynchrone au serveur pour récupérer
04 // les informations d'un utilisateur...
05 // à partir de la réponse du serveur,
06 // on extrait les données de l'utilisateur :
07 let utilisateur = response.data.utilisateur;
08
09 if(response.status === 200) {
10 resolve(utilisateur);
11 } else {
12 reject('Cet utilisateur n\'existe pas !');
13 }
14 })
15 }

Dans cet extrait, la fonction recupererUtilisateur prend en paramètre l'identifiant d'un


utilisateur, et renvoie une promesse qui contient l'objet utilisateur correspondant. En revanche, en
cas d'erreur lors de l'appel, la promesse renvoie un message d'erreur.
Et voilà, vous venez de créer une fonction qui renvoie une promesse, avec les informations
du serveur ! Vous pouvez ensuite utiliser la méthode then dans votre projet :
01 recupererUtilisateur(idUtilisateur)
02 .then(function (utilisateur) {
03 console.log(utilisateur); // en cas de succès
04 this.utilisateur = utilisateur; // en cas de succès
05 }, function (error) {
06 console.log(error); // en cas d'erreur
07 });

Ce code est fonctionnel, mais il n'est pas très lisible, vous ne trouvez pas ?
On peut améliorer cette lisibilité avec les "fonctions fléchées", que nous avons également
vues dans le chapitre sur ES6. Nous les utilisons beaucoup avec la programmation asynchrone,
car elles permettent de remplacer les fonctions anonymes, qui sont omniprésentes dans les appels
asynchrones en JavaScript, avec une syntaxe plus élégante.
Voici le même code que précédemment, mais avec l'utilisation des fonctions fléchées :
01 recupererUtilisateur(idUtilisateur)
02 .then(utilisateur => {
03 console.log(utilisateur); // en cas de succès
04 this.utilisateur = utilisateur; // en cas de succès
05 }, error => console.log(error); // en cas d'erreur
06 );

Bon d'accord, j'ai bien compris les promesses. Mais quel est le rapport avec le titre du
chapitre : la programmation réactive ?

Les promesses peuvent couvrir beaucoup de cas d'appels asynchrones, la gestion des
événements, etc. Mais les promesses ont aussi leurs limites, surtout quand il faut gérer un grand
nombre de requêtes dans un délai très court. Comment gérer proprement plusieurs événements en
même temps, sans saturer notre code de promesses dans tous les sens ? La réponse est la
programmation réactive.

La programmation réactive
Un véritable cours sur Angular ne peut pas passer à côté de la programmation réactive.
Plus qu'une librairie ou un effet de mode, la programmation réactive est une nouvelle manière
d'appréhender la programmation asynchrone... et c'est peut-être l'avenir des applications
modernes, vous n'auriez pas tort de parier dessus !

Introduction
La programmation réactive n'est pas quelque chose de nouveau, c'est simplement une façon
différente de concevoir une application. L'idée est de considérer les interactions qui se déroulent
dans l'application comme des événements, sur lesquels on peut effectuer des opérations :
regroupements, filtrages, combinaisons, etc.
Ainsi, les événements, comme les clics de souris, deviennent des flux d'événements
asynchrones auxquels on peut s'abonner, pour ensuite pouvoir y réagir. Et il possible de créer
des flux à partir de tout et n'importe quoi, pas seulement des clics ! Prenons des exemples
concrets. Il est très fréquent de devoir réagir à des événements :
· côté navigateur, en ajoutant des déclencheurs selon les interactions de l'utilisateur.
· côté serveur, en traitant des requêtes à la base de données ou à des services tiers.
Bref, dans la programmation réactive, toutes ces séquences d'événements sont appelées des
flux. Retenez ceci : Programmation réactive = Programmation avec des flux de données
asynchrones.
De manière générale, tous ces événements sont poussés par un producteur de données, vers
un consommateur. Notre rôle en tant que développeur sera de définir des écouteurs
d'événements (consommateur), sous forme de fonctions, pour réagir aux différents flux
(producteurs de données). Les écouteurs d'événements sont nommées des Observer et le flux lui-
même est le sujet observé, on parle d'Observable.

Lorsqu'on s'abonne à un flux pour capter ses événements, on dit que l'on s'inscrit au flux.

Qu'est-ce qu'un "flux" ?


Comme nous venons de le voir, un flux est une séquence d'événements en cours, qui sont
ordonnés dans le temps. Si on observe un utilisateur qui clique plusieurs fois sur un bouton (pour
une raison quelconque), la succession des clics peut être modélisée comme un flux d'événements.
Ce qui est intéressant, c'est que l'on peut appliquer des opérations sur ce flux d'événements.
Prenons un exemple : on souhaite détecter les doubles clics de l'utilisateur, et ignorer les
clics simples. Pour cela, nous allons considérer qu'il y a un double clic s'il y a moins de 250 ms
d'écarts entre deux clics. Nous pouvons modéliser notre problème grâce au schéma ci-dessous :
Figure 18 - Nous appliquons des opérations sur les flux d’évènements !
Les boîtes grises sur le schéma représentent des fonctions qui permettent de transformer un
flux en un autre flux modifié. En tout, il y a trois opérations qui sont appliquées à partir du flux
initial :
· D'abord, on regroupe les clics qui sont séparés par un délai inférieur à 250 millisecondes.
C'est ce que fait la fonction throttle. Même si nous n'avons pas vu cette fonction, pas de
panique, il s'agit juste de comprendre qu'elle permet de transformer un flux initial en un
nouveau flux, selon des critères donnés.
· On dispose maintenant d'un flux de clics regroupés. On applique dessus la fonction map
pour transformer chaque groupe en un entier, qui correspond à la longueur de la liste. Par
exemple, un groupe de deux clics vaut maintenant la valeur "2", tout simplement.
· Pour finir, il ne nous reste plus qu'à filtrer les groupes dont la valeur est supérieure à
deux. En effet, on considère comme un double clic tous les groupes de plus de deux clics.
On applique donc un filtre sur le flux précédent avec la fonction filter(x >= 2).
Ça y est ! En seulement 3 opérations, on a obtenu le flux souhaité ! On peut maintenant
s'abonner à ce flux et réagir aux événements comme on le souhaite ! Et cet exemple est juste la
partie visible de l'iceberg :
· Le flux sur lequel nous venons de travailler était simple, mais les opérations utilisées
peuvent aussi bien s'appliquer sur d'autres flux, comme par exemple un flux de réponses
d'un serveur distant.
· Et il existe bien sûr plus d'opérations que juste celles que nous venons de voir !

Si vous voulez voir quelles autres opérations sont disponibles pour les flux, je vous
recommande ce site, qui vous permet en plus de comprendre en un coup d'œil le
fonctionnement de telle ou telle opération.

Traitement des flux


On peut faire plus que simplement s'abonner à un flux. En fait, les flux peuvent émettre
trois types de réponses différentes. Pour chaque type de réponses, on peut définir une fonction à
exécuter :
· Une fonction pour traiter les différentes valeurs de la réponse : un nombre, des tableaux,
des objets, etc.
· Une fonction pour traiter le cas d'erreur.
· Une fonction pour traiter le signal de « fin ». Cela signifie simplement que le flux est
"terminé", et qu'il n'émettra plus d'événements.

Ce qu'il faut retenir, c'est que les événements du flux représentent soit les valeurs de la réponse
(en cas de succès), soit des erreurs, ou des terminaisons.

La librairie RxJS
Pour le moment, ce que nous avons vu est très théorique. Vous vous demandez sûrement à
quoi cela ressemble, concrètement. En fait, pour faciliter l'implémentation de la programmation
réactive, ou utilise souvent des librairies spécifiques. La bibliothèque la plus populaire pour la
programmation réactive, dans l'écosystème JavaScript, est RxJS. Et c'est également celle qui a
été choisie par l'équipe de développement d’Angular !
Bien sûr, il existe d'autres librairies pour la programmation réactive, comme bacon.js.
Cependant, dans ce cours nous allons nous concentrer sur RxJS, car c'est la librairie la plus
connue et la plus utilisée.

Les Observables
Dans RxJS, un flux d'événements est représenté par un objet appelé un Observable.
Les Observables sont très similaires à des tableaux. Comme eux, ils contiennent une
collection de valeurs. Un Observable ajoute juste la notion de valeur reportée dans le temps :
dans un tableau, toutes les valeurs sont disponibles immédiatement. Dans un observable en
revanche, les valeurs viendront au fur et à mesure, plus tard dans le temps.
On peut traiter un Observable avec des opérateurs similaires à ceux des tableaux. C'est
exactement ce que nous avons fait lorsque nous avons appliqué des opérations sur les flux. Par
exemple :
· take(n) : cette fonction va récupérer les n premiers éléments d'un flux et se débarrasser
des autres.
· map(fn) : applique la fonction passée en paramètre sur chaque événement et retourne le
résultat. Précédemment, on a appliqué cette méthode aux flux pour récupérer le nombre
d'éléments dans un groupe de clics.
· filter(predicat) : permet de filtrer seulement les événements qui répondent positivement
au prédicat passé en paramètre.
· merge(s1, s2) : permet de fusionner deux flux ! Et oui, c'est possible.
· subscribe(fn) : applique la fonction passée en paramètre à chaque événement reçu du
flux. C'est cette fonction que nous utiliserons le plus souvent. D'ailleurs, cette méthode
accepte aussi une deuxième fonction en paramètre, consacrée à la gestion des erreurs. Et
lorsque le flux est "terminé", il enverra un événement de terminaison que l'on peut détecter
avec une troisième fonction !
· et bien d'autres…
Pour illustrer le fonctionnement, imaginons un Observable, dans lequel nous remplaçons
les événements par des nombres afin de simplifier la compréhension :
01 Observable.fromArray([1, 2, 3, 4, 5])
02 .filter(x => x > 2) // 3, 4, 5
03 .map(x => x * 2) // 6, 8, 10
04 .subscribe(x => console.log(x)); // affiche le résultat

C'est exactement le même fonctionnement que pour les flux ! Un observable est une simple
collection asynchrone, dont les événements arrivent au cours du temps.
Et rappelez-vous, on peut construire des observables depuis une requête AJAX, un
événement du navigateur, une réponse de Web-Socket, une promesse, etc. Bref, tout ce qui est
asynchrone !

La librairie qui forme le cœur d'Angular a même un support basique pour les Observables.
Cependant, on est rapidement limité et il faut augmenter ce support avec des opérateurs et des
extensions issus directement de la librairie RxJS. Nous verrons cela au prochain chapitre.

Observable ou Promesse ?
Les observables sont différents des promesses, même s’ils y ressemblent par certains
aspects, car ils gèrent tous deux des valeurs asynchrones. Mais un observable n'est pas quelque
chose à usage unique : il continuera d'émettre des événements jusqu'à ce qu'il émette un
événement de terminaison ou que l'on se désabonne de lui.
Globalement, l'utilisation des promesses est plus simple, et dans de nombreux cas elles sont
suffisantes pour répondre aux besoins de votre application. Par exemple, pour récupérer des
données depuis un serveur distant et les afficher à l'utilisateur, l'utilisation d'un observable n'est
pas forcément nécessaire.

Dans le chapitre suivant, nous verrons deux exemples concrets, qui illustrent quand est-ce qu'il
est préférable de choisir un observable plutôt qu'une promesse.

Enfin, sachez qu'il est possible de transformer un Observable en une Promesse très
simplement, avec la méthode toPromise de RxJS :
01 import 'rxjs/add/operator/toPromise';
02
03 function giveMePromiseFromObservable() {
04 return Observable.fromArray([1, 2, 3, 4, 5])
05 .filter(x => x > 2) // 3, 4, 5
06 .map(x => x * 2) // 6, 8, 10
07 .toPromise();
08 }

Ensuite, plutôt que d'appliquer la méthode subscribe, propre aux observables, vous pourrez
utiliser la méthode then !
Pour le moment, les Observable ne font pas partie de la spécification ECMAScript officielle,
mais peut-être dans une version future. Des efforts sont faits dans ce sens.

Conclusion
La programmation réactive peut sembler un peu compliquée à appréhender au début, mais
elle permet d'élever le niveau d'abstraction de votre code, et au final vous devrez moins vous
soucier de certains détails d'implémentation.
De plus, les applications web évoluent sans cesse et sont maintenant de plus en plus
dynamiques : l'édition d'un formulaire peut déclencher automatiquement une sauvegarde sur un
serveur distant, un simple commentaire peut se répercuter directement sur l'écran de tous les
utilisateurs connectés, etc. En tant que développeurs, il nous faut des outils capables de gérer tout
ça, et la programmation réactive en fait partie.
Dans le prochain chapitre, nous verrons comment utiliser le module HTTP fourni par
Angular, pour récupérer des données d'un serveur distant. Nous verrons également un cas concret
où les observables nous seront utiles : un champ de recherche avec auto-complétion !

En résumé
· Les promesses sont natives en JavaScript depuis l'arrivée de la norme ES6.
· La programmation réactive implique de gérer des flux de données asynchrones.
· Un flux est une séquence d'événements ordonnés dans le temps.
· On peut appliquer différentes opérations sur les flux : regroupements, filtrages,
troncatures, etc.
· Un flux peut émettre trois types de réponses : la valeur associée à un événement, une
erreur, ou un point de terminaison pour mettre fin au flux.
· La librairie RxJS est la librairie la plus populaire pour implémenter la programmation
réactive en JavaScript.
· Dans RxJS, les flux d'événements sont représentés par un objet appelé Observable.
Chapitre 16 : Effectuer des requêtes HTTP
standards
Après un chapitre plutôt théorique sur la programmation réactive, nous allons maintenant
mettre en pratique nos connaissances pour améliorer notre application de Pokémons ! En effet,
pour le moment, cette application est assez simple et permet seulement d'éditer des Pokémons.
Ce que nous aimerions faire, c'est communiquer avec un serveur distant pour récupérer les
Pokémons, les éditer et sauvegarder les changements sur le serveur, en ajouter ou en supprimer.
Bref, que vous sachiez effectuer toutes les opérations http standards depuis votre application
Angular. C’est parti ?

Vous apercevrez le terme API plusieurs fois à partir de maintenant. Une API est une interface
de programmation. C'est ce qui vous permet de communiquer avec un service distant depuis
votre application. Par exemple, pour stocker les données sur un serveur distant de manière
durable.

Mettre en place le module HttpClientModule


Le module HttpClientModule permet de faire communiquer votre application Angular avec
un serveur distant, via le protocole HTTP.
Il ne s'agit pas d'un module de base d'Angular, et il faut l'importer depuis la librairie
@angular/common/http. Le fichier systemjs.config.js est déjà configuré pour charger cette
librairie, on peut donc l'importer depuis n'importe quel module de l'application. Nous allons le
déclarer dans la liste imports du module racine app.module.ts :
01 // ... on importe le module HttpClientModule
02 import { HttpClientModule } from '@angular/common/http';
03
04 @NgModule({
05 imports: [
06 // ...
07 HttpClientModule, // Ajoutez le module dans la liste 'imports'
08 // ...
09 ],
10 // ...
11 })
12 export class AppModule { }
Voilà, le module est désormais disponible partout dans l'application !

Simuler une API Web


Jusqu'à maintenant, nous stockions et récupérions nos données depuis le service
PokemonsService, en utilisant le fait que l'instance de ce service soit unique à travers toute
l'application. Seulement ce système ne nous convient plus, et nous voulons pouvoir
communiquer avec un serveur distant désormais !
Malheureusement, nous ne disposons pas d'un serveur pour gérer les requêtes de notre
application de Pokémons. D'ailleurs, il faudrait même suivre un autre cours, consacré
exclusivement à cette tâche !

Mais alors, comment va-t-on faire ?

Mais non, mais non ! En fait, Angular nous permet de simuler une API qui fonctionnera
comme si nous utilisions un serveur distant ! Cette simulation est possible grâce au module
InMemoryWebApiModule de la libraire angular-in-memory-web-api. Nous allons voir comment
cela fonctionne.
D’abord, installez cette librairie avec la commande suivante :
01 npm install angular-in-memory-web-api --save-dev

L’option –save-dev permet d’installer cette librairie en tant que dépendance de développement.
Comme nous simulons une API Web, nous en aurons besoin que lors de nos développements
normalement. (Cependant, dans le cadre de notre application de démonstration, nous utiliserons
notre API simulée même lors de déploiement).

Ensuite, créez un fichier nommé in-memory-data.service.ts, dans le dossier app, et ajoutez-


y le code suivant :
01 import { InMemoryDbService } from 'angular-in-memory-web-api';
02 import { POKEMONS } from './pokemons/mock-pokemons';
03
04 export class InMemoryDataService implements InMemoryDbService {
05 createDb() {
06 let pokemons = POKEMONS;
07 return { pokemons };
08 }
09 }

Contrairement aux apparences, ce service fait plus que de simplement renvoyer une liste de
Pokémons.
La classe InMemoryDataService implémente l'interface InMemoryDbService, qui nécessite
d'implémenter la méthode createDb. Cette méthode permet de simuler une petite base de
données et une API pour notre application.
Cette API met en place plusieurs points de terminaisons sur lesquels on peut effectuer des
requêtes :
01 // Exemple avec une collection de 'pokémons'
02 GET api/pokemons // renvoie tous les pokémons
03 GET api/pokemons/1 // renvoie le pokémon avec l'identifiant 1
04 PUT api/pokemons/1 // modifie le pokémon avec l'identifiant 1
05
06 // Cette requête retourne les pokémons dont le nom commence par 'exp'
07 GET api/pokemons?name=^exp // '^exp' est une expression régulière
Bien pratique n'est-ce pas ? Il est quasiment possible de faire toutes les requêtes possibles
pour une petite application de démonstration : ajout, suppression, modification, recherche, etc.
(Nous verrons la recherche dans le chapitre suivant)

Cette méthode peut servir pour des applications de tests ou de démonstrations. Bien sûr, pour
une vraie application vous utiliserez plutôt un serveur distant.

Nous allons voir dans la suite de ce chapitre comment utiliser cette API plutôt que les
données du fichier mock-pokemons.ts.
Il ne nous reste plus qu'à déclarer notre API simulée auprès du reste de l'application. Pour
cela, il est recommandé d'enregistrer les services globaux à toute l'application dans la liste
imports du module racine app.module.ts :
01 // ...
02 // Importations pour charger et configurer l'API simulée.
03 import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
04 import { InMemoryDataService } from './in-memory-data.service';
05 // ...
06
07 @NgModule({
08 imports: [
09 // ...
10 HttpClientModule,
11 HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService,
12 { dataEncapsulation: false }),
13 ],
14 // ...

La méthode de configuration forRoot prend en paramètre la classe InMemoryDataService,


qui apprête la base de données en mémoire avec les données du service :
01 HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService,
02 { dataEncapsulation : false})
Voilà, notre API est prête à être utilisée, au boulot !

Le module HttpClientInMemoryWebApiModule intercepte les requêtes HTTP et retourne les


réponses simulées du serveur. Ainsi, même s'il s'agit d'une API simulée, nous allons interagir
avec cette API comme avec n'importe quel service distant. C'est exactement le même
fonctionnement pour nous, côté Angular. Retirez l’importation quand un « vrai » serveur est
prêt à recevoir vos requêtes.

L’option dataEncapsulation permet de préciser le format des données renvoyées par l’API.
Les données peuvent être encapsulées dans un objet avec une clef data :
01 {
02 data : { // vos données seront ici si vous passez l’option à true … }
03 }
Dans notre cas, laissez cette option à false.

Mettre à jour notre service


Ça y est, le grand jour est arrivé, nous allons modifier notre service PokemonsService pour
effectuer des requêtes sur notre API !
Nous allons réécrire les deux méthodes du service PokemonsService, afin qu'elles appellent
désormais notre API :
· La méthode getPokemons qui retourne tous les Pokémons.
· La méthode getPokemon(id) qui retourne le Pokémon avec l’ identifiant id.
La méthode getPokemonTypes reste inchangée, puisqu'elle n'interagit pas avec notre API.
Commençons par préparer notre service aux appels vers notre API. Nous allons avoir
besoin de plusieurs éléments nouveaux :
· La classe HttpClient, pour effectuer des requêtes ;
· La classe HttpHeaders, pour pouvoir modifier les en-têtes de nos requêtes.
· La classe Observable, nous utiliserons des Observables pour chacune de nos requêtes.
· Les opérateurs tap, of et catchError de la librarie RxJS. Nous allons en avoir besoin lors
de nos appels à l’API.
Ajoutez donc ces nouvelles importations dans le fichier pokemons.service.ts, et injectez la
classe HttpClient dans le constructeur de notre service :
01 // les nouvelles importations dont nous allons avoir besoin.
02 import { HttpClient, HttpHeaders } from '@angular/common/http';
03 import { Observable, of } from 'rxjs';
04 import { catchError, map, tap } from 'rxjs/operators';
05
06 // le constructeur
07 constructor(private http: HttpClient) { }
08
09 private pokemonsUrl = 'api/pokemons'; // le point d’accés à notre API

J’ai également ajouté une nouvelle propriété privée pokemonsUrl, afin de déclarer le point
d’accès vers notre API à un seul endroit. Si jamais ce chemin devez changer, nous pourrions le
modifier simplement sans devoir réécrire toute nos requêtes. (Pour rappel, ce point de
terminaison est généré automatiquement pour notre API, grâce au service InMemoryDbService.)
Maintenant, nous pouvons développer des méthodes pour appeler notre API ! Modifions
tout de suite la méthode getPokemons comme ceci :
01 // Avant (avec 'mock-pokemons.ts')
02 getPokemons(): Pokemon[] {
03 return POKEMONS;
04 }
05
06 // Après (avec une requête Http)
07 getPokemons(): Observable<Pokemon[]> {
08 return this.http.get<Pokemon[]>(this.pokemonsUrl).pipe(
09 tap(_ => this.log(`fetched pokemons`)),
10 catchError(this.handleError('getPokemons', []))
11 );
12 }
On retourne toujours les mêmes Pokémons, mais on le fait différemment. Regardons ligne
par ligne ce que fait cette nouvelle méthode. D'abord la signature a changé :
01 getPokemons() : Observable<Pokemon[]> {…}

La méthode getPokemons retourne désormais un Observable, qui contiendra un tableau de


Pokémons. Ensuite :
01 return this.http.get<Pokemon[]>(this.pokemonsUrl).pipe(…);

La méthode http.get retourne un Observable, qui s'occupe d'envoyer une requête HTTP de
type GET sur la route 'api/pokemons'. Nous pouvons également typer directement les données de
retours grâce à la syntaxe :
01 get<Pokemon[]> // L’observable retournera un tableau d’objet Pokemon

Puis nous effectuons immédiatement deux opérations sur cet Observable grâce aux
opérateurs tap et catchError :
· L’opérateur tap : Il permet d’interagir sur le déroulement des événements générés par
l’Observable, en exécutant une action quelconque. Vous pouvez utilisez cet opérateur pour
le débogage ou l’archivage des logs par exemple. Dans notre cas, on affiche un petit
message dans la console, pour rappeler la méthode qui a été appelée.
· L’opérateur catchError : sans surprise, cette méthode permet intercepter les erreurs
éventuelles. On appelle alors la méthode handleError, que je vous présenterai tout de suite
après.
L’opérateur map peut servir à adapter le format de la réponse. Par exemple, pour convertir la
réponse au format JSON :
01 map(response => response.json())

Cependant nous n’avons pas besoin de faire ça, grâce au HttpClientModule. Ce module
s’occupe déjà pour nous de fournir les données de la réponse au format JSON.
Portez une attention particulière à la forme des données renvoyées par le serveur. Notre API
renvoie directement les données dans un objet. Mais une autre API peut renvoyer ses données
sous un autre format, à l’intérieur d’une propriété ‘data’, par exemple ! Soyez donc attentif à
cela et ajustez votre code en fonction de l'API que vous utilisez.
En attendant, les composants qui vont utilisé ce service n'ont pas à savoir que nous
récupérons les données d'une API ou d'un fichier interne à l'application. Et c'est bien le but :
déléguer l'accès aux données dans un service tiers ! En revanche, nous devons ajuster les
composants car désormais notre service renvoie des Observables.

Et quelle est cette méthode log appelée avec l’opérateur tap ?

C’est une petite méthode très simple, que voici :


01 private log(log : string) {
02 console.info(log) ;
03 }
Ajoutez cette méthode dans le service pokemons.service.ts. Cela permet de centraliser la
gestion des logs de notre service. Si plus tard vous souhaitez archiver vos logs, plutôt que de
simplement les afficher dans la console, il vous suffira de modifier cette méthode.
Gestion des erreurs
Nous devons également gérer les cas d'erreurs éventuelles, dans le cas où la requête
n'aboutirait pas. C'est le rôle de l’opérateur catchError :
01 catchError(this.handleError('getPokemons', []))

C'est une étape importante, nous devons anticiper les erreurs HTTP qui pourraient se
produire (pour des raisons indépendantes de notre volonté, bien sûr).

C’est à ça que va servir la méthode handleError ?

Oui ! Vous devez ajouter cette méthode dédiée à la gestion des erreurs dans le service
pokemons.service.ts :
01 private handleError<T>(operation='operation', result?: T){
02 return (error :any): Observable<T> => {
03 console.log(error);
04 console.log(`${operation} failed: ${error.message}`);
05
06 return of(result as T); // je donne des explications of un peu plus tard.
07 }
08 }

La syntaxe <T> en Typescript désigne un type : number, string…

Comme notre application est une simple démonstration, on se contente d'afficher une
erreur dans la console. Dans le cas d’une application en production, vous pourrez bien sûr mettre
en place un système plus élaboré !
Si vous ne comprenez pas exactement ce code, voici quelques éléments qui pourront vous
aider :
· Le paramètre 'operation' est le nom de la méthode qui a causé l’erreur. Par défaut, ce
paramètre vaut ‘operation’.
· Le paramètre 'results' est une donnée facultative, à renvoyer comme résultat de
l’Observable.
· L’instruction return, à la ligne 6, permet de laisser notre application fonctionnée en
renvoyant un résultat adapté à la méthode qui a levé l’erreur.
En effet, chaque méthode du service renvoie un Observable dont le résultat a un type
différent : un tableau, ou un objet Pokémon dans notre cas. La méthode handleError prend un
paramètre de type (le fameux T de TypeScript), afin que cette méthode puisse renvoyer une
valeur sûre pour chaque méthode, c’est-à-dire le type attendu par cette méthode. Ainsi,
l’application peut continuer à fonctionner même si une erreur survient.
L’opérateur of de RxJS
Nous utilisons l’opérateur of dans la méthode handleError, à la ligne 6 :
01 return of(result as T);

Cet opérateur permet de convertir n’importe quel argument passé en paramètre en


un Observable (un tableau, une chaîne de caractères, etc.). Par exemple :
01 return of([])

Cet extrait de code renvoie un simple tableau vide, mais sous forme d’Observable. C’est
bien pratique pour renvoyer un résultat vide, sans lever une erreur dans votre application (à cause
de la méthode de traitement subscribe que nous verrons un peu plus loin).

Récupérer un Pokémon à partir d'un identifiant


Nous devons également mettre à jour la méthode permettant de récupérer un unique
Pokémon à partir de son identifiant :
01 getPokemon(id: number): Observable<Pokemon> {
02 const url =`${this.pokemonsUrl}/${id}`; // syntaxe ES6
03
04 return this.http.get<Pokemon>(url).pipe(
05 tap(_ => this.log(`fetched pokemon id=${id}`)),
06 catchError(this.handleError<Pokemon>(`getPokemon id=${id}`))
07 );
08 }

Comme vous pouvez le constater, cette méthode est très similaire à celle que nous venons
de voir. La seule différence est l'url utilisée et le type de retour. Cette url retourne un seul
Pokémon, et non un tableau de plusieurs Pokémons.

Utiliser le service mis à jour


Le service PokemonsService renvoie désormais des Observables, et à ce titre nous devons
mettre à jour les composants qui consomment ce service, à savoir :
list-pokemon.component.ts :
01 // AVANT
02 getPokemons(): void {
03 this.pokemons = this.pokemonsService.getPokemons();
04 }
05
05 // APRES
07 getPokemons(): void {
08 this.pokemonsService.getPokemons()
09 .subscribe(pokemons => this.pokemons = pokemons);
10 }

detail-pokemon.component.ts & edit-pokemon.component.ts (cette portion de code est


identique pour les deux fichiers) :
01 // AVANT
02 ngOnInit(): void {
03 let id = +this.route.snapshot.params['id'];
04 this.pokemon = this.pokemonsService.getPokemon(id);
05 }
06
07 // APRES
08 ngOnInit(): void {
09 let id = +this.route.snapshot.params['id'];
10 this.pokemonsService.getPokemon(id)
11 .subscribe(pokemon => this.pokemon = pokemon);
12 }

Vous pouvez essayer de démarrer l'application avec npm start dès maintenant, et
contempler les Pokémons issues de votre API !
Si vous ne l’avez pas encore fait, vous pouvez supprimer l’importation de la constante
POKEMONS dans le fichier pokemons.service.ts. En effet, cette importation est devenue inutile,
car notre service charge les Pokémons depuis l’API simulée désormais.

Modifier un pokémon
Essayez d'éditer un Pokémon depuis l'application. Modifiez quelques-unes de ces
caractéristiques, et cliquez sur le bouton "valider". Vous êtes alors redirigé vers la page avec les
informations du Pokémon, et ... toutes les modifications ont disparu, elles n'ont pas été prise en
compte !

Mais pourquoi cela ne fonctionne plus ?

Eh bien, quand notre application utilisait une liste de données issue d'un fichier statique, les
modifications étaient répercutées directement aux Pokémons de cette liste unique, commune à
toute l'application. Mais maintenant que nous extrayons les données à partir d'une API, si nous
voulons persister nos changements, nous aurons besoin de les écrire sur le serveur, c'est-à-dire de
faire une requête HTTP !

Une méthode de modification


Nous avons besoin d'une nouvelle méthode dans notre service PokemonsService, pour
persister les modifications faites depuis le formulaire d'édition.
La structure de la requête pour modifier un Pokémon est similaire aux deux autres requêtes
précédentes, bien que nous allons utiliser une requête de type PUT (c’est le type de requête
utilisé pour modifier une ressource) afin de persister les changements côté serveur :
01 // ...
02
03 // La méthode updatePokemon persiste les modifications du pokémon via l’API
04 updatePokemon(pokemon: Pokemon): Observable<Pokemon> {
05 const httpOptions = {
06 headers: new HttpHeaders({'Content-Type': 'application/json'})
07 };
08
09 return this.http.put(this.pokemonsUrl, pokemon, httpOptions).pipe(
10 tap(_ => this.log(`updated pokemon id=${pokemon.id}`)),
11 catchError(this.handleError<any>('updatePokemon'))
12 );
13 }
14
15 // …
Il y a quelques éléments à signaler dans cette méthode :
· L'en-tête de la requête (ligne 5) : On déclare un en-tête pour signaler que le format du
corps de la requête sera du JSON. On ajoute ensuite cet en-tête à la requête Http à la ligne
9.
· Le point d’accès à l’API (ligne 9) : Les modifications s'appliquent sur un Pokémon
particulier, il faut donc renseigner l’url de l’API, le pokémon sur lequel on souhaite
effectuer une modification, ainsi qu’une en-tête pour signaler que le format de la requête
sera du format JSON.
· Le traitement de la requête (ligne 11 et 12) : On effectue les traitements « habituels »
avec les opérateurs tap et catchError. Mais vous connaissez déjà !

Chaque requête HTTP contient un en-tête et un corps. Il est possible d'ajuster les deux en
fonction des besoins de votre application.

Sauvegarder les données


Bien, nous avons ajouté une méthode permettant de persister les modifications effectuées
sur un Pokémon, il faut maintenant nous en servir ! Pour cela, nous allons modifier la méthode
qui gère la soumission du formulaire d'édition des Pokémons, dans pokemon-form.component.ts :
01 // La méthode appelée lorsque le formulaire est soumis pour la validation
02 onSubmit(): void {
03 this.pokemonsService.updatePokemon(this.pokemon)
04 .subscribe(() => this.goBack());
05 }
06
07 // J’ai découpé la redirection dans une méthode dédiée goBack
08 goBack() : void() {
09 let link = ['/pokemon', this.pokemon.id];
10 this.router.navigate(link);
11 }

La méthode onSubmit persiste les modifications effectuées sur le pokémon, en utilisant la


méthode updatePokemon que nous venons de développer. Ensuite l'utilisateur est redirigé vers la
page détaillant ce Pokémon, avec les dernières modifications prisent en compte, grâce à la
méthode goBack.
Rafraîchissez le navigateur et réessayer de modifier un Pokémon. Les changements réalisés
via le formulaire devraient maintenant être persistés ! Hourra !
Bien sûr, si vous redémarrez ou rafraîchissez l'application, la base de données sera réinitialisée
avec les données par défaut ! Pour persister les données entre deux sessions, il faudrait être en
possession d'un serveur distant, dédié à la sauvegarde.

Supprimer un pokémon
Je vous propose d’ajouter une nouvelle fonctionnalité permettant de supprimer un
Pokémon. Voici comment je vois les choses :
Figure 19 - On ajoute la possibilité pour l’utilisateur de supprimer un Pokémon
C’est le composant detail-pokemon.component.ts qui va implémenter cette nouvelle
fonctionnalité. Mais pour commencer, nous devons ajouter une méthode pour supprimer un
Pokémon dans notre service de gestion des Pokémons pokemons.service.ts :
01 // La méthode de suppression à ajouter dans le service des Pokémons
02 deletePokemon(pokemon : Pokemon): Observable<Pokemon> {
03 const url = `${this.pokemonsUrl}/${pokemon.id}`;
04 const httpOptions = {
05 headers: new HttpHeaders({ 'Content-Type': 'application/json' })
06 };
07
08 return this.http.delete<Pokemon>(url, httpOptions).pipe(
09 tap(_ => this.log(`deleted pokemon id=${pokemon.id}`)),
10 catchError(this.handleError<Pokemon>('deletePokemon'))
11 );
12 }
13
14 // …

Ensuite nous devons ajouter une méthode de suppression dans le composant detail-
pokemon.component.ts :
01 // Nouvelle méthode de suppression d’un Pokémon
02 delete(pokemon: Pokemon): void {
03 this.pokemonsService.deletePokemon(pokemon)
04 .subscribe(_ => this.goBack());
05 }
Enfin, il ne nous reste plus qu’à mettre à jour notre interface en conséquence. Repérer la
balise avec la classe card-action dans le template pokemon-detail.component.html. C’est le code
HTML qui correspond aux boutons permettant d’agir sur un Pokémon. Voici le nouveau code
HTML :
01 <div class="card-action">
02 <a class="waves-effect waves-light btn" (click)="goBack()">Retour</a>
03 <a class="waves-effect waves-light btn" (click)="goEdit(pokemon)">Editer</a>
04 <a (click)="delete(pokemon)">Supprimer</a>
05 </div>
On a ajouté un nouveau bouton de suppression à la ligne 4, et on a également ajouté des
classes pour mettre en avant les boutons Retour et Editer, afin d’éviter que l’utilisateur ne
supprime un Pokémon par erreur.
Bon, et si maintenant on ajouté un champ de recherche pour pouvoir retrouver un Pokémon
plus rapidement ?

Si vous êtes motivé, vous pouvez ajouter une page ou un message de confirmation lorsque
l’utilisateur clique sur le bouton Supprimer. Cela permettra d’éviter les suppressions « par
accident ».

Ajouter un pokémon
Vous vous en doutiez surement, il reste une dernière opération à voir : celle d’ajouter un
Pokémon. En effet, à force de supprimer des Pokémons, nous commençons à ne plus en avoir
dans notre application. J

Figure 20 - On va pouvoir ajouter de nouveaux Pokémons dans notre application !


Pour cette dernière fonctionnalité, nous allons avoir un peu plus de développement à faire,
car nous devons adapter le formulaire d’édition d’un Pokémon, afin de le réutiliser pour ajouter
un Pokémon.
Voici donc le « plan de bataille » que je vous propose :
· Commencer par ajouter une méthode permettant d’ajouter un Pokémon sur notre
API simulée, ce sera déjà ça de fait.
· Créer un composant AddPokemon pour gérer l’ajout d’un nouveau Pokémon.
· Adapter notre modèle pokemon.ts pour l’ajout d’un pokémon grâce à un
constructeur TypeScript, permettant de déclarer des valeurs par défaut pour un
nouveau Pokémon.
· Déclarer ce nouveau composant auprès du module des Pokémons, et du module de
gestion des routes.
· Adapter notre formulaire d’édition d’un Pokémon pour l’ajout.
· Ajouter un lien sur la page qui liste les Pokémons, pour rediriger l’utilisateur vers
notre le formulaire d’ajout.

Ajouter une méthode POST


Pour ajouter un nouveau Pokémon dans notre API, nous allons avoir besoin d’une requête
http de type POST. C’est le type de requête dédiée à l’ajout de nouveaux éléments. Bien sûr, le
module HttpClient prend en charge ce cas. Ajoutez donc la fonction addPokemon dans notre
service pokemons.service.ts :
01 /** POST pokemon */
02 addPokemon(pokemon: Pokemon): Observable<Pokemon> {
03 const httpOptions = {
04 headers: new HttpHeaders({ 'Content-Type': 'application/json' })
05 };
06
07 return this.http.post<Pokemon>(this.pokemonsUrl, pokemon, httpOptions).pipe(
08 tap((pokemon: Pokemon) => this.log(`added pokemon with id=${pokemon.id}`)),
09 catchError(this.handleError<Pokemon>('addPokemon'))
10 );
11 }

Le code d’ajout est très similaire à ce qu’on a déjà vu, à la différence près qu’ici nous
utilisons requête de type POST, à la ligne 7. Bon, ça c’est fait. Passons maitenant à la suite !

Un composant pour ajouter un Pokémon


Comme nous sommes des développeurs sérieux, nous allons créer un nouveau composant
qui aura pour rôle de gérer l’ajout d’un Pokémon (et non en ajoutant du code dans des fichiers
existants, avec des NgIf ou autre J. Et oui, je vous ai vu venir !)
Le composant pour ajouter un pokémon se nommera add-pokemon.component.ts :
01 import { Component, OnInit } from '@angular/core';
02 import { Pokemon } from './pokemon';
03
04 @Component({
05 selector: 'add-pokemon',
06 templateUrl: './app/pokemons/add-pokemon.component.html'
07 })
08 export class AddPokemonComponent implements OnInit {
09
10 pokemon: Pokemon = null;
11
12 constructor() { }
13
14 ngOnInit(): void {
15 this.pokemon = new Pokemon();
16 }
17 }

Ce composant est plutôt simple, la seule petite nouveauté est à la ligne 15. Nous créeons
une nouvelle instance de Pokémon. Comme nous voulons ajouter un nouveau Pokémon dans
notre API, il faut bien commencer à le créer quelquepart dans notre application. C’est donc ici
que nous le faison.
Par contre, comme nous allons le voir, notre modèle pokemon.ts n’est pas encore tout à fait
prêt. Nous n’avons pas définis de constructeur pour nos Pokémons. Il serait intéressant
d’initialiser un nouveau Pokémon avec des valeurs par défault. Sinon, lorsque l’utilisateur va
ajouter un nouveau Pokémon, l’ensemble des champs du formulaire seront marqués en erreur,
car toutes les propriétés du Pokémon seront vides par défaut !
Mais pour l’instant, passons au template associé à ce composant. Il sera dans le fichier add-
pokemon.component.html, que je vous invite à créer :
01 <h2 class="header center">Ajouter un Pokémon</h2>
02 <pokemon-form [pokemon]="pokemon"></pokemon-form>

Et oui, c’est tout. Comme nous somme de bons développeurs, et que nous avons déjà
développé beaucoup de code, nous réutilsons simplement le formulaire d’édition des Pokémons.
Pratique !

Adapter notre modèle


Comme je l’ai mentionné à l’instant, nous devons définir des valeurs par défaut pour notre
modèle pokemon.ts, grâce au constructeur de notre classe :
01 // … Les propriétés de notre modèle Pokémon …
02 constructor(
03 hp: number = 100,
04 cp: number = 10,
05 name: string = 'name',
06 picture: string = 'http://...',
07 types: Array<string> = ['Normal'],
08 created: Date = new Date()
09 ) {
10 this.hp = hp;
11 this.cp = cp;
12 this.name = name;
13 this.picture = picture;
14 this.types = types;
15 this.created = created;
16 }
17 }

Vous reconnaîtrez la syntaxe des paramètres par défaut d’ES6, de la ligne 3 à la ligne 6.
Cette syntaxe nous permet d’ instancier un Pokémon plus facilement. Par exemple :
01 // pokemon1 a 100 points de vie, c’est la valeur par défaut.
02 let pokemon1 = new Pokemon();
03 // Par contre, pokemon2 a 200 points de vie.
04 let pokemon2 = new Pokemon(hp = 200);

Ainsi, on se donne un peu de « souplesse » pour l’instanciation de nos Pokémons.


Si vous êtes attentif, vous avez peut-être remarquez que nous n’avons pas définis d’identifiant
par défaut pour nos Pokémons. Pour deux raisons : chaque pokémon doit avoir un identifiant
unique, donc il ne faut jamais attribuer deux fois le même identifiant. Et ensuite, et bien c’est le
rôle de l’API, car elle seule vous permet de garantir l’unicité des identifiants attribués. Donc
nous n’avons rien à faire de ce côté-là.

Importer ce nouveau composant AddPokemon dans l’application


Bon ce n’est pas tout, mais nous devons bien déclarer ce composant quelque part.
Il faut d’abord importer notre nouveau composant AddPokemon dans le module des
pokémons :
01 // … autres importations …
02 import { AddPokemonComponent } from './add-pokemon.component';
03
04 @NgModule({
05 imports: [
06 CommonModule,
07 FormsModule,
08 PokemonRoutingModule
09 ],
10 declarations: [
11 ListPokemonComponent,
12 DetailPokemonComponent,
13 EditPokemonComponent,
14 AddPokemonComponent, // <- Ajouter le composant AddPokemon ici.
15 PokemonFormComponent,
16 BorderCardDirective,
17 PokemonTypeColorPipe
18 ],
19 providers: [PokemonsService]
20 })
21 export class PokemonsModule { }

Et aussi le déclarer au niveau du système des routes, car notre composant devra être
accessible à une URL donnée pour nos utilisateurs. Cette route sera pokemon/app :
01 // … autres importations …
02 import { AddPokemonComponent } from './add-pokemon.component'; //<- à importer
03
04 const pokemonsRoutes: Routes = [
05 { path: 'pokemons', component: ListPokemonComponent },
06 { path: 'pokemon/add', component: AddPokemonComponent }, //<- ajouter la route
07 { path: 'pokemon/edit/:id', component: EditPokemonComponent },
08 { path: 'pokemon/:id', component: DetailPokemonComponent }
09 ];
10
11 @NgModule({
12 imports: [
13 RouterModule.forChild(pokemonsRoutes)
14 ],
15 exports: [
16 RouterModule
17 ]
18 })
19 export class PokemonRoutingModule { }

Voilà, nous sommes en règle pour notre nouveau composant, par rapport au reste de notre
application. Nos utilisateurs pourront ajouter un pokémon en accédant à la route : pokemon/add
que nous venons de mettre en place.

Adapter notre formulaire d’édition


Afin de pouvoir gérer le cas d’ajout d’un Pokémon, nous devons adapter notre formulaire
d’édition. Pour cela, nous devons détecter si notre formulaire est en mode édition ou ajout,
puisque que quand l’utilisateur va soumettre le formulaire, le comportement sera différent.

Hey, mais comment on va décter si on doit ajouter ou éditer un pokémon nous ? Côté
développement ? On ne peut pas deviner ce que l’utilisateur a dans la tête…

Vous avez (un peu) raison.


Effectivement, on ne peut pas deviner ce que les utilisateurs a dans la tête, mais on peut
deviner ses intentions. Et cela on peut le savoir facilement, grâce à l’url. Si l’url est de type :
/pokemon/edit/<id>, alors l’utilisateur souhaite éditer un pokémon. Et si l’url est de type
/pokemon/add, alors l’utilisateur veut ajouter un nouveau Pokémon dans l’application. Avouez
que vous n’y aviez pas pensé, si ? J
On va donc avoir besoin d’une nouvelle variable, que j’ai appelé isAddForm, pour savoir si
notre formulaire est en mode édition ou ajout. Ensuite, dans la méthode de cycle de vie ngOnInit
de notre composant, nous allons déterminer le mode du formulaire.
Le composant pokemon-form.component.ts que je propose ressemble à ça :
01 // … Les importations et les annotations …
02 export class PokemonFormComponent implements OnInit {
03
04 @Input() pokemon: Pokemon;
05 types: Array<string>;
06 isAddForm: boolean; // Le formulaire est-il en mode ajout (ou édition) ?
07
08 // … constructeur …
09
10 ngOnInit() {
11 // Initialisation de la propriété types.
12 this.types = this.pokemonsService.getPokemonTypes();
13 // On determine le mode adopté par le formulaire grâce à l’url.
14 // Si l’url contient le mot ‘add’, alors le formulaire est en mode ‘ajout’.
15 // Sinon, c’est que le formulaire doit être en mode édition.
16 this.isAddForm = this.router.url.includes('add');
17 }
18
19 // … autres méthodes …
20
21 // La méthode appelée lorsque le formulaire est soumis est différente
22 // en fonction du mode du formulaire.
23 // Nous effectuons deux actions différentes :
24 // * Soit on ajoute un nouveau Pokémon.
25 // * Soit on sauvegarde les modifications apportées sur un Pokémon.
26 onSubmit(): void {
27 if (this.isAddForm) { // Le formulaire est en mode ajout.
28 this.pokemonsService.addPokemon(this.pokemon)
29 .subscribe(pokemon => {
30 this.pokemon = pokemon;
31 this.goBack()
32 });
33 } else { // Le formulaire est en mode édition.
34 this.pokemonsService.updatePokemon(this.pokemon)
35 .subscribe(_ => this.goBack());
36 }
37 }
38
39 }

Les points les plus intérssants de ce composant sont expliqués dans les commentaires du
code. Cependant, voici quelques explications complémentaires :
· A la ligne 12 : Je détermine le mode du formulaire (ajout ou édtion), grâce à l’url.
Pour cela, on utilise la méthode url du service Router pour récupérer l’url courante.
Ensuite, la méthode include de JavaScript ES6 nous renvoie un booléen, pour savoir
si l’url courante contient le terme add ou non. C’est comme cela que l’on détermine
le mode du formulaire.
· De la ligne 19 à 30 : On gère la soumission du formulaire en fonction du mode
déterminé précédemment. Si le formulaire est en mode ajout, on appelle la méthode
addPokemon de notre service de gestion des pokémon. Cette méthode nous renvoie
le pokémon ajouté sur l’API, et on l’affecte à la propriété pokemon du composant
(c’est important d’attendre la réponse de l’API, car c’est elle qui détermine un
identifiant unique pour votre nouveau Pokémon), puis on redirige l’utilisateur vers la
page du nouveau Pokémon crée par l’utilisateur. Sinon, on effectue le traitement que
nous avions déjà mis en place pour enregistrer les modifications apportées sur un
Pokémon.
Ensuite, concernant le template pokemon-form.component.html, il ne reste qu’à ajouter un
champ de formulaire pour permettre l’ajout d’une image. Ce champ contenant la future image du
Pokémon est accessible uniquement dans le cas de la création d’un nouveau pokémon. On ne
pourra pas éditer l’image d’un Pokémon une fois crée.

Si toutefois vous souhaitez pouvoir éditer l’image d’un Pokémon, il vous suffit de supprimer la
directive NgIf à la ligne 2 ci-dessous :
01 <!-- Pokemon picture -->
02 <div *ngIf="isAddForm" class="form-group">
03 <label for="picture">Image</label>
04 <input type="url" class="form-control" id="picture" required
05 [(ngModel)]="pokemon.picture" name="picture" #name="ngModel">
06 <div [hidden]="name.valid || name.pristine" class="card-panel red accent-1">
08 L'image du pokémon est requise.
09 </div>
10 </div>

Voilà, notre formulaire est maintenant capable de traiter les cas d’ajout et d’édition.
Je vous rassure, on vient de faire la partie la plus dure.
Mais ce n’est pas tout à fait fini pour autant. En effet, comment nos utilisateurs vont
pouvoir accéder à ce formulaire d’ajour d’un Pokemon ? Et oui, on a oublié les utilisateurs dans
tout ça ! J
Ajouter un lien vers le formulaire d’ajout
Pour terminer, nous allons simplement ajouter un petit bouton sur la page qui affiche la
liste des Pokémons. Ce bouton permettra de rediriger l’utilisateur vers le formulaire d’ajout que
nous venons de créer.
Ajouter donc le code suivant à la fin du fichier list-pokemon.component.html :
01 <!-- Bouton pour rediriger l’utilisateur vers le formulaire d’ajout -->
02 <a class="btn-floating btn-large waves-effect waves-light red z-depth-3"
03 style="position: fixed; bottom: 25px; right: 25px;"
04 routerLink="/pokemon/add" href>
05 <strong>+</strong>
06 </a>

Je vous invite maintenant à vous rendre dans votre navigateur, pour admirer notre nouvelle
fonctionnalité en action.
Allez-y, ajoutez autant de Pokémon que vous voulez. Voici ce que j’ai fait de mon côté, je
vous donne cinq secondes pour trouver le petit nouveau J :

Figure 21 – Bon, c’est facile, je l’ai entouré en vert !

Conclusion
Nous avons bien avancé dans ce chapitre. Nous savons maintenant comment mettre en
place toutes les opérations http standards dans nos applications Angular. On parle des opérations
CRUD : Create, Read, Update, Delete. Et puis nous avons utilisés aussi quelques opérateurs de la
librarie RxJS : tap, catchError, of, etc.
Nous avons également pu voir comment simuler une API web en mémoire. C'est très
pratique pour les tests et les démonstrations. Vous pouvez commencer à développer vos requêtes
http avant même d'avoir un serveur à votre disposition !

En résumé
· Le module Angular utilisé pour effectuer des requêtes http sur le réseau est HttpClient.
· Il est possible de mettre en place une API web de démonstration au sein de votre
application. Cela vous permettra d'interagir avec un jeu de données configuré à l'avance.
· Le module HttpClient permet d’effectuer facilement les quatres opérations de base
nécessaire pour interagir avec une API : ajout, récupération, modification et suppression.
· RxJS fournit plusieurs opérateurs basiques pour traiter nos requêtes.
· Un même formulaire peut servir à ajouter ou éditer un élément dans votre application.
· C’est le rôle de l’API de déterminer un identifiant unique pour nos nouveaux objets.
Vous ne devez jamais le faire dans le code de l’interface, car vous ne pouvez pas garantir
que cet identifiant soit unique.
Chapitre 17 : Effectuer des traitements
asynchrones avec RxJS
Dans le chapitre précédent, nous avons pu voir comment effectuer toutes les opérations de
base avec http, mais je vous propose d’aller plus loin afin de profiter au mieux de la puisance de
la programmation réactive.
Ce que nous aimerions faire, c'est ajouter un champ de recherche sur la page d'accueil,
pour permettre à l'utilisateur de retrouver un Pokémon plus facilement. Il suffira de taper le nom
du Pokémon (ou juste le début de son nom), et notre application proposera au fur et à mesure les
Pokémons de notre API, correspondant au terme de la recherche de l’utilisateur. Qu'en pensez-
vous ?
En tous cas c’est un excellent entraînement pour progresser sur la programmation réactive
et RxJS.

Construire un champ de recherche


Je vous propose d'ajouter une nouvelle fonctionnalité pour pouvoir rechercher des
Pokémons via leur nom, sous la forme d'un champ de recherche. Ce champ de recherche devra
implémenter l'auto-complétion. Au fur et à mesure que l'utilisateur tapera un terme de
recherche, nous afficherons immédiatement une liste de Pokémons correspondant à ce critère.

Figure 22 - Notre système d'auto-complétion


Pour le moment, nous avons effectué des requêtes http relativement simple. On peut parler
de requêtes one-shot, on effectue la requête et on récupère un résultat.
Mais les requêtes ne sont pas toujours one-shot. Dans certains cas, on peut lancer une
requête, puis l'annuler, pour finalement faire une requête différente, avant que le serveur ait
répondu à la première !
Cette séquence requête-annulation-nouvelle requête est plus difficile à implémenter. C'est
ce que nous allons voir maintenant !

Un champ de recherche
Pour commencer, créons une nouvelle méthode searchPokemons pour le service
pokemons.service.ts :
01 searchPokemons(term: string): Observable<Pokemon[]> {
02 if (!term.trim()) {
03 // Si le terme de recherche n'existe pas,
04 // on renvoie un tableau vide sous la forme d’un Observable avec ‘of’.
04 return of([]);
05 }
06
07 return this.http.get<Pokemon[]>(`api/pokemons/?name=${term}`).pipe(
08 tap(_ => this.log(`found pokemons matching "${term}"`)),
09 catchError(this.handleError<Pokemon[]>('searchPokemons', []))
10 );
11 }

La fonction searchPokemons retourne un Observable qui renvoie des tableaux de


Pokémons. Dans la méthode get j'utilise une url spéciale, qui permet de filtrer les Pokémons
d'après leur nom (la propriété name), en fonction d’un terme de recherche entré par l’utilisateur.

Consommer un Observable
Comment utiliser l'Observable renvoyer par notre méthode searchPokemons, depuis un
composant ? Pour illustrer le fonctionnement, nous allons créer un nouveau composant
PokemonSearchComponent. Ce composant aura pour rôle d'afficher et de gérer le champ de
recherche des Pokémons, et l'auto-complétion.
Le template de ce composant sera simple : un champ de recherche, et une liste de résultats
correspondants aux termes de la recherche. Voici le code à ajouter dans le fichier search-
pokemon.component.html :
01 <div class="row">
02 <div class="col s12 m6 offset-m3">
03 <div class="card">
04 <div class="card-content">
05 <div class="input-field">
06 <input #searchBox (keyup)="search(searchBox.value)"
07 placeholder="Rechercher un pokémon..." />
08 </div>
09
10 <div class='collection'>
11 <a *ngFor="let pokemon of pokemons$ | async"
12 (click)="gotoDetail(pokemon)" class="collection-item" >
13 {{ pokemon.name }}
14 </a>
15 </div>
16 </div>
17 </div>
18 </div>
19 </div>

Hormis les balises nécessaires pour les feuilles de style, les lignes de code qui nous
intéressent se situent :
· à la ligne 6 : Au fur et à mesure que l'utilisateur tape dans la barre de recherche, la liaison
de l'événement keyup appelle la méthode search du composant avec la dernière valeur
présente dans la barre de recherche.
· de la ligne 10 à 13 : La directive NgFor affiche une liste de Pokémons, issue de la
propriété pokemons du composant associé. Mais, comme nous le verrons bientôt, la
propriété pokemons$ est maintenant un Observable et non plus un simple tableau ! La
directive NgFor ne peut rien faire avec un Observable à moins que nous n'appliquions le
pipe async dessus. Le pipe async appliqué à l'Observable permet de n'afficher le résultat
que lorsque l'Observable a retourné une réponse, et s'occupe de synchroniser la propriété
du composant avec la vue !

Ce pipe async, qui signifie "asynchrone", peut également s'appliquer sur les Promesses !

Et pourquoi est-ce qu’il y a un $ à la fin de la propriété pokemons$ ?

Il s’agit d’une convention pour indiquer que la propriété pokemons$ n’est pas une simple
propriété, mais un Observable, c’est-à-dire un flux.
Et maintenant, passons à la classe du composant ! Créer le fichier search-
pokemon.component.ts dans le dossier app/pokemons :
01 import { Component, OnInit } from '@angular/core';
02 import { Router } from '@angular/router';
03
04 import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators;
05 import { Observable, Subject, of } from 'rxjs';
06
07 import { PokemonsService } from './pokemons.service';
08 import { Pokemon } from './pokemon';
09
10 @Component({
11 selector: 'pokemon-search',
12 template: `./app/pokemons/search-pokemon.component.html`,
13 })
14 export class PokemonSearchComponent implements OnInit {
15 private searchTerms = new Subject<string>();
16 pokemons$: Observable<Pokemon[]>;
17
18 constructor(
19 private pokemonSearchService: PokemonSearchService,
20 private router: Router) {}
21
22 // Ajoute un terme de recherche dans le flux de l'Observable 'searchTerms'
23 search(term: string): void {
24 this.searchTerms.next(term);
25 }
26
27 ngOnInit(): void {
28 this.pokemons$ = this.searchTerms.pipe(
29 // attendre 300ms de pause entre chaque requête
30 debounceTime(300),
31 // ignorer la recherche en cours si c'est la même que la précédente
32 .distinctUntilChanged(),
33 // pour chaque terme de recherche
34 // on renvoie la liste des Pokémons correspondants à cette recherche.
35 .switchMap((term: string) => this.pokemonsService.searchPokemons(term))
36 );
37 }
38
39 gotoDetail(pokemon: Pokemon): void {
40 let link = ['/pokemon', pokemon.id];
41 this.router.navigate(link);
42 }
43 }

Voyons les explications de ce code :

Les termes de la recherche


Commençons par mettre l'accent sur la propriété searchTerms, à la ligne 15 :
01 private searchTerms = new Subject<string>();
02 // …
03
04 // Ajoute un terme de recherche dans le flux de l'Observable 'searchTerms'
05 search(term: string): void {
06 this.searchTerms.next(term);
07 }
La classe Subject n'appartient pas à Angular, mais à la librairie RxJS. C'est une classe
particulière qui va nous permettre de stocker les recherches successives de l'utilisateur dans un
tableau de chaînes de caractères, sous la forme d'Observable, c'est-à-dire avec une notion de
décalage dans le temps. Nous allons voir en quoi cela va nous être utile. Retenez que la classe
Subject hérite d'Observable, et donc searchTerms est bien un Observable.
C'est la propriété searchTerms qui permet de stocker ces recherches successives. Chaque
appelle à search ajoute une nouvelle chaîne de caractère à searchTerms.

On utilise la fonction next car searchTerms n'est pas un tableau, auquel cas on aurait pu
simplement utiliser la fonction push.

Initialiser un Observable
Un Subject est un Observable : c'est ce qui va nous permettre de transformer le flux de
termes de recherche en un flux de tableaux de pokémons, et d'affecter le résultat à la propriété
pokemons. Voici la méthode qui s'occupe de ça :
01 ngOnInit(): void {
02 this.pokemons$ = this.searchTerms.pipe(
03 // attendre 300ms de pause entre chaque requête
04 debounceTime(300),
05 // ignorer la recherche en cours si c'est la même que la précédente
06 .distinctUntilChanged(),
07 // pour chaque terme de recherche
08 // on renvoie la liste des Pokémons correspondants à cette recherche.
09 .switchMap((term: string) => this.pokemonsService.searchPokemons(term))
10 );
11 }

Pourquoi le code paraît aussi "compliqué" ?

En fait, la problématique est la suivante : si nous passons chaque nouvelle saisie de


l'utilisateur au PokemonService, nous allons déclencher une tempête de requêtes HTTP. Et ce
n'est pas forcément une bonne idée ! Nous ne voulons pas surcharger notre API inutilement !
Heureusement, nous pouvons réduire ce flux de requêtes grâce à un certain nombre
d'opérateurs. Nous faisons moins d'appels au PokemonsService, tout en obtenant toujours des
résultats aussi rapides. Voici comment :
· debounceTime(300) : Met en pause l'exécution de nouvelles requêtes tant qu'une
nouvelle recherche a été lancé il y a moins de 300 ms. Ainsi, il n'y a jamais de requêtes
lancées plus fréquemment que toutes les 300ms.
· distinctUntilChanged : S'assure que nous envoyons une requête uniquement si le terme
de la recherche a été modifié. En effet, il est inutile d'effectuer une autre requête pour la
même recherche !
· switchMap : Appelle la méthode searchPokemons pour chaque terme de recherche qui
passe à travers le filtre debounceTime et distinctUntilChanged. Cet opérateur annule et
rejette les termes précédents de la recherche, et retourne seulement le plus récent, c'est-à-
dire celui que l'utilisateur a saisie en dernier. Pratique !

Afficher le champ de recherche


Nous aimerions afficher la barre de recherche au-dessus de la liste de tous les Pokémons.
Pour cela, rien de plus simple, il suffit d'utiliser la balise du composant : <pokemon-search>
</pokemon-search> !
Modifiez donc le template du composant list-pokemon.component.ts, en ajoutant la balise
<pokemon-search> à la ligne 5 :
01 <h1 class='center'>Pokémons</h1>
02 <div class='container'>
03 <div class="row">
04
05 <pokemon-search></pokemon-search>
06
07 <div *ngFor='let pokemon of pokemons' class="col s6 m4">
08 <div class="card horizontal" (click)="selectPokemon(pokemon)"
09 pkmnBorderCard>
10 <div class="card-image">
11 <img [src]="pokemon.picture">
12 </div>
13 <div class="card-stacked">
14 <div class="card-content">
15 <p>{{ pokemon.name }}</p>
16 <p><small>{{ pokemon.created | date:"dd/MM/yyyy" }}</small></p>
17 <span *ngFor='let type of pokemon.types'
18 class="{{ type | pokemonTypeColor }}">{{ type }}</span>
19 </div>
20 </div>
21 </div>
22 </div>
23 </div>
24 </div>

Enfin, on importe PokemonSearchComponent, et on l'ajoute au tableau declarations du


module des Pokémons :
01 // Dans pokemons.module.ts, importez ceci :
02 import { PokemonSearchComponent } from './search-pokemon.component';
03
04 // ...
05 declarations: [
06 // ...
07 PokemonSearchComponent // <= ... et ajoutez cela !
08 ],

Lancez l'application à nouveau, vous serez alors redirigé vers la liste de tous les Pokémons.
Entrez du texte dans le nouveau champ de recherche qui est apparu. Vous devriez voir le système
d’auto-complétion à l’œuvre !
Pas mal, non ? Et on peut encore améliorer l'application !

Ajouter une icône de chargement


Avez-vous remarquez qu'il y a un petit délai de chargement entre les pages ? Depuis que
notre application utilise une API plutôt que les données issues d'un fichier, elle est un peu plus
lente !
Rassurez-vous, c'est un comportement parfaitement "normal". Notre application est plus
lente car nous récupérons les données depuis un service tiers, et nous devons attendre une
réponse avant de pouvoir afficher les résultats. Cependant, nous pourrions rendre ce temps
d'attente plus agréable pour l'utilisateur en ajoutant une icône de chargement.
L'icône de chargement que nous allons utiliser est un élément fourni par Materialize.
Pour cela, je vous propose de créer un nouveau composant pour représenter l'icône de
chargement. Créer donc un fichier loader.component.ts dans le dossier /app :
01 import { Component } from '@angular/core';
02
03 @Component({
04 selector: 'pkmn-loader',
05 template: `
06 <div class="preloader-wrapper big active">
07 <div class="spinner-layer spinner-blue">
08 <div class="circle-clipper left">
09 <div class="circle"></div>
10 </div><div class="gap-patch">
11 <div class="circle"></div>
12 </div><div class="circle-clipper right">
13 <div class="circle"></div>
14 </div>
15 </div>
16 </div>
17 `
18 })
19 export class LoaderComponent {}

Ce composant n'a pas de logique propre, il se contente d'afficher une petite icône circulaire.
Il peut ensuite être injecté à tel ou tel endroit dans l'application, selon vos besoins. Nous allons
l'afficher sur la page de détail d'un Pokémon, en attendant d'avoir récupérer les données
nécessaires depuis l'API.
Dans le template detail-pokemon.component.html :
01 <!-- Avant -->
02 <h4 *ngIf='!pokemon' class="center">Aucun pokémon à afficher !</h4>
03
04 <!-- Après -->
05 <h4 *ngIf='!pokemon' class="center">
06 <pkmn-loader></pkmn-loader>
07 </h4>

Dans le fichier pokemon-form.component.html, remplacer à la fin :


01 <!-- Avant -->
02 <h3 *ngIf="!pokemon" class="center">Aucun pokémon à éditer...</h3>
03
04 <!-- Après -->
05 <h4 *ngIf='!pokemon' class="center">
06 <pkmn-loader></pkmn-loader>
07 </h4>

Il ne nous reste plus qu'à déclarer le nouveau composant dans le module Pokémons :
01 // pokemons.module.ts, ajouter ceci…
02 import { LoaderComponent } from '../loader.component';
03
04 // ...
05 declarations: [
06 // ...
07 LoaderComponent // ... et cela !
08 ],
Voilà, c'est bon ! Vous pouvez redémarrer l'application et profiter des icônes de
chargement !

Conclusion
Nous avons pu voir un aperçu de l'utilisation des Observables. Même si les Observables
peuvent être difficile à appréhender, essayez de vous habituer à la programmation réactive, et au
changement de paradigme que cela représente. Le développement web va vraiment dans ce sens,
voyez-le comme un investissement pour l'avenir.

En résumé
· Les Observables permettent de faciliter la gestion des événements asynchrones.
· Les Observables sont adaptés pour gérer des séquences d'événements.
· Les opérateurs RxJS ne sont pas tous disponibles dans Angular. Il faut étendre cette
implémentation en important nous-même les opérateurs nécessaires.
· Les opérateurs RxJS permettent de traiter des flux.
· Le traitement des flux de RxJS permet de transformer un flux de chaîne de caractères en
un flux d’objets métiers. Dans notre cas, on a pu transformer les termes de recherches de
l’utilisateur en une liste de Pokémons correspond aux critères de la recherche. Plutôt
puissant !
Chapitre 18 : Authentification
Pour le moment, notre application n'est pas très fiable en termes de sécurité. N'importe quel
utilisateur peut consulter ou modifier des Pokémons dans l'application. Or ce n'est pas forcement
ce que nous voulons !
Il serait plus sûr de demander à l'utilisateur de s'authentifier avant de pouvoir interagir avec
des Pokémons. Si l'utilisateur entre les bons identifiants, alors il est redirigé vers la liste des
Pokémons, sinon il reste bloqué sur le formulaire de connexion.
Pour mettre en place une authentification, nous allons avoir besoin des Guards. Un guard
est un mécanisme de protection utilisé par Angular pour mettre en place l'authentification, mais
pas seulement. En avant !

Un Guard
Les Guards peuvent être utilisé pour gérer toutes sortes de scénarios liés à la navigation :
rediriger un utilisateur qui tente d'accéder à une route, l'obliger à s'authentifier pour accéder à
certaines fonctionnalités, etc.
Pour autant, les Guards reposent sur un mécanisme relativement simple, ils retournent un
booléen, qui permet de contrôler le comportement de la navigation. Par exemple :
· Si le Guard retourne true, alors le processus de navigation continue.
· Si le Guard retourne false, alors le processus de navigation cesse, et l'utilisateur reste sur
la même page.
Un Guard peut retourner un booléen de manière synchrone ou asynchrone. Mais dans la
plupart des cas, un Guard ne peut pas renvoyer un résultat de manière synchrone, car il doit
attendre une réponse. En effet, il peut être nécessaire de poser une question à l'utilisateur, de
sauvegarder des changements, ou de récupérer des données plus récentes sur le serveur. Toutes
ces actions sont asynchrones !

Dans la plupart des cas, le type de retour d'un Guard est Observable<boolean> ou
Promise<boolean>, et le Router attendra la réponse pour agir sur la navigation.

Même si un Guard est seulement conçu pour interagir avec la navigation, il en existe des
types différents :
· Le Guard CanActivate peut influencer sur la navigation d'une route, et notamment la
bloquer. C'est ce type de Guard que l'on va utiliser pour construire un système
d'authentification dans notre application.
· Le Guard CanActivateChild peut influencer sur la navigation d'une route fille.
· Le Guard CanDeactivate peut empêcher l'utilisateur de naviguer en dehors de la route
courante. Cela peut s'avérer utile lorsque l'utilisateur à oublié de valider un formulaire,
avant de continuer à naviguer.
· Le Guard Resolve peut effectuer une récupération de données avant de naviguer.
· Le Guard CanLoad peut gérer la navigation vers un sous-module, chargé de manière
asynchrone.
On peut avoir différents Guards, à tous les niveaux du système de navigation. Cependant, si à
un moment un Guard retourne false, tous les autres Guards en attente seront annulés, et la
navigation entière sera bloquée.

Dans ce chapitre, nous n'aurons besoin que du Guard de type CanActivate, pour construire le
système d'authentification. La documentation officielle explique le fonctionnement de chacun
des autres types de Guards, mais les explications sont en anglais !

Mise en place d'un Guard


Les applications ont souvent besoin de restreindre l'accès à certaines fonctionnalités, en
fonction de l'utilisateur. Par exemple, obliger l'utilisateur à s'authentifier pour accéder à son
espace membre. Il faut alors bloquer ou limiter l'accès jusqu'à ce que l'utilisateur se soit
connecté.
Afin d'illustrer la mise en place d'un Guard, nous allons nous contenter d'afficher un
message dans le navigateur lorsque l'utilisateur essaye d'accéder à la page d'édition d'un
Pokémon, car il s'agit d'une route sensible, on ne veut pas que tout le monde puisse modifier les
Pokémons.

Comme nous l'avons vu précédemment, nous utiliserons un Guard de type CanActivate.

Nous allons développer ce Guard dans le fichier auth-guard.service.ts. Ce fichier sera


placé dans le dossier app :
01 import { Injectable } from '@angular/core';
02 import { CanActivate } from '@angular/router';
03
04 @Injectable()
05 export class AuthGuard implements CanActivate {
06 canActivate() {
07 alert('Le guard a bien été appelé !');
08 return true;
09 }
10 }

Bien sûr, pour le moment nous sommes seulement intéressés de voir comment mettre en place
un Guard, donc cette première version ne fait rien de très d'utile. Il affiche un message
d'information à l'utilisateur et renvoie immédiatement true, ce qui permet à la navigation de
s'effectuer normalement par la suite, pour ne pas être bloquée.
Ensuite nous devons déclarer ce Guard auprès de notre système de navigation. Comme
nous voulons surveiller la route d'édition d'un Pokémon, ouvrez le fichier pokemons-
routing.module.ts et modifiez le comme suit :
01 // ... les autres importations ...
02 // Le guard de l'authentification
03 import { AuthGuard } from '../auth-guard.service';
04
05 // routes definition
06 const pokemonsRoutes: Routes = [
07 { path: 'pokemons', component: ListPokemonComponent },
08 { path: 'pokemon/add', component: AddPokemonComponent },
09 { path: 'pokemon/edit/:id', component: EditPokemonComponent,
10 canActivate: [AuthGuard] }, // <-- Ajouter le guard.
11 { path: 'pokemon/:id', component: DetailPokemonComponent }
12 ];

Ensuite, il ne nous reste plus qu'à déclarer notre Guard au niveau du module des Pokémons
:
01 // on importe notre guard
02 import { AuthGuard } from '../auth-guard.service';
03 // ...
04
05 // ajouter notre guard au fournisseur
06 providers: [
07 PokemonsService,
08 AuthGuard
09 ]

Lancez l'application avec npm start si ce n'est pas encore fait, et essayez de vous rendre sur
la page d'édition d'un pokémon. Une pop-up s'ouvre et vous indique que le Guard a bien été
appelé !

Mais, pour protéger dix routes, je vais devoir déclarer dix fois le même guard ?

Non, rassurez-vous ! En fait on peut déclarer nos routes différemment dans pokemons-
routing.module.ts :
01 const pokemonsRoutes: Routes = [
02 {
03 path: 'pokemon',
04 canActivate: [AuthGuard],
05 children: [
06 { path: 'all', component: ListPokemonComponent },
07 { path: 'add', component: AddPokemonComponent },
08 { path: 'edit/:id', component: EditPokemonComponent },
09 { path: ':id', component: DetailPokemonComponent }
10 ]
11 }
12 ];
Ainsi, toutes nos routes sont protégées d'un coup. A la ligne 3, l'élément path permet de
préfixer toutes les routes de notre module. Je me suis permis de renommer la route /pokemons en
/pokemon/all pour avoir un préfixe commun à toute les routes du module. C'est plus "élégant" au
niveau du code.
Avant de lancer l'application, nous devons encore faire deux choses :
· Retirer la pop-up d'alert dans le guard et la remplacer par un message dans la console.
Sinon, des pop-up s'ouvriront en permanence lorsque l'utilisateur naviguera dans
l'application ! Cela pourrait ressembler à du harcèlement publicitaire.
· Modifier la configuration des routes du module principale pour faire pointer la route par
défaut sur la nouvelle route /pokemon/all. Et aussi dans les composants qui pointent vers
l’ancienne url (PageNotFoundComponent et DetailPokemonComponent).
D'abord, commençons par modifier notre Guard :
01 // avant
02 alert('Le guard a bien été appelé !');
03
04 // après
05 console.info('Le guard a bien été appelé !');

La modification est simple. Il ne nous reste plus qu'à modifier la route par défaut dans app-
routing.module.ts :
01 // ...
02 const appRoutes: Routes = [
03 { path: '', redirectTo: 'pokemon/all', pathMatch: 'full' },
04 { path: '**', component: PageNotFoundComponent }
05 ];
06 // ...
... ainsi que les redirections du composant page-not-found-component.ts ...
01 <!-- Utiliser la nouvelle url dans le template du composant -->
02 <!-- ... -->
03 <a routerLink="/pokemon/all" class="waves-effect waves-teal btn-flat">
04 Retourner à l' accueil
05 </a>
06 <!-- ... -->
... et detail-pokemon.component.ts :
01 // Mettez à jour la méthode goBack du composant
02 goBack(): void {
03 this.router.navigate(['/pokemon/all']);
04 }

Parcourez à nouveau l'application, vous verrez le message du Guard s'afficher dans la


console !
Voilà, nos pokémons sont protégés par le Guard, ... mais bien mal protégés en fait !

Vers un système d'authentification plus complet


Actuellement toutes les routes de notre module Pokémons sont accessibles, à tous le
monde. Mais nous voulons rendre ce module accessible uniquement aux utilisateurs authentifiés,
et avec un système fiable !
Nous allons donc améliorer notre Guard afin de rediriger les utilisateurs anonymes vers un
formulaire de connexion lorsqu’ ils essayeront d'accéder à l'application. Ce formulaire sera
constitué d’un champ « name » et « password », comme la plupart des formulaires :
Figure 23 - Notre formulaire de connexion pour accéder aux Pokémons
Notre Guard va avoir besoin de l'aide d'un nouveau service, dédié à l'authentification. Cela
nous permettra de bien découper le Guard et le service, dédié aux éléments métiers de la
connexion.
Rappelez-vous : un fichier, une tâche !
Ce service sera chargé de connecter ou de déconnecter l'utilisateur courant, et de retenir
l'url à laquelle il souhaitait accéder avant d'être bloqué par le Guard, afin d'être redirigé au bon
endroit après son authentification !
Créez donc un fichier auth.service.ts dans le dossier app :
01 import { Injectable } from '@angular/core';
02
03 import { Observable, of } from 'rxjs;
04 import { tap, delay } from 'rxjs/operators';
05
06 @Injectable()
07 export class AuthService {
08 isLoggedIn: boolean = false; // L'utilisateur est-il connecté ?
09 redirectUrl: string; // où rediriger l'utilisateur après l'authentification ?
10
11 / / Une méthode de connexion
12 login(name: string, password: string): Observable<boolean> {
13 // Faites votre appel à un service d'authentification si besoin ...
14 let isLoggedIn = (name === 'pikachu' && password === 'pikachu');
15
16 return of(true).pipe(
10 .delay(1000),
17 .tap(val => this.isLoggedIn = isLoggedIn);
18 }
19
20 // Déconnexion
21 logout(): void {
22 this.isLoggedIn = false;
23 }
24 }

Ce service met à disposition plusieurs propriétés et méthodes :


· La propriété isLoggedIn (ligne 8) : C’est un booléen qui permet de savoir si l'utilisateur
courant est connecté ou non. Par défaut, l’utilisateur est déconnecté lorsque l’application
démarre.
· La proprieté redirectUrl (ligne 9) : elle stocke l'url demandé par l'utilisateur quand il
n'était pas encore connecté, pour pouvoir le rediriger vers cette page après son
authentification.
· La méthode login (ligne 12) : cette méthode simule une connexion à une API externe en
retournant un Observable qui se résout avec succès si l’utilisateur a entré les identifiants
« admin/admin », après un délai d'une seconde. Bien sûr, vous pouvez personnaliser cette
méthode pour correspondre à vos propres besoin de connexion par la suite.
· La méthode logout (ligne 21) : sans surprise, cette méthode permet de déconnecter
immédiatement l'utilisateur courant. D'ailleurs, attention ! L'utilisateur se verra alors
redirigé vers la page de connexion, sans qu'on lui demande son avis !
Il ne s'agit pas d'une « vraie » connexion ici, mais cela permet de faire fonctionner notre
application de démonstration. En revanche, vous avez tout ce dont vous avez besoin pour
construire un formulaire d'accès pour vos utilisateurs, avec votre propre API, grâce aux
chapitres précédents.
Maintenant modifions notre guard auth-guard .service.ts, pour consommer ce nouveau
service :
01 import { Injectable } from '@angular/core';
02 import {
03 CanActivate, Router,
04 ActivatedRouteSnapshot,
05 RouterStateSnapshot
06 } from '@angular/router';
07 import { AuthService } from './auth.service';
08
09 @Injectable()
10 export class AuthGuard implements CanActivate {
11 constructor(
12 private authService: AuthService, private router: Router){}
13
14 // La méthode du Guard :
15 // détermine si l'utilisateur peut se connecter ou non !
16 canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot)
17 : boolean {
18 let url: string = state.url;
19 return this.checkLogin(url);
20 }
21
22 // Méthode d'aide pour le Guard, qui interroge notre service.
23 checkLogin(url: string): boolean {
24 if (this.authService.isLoggedIn) { return true; }
25 this.authService.redirectUrl = url;
26 this.router.navigate(['/login']);
27
28 return false;
29 }
30 }

Désormais, on fait appel à notre nouveau service pour déterminer si un utilisateur est
authentifié ou non. Remarquez qu'on injecte en plus le AuthService et le Router dans le
constructeur.
En paramètre de la méthode canActivate, l'objet route, de type ActivatedRouteSnapshot,
contient la future route qui sera appelée. L'objet state, de type RouterStateSnapshot, contient le
futur état du Routeur de l'application, qui devra passer la vérification du Guard.
Nous n'avons pas encore défini de fournisseur pour le AuthService, mais rien ne vous empêche
d'injecter des services dans vos Guards.
Ce Guard retourne un booléen de manière synchrone comme résultat. Si l'utilisateur est
connecté, alors le guard retourne true et la navigation continue. Sinon, nous stockons l'url de la
route à laquelle l'utilisateur a tenté d'accéder dans la propriété redirectUrl, à la ligne 25. Ensuite
on redirige l'utilisateur vers la page de connexion. Une page que nous allons créer tout de suite !

Une page de connexion


Il nous manque encore une chose : nous avons besoin d'un composant qui se charge
d'afficher une page de connexion à l'utilisateur. Créez donc un fichier nommé
login.component.ts, dans le dossier app.
Pour être plus concis, j’ai mis le template et le composant dans le même fichier. Le fichier
étant assez long, et n’apportant aucun élément nouveau (il s’agit d’un formulaire piloté par le
template comme nous l’avons déjà vu), je vous invite à récupérer le code en ligne.
Vous remarquerez que j’ai ajouté deux nouvelles propriétés et une méthode :
· Une propriété name de type string.
· Une propriété password de type string également.
· Une méthode setMessage pour prévenir l’utilisateur s’il a réussi à s’authentifier ou non.
Après l’authentification, l'utilisateur sera redirigé vers l'url stockée dans la propriété
redirect (si cette propriété est non nulle, sinon on utilisera l'url par défaut "/ pokemon/all", qui
redirigera l'utilisateur vers la liste des pokémons).
Ensuite, nous ajoutons le fichier de configuration de la connexion dans le dossier app,
login-routing.module.ts :
01 import { NgModule } from '@angular/core';
02 import { RouterModule } from '@angular/router';
03 import { AuthGuard } from './auth-guard.service';
04 import { AuthService } from './auth.service';
05 import { LoginComponent } from './login.component';
06
07 @NgModule({
08 imports: [
09 RouterModule.forChild([
10 { path: 'login', component: LoginComponent }
11 ])
12 ],
13 exports: [
14 RouterModule
15 ],
16 providers: [
17 AuthGuard,
18 AuthService
19 ]
20 })
21 export class LoginRoutingModule {}

On utilise ce module pour enregistrer la route /login, et également pour déclarer les
fournisseurs du Guard et de notre service d'authentification, dans le tableau providers à la ligne
16.
Pour terminer, dans le fichier app.module.ts, nous devons importer le composant
LoginComponent et l'ajouter dans les déclarations du module racine app.module.ts. On importe
aussi LoginRoutingModule dans les imports du AppModule, sans oublier d’ajouter le
FormsModule car nous avons ajouté un nouveau formulaire dans notre application. Les
commentaires ci-dessous indiquent les lignes que vous devez ajouter :
01 // ... autres importations ...
02 import { FormsModule } from '@angular/forms';
03 import { LoginComponent } from './login.component';
04 import { LoginRoutingModule } from './login-routing.module';
05
06 @NgModule({
07 imports: [
08 // ...
09 FormsModule, // <- ici, le FormsModule
10 PokemonsModule,
11 LoginRoutingModule, // <- ici, au-dessus du AppRoutingModule !
12 AppRoutingModule
13 ],
14 declarations: [
15 AppComponent,
16 LoginComponent, // <- ... et ici !
17 PageNotFoundComponent
18 ],
19 bootstrap: [ AppComponent ]
20 })
21 export class AppModule { }
Les Guards et les fournisseurs de service dont ils ont besoin doivent être fournis au niveau
du module racine ! Cela permet au Routeur de récupérer ces services à partir de l'injecteur
pendant le processus de navigation.
Maintenant, essayer d'accéder à votre application. Automatiquement, vous serez redirigé
vers la page de connexion, et vous devrez vous authentifier avant de pouvoir continuer ! On
dirait que ça fonctionne !

Conclusion
Nous savons désormais comment mettre en place un vrai système d'authentification dans
notre application ! Cela a été possible grâce aux Guards. Ils en existent d’autres, mais tous ont en
commun de pouvoir modifier le comportement par défaut de la navigation.
Dans le prochain chapitre, nous allons voir un point important : comment déployer notre
application ? C'est une problématique qui se pose dans tous les projets, et nous allons voir
comment cela se passe pour Angular.

En résumé
· L'authentification nécessite la mise en place d'un système fiable : on utilise pour cela les
Guards.
· Les Guards permettent de gérer toutes sortes de scénarios liés à la navigation :
redirection, connexion, etc.
· Les Guards reposent sur un mécanisme simple. Ils retournent un booléen de manière
synchrone ou asynchrone, qui permet d'influencer le processus de navigation.
· Il existe plusieurs types de Guards différents. Le type utilisé pour l'authentification est
CanActivate.
· Il faut toujours déclarer les Guards au niveau du module racine, ainsi que les services
tiers qu'ils utilisent.
Chapitre 19 : Déployer votre application
C'est la dernière étape, nous allons voir comment préparer notre application pour la
production ! Pour le moment votre application est seulement sur votre machine, et personne
d'autre que vous ne peut la voir. Il serait dommage de ne pas en faire profiter les autres !

Le terme "Production" désigne un environnement spécifique en informatique. Il y a


l'environnement de développement lorsque vous développez, l'environnement de test lorsque
vous testez, ... et l'environnement de production, lorsque votre application est prête pour être
montrée aux utilisateurs. C'est d'ailleurs le seul environnement auquel vos utilisateurs ont accès
normalement !

Dans ce chapitre, nous verrons comment déployer notre application sur Firebase Hosting.
C'est une entreprise, appartenant à Google, qui propose d'héberger gratuitement des sites
statiques en dessous d'un certain quota d'utilisation. C'est exactement ce dont nous avons besoin !
Le seul pré-requis est de disposez d'un compte Google. Si vous n'en n'avez pas, vous pouvez en
créez un gratuitement ici.
Voici un aperçu de l'application développée pendant ce cours, directement sur les serveurs
de Firebase.
Nous verrons dans ce chapitre comment effectuer un déploiement simple et surtout
fonctionnel. Pour des techniques plus poussées, je vous recommande de jeter un œil aux concepts
exposés dans la documentation officielle pour le déploiement.

Le processus de déploiement
L'application fonctionne déjà sur notre machine locale, nous pouvons donc déployer
l'application simplement en copiant tous les fichiers du projet vers le serveur distant. Cependant
cette méthode n'est pas très optimisée, l'application ne sera pas rapide pour les utilisateurs, et en
plus Firebase Hosting dispose de place limitée dans sa version gratuite. Bref, nous devons
effectuer un certain nombre d'opérations sur notre projet avant de le déployer.
Cependant, le processus de déploiement entier dépasse le cadre de ce chapitre !
En effet, il faudrait que nous voyions comment souscrire un compte chez un hébergeur,
voir toutes les techniques d'optimisations possibles avant le déploiement, comment configurer
votre serveur de production, etc ...
Pour être clair :
· Ce que nous verrons dans ce chapitre : Quels sont les éléments intéressants à optimiser
avant le déploiement sur Firebase Hosting. L'objectif est fonctionnel et permet de conclure
ce cours sur une note positive ! Votre site sera en ligne et vous pourrez envoyer un lien
vers votre application Angular à tous vos ami(e)s et collègues...
· Ce que nous ne verrons pas dans ce chapitre : Comment déployer cette archive chez
un hébergeur classique ou sur vos propres serveur. Mais là je vous redirige vers ce cours
qui explique très bien comment faire. Vous verrez comment déployer un site statique ne
contenant que des fichiers HTML, CSS et JavaScript. C'est exactement ce que contient le
dossier dist de notre application !

Liste des tâches


Avant de commencer, voici une courte liste des tâches que nous devons accomplir pour
déployer notre projet :
· Préparer notre projet en local avant le déploiement. Nous verrons quels sont les éléments
les plus importants à optimiser.
· Créer le projet sur le site de Firebase. Cela vous donnera accès à une console
d'administration d'où vous pourrez consulter l'historique de vos déploiements, revenir à une
version plus ancienne de votre projet, ...
· Enfin, déployer votre application sur Firebase pour de bon !
Allez hop, c'est parti ! Le monde entier va bientôt pouvoir découvrir votre travail !

Se préparer pour la production


Comme on l'a vu plus haut, on ne peut pas se contenter de déployer notre application en
copiant tous les fichiers locaux vers le serveur. Il faut effectuer au moins deux opérations
auparavant :
· Supprimer le dossier nodes_modules : Ce dossier contient beaucoup plus de code que
nécessaire pour exécuter votre application. À titre d'exemple, le dossier node_modules
d'une simple application « Hello, World !» contient typiquement plus de 20 000 fichiers et
pèse plus de 180 MB ! Cela fait beaucoup de fichiers inutiles.
· Passer l'application en mode production avant de la déployer sur le serveur. Angular
pourra ainsi optimiser plusieurs éléments en interne.
Voyons pas à pas ensemble comment réaliser ces deux tâches :

Charger les modules depuis le web


Il faut du temps pour télécharger tous les fichiers du dossier node_modules, d’autant que
nous ne les utilisons pas tous. Et les utilisateurs vont devoir attendre que ces fichiers se
téléchargent peu à peu. Or notre application ne nécessite qu'une infime fraction de ces fichiers
pour fonctionner !
A la place, nous allons charger seulement les fichiers dont nous avons besoin depuis le
Web, grâce au site unpgk. Il s'agit d'un site qui met à disposition des librairies sur Internet. Nous
pouvons l'utiliser pour charger les paquets dont nous avons besoin, avec la syntaxe suivante
: https://unpkg.com/package@version/file. Par exemple, pour charger la version 2.4.0 du module
@angular/core, nous appellerons cette adresse :
https://unpkg.com/@angular/core@4.3.4/bundles/core.umd.min.js
Ce que je vous propose, c'est de charger les modules dont nous avons besoin depuis ce site.
Il faut donc modifier la configuration de SystemJS car nous ne voulons plus charger nos modules
depuis node_modules mais depuis unpkg.com. Créez donc un fichier systemjs.config.server.js,
dans le même dossier que le fichier systemjs.config.js, dont le contenu disponible à l’adresse
suivante :
https://awesome-angular.com/ebook/
Il s’agit d’un fichier de configuration assez long, il n’y pas beaucoup d'intérêt à l’ajouter
dans cet ouvrage. Il est donc à disposition en ligne.

Comme vous pouvez le constater, à la ligne 8 nous avons changé le chemin de l'alias. Nous
chargeons directement les packets depuis le web désormais !

Modifier le fichier index.html


Toutes les requêtes HTTP effectuées vers votre application passent par le fichier
index.html, il s'agit du principal point d'entrée ! Nous devons donc adapter ce fichier pour
l'environnement de production. Il y a plusieurs modifications à apporter.
D'abord, il faut modifier certaines importations : Polyfills, RxJS et SystemJS. Ces libraires
sont chargées depuis node_modules actuellement. Il faut donc modifier ces importations :
01 <!-- Polyfills -->
02 <script src="https://unpkg.com/core-js/client/shim.min.js"></script>
03 <!-- RxJS & SystemJS -->
04 <script src="https://unpkg.com/zone.js@0.8.4?main=browser"></script>
05 <script
06 src="https://unpkg.com/systemjs@0.19.40/dist/system.src.js"></script>

Maintenant ces librairies sont aussi chargées depuis le web !


Ensuite, nous devons charger la nouvelle configuration de SystemJS :
01 <!-- Configuration de SystemJS qui charge les packets depuis le web -->
02 <script src="systemjs.config.server.js"></script>
Enfin, vérifiez que l’on charge directement les fichiers depuis le dossier dist, dédié à la
production :
01 <script>
02 System.import('dist/main.js')
03 .catch(function(err){ console.error(err); });
04 </script>
Voilà, notre projet est presque prêt.

Activer le mode production


Les applications Angular s'exécutent en mode développement par défaut. D'ailleurs, ce
message doit s'afficher dans la console de votre navigateur depuis le début du cours :
“Angular is running in the development mode. Call enableProdMode() to enable the
production mode.”
Le passage en mode production permet de rendre l'application plus rapide en désactivant
les vérifications spécifiques au développement. Pour activer le mode production lorsque
l'application sera exécutée sur le serveur distant, ajouter le code suivant au fichier main.ts :
01 import { platformBrowserDynamic }
02 from '@angular/platform-browser-dynamic';
03 import { AppModule } from './app.module';
04 import { enableProdMode } from '@angular/core';
05
06 // Activer le mode production,
07 // à moins que l'application s'éxecute en local
08 if (!/localhost/.test(document.location.host)) {
09 enableProdMode();
10 }
11 platformBrowserDynamic().bootstrapModule(AppModule)
12 .catch(err => console.log(err));

L'application tournera en mode production quand elle s'exécutera sur le serveur distant, et
continuera à fonctionner en mode développement sur votre machine locale.

Une dernière vérification


Avant de passer au déploiement, nous allons vérifier que tout fonctionne bien sur votre
machine. Lancez donc la commande npm start. Ensuite, laissez tourner la commande,
n'interrompez pas le script !

Si l'application ne se lance pas correctement, reprenez les étapes ci-dessus une à une, et
assurez-vous de n'avoir rien manqué.

Maintenant, allez dans votre explorateur de fichiers, et supprimez le dossier node_modules.


Retournez dans votre navigateur : l'application fonctionne toujours !
Vous pouvez vérifier dans la console de votre navigateur la provenance des packets
chargés, ils proviennent bien du site unpkg.com :

Figure 24 - Les packets de l'application se chargent bien depuis le web désormais !


Les paquets nécessaires à l'application sont bien chargés depuis le web, super !
Notre application peut maintenant être déployée !
Si vous voulez reprendre le développement de l'application, il faudra quand même réinstaller le
dossier node_module avec la commande npm install. En effet, nous ne chargeons pas
TypeScript depuis le web.

Déployer sur Firebase Hosting


Le déploiement sur Firebase Hosting est assez simple, mais nécessite certaines tâches
préliminaires lors du premier déploiement :
· Créer un projet dans Firebase : cela vous donnera accès à une console d'administration
pour votre projet.
· Installer Firebase-CLI en local : il s'agit d'une sorte de boîte à outil, mise à disposition
par Firebase, pour vous permettre de déployer votre application en une seule ligne de
commande sur leurs serveurs !
· Configurer votre projet auprès de Firebase : pour préparer les éléments spécifiques au
déploiement.
· Déployer votre application de pokémons sur Firebase : et y accéder sur Internet
ensuite !
Pour rappel, il s'agit d'un déploiement fonctionnel minimum, et il y a des tas d’autres
optimisations possibles !

Créer le projet dans Firebase


Pour commencer, connectez-vous à la console d'administration de Firebase. Vous devez
entrer vos identifiants Google si vous n'étiez pas déjà connecté.
Vous verrez un bouton, au milieu à droite de la page, intitulé "Créer un projet". Cliquez
dessus. Une boîte modale va s'ouvrir et vous demander de choisir un nom et un pays pour votre
projet. Le choix du pays permet d'indiquer l'emplacement de vos données et une devise pour vos
éventuels revenus, mais ça n'a pas beaucoup d'importance pour la suite.
De mon côté, j'ai renseigné "ng6-pokemons-app" comme nom, et j'ai sélectionné "France"
pour le pays :

Figure 25 - Création d'un nouveau projet sur le site de Firebase


Cliquez sur "Créer un projet". Vous arrivez ensuite dans un espace dédié à l'administration
de votre application. Dans le menu, à gauche de l'interface, vous trouvez un onglet intitulé
"Hosting". Il s'agit du service que nous allons utiliser, et c'est à cet endroit que vous pourrez
consulter l'historique de vos déploiements sur Firebase.
Maintenant, vous avez un projet disponible sur Firebase, prêt à accueillir les fichiers de
votre application.

Installer la boîte à outil de Firebase


Nous devons maintenant installer l'utilitaire fournis par Firebase : Firebase-CLI. Cet outil
permet de déployer facilement des applications sur les serveurs de Firebase.

CLI est l'acronyme de "Command-line Interface" ou "Interface en ligne de commande", en


Français. Cela vous permet d'exécuter certaines commandes de l’utilitaire de Firebase depuis
un terminal.

L'installation de Firebase-CLI est plutôt simple, tapez la commande suivante dans votre
console :
npm install –g firebase-tools

Ensuite patientez un peu ... La commande peut prendre quelques minutes à s'exécuter, le
temps de télécharger tous les éléments nécessaires.
Pour mettre à jour Firebase-CLI plus tard, il suffit de tapez à nouveau la même commande !
Une fois l'installation terminée, on peut exécuter les commandes de FirebaseCLI. Avant de
commencer à utiliser Firebase-CLI, exécutez ces deux commandes :
· firebase --version : Cela permet de vérifier que l'utilitaire est bien installé sur votre
machine. Pour vous donner un ordre d'idée, j'ai la version 3.18.4 installée sur ma machine.
· firebase login : Cette instruction permet de lier votre compte Google (et donc vos projets
Firebase) à l'utilitaire que vous venez d'installer. Suivez donc les instructions dans votre
terminal jusqu'à obtenir un message du type "You are logged in as <l’adresse email de
votre compte Google>".
Si ce message apparaît, cela signifie que vous êtes bien connecté ! Vous pouvez passer à
l'étape suivante !

Configurer votre projet pour le déploiement


Avant de configurer votre projet pour le déploiement, relancez les commandes npm install
et npm start pour être sûr que tout fonctione correctement.
Maintenant, nous allons à nouveau utiliser Firebase-CLI pour configurer notre projet avant
le déploiement. Voilà ce que l'on veut pouvoir dire à Firebase : "Ok, ce dossier local sur ma
machine doit correspondre au projet que j'ai créé sur Firebase tout à l'heure, et je veux déployer
mes fichiers sur tes serveurs distants".
La commande magique qui permet de faire tout cela est :
firebase init
Vous devez vous placer à la racine de votre projet avant d’exécuter cette commande.
Un certain nombre d'éléments vous seront demandés lorsque vous exécuterez cette
commande :
Figure 26 - La commande de configuration "firebase init" en action !
Voici les questions qui vous seront posées, et comment y répondre :
· Which Firebase CLI features do you want to setup for this folder ? Suivez les
instructions affichées dans la console et séléctionnez uniquement l’option Hosting :
Configure and deploy Firebase Hosting sites. Nous ne nous servirons que de cette
fonctionnalité proposée par Firebase pour notre déploiement.
· Select a default Firebase project for this directory ? Déplacez le curseur sur le nom de
l'application que vous avez créée dans la console d'administration de Firebase
précédemment. Ainsi, Firebase pourra faire le lien entre votre archive locale et le projet en
ligne.
· What do you want to use as your public directory ? Firebase veut savoir le chemin
relatif du dossier de votre projet à déployer. Il s'agit généralement du dossier contenant le
fichier index.html. Dans notre cas, mettez simplement "src", pour signaler que le fichier
index.html dans ce dossier. Nous verrons ensuite comment dire à Firebase quels fichiers du
projet nous ne voulons pas déployer.
· Configure as a single page app (rewrite all urls to /index.html) ? Parfait, Firebase
nous propose de s'occuper de configurer les redirections sur le serveur ! Taper donc "y"
pour dire que vous êtes d'accord. Nous verrons où est-ce que Firebase a écrit ces règles de
redirection.
· File ./index.html already exists. Overwrite ? Surtout pas ! Firebase nous demande
gentiment s'il peut écraser notre fichier index.html. Bien sûr, ce n'est pas ce que nous
voulons. Tapez "N" puis appuyer sur Entrée pour refuser.
Ça y est, nous avons fini de répondre aux questions. L'utilitaire a créé deux fichiers pour
nous : .firebaserc et firebase.json. Seul le dernier fichier nous intéresse, car il permet de voir les
fichiers qui ne seront pas déployer. Ouvrez donc le fichier firebase.json :
01 {
02 "hosting": {
03 "public": "src",
04 "ignore": [
05 "firebase.json",
06 "**/.*",
07 "**/node_modules/**"
08 ],
09 "rewrites": [
10 {
11 "source": "**",
12 "destination": "/index.html"
13 }
14 ]
15 }
16 }

Ce fichier a été créé en fonction des réponses que nous avons données au-dessus. On peut
vérifier que les règles de redirection vers index.html sont bien présentes de la ligne 9 à 12. C'est
une configuration du serveur indispensable pour que votre application fonctionne.
Si vous déployez sur d’autres serveurs que les serveurs de Firebase, vous devez adapter la
configuration du serveur.
Sur la documentation pour la configuration du fichier firebase.json, on apprend que
l’option ignore indique à Firebase-CLI les fichiers ou dossiers que nous ne voulons pas déployer
sur les serveurs de production, de la ligne 4 à 7.
Voilà, nous pouvons déployer. Enfin !

Déployer l'application
C'est maintenant que Firebase-CLI va se révéler être vraiment génial. Nous allons déployer
notre application en une seule commande :
firebase deploy
Une fois que la commande a fini de s’exécuter, votre site est disponible en ligne
immédiatement ! L'adresse par défaut pour accéder à votre site est : https://<nom-du-
projet>.firebaseapp.com.

Si vous le souhaitez, Firebase vous propose d'associer un autre nom de domaine à votre
application, si celui donné par défaut ne vous convient pas.

Si vous êtes vraiment fainéant, vous pouvez même taper une autre commande pour
demandez à Firebase d'afficher le site à votre place dans un navigateur :
firebase open

Ensuite sélectionner l'option Hosting : Deployed Site. Firebase-CLI va s'occuper pour vous
d'ouvrir un navigateur à la bonne adresse !
Sinon, regardez dans votre invite de commande, la propriété Hosting Url correspond à l’url
de votre site en ligne.
Pour rappel, si vous utilisez Chrome et que l’extension AdBlock est activée, il se peut que votre
application affiche une simple page blanche. Cela est du à un conflit entre l’extension AdBlock
et la libraire zone.js, requise pour faire fonctionner Angular.
Pour remédier à ce problème, il suffit de désactiver l’extension AdBlock pour votre nouvelle
application. De toute façon, vous n’avez pas besoin de cette extension pour vos sites locaux, qui
n’affichent pas de publicité !

Conclusion
Et bien, félicitations ! Votre site est en ligne et accessible au monde entier !
Vous êtes parvenus au bout de ce cours sur Angular avec succès, et vous avez déployé
votre application sur un serveur distant. Et ça, ce n'est pas rien. Encore bravo !
Avant de vous laisser, je me dois de vous rappeler que nous sommes loin d'avoir optimisé
tout ce que l'on pouvait. Je vous redonne le lien vers la documentation officielle qui offre
plusieurs pistes pour un déploiement plus optimisé.
Et pour finir sur une note plus positive, sachez que vous pouvez regarder les autres services
que Firebase propose pour améliorer votre application : ajouter une base de données, associer un
nouveau nom de domaine pour votre site...
Bonne continuation dans vos futurs apprentissages, et merci d'avoir suivi ce cours en
entier. Pour terminer en beauté, un dernier questionnaire vous attend ! J

En résumé
· Le déploiement est une étape dans un projet qui consiste à faire fonctionner une
application sur l'environnement de production.
· Avant de déployer un projet local sur une machine de développement, il est nécessaire de
prévoir une étape d'optimisation.
· Le site unpkg.com permet de charger des paquets npm depuis le web.
· Firebase propose un utilitaire en ligne de commande nommé Firebase-CLI, afin de
déployer en ligne facilement des applications web statiques. Cela fonctionne aussi bien
pour les applications qui ne sont pas développées avec Angular.
· Je vous recommande de lire la documentation officielle pour mieux connaître toutes les
optimisations possibles que vous pouvez faire en local, avant le déploiement.
Questionnaire n°3
L'objectif de ce questionnaire est de valider les acquis théoriques et pratiques de la dernière
partie du cours, notamment le fonctionnement du module HTTP et du processus
d'authentification.
Le questionnaire est noté sur 10 points.
Le questionnaire est accessible en ligne à cette adresse. (https://goo.gl/bp51qL)
Bon courage !
Partie 4

Annexes
Annexe 1 : Des titres pour vos pages
Actuellement, nous sommes capables de définir un titre pour les pages de notre application,
en ajoutant la balise <title> dans le fichier index.html. Cependant, notre application devrait être
en mesure de modifier ce titre dynamiquement en fonction de la page qui est affichée à
l'utilisateur. Nous allons voir dans ce chapitre comment le faire.

Pour information, un des éléments de base de toute stratégie de référencement est l'optimisation
de la balise <title> !

Le problème avec la balise <title>


La question qui se pose à nous est la suivante :

Comment définir dynamiquement un titre aux pages de notre application ?

L'approche la plus évidente serait à première vue d'utiliser l'interpolation pour lier une
propriété du composant à la balise HTML <title> :
01 <title>{{ title_of_component }}</title>

Malheureusement, cela ne marchera pas !

Et pourquoi pas ? C'est bien comme ça que l'on fait de l'interpolation, non ?

En fait, le problème n'est pas là. Le composant racine de votre application est un élément
contenu à l'intérieur de la balise <body>. La balise HTML <title> est dans le document
<head>, en dehors du corps, ce qui le rend inaccessible à la liaison de données Angular !
Nous pourrions récupérer l'objet du navigateur nommé document et définir le titre
manuellement. Ce n'est pas très propre et compromet nos chances d'exécuter un jour l'application
en dehors du navigateur.
Pour rappel, vous n'êtes pas obligé d'exécuter votre application à l'intérieur d'un navigateur !
Vous pouvez tirer profit du pré-rendu côté serveur pour servir l'application de manière quasi-
instantanée. Vous pouvez également utiliser Electron.js ou Windows Universal pour développer
des applications à destination des clients de bureaux lourds.

Utiliser le service Title


Heureusement, Angular fournit un service nommé Title. Ce service est une simple classe,
qui fournit deux méthodes pour récupérer et modifier le titre du document HTML courant.
· getTitle() : string —Récupère le titre du document HTML courant.
· setTitle(newTitle : string) — Définit un nouveau titre pour le document HTML courant.

La classe de ce service est sur la documentation officielle.

Maintenant nous allons utiliser ce service Title. Il faut injecter le service dans le composant
où l'on souhaite l'utiliser. Etant donné que ce service est appelé à être utilisé dans toute
l'application, nous allons l'injecter dans le composant racine, AppComponent. Ensuite on peut
définir un titre à la page, grâce à la méthode setTitle à la ligne 5 :
01 // … autres importations…
02 import { Title } from '@angular/platform-browser';
03
04 export class AppComponent {
05 public constructor(private titleService: Title ) { }
06
07 public updateTitle(title: string) {
08 this.titleService.setTitle(title);
09 }
10 }

Il n'y a rien d'extraordinaire dans ce code : nous utilisons le mécanisme classique


d'injection de dépendance pour rendre le service disponible dans le composant.
Bien entendu, il faut également ajouter un fournisseur pour ce service, dans le module
racine de notre application :
01 import { NgModule } from '@angular/core';
02 // On importe le service 'Title'.
03 import { BrowserModule, Title } from '@angular/platform-browser';
04 import { AppComponent } from './app.component';
05
06 @NgModule({
07 imports: [
08 BrowserModule
09 ],
10 declarations: [
11 AppComponent
12 ],
13 providers: [
14 Title // On fournis le service 'Title' à l’ensemble de l’application.
15 ],
16 bootstrap: [ AppComponent ]
17 })
18 export class AppModule { }

Conclusion
Vous savez désormais comment définir des titres dynamiques pour vos pages. Vous
pouvez proposer une expérience plus agréable à vos utilisateurs, en précisant le titre de la page
sur laquelle ils se trouvent, dans l'onglet du navigateur. J'espère que vous n'hésiterez pas à utiliser
ce service dans vos projets !

En résumé
· Il n'est pas possible d'utiliser l'interpolation pour définir un titre à vos documents.
· Angular fournit un service nommé Title pour déterminer dynamiquement un titre sur vos
documents.
· Ce service est une simple classe avec deux méthodes : une pour récupérer le titre de la
page courante, et une autre pour le modifier.
· On utilise ce service comme n'importe quel autre service, mais il est recommandé de le
fournir au niveau du module racine de l'application, étant donné qu'il est destiné à servir au
niveau global de l’application.
Annexe 2 : Bonnes pratiques de
développement
Si vous travaillez dans une équipe de développeurs, vous avez remarqué que souvent
chacun a ses petites habitudes. On n'arrive pas à se mettre d'accord sur le nom des variables à
utiliser, le nom des fichiers, le nombre de fonctions qu'il faut développer... Bref, on aurait grand
besoin d'un petit guide qui rassemble au même endroit toutes les bonnes pratiques de
développement recommandées par la documentation officielle, avec des explications claires afin
de mettre tout le monde d'accord !
Si vous êtes à la recherche de ce guide pour connaître la syntaxe, les conventions et la
meilleure structure possible pour développer une application Angular, vous êtes au bon endroit !
Le but de ce chapitre est de résumer les recommandations officielles, et voir pourquoi ce
sont elles qui ont été retenues. Ensuite je vous présenterai deux outils pour vous permettre
d'appliquer ces recommandations sans avoir à relire tout votre code vous-même.
Il n'est pas important de connaître TOUTES les recommandations. Retenez juste celles qui sont
les plus importantes pour vous et gardez-les dans un coin de votre tête pour vos futurs
développements.

Le principe de "Responsabilité Unique"


Il est recommandé d'appliquer ce principe à tous vos composants, vos services et autres
éléments. Il permet de rendre votre application plus facile à lire, à maintenir et à tester.

Règle 1
Définir un seul élément par fichier : un composant, un module, un service, etc.
Le fait de définir un seul élément par fichier permet plusieurs choses :
· Un unique élément par fichier le rend plus facile à lire et à maintenir, et évide les
collisions avec les autres développeurs lorsque vous versionnez votre code.
· Cela évite également d'avoir des variables qui peuvent être partagées entre deux
composants dans le même fichier, ou autres couplages indésirables.
Essayer de limiter vos fichiers à 400 lignes. S'ils sont plus longs, c'est que vous avez
probablement au moins deux éléments à l’intérieur.

Règle 2
Définissez des petites fonctions plutôt que des fonctions trop importantes.
Les petites fonctions sont plus faciles à tester, à lire et à maintenir, et encouragent la
réutilisation. Elles aident à éviter les bugs qui se produisent avec les fonctions trop importantes
qui partagent des variables avec un contexte externe, ou des couplages indésirables avec les
dépendances.
Essayer de limiter vos fonctions à 75 lignes.
Les conventions de nommage
Les conventions de nommage sont importantes pour la cohésion de votre application. Nous
allons voir que ces conventions s'appliquent aux noms des fichiers, et aussi aux noms des
éléments eux-mêmes : composants, services, etc ...

Règle 1
Utilisez des noms de fichiers consistants pour les éléments que vous définissez :
feature.type.extension.
Les noms de fichiers list-pokemons.component.ts, list-pokemons.component.html ou encore
pokemons.service.ts respectent cette recommandation. Cela permet aux développeurs d'avoir une
idée sur le contenu du fichier en un coup d'oeil. Cela permet également d'avoir une certaine
consistance sur les noms des fichiers de l'application, ce qui est primordial lorsqu'on travaille en
équipe.

Règle 2
Utilisez la syntaxe CamelCase pour nommer vos éléments, en suffixant leur nom avec leur
type.
De cette manière, vous pourrez identifier rapidement les éléments de votre application. Le
tableau ci-dessous illustre l'application de cette règle :

Nom de l’élément Nom du fichier associé

La classe du composant app.component.ts


AppComponent

La classe du composant pokemon-list.component.ts


PokemonListComponent

Le template de pokemon-list.component.html
PokemonListComponent

La feuille de style de pokemon-list.component.css


PokemonListComponent

La directive AwesomeDirective awesome.directive.ts

Le service PokemonsService pokemons.service.ts

Les types conventionnels sont service, component et pipe. Vous pouvez créer les vôtres aussi,
mais faites attention à ne pas en créer trop.

Règle 3
Utiliser la syntaxe kebab-case pour nommer les sélecteurs de vos directives.
Cette règle peut sembler évidente car les sélecteurs de vos directives seront liés aux
attributs d'un élément HTML, qui ont des noms définis en minuscule. De plus, le parseur HTML
d'Angular est sensible à la casse, donc vous n'avez pas le choix !

Règle 4
Utiliser un préfixe personnalisé pour les sélecteurs de vos composants et directives. Par
exemple, le préfixe pa pour l'application Pokemon-App ou admin pour une zone dédiée aux
fonctionnalités d'administration.
Préfixer les sélecteurs de vos composants et de vos directives ne coûte pas grand-chose, et
permet d'éviter les collisions dans votre application :
01 @Component({
02 selector: 'pa-pokemon'
03 })
04 export class PokemonComponent {}
05
06 @Component({
07 selector: 'admin-users'
08 })
09 export class UsersComponent {}
10
11 @Directive({
12 selector: '[paValidate]'
13 })
14 export class ValidateDirective {}

Les conventions de code


Règle 1
Déclarez des constantes plutôt que des variables si leur valeur ne doit pas changer, avec la
syntaxe camelCase plutôt que UPPER_SNAKE_CASE.
Utiliser des constantes plutôt que des variables lorsque c'est nécessaire nous permet
d'informer les autres développeurs que cette valeur est invariable. De plus, TypeScript nous aide
en obligeant une initialisation immédiate et en empêchant une réaffectation ultérieure. La syntaxe
camelCase a été retenue car elle est plus facile à lire que la syntaxe traditionnelle pour les
constantes UPPER_SNAKE_CASE.
Cependant, on retrouve souvent dans les librairies tierces cette syntaxe traditionnelle, qui
est encore très populaire. C'est pourquoi cette syntaxe est tolérée, même s'il est recommandé de
déclarer vos constantes avec camelCase :
01 export const mockPokemons = ['Salamèche', 'Bulbizzare']; // préférable
02 export const pokemonsUrl = 'api/pokemons'; // préférable
03 export const POKEMONS_URL = 'api/pokemons'; // toléré

Règle 2
Utilisez la syntaxe camelCase pour nommer vos propriétés et méthodes, et n'utilisez pas le
préfixe underscore sur les propriétés privées !
TypeScript fait très bien la différence entre une méthode privée ou publique, de même pour
les propriétés. Ne vous embêtez pas à ajouter des underscores partout !

Règle 3
Séparez par un espace les importations des librairies tierces de celles que vous avez codées
vous-même.
La ligne vide vous permettra de rendre les importations plus faciles à lire et à localiser.
01 import { Component } from '@angular/core';
02 import { Http } from '@angular/http';
03
04 import { Pokemon } from './pokemon.model';
05 import { OneService, AnotherService, LastService } from '../../shared';

La structure de l'application
Même si vous venez juste de commencer le développement de votre projet, ayez une vision
à long terme de la mise en œuvre de votre application en suivant les recommandations ci-dessous
dès le départ de votre projet :

Règle 1
LIFT : Localiser votre code rapidement, Identifier le code en un coup d'oeil, conserver la
structure aussi Flat que possible (moins de sept fichiers pas dossier pour être exact), et enfin
« Try to be DRY ».

DRY signifie “Don't Repeat Yourself”. C'est un adage connu des développeurs, qui invite à la
centralisation et à la réutilisation d'éléments déjà développés, afin d'éviter de répéter plusieurs
fois le même code

Définissez la structure de votre application en respectant ces grandes lignes directrices,


indiqué ici par ordre d'importance. Cela vous permettra de garder une architecture cohé- rente
lorsque votre application se développera. Cette architecture modulaire permet aux développeurs
de se repérer rapidement dans leur code.
N'hésitez pas à créer un dossier par composant pour respecter le F de LIFT. Dans ce dossier,
vous pourrez mettre le fichier TypeScript du composant, sa vue HTML, son style CSS et ses
fichiers de tests éventuels.

Règle 2
Mettez tous les fichiers partagés par un module dans un dossier partagé nommé shared.
N'hésitez pas à séparer les fichiers communs à plusieurs composants dans un dossier
spécifique. Cela vous permettra de localiser plus rapidement les fichiers partagés. Vous pouvez
créer deux types de dossier partagé :
· Au niveau du dossier global app de votre application : vous pouvez définir y placer la
barre de navigation, ou un service pour gérer les exceptions de votre application par
exemple.
· Au niveau des modules de fonctionnalités : par exemple dans le module des Pokémons,
on pourrait placer le service pokemons.service.ts et le modèle pokemon.model.ts dans un
dossier partagé.

Règle 3
Il est possible de créer un fichier qui importe, agrège, et réexporte des éléments. Cette
technique s'appelle le barrel et le fichier utilisé pour cela se nomme index.ts, par convention.
Un barrel permet agréger plusieurs importations en une seule, ce qui réduit le nombre
d'importations que l'on peut déclarer dans un fichier. On a souvent recours à un barrel pour
exporter tous les éléments d'un même dossier :
01 import {CONFIG, EntityService, ExceptionService, SpinnerService, ToastService}
02 from '../shared';

Cette technique ne fonctionne que si le dossier shared contient un fichier index.ts avec le
contenu suivant :
01 export * from './config';
02 export * from './entity.service';
03 export * from './exception.service';
04 export * from './filter-text';
05 export * from './init-caps.pipe';
06 export * from './modal';
07 export * from './nav';
08 export * from './spinner';
09 export * from './toast';

C'est assez pratique puisque nous n'avons plus qu'une ligne d'importation dans le reste de
l'application !

Les composants
Règle 1
Utiliser la syntaxe kebab-case pour les sélecteurs de vos composants.
Cette syntaxe vous permet de préfixer plus facilement vos sélecteurs avec un espace de
nom.

Règle 2
Il faut extraire du composant les templates et la feuille de style du fichier quand ils font
respectivement chacun plus de trois lignes.
De plus, vous devez nommer le template et les feuilles de style de la même manière que le
composant, en changeant juste l'extension du fichier :
· [component-name].component.hml pour le template.
· [component-name].component.css pour la feuille de style.

Règle 3
Déclarez les propriétés avant les méthodes dans un composant, et les éléments privés après
ceux qui sont publics, par ordre alphabétique.
Il s'agit simplement d'une recommandation pour avoir une structure commune entre tous
vos composants.

Règle 4
Limiter la logique d'un composant aux seules nécessités de la vue. Toutes les autres
logiques métiers doivent être déléguées dans des services.
La logique d'un composant pouvant être réutilisé par plusieurs composants doit être placée
dans un service, afin d'éviter la redondance dans votre application. Vos composants doivent être
uniquement concentrés sur la gestion de la vue, c'est leur rôle principal.
Pour rappel, évitez d'avoir votre logique métier à l'intérieur des templates : déplacez-là au
niveau de la classe du composant !

Les services
Règle 1
Utiliser les services comme des singletons, avec le même injecteur.
Lorsque vos services sont des singletons, il est plus simple de les utiliser pour partager des
données ou des méthodes à travers un module en particulier, ou dans toute l'application.

Règle 2
Fournissez les services à l'injecteur d'Angular au niveau du plus haut élément où ils seront
partagés.
Cette règle est principalement due au fait que l'injecteur d'Angular est hiérarchique.
Lorsque que l'on fournit un service au composant le plus élevé, cette instance du service est
partagée et rendue disponible pour tous les composants fils !

Règle 3
Séparer l'accès aux données dans un service.
La logique de votre application qui concerne les opérations et l'interaction avec des
données distantes ou locales doit se faire dans un service dédié, à savoir : les appels XHR, l'accès
au local storage, aux données en mémoire et autres...
Nous l'avons déjà vu : le rôle d'un composant est de recueillir et de présenter les données
dans la vue. Il ne doit pas se préoccuper de la façon dont ces données sont obtenues, mais de la
façon dont elles seront affichées dans la vue !

Les outils à connaître


Les règles que nous venons de voir sembles intéressantes, et vous aimeriez sans doute les
appliquer à votre code. Mais comment faire ?
Vous n'allez quand même pas relire tout le code de votre application, en vérifiant si chaque
fichier respecte telle ou telle recommandation ! Ce serait très long, ennuyeux et inefficace.
Heureusement, la communauté des développeurs d'Angular propose deux outils différents :
· Des outils de validation du code, qui permettent de vérifier que votre code respecte bien
les bonnes pratiques de développement recommandées par Angular.
· Des outils pour vous aider à développer du code valide (une sorte d'anticipation de la
règle précédente).
Chacun de ces outils pourrait faire l'objet d'un chapitre entier, je vais simplement vous
présenter leur fonction au sein d'un projet Angular, et pour la suite je fais confiance à votre
curiosité.

Vérifier la validité du code


L'outil recommandé pour vérifier si son code respecte les bonnes pratiques de
développement est Codelyzer.
Cet outil s'occupe pour vous de vérifier l'ensemble de votre code, et de vous indiquer dans
quel fichier vous avez omis de respecter une recommandation. Pour chaque oubli, Codelyzer
vous fournit un lien vers la documentation officielle afin que vous puissiez voir par vous même
quelle règle vous avez enfreint !
Il vous rédige une sorte de rapport sur l'état de votre application, et c'est ensuite à vous de
décider si vous prenez en compte ses retours.
Cette vidéo illustre très bien ce qu'il est possible de faire avec Codelyzer.

Accélérez vos développements


Lorsque vous développez, vous avez dû remarquer que vous effectuez souvent les mêmes
tâches. Par exemple, quand vous créer un nouveau composant, il y a beaucoup de code commun
à tous les composants. Vous devez importer l'annotation Component, déclarer un sélecteur,
exporter une classe vide au départ, etc.
Il existe des outils qui vous évitent de devoir développer vous-même ce genre de code
redondant. On parle de Snippets. Il en existe plusieurs en fonction de votre IDE :

Editeur de code Snippets

Visual Studio Code snippet

Atom snippet

Sublime Text snippet

Vim snippet

Les snippets définissent des raccourcis qui vous permettent de mettre en place en quelques
clics l'architecture de certains éléments. Par exemple pour mettre en place l'architecture de base
d’un composant, on pourra utiliser le raccourci ng-component. La snippet s'occupera alors de
générer un composant générique, qui respecte les conventions de nommage que nous avons
vues !

Conclusion
Nous avons vu dans ce chapitre un certain nombre de recommandations concernant les
conventions de nommage, la structure de l'application, etc. Ces règles ne sont bien sûr pas là
pour nous embêter, mais pour nous permettre de développer un code plus cohérent et de
meilleure qualité.
Cela permet également de se mettre d'accord si vous travaillez à plusieurs, quant à la
manière de nommer les variables ou d'organiser le projet. Je vous invite fortement de suivre ces
recommandations !

En résumé
· La documentation officielle d'Angular propose un guide qui répertorie toutes les bonnes
pratiques de développement, avec une justification pour chaque recommandation.
· Codelyzer est l'outil recommandé pour vérifier automatiquement si notre code respecte
les recommandations de la documentation.
· Il existe des snippets pour nous accélérer nos développements, en générant des bouts de
code génériques qui respectent les recommandations d'Angular.
Annexe 3 : Configuration de TypeScript
Lorsque nous avons développé l'application de ce cours, nous nous sommes contentés de
copier-coller le fichier de configuration de TypeScript fournis par la documentation officielle.
Mais que savons-nous exactement de ce fichier, et de son fonctionnement ? Pas grand chose, en
fait.
Le but de ce chapitre est de faire la lumière sur le fichier de configuration tsconfig.json,
que l'on retrouve dans toutes les applications développées avec TypeScript !

Le fichier de configuration de TypeScript


Lorsque l'on utilise TypeScript dans un projet, il faut ajouter le fichier de configuration
tsconfig.json dans le dossier du projet, pour guider le compilateur dans la génération du code
JavaScript.
Voici le fichier de base, tel qu'il est configuré pour faire fonctionner le projet que vous
avez réalisé pendant ce cours :
01 {
02 "compilerOptions": {
03 "target": "es5",
04 "module": "commonjs",
05 "moduleResolution": "node",
06 "sourceMap": true,
07 "emitDecoratorMetadata": true,
08 "experimentalDecorators": true,
09 "removeComments": false,
10 "lib": [ "es2015", "dom" ],
11 "noImplicitAny": true,
12 "suppressImplicitAnyIndexErrors": true
13 }
14 }

Ce fichier contient seulement les éléments et les options qui sont essentiels pour une
application Angular.
Nous allons d'abord voir les options existantes, et ensuite quels sont les éléments qui
peuvent être ajoutés pour améliorer cette configuration initiale.

Vous trouverez plus d'information sur le fichier tsconfig.json sur la page dediée dans la
documentation officielle de TypeScript.

La configuration existante
Voici point par point les éléments de notre configuration existante :

Attribut Description

target Cette option permet de spécifier la version


d'ECMAScript souhaitée : es3, es5 ou es6. Il est
recommandé d'utiliser la version es5 actuellement.

module Permet de Spécifier le module de génération de


code. Les valeurs possibles sont : 'none',
'commonjs', 'amd', 'system', 'umd', 'es6' ou
'es2015'. Choississez la manière dont vous
souhaitez apporter la modularité dans votre
application.

moduleResolution S'occupe de la modularité de votre application.


Mettez la valeur 'classic' si vous utilisez 'amd',
'es6' ou 'system', sinon laissez 'node'.

sourceMap Souhaitez-vous générer des fichiers *.jsmap ou


non ? Ces fichiers map servent débogueur de
votre IDE pour faire le lien entre le code
JavaScript exécuté et les fichiers sources
originaux écrits en TypeScript, rendant ainsi le
débogage de votre application plus facile.

emitDecoratorMetadata Vous devez renseigner un booléen. Faut-il générer


des méta-données pour les annotations présentes
dans le code source (c'est-à-dire prendre en
compte les annotations des fichiers sources)? La
réponse est oui, bien sûr !

experimentalDecorators Activer le support expérimental pour les


décorateurs ES7.

removeComments Souhaitez-vous retirer les commentaires contenus


dans les fichiers sources ? (excepté les en-têtes
avec /*!, qui sont utilisés pour spécifier des
copyright).

lib Liste des fichiers de librairies à inclure dans la


compilation.

noImplicitAny Faut-il soulever des erreurs sur les variables dont


le type any a été attribué implicitement par
TypeScript. Choisir une valeur pour cette option
peut être délicat, nous allons pourquoi tout de
suite après.

suppressImplicitAnyIndexErrors Sans rentrer dans les détails, cette option permet


d'ajouter un peu plus de souplesse à l'option
noImplicitAny.
Le "débat" sur l'option noImplicitAny
Il n'y a pas de vérité absolue à propos de la valeur à attribuer à l'élément noImplicitAny.
Cependant, votre choix peut être impactant si vous travaillez sur des projets de taille importante.
Quand l'option noImplicitAny est définie à false (qui est la valeur par défaut), si le
compilateur ne peut pas déduire le type d'une variable en fonction de la façon dont elle est
utilisée, le compilateur ajoutera implicitement le type any à cette variable.
Lorsque TypeScript détermine tout seul le type d'un variable, on dit que le type est inféré par le
compilateur.
Jusqu'à maintenant, on a laissé cette valeur à false pour faciliter l'apprentissage de
TypeScript.
Cependant, quand cette valeur est définie à true, et que le compilateur ne peut pas inférer le
type d'une variable, il va générer les fichiers JavaScript, mais il lèvera également une erreur !
Beaucoup de développeurs chevronnés préfèrent ce paramètre, plus strict, parce que cette
vérification de type permet de lever plus d'erreurs involontaires lors de la compilation.
Si vous définissez l'option notImpicitAny à true, vous pourriez obtenir des erreurs
implicites du compilateur. Ces erreurs particulières sont plus ennuyeuses qu'utiles. On peut
supprimer ces erreurs en ajoutant l'option suivante :
01 ‘suppressImplicitAnyIndexErrors’ : true

On peut définir le type d'une variable à any même quand l'option noImplicitAny est définie à
true !

Les configurations supplémentaires


Il existe certaines options que vous pouvez ajouter à la configuration de base du
compilateur :

Nom de l’option Valeur Description

outDir Une chaîne de Cette option permet de définir un


caractère nom de dossier dans lequel seront
ajoutés les fichiers JavaScript
générés par le compilateur. Nous
avons utilisé cette option au début
de ce cours, dans le chapitre
"Hello, World !" avec Angular. On
utilise généralement le nom de
dossier "dist", qui signifie
distribution.

pretty Un booléen Si définit à true, cette option


permet d'ajouter des couleurs aux
messages d'erreurs du compilateur
dans la console. Cela ne coûte pas
cher et c'est bien pratique !
charset UTF-8 Permet d'encoder en UTF-8 les
fichiers générés par le compilateur.

locale fr, par exemple La locale à utiliser pour afficher


des messages d'erreurs.

Voici une liste exhaustive des configurations possibles pour tsconfig.json, en Anglais !

Conclusion
Nous avons pu voir plus en profondeur comment configurer TypeScript dans un projet.
N'hésitez pas à vous référez à la documentation officielle si vous souhaitez en savoir plus.
Cependant, avec ce que nous venons de voir, vous en savez bien assez pour commencer le
développement de vos applications sereinement, plus d'excuses !

En résumé
· La configuration de TypeScript dans un projet repose sur le fichier tsconfig.json.
Annexe 4 : Dépendances d'un projet Angular
Ce chapitre a pour objectif de détailler les dépendances nécessaires au démarrage d'un
projet Angular. Nous allons voir précisément quelles sont ces dépendances, et pourquoi elles
sont indispensables.
Bien sûr, vous n'avez pas besoin de connaître parfaitement les dépendances d'un projet
avant de commencer à développer. Cependant, c'est toujours un plus si vous maîtrisez un peu
mieux votre environnement de développement.

Les dépendances d'un projet Angular


Les applications Angular (et Angular lui-même) dépendent de fonctionnalités fournies par
des librairies. Ces librairies sont maintenues et installées avec le Node Package Manager : on
parle de paquets.
Node.js et NPM sont essentiels aux développements d'applications Angular: installez-les
sur votre machine avant de pouvoir commencer à développer.
Vérifiez que vous disposez de node 8.x.x ou plus, grâce à la commande node -v dans une fenêtre
de terminal. Angular 6 nécessite une version égale ou supérieur à 8 pour NodeJS.
Il est recommandé de démarrer son projet Angular avec un ensemble de paquets, spécifié
dans le fichier package.json :
01 "dependencies": {
02 "@angular/animations": "^6.0.3",
03 "@angular/common": "^6.0.3",
04 "@angular/compiler": "^6.0.3",
05 "@angular/core": "^6.0.3",
06 "@angular/forms": "^6.0.3",
07 "@angular/http": "^6.0.3",
08 "@angular/platform-browser": "^6.0.3",
09 "@angular/platform-browser-dynamic": "^6.0.3",
10 "@angular/router": "^6.0.3",
12 "core-js": "^2.5.4",
13 "rxjs": "^6.2.0",
14 "systemjs": "0.19.40",
15 "zone.js": "^0.8.26"
16 }, ...

Bien sûr, vous pouvez utiliser d'autres versions des paquets que celles indiqués ici, il s'agit juste
des dernières versions disponibles au moment où j'écris ces lignes.
Nous allons voir le rôle de chacun de ces paquets. Vous pourrez faire des substitutions plus
tard, en fonction de vos goûts et de votre expérience.

Les dépendances de l’application


Le fichier package.json inclut deux ensembles de paquets différents : dependencies et
devDependencies.
· La section dependencies est essentielle pour l'exécution de l'application.
· Les devDependencies sont seulement nécessaires pour développer l'application.
On peut exclure les paquets devDependencies de l'installation grâce à la commande suivante :
npm install —production.

La section Dependencies
La section dependencies du package.json contient plusieurs types de paquets : les paquets
de fonctionnalités, des polyfills et des utilitaires en plus.
Les paquets de fonctionnalités
Nom du paquet Description

@angular/core C'est la pièce maitresse du


framework, nécessaire pour toutes les
applications. Inclut toutes les
annotations @Component,
@Directive, l'injection de
dépendances et les cycles de vie des
composants.

@angular/common Ce paquet contient les services


couramment nécessaires, les pipes et
les directives fournis par l'équipe de
développement d'Angular

@angular/compiler Le compilateur de template d'Angular


: il intérprête les templates et les
convertit en code pour rendre
l'application fonctionnelle. En
générale, on n'interagit pas
directement avec le compilateur. A la
place, on utilise le paquet platform-
browser-dynamic ou platform-
browser.

@angular/platform-browser Ce paquet inclut la méthode


bootstrapStatic pour démarrer
l'application à destination de la
production, pre-compilant tous les
templates à l'avance, avant de les
envoyer dans le navigateur.

@angular/plateform-browser- Inclut la méthode bootstrap pour les


dynamic applications qui compilent les
templates côté client. Utiliser ce
paquet pour démarrer l'application
plutôt durant le développement.

@angular/common/http Le client HttpClient d'Angular.

@angular/router Le routeur, et d'autres utilitaires pour


faire fonctionner la navigation dans
votre application.

System.js Un chargeur dynamique de modules


compatible avec la spécification des
modules ES2015.

Vos applications futures sont susceptibles d'avoir besoin de paquets supplémentaires, pour
fournir des directives, des thèmes, faciliter l'accès aux données et autres utilitaires. Il faudra
alors ajouter ces dépendances dans le fichier package.json.

Les paquets de supports & Polyfill


Angular requiert certains paquets de supports et polyfills pour faire fonctionner une
application. Vous devez lister ces éléments parmi les paquets de la section dependencies. Il y a
trois paquets recommandés au démarrage d'un projet :
· CoreJs : Un polyfill qui corrige le contexte global (window) avec les fonctionnalités
essentielles d'ES6. Quand ES6 sera implémentée par les principaux navigateurs, cette
dépendance deviendra inutile.
· RxJs : Un paquet de support concernant la spécification des Observables. Voici pouvez
choisir la version de RxJs que vous préférez (dans une plage de version compatible), sans
avoir besoin d'attendre les mises à jour d'Angular.
· ZoneJs : Un paquet de support pour la spécification des zones, qui fournissent un
contexte d'exécution pour les opérations asynchrones. Comme pour RxJs, vous pouvez
choisir la version de zone que vous préférez (dans une plage de version compatible
également, bien sûr !).
Les autres librairies
· angular-in-memory-web-api : Une librairie qui simule un serveur web distant sans avoir
besoin d'en installer un. C'est très pratique pour les démonstrations, les exemples, et les
développements à un stade précoce.

Les dépendances de développement


Les paquets listés dans la section devDependencies nous aident seulement à développer
l'application. Nous n'avons pas besoin de les déployer en production. Voici les paquets les plus
importantes en rapport avec ce cours :
· concurrently : Un utilitaire pour pouvoir exécuter des commandes NPM sur différents
systèmes d'opérations comme OS/X, Windows et Linux ;
· lite-server : Un serveur de fichiers statiques léger, avec un excellent support pour les
applications Angular qui utilisent le Router ;
· typescript : Tout ce qui est nécessaire pour pouvoir utiliser TypeScript, incluant le
compilateur tsc.

Conclusion
Nous avons pu décortiquer toutes les dépendances initiales d'un projet Angular. Même si
ce n'est pas indispensable de connaître le fonctionnement de chaque dépendance, cela nous
permet de savoir un peu mieux où l'on met les pieds, et on se sent toujours plus à l'aise quand on
développe avec des outils que l'on connaît !

En résumé
· Il est nécessaire d'installer Node.js et NPM sur sa machine pour pouvoir développer des
applications Angular.
· Les librairies chargées par NPM sont appelées des paquets.
· Il y a deux types de paquets à déclarer : les dépendances et les dépendances de
développement.
· Les dépendances sont les paquets nécessaires pour que l'application puisse fonctionner.
· Les dépendances de développement sont des paquets permettant aux développeurs de
développer l'application.

Vous aimerez peut-être aussi