Le DHTML, signifie simplement Dynamic HTML. Il ne s'agit pas d'un langage particulier, mais d'une notion de développement web qui permet la manipulation de la page, de ses éléments, de leur contenu et leur apparence, une fois que la page est chargée.
Historiquement, le HTML ne permettait pas de modifier le contenu d'une page une fois que l'événement onload
était déclenché. La seule solution pour le développeur était d'utiliser des formulaires pour y faire apparaitre des résultats.
Grâce aux travaux de normalisation des langages JS et HTML et des navigateurs, le DHTML est à l'origine de toutes les interfaces web qui remplacent les applications et logiciels exécutables traditionnels, comme les outils de messagerie mail, d'éditeurs de document, les tableurs ou même les sites comme Facebook.
Utilisons un premier exemple simple de ce que permet le DHTML : mise à jour de contenu, modification de l'apparence, réaction aux actions de l'utilisateur.
J'ai commencé la mise à jour de ce tutoriel le jour où le Bitcoin a dépassé les 10 000 $ (le 28/11/2018). L'occasion était évidente pour en faire un exemple didactique. (Envie d'en savoir plus sur le Bitcoin ?)
Le module a ce code HTML :
<div id="exemple"> <div id="titre">Cours du Bitcoin (BTC)</div> <div id="cours" data-devise="USD"> </div> <div class="footer"> <div id="euro" class="deviseOFF" onclick="clickDevise('euro')"> EUR </div> <div id="usd" class="deviseON" onclick="clickDevise('usd')"> USD </div> </div> <div id="info">Récupération de la valeur du Bitcoin...</div> <div id="chrono"> </div> <div id="legal">Cours provenant de cryptocompare.com</div> </div>
Le code JS est assez long. Il n'est pas affiché, mais vous pouvez le voir en cliquant sur le lien :
/* Formatage de nombre */ function formatNombre(n) { var intlN=new Intl.NumberFormat(); return intlN.format(n); } /* Formatage de date et heure */ function formatHeure(dt) { var options={hour12: false, hour: "2-digit", minute: "2-digit", second:"2-digit"}; var intlD=new Intl.DateTimeFormat("fr-FR", options); return intlD.format(dt); } /* Gestion du clic sur EUR ou USD et mise à jour de l'affichage */ function clickDevise(devise) { if (devise=="euro") { document.getElementById("euro").className="deviseON"; document.getElementById("usd").className="deviseOFF"; document.getElementById("cours").dataset.devise="EUR"; } if (devise=="usd") { document.getElementById("usd").className="deviseON"; document.getElementById("euro").className="deviseOFF"; document.getElementById("cours").dataset.devise="USD"; } afficheCours(); } /* Récupération des données de cours du Bitcoin */ function getCours() { /* Affichage de l'appel en cours */ document.getElementById("info").innerHTML="Récupération de la valeur du Bitcoin..."; document.getElementById("chrono").innerHTML=" "; /* Appel AJAX vers cryptocompare.com */ var ajax = new XMLHttpRequest(); ajax.onload = function() { if (this.status == 200) { var json=JSON.parse(this.response); var usd=json.USD, eur=json.EUR; var dt=new Date(); var cours=document.getElementById("cours"); cours.dataset.last=formatHeure(dt); cours.dataset.euro=json.EUR; cours.dataset.usd=json.USD; cours.dataset.timestamp=dt.getTime()+60*1000; /* Timestamp du prochain appel ajax */ afficheCours(); chrono(); } } ajax.open("GET", "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD,EUR", true); ajax.send(); } /* Mise à jour de l'affichage */ function afficheCours() { /* Recherche des infos de cours à utiliser */ var cours=document.getElementById("cours"); var devise=cours.dataset.devise; if (devise=="EUR") { var valeur=formatNombre(cours.dataset.euro)+" €"; } if (devise=="USD") { var valeur=formatNombre(cours.dataset.usd)+" $"; } cours.innerHTML = valeur; document.getElementById("info").innerHTML = "Cours récupéré à "+cours.dataset.last; } /* Fonction de chrono qui affiche le temps restant et lance l'appel AJAX */ function chrono() { var dt=new Date(); var timestamp=parseFloat(document.getElementById("cours").dataset.timestamp); var delai=Math.round((timestamp-dt.getTime())/1000); var chrono=document.getElementById("chrono"); chrono.innerHTML="Prochaine mise à jour du cours dans "+delai+" seconde"+( delai>1 ? "s":"" ); if (delai <= 0) { getCours(); } else { setTimeout("chrono()", 500); } } /* Démarrage du traitement */ getCours();
Ce qui donne ce module :
Le module d'affichage du cours du Bitcoin propose les fonctionnalités suivantes :
- Recherche du cours du Bitcoin via un appel AJAX vers le service de cryptocompare.com
- Traitement du retour au format JSON
- Enregistrement des données de cours dans le dataset
- Mise à jour de l'affichage
- Détection du clic de l'utilisateur sur les boutons de devises EUR ou USD et mise à jour de l'affichage
- Lancement d'un chrono pour mettre à jour le cours toutes les 60 secondes
Ce petit script utilise, en plus du DHTML qui sera détaillé dans les prochains paragraphes, de nombreux éléments du JavaScript comme le traitement du format JSON, les appels AJAX avec XMLHttpRequest, la création de timers avec setTimeout(), le formatage de nombre avec NumberFormat et le formatage de date et heure avec DateTimeFormat.
Retrouvez ce script dans un formulaire de conversion automatique de montants en BTC/$/US
Un mot sur la compatibilité entre navigateurs est indispensable, tant le souvenir de différences flagrantes hantent encore les rêves de développeurs web de plus d'une dizaine d'années d'ancienneté.
Il n'est maintenant plus nécessaire de prévoir plusieurs versions radicalement différentes de script en détectant le navigateur
Maintenant tous les navigateurs récents sur PC sont compatibles entre eux, au moins pour les fonctionnalités de base présentées ici. Les navigateurs mobiles sont eux encore plus en avance.
Dans tous les cas, la bonne habitude de tester vos développements sur plusieurs appareils et dans différentes configurations de connectivité et de performance reste d'actualité : Un script validé sur PC avec une connexion fibre risque de ne pas être aussi agréable sur un smartphone peu puissant avec un accès réseau limité en situation de mobilité.
La première fonction à laquelle on pense en parlant de DHTML est getElementById("identifiant") qui retourne un objet JavaScript de type HTMLElement correspondant au premier élément HTML du document ayant la propriété id="identifiant"
. C'est cet objet que le JavaScript va pouvoir manipuler pour modifier son contenu, son apparence, sa position, ses classes CSS et même le détruire.
Ainsi l'objet div
suivant :
<div id="monDiv">Contenu du DIV</div>
sera accessible en JavaScript par cette simple ligne :
var element=document.getElementById("monDiv")
Attention, getElementById() retourne null
si aucun élément n'est trouvé dans le document. Il n'est donc pas possible de l'utiliser par la suite sans provoquer une erreur JavaScript.
De nombreuses autres fonctions, au fonctionnement similaire, sont également disponibles pour faciliter l'accès à tous les éléments du document. Ces fonctions retournent un ou plusieurs objets HTMLElement.
Pour récupérer tous les éléments dont la balise est <tag>
, utilisez getElementsByTagName("tag"). Notez le s
à getElements
qui indique que le retour est un tableau d'éléments HTML. Pour traiter chacun des éléments retournés, il sera nécessaire de faire une boucle sur le tableau ou d'utiliser la méthode rapide forEach().
Pour récupérer tous les éléments possédant une certaine classe, utilisez getElementsByClassName("className").
Les méthodes querySelector() et querySelectorAll() attendent en paramètre une chaîne de caractères au format CSS.
Attention avec les sélecteurs trop complexes : les incompatibilités sont possibles dans certains navigateurs qui n'implémentent cette recherche que de manière plus récente.
Le contenu d'un élément est accessible et modifiable par sa propriété innerHTML. Cette propriété contient une chaîne de caractères au format HTML qu'il est possible de modifier par programmation. Chaque modification a un impact direct dans le rendu sur la page.
Il est donc très facile de modifier le contenu d'un élément. Dans l'exemple du bitcoin, nous utilisons la syntaxe :
document.getElementById("cours").innerHTML=valeur+" $";
Il existe un ensemble de propriétés de balise valides au sens de la norme W3C sur les. Toutes ces propriétés de balise sont accessibles par la notation pointée sur dataset. L'exemple Bitcoin utilise largement cette possibilité pour stocker les informations reçues de l'appel AJAX et les réutiliser lors des interactions de l'utilisateurs.
Dans l'exemple Bitcoin, les informations sont stockées avec à partir du retour au format JSON :
var cours = document.getElementById("cours"); cours.dataset.euro=json.EUR; cours.dataset.usd=json.USD;
Dans le document, l'élément HTML id="cours"
contient ce type de valeurs :
<div id="cours" data-timestamp="1511885079594" data-last="17:03:39" data-euro="8356.15" data-usd="9902.12" data-devise="USD"> 9 902,12 $ </div>
Vous pouvez l'observer facilement sur la page grâce à la console du navigateur et à l'inspecteur de document.
Les classes d'un élément sont accessibles par className, chaîne de caractères équivalente à la propriété class
de la balise ou par classList, tableau contenant toutes les classes de l'élément.
Tous les éléments de style d'un élément HTML sont accessibles par la propriété style
. Le JavaScript reprend l'ensemble des propriétés CSS. Leur nom est identique. Il suffit de remplacer les -
dans les noms CSS par une majscule. Ainsi, background-color
devient style.backgroundColor
en JS.
Il est temps de regrouper les premières notions de DHTML dans un exemple complet, avec quelques éléments HTML à manipuler :
<div id="monDiv" class="monDiv1 monDiv2"></div>
Et la partie script :
var monDiv=document.getElementById("monDiv"); console.log(monDiv); console.log(monDiv.classList); monDiv.innerHTML="Contenu ajouté par le script"; monDiv.style.border="2px solid #E00";
Le script accède à l'élément monDiv
, affiche ses classes dans la console, modifie son contenu et lui ajoute une bordure rouge :
Si innerHTML permet de modifier le contenu d'un élément avec du code HTML et donc d'ajouter des éléments facilement, il existe d'autres fonctions plus efficaces pour créer des éléments et les ajouter précisément dans le document.
createElement(tagName) retourne un objet HTML ayant la balise tagName
.
L'appel à setAttribute() permet de lui affecter des propriétés et leurs valeurs.
La méthode appendChild(enfant) ajoute l'objet enfant
en dernière position sur l'objet sur lequel elle est appelée.
La méthode insertBefore(enfant, position) ajoute l'objet enfant
avant l'objet position
.
De manière réciproque, la méthode removeChild(enfant) supprime l'objet enfant
de l'objet sur lequel elle est appelée.
h2
de la page pour les surligner :
<button class="btn radius btn-outline cursor-pointer" onclick="surlignerMenu()">Surligner le plan</button>
Avec la fonction de surlignage :
function surlignerMenu() { h2s=document.getElementsByTagName("h2"); console.log(h2s.length+" éléments h2 trouvés dans la page "); for (var i=0; i<h2s.length; i++) { h2s[i].className+=" H2ON"; /* Ajout de la classe de surlignage */ /* Création du picto h2 */ var elt=document.createElement("div"); elt.innerHTML="h2"; elt.className="h2Info"; /* Ajout du picto dans l'élément de plan */ h2s[i].appendChild(elt); } }
Cliquez sur le bouton pour surligner le plan de la page :
Il est possible d'animer des éléments de la page en utilisant le JavaScript pur, en incrémentant une variable ou en effectuant un calcul à chaque intervalle de temps. Voici un exemple pour faire apparaitre un élément div
:
<div id="anim1">Mon élément</div> <div class="btn radius btn-outline cursor-pointer" onclick="anim1()">Lancer l'animation JS</div>
Et la fonction anim1()
:
var opacity1=1; function anim1() { opacity1-=0.05; if (opacity1 < 0) {opacity1=0;} document.getElementById("anim1").style.opacity=opacity1; if (opacity1>0) { setTimeout("anim1()",50); } else { opacity1=1; document.getElementById("anim1").style.opacity=1; } }
Cliquez sur le bouton pour lancer l'animation :
La fonction lance un timer avec setTimeout() renouvelé tant que opacity1
est positif. Une fois que l'objet a complétement disparu, il est réaffiché.
Les navigateurs intègrent nativement des mécanismes d'optimisation des traitements graphiques bien plus performants avec les propriétés CSS, parfois en utilisant le co-processeur graphique de l'appareil de l'utilisateur. Les animations via CSS sont à privilégier : elles sont nettement plus fluides et moins gourmandes en puissance et en énergie.
Reprenons notre code HTML :
<div id="anim2">Mon élément</div> <div class="btn radius btn-outline cursor-pointer" onclick="anim2()">Lancer l'animation CSS</div>
Voici la définition de l'animation CSS :
@keyframes disparition { from { opacity: 1; } to { opacity: 0; } } .disparait { animation-duration: 1s; animation-timing-function: linear; animation-delai: 0s; animation-iteration-count: 1; animation-name: disparition; }
Ici, disparition
est l'ensemble des étapes de l'animation simplifiée au maximum partant de from
à to
.
La classe disparait
contient toutes les propriétés de l'animation.
Et la fonction anim2()
qui affecte la classe CSS disparait
à div#anim2
:
/* Détection du lancement et de la fin de l'animation */ document.getElementById("anim2").addEventListener("animationstart", function() { console.log("animation disparait START") }, false); document.getElementById("anim2").addEventListener("animationend", function() { console.log("animation disparait END") this.className=""; }, false); function anim2() { document.getElementById("anim2").className="disparait"; }
Comparer les 2 exécutions, la version CSS est plus agréable à l'oeil, tout en étant moins gourmande pour le navigateur.
Notez que la console affiche un message grâce à la détection des événements de lancement et de fin de l'animation générée par addEventListener.
En plus de la fluidité, les animations CSS apportent une simplicité de programmation intéressante. Il est possible de créer des animations simplement par la déclaration CSS sans avoir de calcul de timing ou de transformation à effectuer (comme un code couleur RGB par exemple).
PS : Le but n'est pas ici de faire un cours sur les transformations et les transitions CSS, mais de montrer l'interaction puissante que le JavaScript peut apporter.
Les animations doivent être utilisées à bon escient. Le but d'un site classique est de rendre service à ses utilisateurs, pas de faire une démonstration technologique ou artistique.
Le DHTML permet de faire des traitements directement dans la page sur les données présentes et afficher un rendu très rapidement, sans avoir à faire appel au serveur et donc sans latence réseau supplémentaire.
Un bon exemple est le tri de tableau par clic sur les entêtes de colonnes. Imaginons un tableau de clients qui contient le nombre de commandes, le total de chiffre d'affaires et le panier moyen :
<table id="tritable"> <tr> <th onclick="triTable(this, 0)">Client</th> <th onclick="triTable(this, 1)">Commandes</th> <th onclick="triTable(this, 2)">Chiffre Affaires</th> <th onclick="triTable(this, 3)">Panier Moyen</th> </tr> <tr><td>Raymond</td><td class="number">2</td><td class="number">250</td><td class="number">125</td></tr> <tr><td>Gérard</td><td class="number">1</td><td class="number">150</td><td class="number">150</td></tr> <tr><td>Lucie</td><td class="number">10</td><td class="number">850</td><td class="number">85</td></tr> <tr><td>Arthur</td><td class="number">5</td><td class="number">50</td><td class="number">10</td></tr> </table>
Définissons la fonction de tri triTable()
:
function triTable(th, colonne) { /* th est l'élément cliqué qui permet de remonter dans le DOM à la table à trier */ /* colonne est le numéro de colonne à utiliser pour le tri */ var table=th.parentNode.parentNode.parentNode; console.log(table.getAttribute("id")); var tbody=table.getElementsByTagName("tbody")[0]; var trs=tbody.getElementsByTagName("tr"); /* Liste des lignes */ var triABulle=true; /* Indicateur que le tri n'est pas terminé */ var nbMouvement=0; while (triABulle) { var isMouvement=false; for (var i=1; i < trs.length - 1; i++) { var tdi=trs[i].getElementsByTagName("td")[colonne]; var tdiplus1=trs[i+1].getElementsByTagName("td")[colonne]; var ei=tdi.innerText.toLowerCase(); var eiplus1=tdiplus1.innerText.toLowerCase(); if (tdi.className=="number") { /* Conversion en nombres */ var ei=parseFloat(tdi.innerText); var eiplus1=parseFloat(tdiplus1.innerText); } if ( ei > eiplus1 ) { /* Un mouvement à effectuer a été détecté */ isMouvement=true; nbMouvement++; break; /* La boucle for est interrompue */ } } if (isMouvement) { /* il faut inverser les lignes i et i+1 */ trs[i].parentNode.insertBefore(trs[i + 1], trs[i]); console.log("mouvement sur la ligne "+i); } else { /* Pas de mouvement, le tri à bulles est terminé */ triABulle=false; } } }
Le tableau s'affiche sous la forme classique. Cliquez sur les titres de colonnes pour changer l'ordre des lignes.
L'algorithme utilisé est le tri à bulles qui consiste à parcourir le tableau autant de fois que nécessaire pour remonter (comme une bulle) chaque élément selon son classement et le tri à effectuer. Le parcourt du tableau s'arrête quand il n'y a plus de mouvements de ligne.
Une fonction de quelques lignes appelée au clic d'une entête de colonne peut trier un tableau et être réutilisée partout.
Naturellement, cet exemple est simpliste, avec des capacités de tris limitées. Sur les dates par exemple, le tri ne serait pas correct et un traitement spécial (comme pour les nombres serait nécessaire). L'exemple ne gère pas non plus la pagination pour les tableaux très longs. On pourrait ajouter des flèches dans les titres de colonnes pour indiquer le sens et la colonne triée.
Des développeurs de talent ont partagé leurs travaux sur l'optimisation des accès au DOM, en rendant le code plus compact, plus lisible et en gérant les cas particuliers entre navigateurs de manière transparente pour l'utilisateur.
Lisez l'introduction à jQuery, le framework indispensable à connaître.