e) Création dynamique de UI
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
:
js1import 'bootstrap/dist/css/bootstrap.min.css';2import './stylesheets/main.css';3import 'animate.css';45const MENU = [6{7id: 1,8title: '4 fromages',9content: 'Gruyère, Sérac, Appenzel, Gorgonzola, Tomates',10},11{12id: 2,13title: 'Vegan',14content: 'Tomates, Courgettes, Oignons, Aubergines, Poivrons',15},16{17id: 3,18title: 'Vegetarian',19content: 'Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives',20},21{22id: 4,23title: 'Alpage',24content: 'Gruyère, Mozarella, Lardons, Tomates',25},26{27id: 5,28title: 'Diable',29content: 'Tomates, Mozarella, Chorizo piquant, Jalapenos',30},31];3233const body = document.querySelector('body');3435body.addEventListener('click', startOrStopSound);3637renderMenuFromString(MENU);3839function startOrStopSound() {40const myAudioPlayer = document.querySelector('#audioPlayer');4142if (myAudioPlayer.paused) myAudioPlayer.play();43else myAudioPlayer.pause();44}4546function renderMenuFromString(menu) {47const menuTableAsString = getMenuTableAsString(menu);4849const main = document.querySelector('main');5051main.innerHTML += menuTableAsString;52}5354function getMenuTableAsString(menu) {55const menuTableLines = getAllTableLinesAsString(menu);56const menuTable = addLinesToTableHeadersAndGet(menuTableLines);57return menuTable;58}5960function addLinesToTableHeadersAndGet(tableLines) {61const 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`;72return menuTable;73}7475function getAllTableLinesAsString(menu) {76let pizzaTableLines = '';7778menu?.forEach((pizza) => {79pizzaTableLines += `<tr>80<td>${pizza.title}</td>81<td>${pizza.content}</td>82</tr>`;83});8485return 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
:
js1import 'bootstrap/dist/css/bootstrap.min.css';2import './stylesheets/main.css';3import 'animate.css';45const MENU = [6{7id: 1,8title: '4 fromages',9content: 'Gruyère, Sérac, Appenzel, Gorgonzola, Tomates',10},11{12id: 2,13title: 'Vegan',14content: 'Tomates, Courgettes, Oignons, Aubergines, Poivrons',15},16{17id: 3,18title: 'Vegetarian',19content: 'Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives',20},21{22id: 4,23title: 'Alpage',24content: 'Gruyère, Mozarella, Lardons, Tomates',25},26{27id: 5,28title: 'Diable',29content: 'Tomates, Mozarella, Chorizo piquant, Jalapenos',30},31];3233const body = document.querySelector('body');3435body.addEventListener('click', startOrStopSound);3637renderMenuFromString(MENU);3839attachOnMouseEventsToGoGreen();4041function startOrStopSound() {42const myAudioPlayer = document.querySelector('#audioPlayer');4344if (myAudioPlayer.paused) myAudioPlayer.play();45else myAudioPlayer.pause();46}4748function renderMenuFromString(menu) {49const menuTableAsString = getMenuTableAsString(menu);5051const main = document.querySelector('main');5253main.innerHTML += menuTableAsString;54}5556function getMenuTableAsString(menu) {57const menuTableLines = getAllTableLinesAsString(menu);58const menuTable = addLinesToTableHeadersAndGet(menuTableLines);59return menuTable;60}6162function addLinesToTableHeadersAndGet(tableLines) {63const 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`;74return menuTable;75}7677function getAllTableLinesAsString(menu) {78let pizzaTableLines = '';7980menu?.forEach((pizza) => {81pizzaTableLines += `<tr>82<td>${pizza.title}</td>83<td>${pizza.content}</td>84</tr>`;85});8687return pizzaTableLines;88}8990function attachOnMouseEventsToGoGreen() {91const table = document.querySelector('table');92table.addEventListener('mouseover', () => {93table.className = 'table table-success';94});9596table.addEventListener('mouseout', () => {97table.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 :
jsfunction 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 :
jsrenderMenuFromString(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 :
jsconst mainWrapper = document.querySelector('main');// Create the child elementconst h1 = document.createElement('h1');// Change its propertyh1.innerText = 'Hello World';// Attach the child element to its parentmainWrapper.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 :
js1import 'bootstrap/dist/css/bootstrap.min.css';2import './stylesheets/main.css';3import 'animate.css';45const MENU = [6{7id: 1,8title: '4 fromages',9content: 'Gruyère, Sérac, Appenzel, Gorgonzola, Tomates',10},11{12id: 2,13title: 'Vegan',14content: 'Tomates, Courgettes, Oignons, Aubergines, Poivrons',15},16{17id: 3,18title: 'Vegetarian',19content: 'Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives',20},21{22id: 4,23title: 'Alpage',24content: 'Gruyère, Mozarella, Lardons, Tomates',25},26{27id: 5,28title: 'Diable',29content: 'Tomates, Mozarella, Chorizo piquant, Jalapenos',30},31];3233const DRINKS = [34{35id: 1,36title: 'Lemonade',37content: 'Sparkling water, lemon, ice cubes',38},39{40id: 2,41title: 'Ice tea',42content: 'Mint, ginger, water',43},44{45id: 3,46title: 'Exotic Kombucha',47content: 'Mango, Sparkling water, Fermented tea',48},49];5051const body = document.querySelector('body');5253body.addEventListener('click', startOrStopSound);5455renderMenuFromString(MENU);5657attachOnMouseEventsToGoGreen();5859renderDrinksFromNodes(DRINKS);6061function startOrStopSound() {62const myAudioPlayer = document.querySelector('#audioPlayer');6364if (myAudioPlayer.paused) myAudioPlayer.play();65else myAudioPlayer.pause();66}6768function renderMenuFromString(menu) {69const menuTableAsString = getMenuTableAsString(menu);7071const main = document.querySelector('main');7273main.innerHTML += menuTableAsString;74}7576function getMenuTableAsString(menu) {77const menuTableLines = getAllTableLinesAsString(menu);78const menuTable = addLinesToTableHeadersAndGet(menuTableLines);79return menuTable;80}8182function addLinesToTableHeadersAndGet(tableLines) {83const 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`;94return menuTable;95}9697function getAllTableLinesAsString(menu) {98let pizzaTableLines = '';99100menu?.forEach((pizza) => {101pizzaTableLines += `<tr>102<td>${pizza.title}</td>103<td>${pizza.content}</td>104</tr>`;105});106107return pizzaTableLines;108}109110function attachOnMouseEventsToGoGreen() {111const table = document.querySelector('table');112table.addEventListener('mouseover', () => {113table.className = 'table table-success';114});115116table.addEventListener('mouseout', () => {117table.className = 'table table-danger';118});119}120121function renderDrinksFromNodes(drinks) {122const drinksTableAsNode = getDrinksTableAsNode(drinks);123124const main = document.querySelector('main');125126main.appendChild(drinksTableAsNode);127}128129function getDrinksTableAsNode(drinks) {130const tableWrapper = document.createElement('div');131tableWrapper.className = 'table-responsive pt-5';132const table = document.createElement('table');133const tbody = document.createElement('tbody');134table.id = 'table-drinks';135table.className = 'table table-success';136tableWrapper.appendChild(table);137table.appendChild(tbody);138const header = document.createElement('tr');139const header1 = document.createElement('th');140header1.innerText = 'Drink';141const header2 = document.createElement('th');142header2.innerText = 'Description';143header.appendChild(header1);144header.appendChild(header2);145tbody.appendChild(header);146147drinks?.forEach((drink) => {148const line = document.createElement('tr');149const title = document.createElement('td');150const description = document.createElement('td');151title.innerText = drink.title;152description.innerText = drink.content;153line.appendChild(title);154line.appendChild(description);155tbody.appendChild(line);156});157158return 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
:
js1function getDrinksTableAsNode(drinks) {2const tableWrapper = document.createElement('div');3tableWrapper.className = 'table-responsive pt-5';4const table = document.createElement('table');5const tbody = document.createElement('tbody');6table.id = 'table-drinks';7table.className = 'table table-success';8tableWrapper.appendChild(table);9table.appendChild(tbody);10const header = document.createElement('tr');11const header1 = document.createElement('th');12header1.innerText = 'Drink';13const header2 = document.createElement('th');14header2.innerText = 'Description';15header.appendChild(header1);16header.appendChild(header2);17tbody.appendChild(header);1819drinks?.forEach((drink) => {20const line = document.createElement('tr');21const title = document.createElement('td');22const description = document.createElement('td');23title.innerText = drink.title;24description.innerText = drink.content;25line.appendChild(title);26line.appendChild(description);27tbody.appendChild(line);28});2930table.addEventListener('mouseover', () => {31table.className = 'table table-danger';32});3334table.addEventListener('mouseout', () => {35table.className = 'table table-success';36});3738return 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 :
jsrenderDrinksFromNodes(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
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>
:
jsimport 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
💭 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 :

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 :

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 unArray
à 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 unArray
; 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'uninput
. - 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é".