e) Création dynamique de UI

YoutubeImage

Pourquoi générer dynamiquement une UI ?

Pour différentes raisons, nous souhaitons parfois générer des parties d'IHM dynamiquement, à l'aide de JS.

Par exemple, si nous souhaitions afficher différentes pages sans faire appel au serveur. Lors d'un clic sur un élément d'une Navbar, nous pourrions générer dynamiquement l'HTML de la page associée à cet élément.

Un autre exemple serait l'affichage d'un tableau HTML suite à la réception de données envoyées par un service web.

Il existe principalement deux façon de générer des éléments HTML, soit à partir de String, soit à partir de Nodes (ou "HTML elements").

Génération d'HTML à partir d'une string

Nous pouvons utiliser la propriété innerHTML d'un élément existant pour créer de l'HTML à l'aide d'une String.

Lorsque nous allons ajouter une String à la propriété innerHTML d'un élément, le browser va s'occuper de créer des "Node elements" et les attacher au "DOM tree" au sein de l'élément que nous modifions.

Dans votre repo web2, veuillez copier/coller le répertoire /tutorials/pizzeria/hmi/-esthetic et le renommer en /tutorials/pizzeria/hmi/modern-dynamic.

En cas de souci, vous pouvez télécharger le code du tutoriel précédent ici : modern-esthetic-hmi.

Veuillez ouvrir un terminal au niveau de ce répertoire.

Pour la suite du tutoriel, nous considérons que tous les chemins absolus démarrent du répertoire /tutorials/pizzeria/hmi/modern-dynamic (ou /web2/tutorials/pizzeria/hmi/modern-dynamic si l'on considère le nom du répertoire du repo).

Nous souhaitons ajouter un menu à notre site de la pizzeria à générer à partir d'un array.

Voici le nouveau code à ajouter dans votre projet pour générer ce menu au sein de /src/index.js :

js
1
import 'bootstrap/dist/css/bootstrap.min.css';
2
import './stylesheets/main.css';
3
import 'animate.css';
4
5
const MENU = [
6
{
7
id: 1,
8
title: '4 fromages',
9
content: 'Gruyère, Sérac, Appenzel, Gorgonzola, Tomates',
10
},
11
{
12
id: 2,
13
title: 'Vegan',
14
content: 'Tomates, Courgettes, Oignons, Aubergines, Poivrons',
15
},
16
{
17
id: 3,
18
title: 'Vegetarian',
19
content: 'Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives',
20
},
21
{
22
id: 4,
23
title: 'Alpage',
24
content: 'Gruyère, Mozarella, Lardons, Tomates',
25
},
26
{
27
id: 5,
28
title: 'Diable',
29
content: 'Tomates, Mozarella, Chorizo piquant, Jalapenos',
30
},
31
];
32
33
const body = document.querySelector('body');
34
35
body.addEventListener('click', startOrStopSound);
36
37
renderMenuFromString(MENU);
38
39
function startOrStopSound() {
40
const myAudioPlayer = document.querySelector('#audioPlayer');
41
42
if (myAudioPlayer.paused) myAudioPlayer.play();
43
else myAudioPlayer.pause();
44
}
45
46
function renderMenuFromString(menu) {
47
const menuTableAsString = getMenuTableAsString(menu);
48
49
const main = document.querySelector('main');
50
51
main.innerHTML += menuTableAsString;
52
}
53
54
function getMenuTableAsString(menu) {
55
const menuTableLines = getAllTableLinesAsString(menu);
56
const menuTable = addLinesToTableHeadersAndGet(menuTableLines);
57
return menuTable;
58
}
59
60
function addLinesToTableHeadersAndGet(tableLines) {
61
const menuTable = `
62
<div class="table-responsive pt-5">
63
<table class="table table-danger">
64
<tr>
65
<th>Pizza</th>
66
<th>Description</th>
67
</tr>
68
${tableLines}
69
</table>
70
</div>
71
`;
72
return menuTable;
73
}
74
75
function getAllTableLinesAsString(menu) {
76
let pizzaTableLines = '';
77
78
menu?.forEach((pizza) => {
79
pizzaTableLines += `<tr>
80
<td>${pizza.title}</td>
81
<td>${pizza.content}</td>
82
</tr>`;
83
});
84
85
return pizzaTableLines;
86
}

Dans un premier temps, nous avons accédé à l'élément main en tant qu'HTML element (ou Node).

Nous avons ensuite généré une String multilignes à l'aide des template literals pour représenter le menu, pour finalement ajouter au main de nouveaux éléments en concaténant cette String à sa propriété innerHTML.

Ajout dynamique d'écouteurs d'événements une fois le DOM rendu

A ce stade-ci, nous souhaiterions ajouter un gestionnaire de passage de souris sur le menu.
Au passage sur le menu, nous souhaitons changer la couleur du background afin que le menu devienne vert. Lorsqu'on quitte le menu, celle-ci doit reprendre sa couleur initiale.

Voici le code à mettre à jour de index.js :

js
1
import 'bootstrap/dist/css/bootstrap.min.css';
2
import './stylesheets/main.css';
3
import 'animate.css';
4
5
const MENU = [
6
{
7
id: 1,
8
title: '4 fromages',
9
content: 'Gruyère, Sérac, Appenzel, Gorgonzola, Tomates',
10
},
11
{
12
id: 2,
13
title: 'Vegan',
14
content: 'Tomates, Courgettes, Oignons, Aubergines, Poivrons',
15
},
16
{
17
id: 3,
18
title: 'Vegetarian',
19
content: 'Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives',
20
},
21
{
22
id: 4,
23
title: 'Alpage',
24
content: 'Gruyère, Mozarella, Lardons, Tomates',
25
},
26
{
27
id: 5,
28
title: 'Diable',
29
content: 'Tomates, Mozarella, Chorizo piquant, Jalapenos',
30
},
31
];
32
33
const body = document.querySelector('body');
34
35
body.addEventListener('click', startOrStopSound);
36
37
renderMenuFromString(MENU);
38
39
attachOnMouseEventsToGoGreen();
40
41
function startOrStopSound() {
42
const myAudioPlayer = document.querySelector('#audioPlayer');
43
44
if (myAudioPlayer.paused) myAudioPlayer.play();
45
else myAudioPlayer.pause();
46
}
47
48
function renderMenuFromString(menu) {
49
const menuTableAsString = getMenuTableAsString(menu);
50
51
const main = document.querySelector('main');
52
53
main.innerHTML += menuTableAsString;
54
}
55
56
function getMenuTableAsString(menu) {
57
const menuTableLines = getAllTableLinesAsString(menu);
58
const menuTable = addLinesToTableHeadersAndGet(menuTableLines);
59
return menuTable;
60
}
61
62
function addLinesToTableHeadersAndGet(tableLines) {
63
const menuTable = `
64
<div class="table-responsive pt-5">
65
<table class="table table-danger">
66
<tr>
67
<th>Pizza</th>
68
<th>Description</th>
69
</tr>
70
${tableLines}
71
</table>
72
</div>
73
`;
74
return menuTable;
75
}
76
77
function getAllTableLinesAsString(menu) {
78
let pizzaTableLines = '';
79
80
menu?.forEach((pizza) => {
81
pizzaTableLines += `<tr>
82
<td>${pizza.title}</td>
83
<td>${pizza.content}</td>
84
</tr>`;
85
});
86
87
return pizzaTableLines;
88
}
89
90
function attachOnMouseEventsToGoGreen() {
91
const table = document.querySelector('table');
92
table.addEventListener('mouseover', () => {
93
table.className = 'table table-success';
94
});
95
96
table.addEventListener('mouseout', () => {
97
table.className = 'table table-danger';
98
});
99
}

Une fois le menu rendu, nous y accédons en obtenant une référence vers le Node représentant ce menu au sein du DOM tree.

Nous accédons à l'attribut class d'une table en modifiant, en JS, l'attribut className.
Il est aussi possible d'affiner la gestion des classes CSS en ajoutant ou supprimant des classes via la propriété classList d'un Node (ou HTML element).

Ici, nous avons utilisé les classes Bootstrap table-success pour mettre en vert, et table-danger pour que ça soit en rouge.

Il est à noter ces dangers quand on utilise des Strings pour générer de l'HTML.

⚡ Danger N°1 : oubli de mettre à jour le DOM avant d'attacher ses écouteurs

Il arrive très souvent aux jeunes développeurs web de faire une erreur de ce genre.
Regardez le code d'index.js et imaginez que vous attachiez les écouteurs d'événements dans renderMenuFromString() et plus après l'appel de cette fonction :

js
function renderMenuFromString(menu) {
const menuTableAsString = getMenuTableAsString(menu);
const main = document.querySelector('main');
attachOnMouseEventsToGoGreen();
main.innerHTML += menuTableAsString;
}

Comme la table n'a pas été rendue dans le DOM, il n'est pas possible d'accéder à celle-ci !
Une exception serait donc lancée dans la console de votre browser.

Tout cela semble logique, mais pensez-y en cas de souci avec vos écouteurs d'événements 😉.

⚡ Danger N°2 : mise à jour du DOM après avoir ajouté ses écouteurs

Il arrive aussi régulièrement aux jeunes développeurs web de faire une erreur de ce style.
Regardez le code d'index.js et imaginez que vous souhaitiez ajouter une légende au menu :

js
renderMenuFromString(MENU);
attachOnMouseEventsToGoGreen();
const mainWrapper = document.querySelector('main');
mainWrapper.innerHTML += '<figcaption class="text-light text-decoration-underline">Our pizzas</figcaption>';

⚡ L'update du DOM d'un élément fait un reset de tous les écouteurs d'événements précédemment attachés ! Pensez-y en cas de souci avec vos écouteurs d'événements 😉.
Dans notre exemple, la gestion des passages de la souris sur le menu des pizzas ne serait plus fonctionnelle.

Génération d'HTML à partir de Nodes

Il est possible de directement créer un Node (ou HTML element) à l'aide de la méthode document.createElement().

Une fois un Node créé, on demande de l'ajouter au "DOM tree" via la méthode appendChild().

Voici un petit exemple d'ajout d'un titre au main :

js
const mainWrapper = document.querySelector('main');
// Create the child element
const h1 = document.createElement('h1');
// Change its property
h1.innerText = 'Hello World';
// Attach the child element to its parent
mainWrapper.appendChild(h1);

Nous souhaitons ajouter un nouveau menu pour les boissons à notre site de la pizzeria, à générer à partir d'un array.

Voici le nouveau code à ajouter à votre projet pour générer la liste des boissons au sein de /src/index.js :

js
1
import 'bootstrap/dist/css/bootstrap.min.css';
2
import './stylesheets/main.css';
3
import 'animate.css';
4
5
const MENU = [
6
{
7
id: 1,
8
title: '4 fromages',
9
content: 'Gruyère, Sérac, Appenzel, Gorgonzola, Tomates',
10
},
11
{
12
id: 2,
13
title: 'Vegan',
14
content: 'Tomates, Courgettes, Oignons, Aubergines, Poivrons',
15
},
16
{
17
id: 3,
18
title: 'Vegetarian',
19
content: 'Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives',
20
},
21
{
22
id: 4,
23
title: 'Alpage',
24
content: 'Gruyère, Mozarella, Lardons, Tomates',
25
},
26
{
27
id: 5,
28
title: 'Diable',
29
content: 'Tomates, Mozarella, Chorizo piquant, Jalapenos',
30
},
31
];
32
33
const DRINKS = [
34
{
35
id: 1,
36
title: 'Lemonade',
37
content: 'Sparkling water, lemon, ice cubes',
38
},
39
{
40
id: 2,
41
title: 'Ice tea',
42
content: 'Mint, ginger, water',
43
},
44
{
45
id: 3,
46
title: 'Exotic Kombucha',
47
content: 'Mango, Sparkling water, Fermented tea',
48
},
49
];
50
51
const body = document.querySelector('body');
52
53
body.addEventListener('click', startOrStopSound);
54
55
renderMenuFromString(MENU);
56
57
attachOnMouseEventsToGoGreen();
58
59
renderDrinksFromNodes(DRINKS);
60
61
function startOrStopSound() {
62
const myAudioPlayer = document.querySelector('#audioPlayer');
63
64
if (myAudioPlayer.paused) myAudioPlayer.play();
65
else myAudioPlayer.pause();
66
}
67
68
function renderMenuFromString(menu) {
69
const menuTableAsString = getMenuTableAsString(menu);
70
71
const main = document.querySelector('main');
72
73
main.innerHTML += menuTableAsString;
74
}
75
76
function getMenuTableAsString(menu) {
77
const menuTableLines = getAllTableLinesAsString(menu);
78
const menuTable = addLinesToTableHeadersAndGet(menuTableLines);
79
return menuTable;
80
}
81
82
function addLinesToTableHeadersAndGet(tableLines) {
83
const menuTable = `
84
<div class="table-responsive pt-5">
85
<table class="table table-danger">
86
<tr>
87
<th>Pizza</th>
88
<th>Description</th>
89
</tr>
90
${tableLines}
91
</table>
92
</div>
93
`;
94
return menuTable;
95
}
96
97
function getAllTableLinesAsString(menu) {
98
let pizzaTableLines = '';
99
100
menu?.forEach((pizza) => {
101
pizzaTableLines += `<tr>
102
<td>${pizza.title}</td>
103
<td>${pizza.content}</td>
104
</tr>`;
105
});
106
107
return pizzaTableLines;
108
}
109
110
function attachOnMouseEventsToGoGreen() {
111
const table = document.querySelector('table');
112
table.addEventListener('mouseover', () => {
113
table.className = 'table table-success';
114
});
115
116
table.addEventListener('mouseout', () => {
117
table.className = 'table table-danger';
118
});
119
}
120
121
function renderDrinksFromNodes(drinks) {
122
const drinksTableAsNode = getDrinksTableAsNode(drinks);
123
124
const main = document.querySelector('main');
125
126
main.appendChild(drinksTableAsNode);
127
}
128
129
function getDrinksTableAsNode(drinks) {
130
const tableWrapper = document.createElement('div');
131
tableWrapper.className = 'table-responsive pt-5';
132
const table = document.createElement('table');
133
const tbody = document.createElement('tbody');
134
table.id = 'table-drinks';
135
table.className = 'table table-success';
136
tableWrapper.appendChild(table);
137
table.appendChild(tbody);
138
const header = document.createElement('tr');
139
const header1 = document.createElement('th');
140
header1.innerText = 'Drink';
141
const header2 = document.createElement('th');
142
header2.innerText = 'Description';
143
header.appendChild(header1);
144
header.appendChild(header2);
145
tbody.appendChild(header);
146
147
drinks?.forEach((drink) => {
148
const line = document.createElement('tr');
149
const title = document.createElement('td');
150
const description = document.createElement('td');
151
title.innerText = drink.title;
152
description.innerText = drink.content;
153
line.appendChild(title);
154
line.appendChild(description);
155
tbody.appendChild(line);
156
});
157
158
return tableWrapper;
159
}

Ajout dynamique d'écouteurs d'événements avant de rendre le DOM

Lorsque l'on vient de créer un Node, il est possible de directement lui attacher un écouteur d'événements.

Nous souhaiterions ajouter un gestionnaire de passage de souris sur la liste des boissons.
Au passage sur la liste, nous souhaitons changer la couleur du background afin qu'elle devienne rouge. Lorsqu'on quitte la liste, celle-ci doit reprendre sa couleur initiale.

Voici le code que vous devez ajouter à la méthode getDrinksTableAsNode() au sein de index.js :

js
1
function getDrinksTableAsNode(drinks) {
2
const tableWrapper = document.createElement('div');
3
tableWrapper.className = 'table-responsive pt-5';
4
const table = document.createElement('table');
5
const tbody = document.createElement('tbody');
6
table.id = 'table-drinks';
7
table.className = 'table table-success';
8
tableWrapper.appendChild(table);
9
table.appendChild(tbody);
10
const header = document.createElement('tr');
11
const header1 = document.createElement('th');
12
header1.innerText = 'Drink';
13
const header2 = document.createElement('th');
14
header2.innerText = 'Description';
15
header.appendChild(header1);
16
header.appendChild(header2);
17
tbody.appendChild(header);
18
19
drinks?.forEach((drink) => {
20
const line = document.createElement('tr');
21
const title = document.createElement('td');
22
const description = document.createElement('td');
23
title.innerText = drink.title;
24
description.innerText = drink.content;
25
line.appendChild(title);
26
line.appendChild(description);
27
tbody.appendChild(line);
28
});
29
30
table.addEventListener('mouseover', () => {
31
table.className = 'table table-danger';
32
});
33
34
table.addEventListener('mouseout', () => {
35
table.className = 'table table-success';
36
});
37
38
return tableWrapper;
39
}

Nous avons ajouté deux écouteurs d'événements à la table en cours de construction.
Notons que ces écouteurs peuvent être ajoutés avant ou après avoir fait l'ajout de la table dans le "DOM tree", cela ne change rien.

Regardez le code d'index.js et imaginez que vous souhaitiez ajouter une légende à la liste de boissons, en utilisant des Nodes :

js
renderDrinksFromNodes(DRINKS);
const mainWrapper = document.querySelector('main');
const figcaption = document.createElement('figcaption');
figcaption.innerText = 'Our drinks';
figcaption.className = 'text-light text-decoration-underline';
mainWrapper.appendChild(figcaption);

Le fait d'ajouter un Node au "DOM tree" ne fait pas de reset des écouteurs d'événements précédemment attachés ! C'est un avantage à générer de l'HTML à partir de Nodes plutôt qu'à partir d'une string.

N'hésitez pas à tester ce morceau de code pour vous rendre compte que la liste de boissons continue à bien gérer les mouvements de la souris.

Ajout dynamique d'images ou d'autres assets

YoutubeImage

Lorsque l'on souhaite ajouter une image ou tout autre assets (son, vidéo...) via du JS et que l'on utilise un module bundler comme Webpack, on ne peut pas juste ajouter une balise <image> avec le chemin en relatif vers celle-ci.

Pourquoi pas ? Parce qu'en fait, le bundler va s'occuper de copier, et parfois d'optimiser les assets dans le "build", généralement généré dans le répertoire /dist de votre projet. Ainsi, lorsque vous développez votre code, l'image se trouve à un endroit différent d'où se trouvera l'image lors du build.

Pour bien gérer les URL au sein de votre JS, vous devez d'abord importer vos assets.
Voici un exemple pour ajouter une image dynamiquement au sein du <footer> :

js
import pizzaImage from './img/pizza2.jpg';
renderPizzaImage(pizzaImage);
function renderPizzaImage(pizzaUrl) {
const image = new Image(); // or document.createElement('img');
image.src = pizzaUrl;
image.height = 50;
const footer = document.querySelector('footer');
footer.appendChild(image);
}

Le type du Asset Module configuré dans le fichier webpack.config.js est asset/resource.
Cela signifie que pour chaque fichier importé dans le JS, il sera émis dans le "output directory", (ou "build directory"), généralement dans /dist, avec comme nom de fichier quelque chose qui ressemble à un hash (par exemple 151cfcfa1bd74779aadb.png) et leur chemins (paths) seront injectés dans le bundle.

Dans l'exemple, le chemin de l'image pizza2.png lors de l'exécution de l'application sera donné dans la variable pizzaImage.

Veuillez mettre à jour votre site de la pizzeria pour afficher cette image dans le footer : Pizza à ajouter dans le footer [R.38].

Si tout fonctionne bien, faites un commit de votre repo (web2) avec comme message : modern-dynamic-hmi tutorial.

En cas de souci, vous pouvez accéder au code de cette étape du tutoriel ici : modern-dynamic-hmi.

Si vous souhaitez plus d'informations sur la gestion des assets via Webpack, vous pouvez le faire via les Asset Modules [R.37].

Debbuging d'un frontend tournant sous Webpack

YoutubeImage

💭 Qui est votre meilleur ami ?

Il est possible qu'à ce stade-ci, vous ignorez une des bonnes réponses, car pour les développeurs, le debugger est leur meilleur ami !

Le debugger est toujours là pour vous, prêt à vous faire voyager pas à pas dans votre code, à vous donner des pistes dans les moments difficiles, sans imposer de solutions, il vous offre une liberté totale ! Et il acceptera toujours votre code tel qu'il est, sous réserve bien sûr que celui-ci compile.
C'est exactement ce que l'on attend d'un ami 😁.

Le tutoriel qui suit apprend à débugger un frontend qui est développé en JS et qui utilise Webpack comme module bundler.

Projet 2.8 : Génération dynamique d'une HomePage

Veuillez faire un refactor de la HomePage que vous avez développée pour Projet 2.7.

Vous devez utiliser un container statique, ou autrement dit un "wrapper", dans votre page index.html (par exemple une balise main ou une div). Vous allez dynamiquement ajouter les images et le texte associé à votre Homepage dans votre "wrapper".

Veuillez repartir du code de Projet 2.7.
Le code de votre application doit se trouver dans votre repository local et votre web repository (normalement appelé web2) dans le répertoire nommé /project/2.8.

Quand votre application est finalisée, veuillez faire un commit de votre code avec comme message : 2.8: dynamic HomePage.

🤝 Tips

  • Pensez à faire l'import de vos images au sein de index.js.

Projet 2.9 : Changement dynamique du contenu d'une page

Vous allez mettre à jour la HomePage développée pour Projet 2.8 afin de permettre d'afficher un texte sur le ou les auteur(s) de l'application lorsqu'on clique sur un bouton.

Veuillez ajouter un bouton (nommé "About" par exemple) :

  • lorsqu'on clic dessus, vous devez remplacer tout le contenu de la HomePage par un texte présentant le ou les auteur(s) de l'application.
  • quand le contenu de type "About" est affiché, un bouton (nommé "Back" par exemple) doit permettre de revenir au contenu initial de la HomePage.

Veuillez repartir du code de Projet 2.8.
Le code de votre application doit se trouver dans votre repository local et votre web repository (normalement appelé web2) dans le répertoire nommé /project/2.9.

Quand votre application est finalisée, veuillez faire un commit de votre code avec comme message : 2.9: dynamic page content.

🤝 Tips

  • Les boutons sont utilisés pour afficher deux contenus différents, un contenu de type "HomePage", ou un contenu de type "AboutPage".

🍬 Exercice 2.10 : Génération dynamique d'une table sur base d'un formulaire

Vous allez réaliser une application web permettant de générer une table sur base d'un formulaire.

Créez un formulaire permettant d'introduire un nombre de lignes, un nombre de colonne, et une chaine de base.
Utilisez Bootstrap pour formater votre application web.
Néanmoins, si vous êtes à l'aise avec une autre technologie, n'hésitez pas à créer votre UI via du Vanilla CSS ou une autre librairie (tailwindcss…).

Voilà à quoi pourrait ressembler votre formulaire :

GatsbyImage

Veuillez valider chacun des champs du formulaire lors du clic sur le bouton.

Si tous les champs sont validés, veuillez générer et afficher une table HTML.
Voici un exemple de résultat :

GatsbyImage

Afin de réaliser cet exercice nous vous proposons ces contraintes d'implémentation :

  • Créez une 1ère fonction nommée createArray qui retourne un Array à deux dimensions avec :
    • Comme valeur pour chaque élément : une String au format "chaine de base[numéro de ligne][numéro de colonne]",
    • Sur base de 3 paramètres : le nombre de lignes, de colonnes, et la chaine de base à afficher dans chaque élément du tableau.
  • Créez une deuxième fonction nommée createHtmlTableAsString qui renvoie, sous forme de string, une table HTML basée sur un Array ; vous lui passerez l'array créé par la 1ère fonction.
  • Appelez ces deux fonctions afin d'afficher la table de manière dynamique au sein d'un "wrapper" (une div est conseillée).

Veuillez créer un nouveau projet dans votre repository local et votre web repository (normalement appelé web2) nommé /exercises/2.10 sur base du boilerplate : boilerplate de base.

Quand votre application est finalisée, veuillez faire un commit de votre code avec comme message : 2.10: dynamic table.

🤝 Tips

  • Vous pouvez utiliser la propriété .innerHtml de votre wrapper pour afficher dynamiquement la table.
  • Pour gérer le submit d'un formulaire, il existe plusieurs façons de le faire. La façon recommandée est d'écouter les événements de type "submit" sur le formulaire. Il est ainsi possible de faire un submit d'un formulaire à l'aide de la touche Enter.
  • Comment accéder aux champs de votre formulaire lors du "submit" ?
    La valeur d'un champs est accessible via la propriété value d'un input.
  • N'oubliez pas que lors d'un clic sur un élément qui amène à un "submit", le comportement par défaut du formulaire est de recharger la page. Vous devez donc stopper ce comportement par défaut pour assurer que le tableau que vous générez ne soit pas "effacé".