Ce document est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International.
2. Mise en place
Les étapes nécessaires pour bien démarrer le travail sont les suivantes.
2.1. Récupération des sources
Pour disposer des sources, utiliser git :
git clone https://github.com/formations/angular-foundations
L’étape de départ est la branche master
.
2.2. Outillage
Le workshop est un projet basé sur Angular. Pour travailler, les outils suivants sont requises :
Il faut tout d’abord installer npm. Puis, installer les dépendances npm en local :
npm install
Il est nécessaire d’installer la ligne de commande (CLI) Angular:
npm install -g @angular/cli
Par rapport à AngularJS, Angular fournit tout le reste de l’outillage nécessaire du projet (transpilation TypeScript en JavaScript, tests, etc.). |
2.3. Création du projet
Cette étape est à visée informative uniquement et peut être passée. En effet, le projet disponible a déjà pris soin de réaliser celle-ci. |
2.4. Structure de départ
La structure initiale du projet cloné est la suivante :
![Structure initiale du projet](images/project-structure.png)
Les principaux dossiers/fichiers sont les suivants :
Dossier / fichier | Description |
---|---|
|
Tests de bout-en-bout |
|
Modules node.js |
|
Ecran A propos |
|
Ecran de détail de comic |
|
Ecran d’accueil |
|
Composant de barre de navigation |
|
Composant de barre d’onglets |
|
Feuille de style du composant racine |
|
Fragment HTML du composant racine |
|
Tests relatifs au composant racine |
|
Code relatif au composant racine |
|
Module principal de l’application |
|
Données "bouchon" |
|
Modèle de classes TypeScript |
|
Resources statiques, y compris les images |
|
Liste des environnements disponibles. Par défaut, dev et prod. |
3. Premiers pas
3.1. Lancement de l’application
Dans le répertoire racine du projet, lancer l’application :
ng serve --open
L’option --open permet d’ouvrir automatiquement une fenêtre de navigateur.
Si elle n’est pas utilisée, naviguer à http://localhost:4200/.
|
Le résultat qui doit s’afficher est le suivant :
![Etat initial de l’application](images/initial-state.png)
3.2. Affichage de données
Objectif
Le but de cette section est d’afficher les attributs du composant racine dans la page.
|
Voici le code source du composant racine :
import { Component } from '@angular/core'; (1)
@Component({
selector: 'app-root', (2)
templateUrl: './app.component.html', (3)
styleUrls: ['./app.component.css'] (4)
})
export class AppComponent { (5)
title = 'app'; (6)
id = 21464; (6)
title = 'Powers (2000)'; (6)
issueNumber = 18; (6)
thumbnail = 'image/image_not_available.jpg'; (6)
price = 3.45; (6)
description = 'Walker and Pilgrim investigate...'; (6)
}
1 | TypeScript nécessite l’utilisation d’imports. Dans le cas contraire, le type ne peut pas être utilisé. |
2 | Sélecteur HTML qui doit être ajouté dans la page pour afficher le fragment HTML |
3 | Fragment HTML associée au composant. C’est le contenu de ce fichier HTML qui sera ajouté à la page web. |
4 | Feuille de style associée au composant |
5 | Le mot-clé export est nécessaire pour que le composant soit utilisé |
6 | Attributs disponibles dans le composant HTML |
En utilisant la syntaxe {{ }}
, afficher les attributs dans la page.
<div class="container">
<h1>Comics Library</h1>
<div class="row">
<div class="col-xs-12">
<div class="media">
<img class="mr-3" alt="Comic cover"> (1)
<div class="media-body">
<h2 class="media-heading"> (2)
<span class="badge badge-danger"># (3)
<span class="badge badge-pill badge-secondary">$</span> (4)
</h2>
<p></p> (5)
</div>
</div>
</div>
</div>
</div>
1 | Ajouter l’attribut src pour afficher l’image du composant
|
||
2 | Afficher l’attribut title |
||
3 | Afficher l’attribut issueNumber |
||
4 | Afficher l’attribut price |
||
5 | Afficher l’attribut description |
Le résultat attendu est le suivant :
![Etat initial de l’application](images/simple-data-display.png)
3.3. Gestion de composant
Objectif
Le but de cette section est de créer un composant personnalisé.
|
- Création de composants :
La création de composants se fait via la ligne de commande.
Pour créer un composant nommé comicsList
, utiliser la commande suivante :
ng generate component comicsList
Après l’exécution de la commande, un nouveau répertoire src/app/comics-list
a été généré qui contient les fichiers suivants :
Fichier | Description |
---|---|
|
Feuille de style du composant liste |
|
Fragment HTML du composant liste |
|
Tests relatifs au composant liste |
|
Code relatif au composant liste |
- Personnalisation :
Par défaut :
-
le nom de la classe est
ComicslistComponent
dans le code -
et le sélecteur
<app-comics-list>
Il est conseillé d’utiliser un préfixe quel qu’il soit, afin d’éviter que des balises de bibliothèques différentes aient le même nom.
Le préfixe par défaut est app
Il est configuré dans le fichier angular.json .
|
- Utilisation du composant :
Déplacer le code du composant depuis le composant racine app.component
vers le nouveau composant créé.
Puis modifier le composant racine ainsi :
<div class="container">
<h1>Comics Library</h1>
<app-comics-list></app-comics-list> (1)
</div>
1 | Ajout du sélecteur, qui va afficher le fragment HTML sur la page |
L’affichage doit être exactement similaire à l’affichage de l’étape précédente.
3.4. Utilisation de classe et de données
-
Remplacer les différents attributs de la classe
ComicsListComponent
par un unique attribut de typeSimpleComic
:src/app/comics-list/comics-list.component.tsexport class ComicsListComponent { comic: SimpleComic = { (1) id: 21464, title: 'Powers (2000)', issueNumber: 18, thumbnail: 'image/image_not_available.jpg', price: 3.45, description: 'Walker and Pilgrim investigate...' } }
1 Remplacement des différent attributs par un unique attribut La classe
SimpleComic
est déclarée dans le fichiermodel.ts
. Ne pas oublier de l’importer.Modifier le composant HTML en conséquence.
-
Remplacer la définition de la donnée dans le composant par la constante
COMIC
:src/app/comics-list/comics-list.component.tsexport class ComicsListComponent { comic: SimpleComic = COMIC; (1) }
1 Utilisation de la constante existante La constante
COMIC
est déclarée dans le fichierdata.ts
. Ne pas oublier de l’importer.
Au cours des deux manipulations précédentes, l’affichage doit être similaire à l’étape précédente.
4. Directives
4.1. Affichage de collection d’éléments
Objectif
Le but de cette section est d’afficher non plus un unique comic mais une collection de ceux-ci.
|
-
Dans la classe
ComicsListComponent
:-
Modifier l’attribut
comic
de typeSimpleComic
en attributcomics
de typeComic[]
src/app/comics-list/comics-list.component.tsexport class ComicsListComponent { comic: SimpleComic = COMIC; (1) }
1 Modifier cette ligne -
Affecter la constante
COMICS
du fichierdata.ts
à ce dernier. Ne pas oublier l’import !
-
-
Modifier le fragment HTML pour boucler sur les éléments de la collection en utilisant la directive
ngForOf
src/app/comics-list/comics-list.component.html<ul class="list-unstyled"> <li class="media mt-5"> (1) <img class="mr-3" alt="Comic cover"> (2) <div class="media-body"> <h2 class="media-heading"> (3) <span class="badge badge-danger">#</span> (4) <span class="badge badge-pill badge-secondary">$</span> (5) </h2> <p></p> (6) </div> </li> </ul>
1 Boucler sur les comics de la collection 2 Utiliser l’attribut src
pour afficher l’image3 Afficher le titre 4 Afficher le numéro 5 Afficher le prix 6 Afficher la description La structure des classes SimpleComic
etComic
est légèrement différente. Par exemple, le chemin de l’image n’est plus stocké dans un attribut de type simplestring
, mais dans un type qui comporte deux attributs,path
etextension
. Adapter le binding pour refléter la nouvelle structure.
Le résultat attendu est le suivant :
![Affichage d’une collection d’éléments](images/collection-display.png)
4.2. Affichage conditionnel d’éléments
Objectif
Le but de cette section est d’afficher un élément si une condition est vraie.
|
Dans l’état actuel, le prix est affiché dans tous les cas.
Or, il peut s’avérer que le prix ne soit pas renseigné (price: 0
).
Dans ce cas, il ne fait pas de sens de l’afficher.
Dans le fragment HTML, utiliser la directive ngIf
pour afficher le prix uniquement si celui est supérieur strictement à 0.
Le résultat attendu est le suivant :
![Affichage conditionnel du prix](images/conditional-display.png)
4.3. Classe conditionnelle
Objectif
Le but de cette section est d’appliquer une classe CSS à un élément si une condition est vraie.
|
- Préparation :
Le composant de barre d’onglets a déjà été créé et est prêt à être utilisé. L’ajouter au fragment HTML racine :
<div class="container">
<h1>Comics Library</h1>
<app-tab-bar></app-tab-bar> (1)
<app-comics-list></app-comics-list>
</div>
1 | Ajouter le composant de barre d’onglets |
Le résultat attendu est le suivant :
![Affichage initial des onglets](images/start-tab.png)
- Gestion de l’état "sélectionné" :
Modifier la classe TabBarComponent
ainsi :
-
Créer un attribut
selectedIndex
de typenumber
initialisé à0
-
Créer une fonction
isSelected(index: number): boolean
qui retourne :-
true
siindex
est égal àselectedIndex
-
false
sinon
-
- Mise en exergue de l’onglet sélectionné :
Utiliser la directive ngClass
pour faire en sorte que la classe CSS active
soit affecté à l’onglet si et seulement s’il est sélectionné.
Le résultat attendu est le suivant :
![Highlight de l’onglet sélectionné](images/highlight-tab.png)
5. Event Binding
Objectif
Le but de cette section est d’associer l’appel d’une fonction au clic sur un élément.
|
-
Dans la classe
TabBarComponent
, créer une fonctionselect(index: number)
qui modifie l’attributselectedIndex
avec l’index passé en paramètre -
Modifier le fragment HTML pour qu’un clic sur l’onglet appelle la méthode précédente
Le résultat attendu est le suivant :
![Sélection de l’onglet](images/select-tab.png)
6. Communication inter-composants
Objectif
Le but de cette section est d’envoyer des informations depuis le composant onglet vers le composant liste.
|
A cette étape, le composant onglet connaît l’onglet sélectionné, et doit en faire part au composant liste lorsqu’il change. Plusieurs méthodes sont disponibles pour l’interaction de 2 composants. Comme l’onglet et la liste n’ont pas de relation parent-enfant, un service intermédiaire est requis.
Voici le diagramme qui va être implémenté :
6.1. Génération du service
A l’instar des composants, les services sont créés à l’aide de la ligne de commande.
Créer un service tab
:
ng generate service tab
Après l’exécution de la commande, les fichiers suivants ont été créés :
Fichier | Description |
---|---|
|
Tests relatifs au service |
|
Code du service |
En tant qu’intermédiaire, le service doit permettre de publier et de souscrire à des évènements. Angular intègre nativement le framework RxJs.
Il est toujours nécessaire d’utiliser import pour utiliser les objets d’un autre espace de nommage.
|
Le service doit adopter la conception suivante :
-
l’attribut
titleChangedSource
est un nouvel objet de typeSubject<string>
-
l’attribut
titleChanged$
est le même objet que l’objet précédent, mais trans-typé enObservable<String>
Par convention, les attributs de type Observable
sont suffixés par$
. -
la fonction
changeTitle(title: string)
appelle la fonctionnext()
detitleChangedSource
avec comme paramètretitle
6.2. Injection du service
Avec Angular, les services ne sont pas instanciés manuellement mais par le framework (comme les composants). Il est donc nécessaire d’injecter l’instance existante dans les composants.
Pour ce faire, modifier le constructeur des composants TabComponent
et ComicsListComponent
en passant le type demandé :
export class XComponent {
constructor(tabService: TabService) { }
}
6.3. Enregistrement du callback
Le composant ComicsListComponent
joue le rôle d' Observer:
il doit enregistrer à l’initialisation une fonction de callback qui sera appelée à chaque fois que le titre sera modifié.
L’initialisation se fait via l’implémentation de l’interface OnInit
, et sa fonction associée ngOnInit()
.
Pour s’abonner aux changements, utiliser la fonction subscribe()
avec comme paramètre une fonction de callback dont l’algorithme est le suivant :
-
SI le paramètre est
null
-
ALORS retourner la liste complète
-
-
SINON
-
retourner uniquement la liste de comics dont le titre contient le paramètre
-
Consulter la documentation de la méthode filter()
|
7. Pipes
Objectif
Le but de cette section est d’implémenter un pipe Angular.
|
Jusqu’à présent, les comics ne sont pas ordonnés. Il serait souhaitable qu’ils le soient, d’abord par titre, puis par numéro dans le titre.
Une solution pour aboutir à cela est d’appeler la fonction sort()
(avec comme paramètre un comparateur) à chaque fois que l’on retourne la collection de comics.
Fonction comparateur
La fonction comparateur passée en argument de la fonction
A titre d’exemple, voici le code source de la fonction comparateur qui compare deux comics, d’abord en fonction de leur attribut title, puis de leur attribut issueNumber en cas d’égalité.
|
Le problème d’appeler la fonction sort()
explicitement est qu’il est possible d’oublier un tel appel.
La solution proposée par Angular est de configurer le filtre sur le fragment HTML via un pipe.
7.1. Création de pipe
Comme précédemment, la création de pipe se fait via la ligne de commande.
Pour créer un pipe nommé sortByTitle
, utiliser la commande suivante :
ng generate pipe sortByTitle
Le résultat est le suivant :
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ (1)
name: 'sortByTitle' (2)
})
export class SortByTitlePipe implements PipeTransform { (3)
transform(value: any, args?: any): any { (4)
return null;
}
}
1 | Identifie la classe comme un pipe |
2 | Nom du pipe à utiliser dans le fragment HTML |
3 | L’interface est optionelle, mais conseillée |
4 | La fonction transform() est essentielle au pipe.
Le paramètre value représente l’entrée, et la valeur retournée par la fonction la sortie.
Le paramètre args? permet d’ajouter des paramètres au pipe. |
Adapter la signature de la fonction pour trier la collection de comics comme ci-dessus.
Puis, réutiliser la méthode byTitle()
à bon escient.
8. Routage
Objectif
Le but de cette section est d’associer des éléments cliquables à des composants, de telle sorte qu’un clic sur un tel élément affiche le composant associé.
|
8.1. Création de module
En plus des composants et des services, les modules sont créés à la ligne de commande.
Créer un module routing
:
ng generate module routing --flat --module=app
Signification des options
|
L’exécution de la commande crée les fichiers suivants :
Fichier | Description |
---|---|
|
Tests relatifs au module |
|
Code du module |
Dès lors que l’application comporte plus d’un module, la génération d’un nouveau fichier via ng generate nécessite d’indiquer le module auquel celui-ci sera rattaché
|
8.2. Implémentation des routes
Implémenter les routes suivantes :
Chemin | Composant |
---|---|
|
|
|
|
|
|
Les composants HomeComponent et AboutComponent sont respectivement disponibles dans les répertoires home et about
|
Utiliser les routes définies précédemment dans le fragment src/app/nav-bar/nav-bar.component.html
à l’aide de la directive routerLink
.
8.3. Affichage des fragments
Au niveau de l’affichage du composant app.component.html
:
-
Supprimer la balise
<h1>
-
Ajouter le fragment
<app-nav-bar>
à la racine -
Déplacer le fragment
<app-tab-bar>
au niveau du composantcomics-list.component.html
-
Remplacer le fragment
<app-comics-list>
par le fragment dédié au routage
Le résultat attendu est le suivant :
![Utilisation de la barre de navigation](images/navigation-bar.png)
9. Module Client HTTP
Objectif
Le but de cette section est d’utiliser le module client HTTP pour récupérer les données d’un serveur en lieu et place des donnée statiques.
|
9.1. Refactoring
- Nettoyage du code :
D’un point de vue conceptuel, un composant ne doit pas gérer lui-même de données, mais utiliser un service pour cela.
-
Créer un service
comic
ng generate service comic
-
Dans le nouveau service
ComicService
, créer une fonctiongetComics()
qui retourne la constanteCOMICS
-
Dans le composant
ComicsListComponent
, remplacer l’utilisation de cette dernière par le service
- Changement de type :
Le module client HTTP utilise des types de retour asynchrones - Observable
.
Il est nécessaire d’adapter la chaîne de traitement dès à présent.
-
Dans la classe
ComicService
, remplacer le type de retour de la fonctiongetComics()
deComics[]
àObservable<Comic[]>
Utiliser la fonction of
-
Adapter la classe
ComicListComponent
:-
Modifier le type de l’attribut
comics
enObservable<Comic[]>
-
Modifier son nom en
comics$
-
Adapter l’implémentation de la fonction
ngInit()
-
-
Pour disposer d’un tableau dans le fragment HTML, utiliser le pipe
async
9.2. Création et configuration de compte
-
La première étape consiste à créer un compte développeur sur developer.marvel.com.
-
Puis, récupérer la clé d’API publique.
-
Ne pas oublier de mettre à jour la liste des site habilités avec
localhost
.
![Marvel Comics developer account](images/marvel-account.png)
9.3. Utilisation du module
-
Dans la classe
AppModule
, importer le moduleHttpClientModule
depuis@angular/common/http
-
Abandonner la notion d’onglets. Dans le fragment
comics-list.component.html
, supprimer<app-tab-bar>
-
Abandonner le filtre. Supprimer également l’utilisation du pipe
sortByTitle
(mais pas celle du pipeasync
!) -
Dans le fichier
environment.ts
, créer deux entrées supplémentaires :Clé Valeur baseUrl
"https://gateway.marvel.com:443"
apiKey
Clé d’API disponible sur marvel.com
-
Dans le service
ComicService
, injecter le serviceHttpClient
. Utiliser ce dernier en conjonction avec les paramètres d’environnement pour exécuter la requête qui retourne la liste des comics. -
Dans le composant
ComicsListComponent
, injecter le serviceComicService
et supprimer l’injection duTabService
Le résultat attendu doit être semblable à la copie d’écran suivante :
![Affichage de la liste des comics depuis le serveur](images/marvel-comics-list.png)
Une fois la liste affichée, modifier le code pour qu’un clic sur un comics affiche son détail.
Utiliser le composant comic-detail.component et les routes avec paramètres
|
Voici un exemple de résultat attendu :
![Affichage d’un comic depuis le serveur](images/marvel-comic-detail.png)