LIFWEB - CM2

Document Object Model - DOM

Romuald THION

Semestre printemps 2023-2024 UCBL

Chargement JavaScript côté client

Cf. Exercice 0 : Préparation de TP du TP2.

Différentes méthodes

  • ⚠️ directement en ligne dans le HTML
  • ⚠️ dans l’élément <script>
  • 👌 attribut src de l’élément <script> (MDN)
    • options async et defer
    • option type=module"

Options async et defer

  • 👌 <script defer src="..."> 👌
    • Exécuté après l’analyse de toute la page
    • Juste avant l’événement DOMContentLoaded.
    • Si plusieurs defer, ils sont exécutés dans l’ordre
  • ⚠️ <script src="..."> ⚠️
    • Le script est exécuté immédiatement
    • Seul le contenu avant l’élément est disponible
  • ⚠️ <script async src="..."> ⚠️
    • Exécuté quand le navigateur le voudra bien…
    • Aucun de contrôle sur l’ordre d’exécution

Option type="module" ou type="text/javascript" (défaut)

Chargement du JS en module (javascript.info, MDN) :

  • Implicitement defer ;
  • Implicitement en mode strict ;
  • Exécuté au plus une fois (e.g., singleton) ;
  • Espace de nom propre (pas de pollution) ;
  • Autorise les top level await ;
  • 👎 ne fonctionne pas sur file:// :

Document Object Model

javascript.info - Browser: Document, Events, Interfaces

MDN - Document Object Model (DOM)

MDN - Examples of […] development using the DOM

Introduction

Qu’est-ce que le Document Object Model (DOM) ?

  • Le (DOM) n’est pas JavaScript, c’est une API (Application Programming Interface)
  • Cette interface standard permet d’interagir programmatiquement avec le navigateur
    • documents HTML, XML, SVG, MathML, CSS, etc.
    • navigateur, onglets, etc.
  • Spécification DOM Level 4 (spécification, Wikipédia)

Les navigateurs proposent de nombreuses Web APIs autres que le DOM :

Tous les navigateurs proposent une interface JS avec une implémentation du DOM :

  • L’API DOM est aussi disponible dans d’autres langages, sous forme de bibliothèque (tierce), e.g., xml.dom en Python.
  • Sans le DOM et les autres APIs, JS ne ferait rien
    • ☢️ JS s’exécute dans un bac à sable par sécurité ☢️

Arbre HTML via le DOM

Le DOM permet de naviguer et d’éditer les documents via leurs représentations en mémoire, le DOM tree.

Le document HTML
L’arbre DOM associé

Source javascript.info

Dynamique de l’arbre DOM

  • La racine de l’arbre est globalThis.document
  • Les éléments HTML sont des Node de l’arbre
  • Tout est représenté dans l’arbre
  • L’arbre DOM est live :
    • les modifications effectuées en JS sont immédiatement visibles dans le navigateur ;
    • les modifications effectuées dans le navigateur via les DevTools sont visibles en JS.
Exemple d’arbre DOM Live

Sur une page HTML initialement vide

const $body = document.body; // HTMLBodyElement

console.debug($body.childNodes);
// NodeList []

$body.innerHTML = "<h1>Titre</h1><p>Bonjour</p>";

console.debug($body.childNodes);
//NodeList [ h1, p ]

$body.innerHTML = "";

console.debug($body.childNodes);
// NodeList []

Principaux types du DOM

Le système de classes du DOM est riche avec de nombreuses interfaces et abstractions, javascript.info
Interface Description
Document La classe de la racine de l’arbre DOM, l’objet document.
EventTarget La classe la plus abstraite, la gestion événementielle.
Interface Description
Node Un nœud abstrait de l’arbre, soit un Element, soit du texte.
Element Une interface abstraite pour les éléments, hérite de Node, commun à HTMLElement et SVGElement.
Interface Description
HTMLElement Un élément HTML est spécialisé par les éléments concrets comme HTMLAnchorElement
NodeList Un tableau de nœuds, pour stocker les fils par exemple. Similaire, mais non identique à Array JS.
et bien d’autres.

Chaîne de prototypes

Hiérarchie d’interfaces/classes du DOM des éléments anchors <a> (MDN - HTMLAnchorElement)
const $anchor = document.querySelector("a");
console.assert($anchor instanceof HTMLAnchorElement);
console.assert($anchor instanceof HTMLElement);
console.assert($anchor instanceof Element);
console.assert($anchor instanceof Node);
console.assert($anchor instanceof EventTarget);

Sélection des éléments

  • Element.querySelector(selector) sélectionne le premier élément
  • Element.querySelectorAll(selector) sélectionne tous les éléments
  • autres méthodes, à éviter
    • globalThis.id ⚠️
    • Element.getElementById(id)
    • eEement.getElementByClassName(id)
    • Element.getElementByName(name)

Les sélecteurs sont des strings dans la syntaxe des sélecteurs CSS. Voir javascript.info.

document.querySelectorAll("#box li > a:visited");

Il faut donc connaître les sélecteurs CSS 🤔

Parcours explicite de l’arbre

SO - What is the difference between children and childNodes in JavaScript?

  • Propriété Node.childNodes (MDN) : renvoie une NodeList live.
  • Propriété Element.childNodes (MDN) : renvoie une HTMLCollection live.

📔 Il est conseillé de transformer ces collections avec Array.from(iterable) (MDN - Array.from), voir MDN - HTMLCollection

Création et ajout d’éléments

  • document.createElement(tag, options) créer un élément HTML (MDN)
    • ⚠️ l’élément est créé, mais est invisible
  • pour ajouter l’élément à l’arbre DOM :
    • 👎 Node.appendChild(aNode) ajoute un dernier fils à un Node (MDN)
    • 👍 Element.append(nodeOrStr1, …, nodeOrStrN) similaire pour la classe Element (MDN) (unicorn)

L’API DOM est complète : de nombreuses méthodes pour le remplacement, la suppression, etc. :

  • Node.cloneNode(deep) (MDN)
  • Node.replaceChild(newN, oldN) (MDN)
  • Element.replaceWith(e1, …, eN) (MDN)
  • Element.replaceChildren(e1, …, eN) (MDN)
  • Element.remove() (MDN)

Gestion des attributs (HTML)

  • Element.getAttribute(name) (MDN)
  • Element.setAttribute(name, val) (MDN)
  • Element.removeAttribute(name) (MDN)
    • ou par réflexion Element.attribute;

Exemple sur MDN - HTMLImageElement.src

document.querySelector("img").getAttributeNames();
// Array [ "src", "alt" ]
document.querySelector("img").getAttribute("src");
// "../img/javascriptinfo_html.png"
document.querySelector("img").src;
// "http://127.0.0.1:8080/img/javascriptinfo_html.png" (FQDN)

Enfants et contenu

  • ⚠️ Element.innerHTML (MDN)
    • pour lire ou écrire un contenu HTML
    • à éviter pour performance et sécurité
  • HTMLElement.innerText (MDN)
  • 👍 Node.textContent (MDN)

⚠️ innerText et textContent suppriment tout le contenu. ⚠️

On préfère textContent à innerText (et à innerHTML) (unicorn, SO - What’s the use of textContent/innerText when innerHTML does the job better? SO - Difference between textContent vs innerText).

Danger de Element.innerHTML

Les failles de type Cross Site Scripting (XSS) (OWASP - XSS et OWASP - DOM Based XSS Prevention)

const $div = document.createElement("div");
$div.innerHTML = `<img src="!" onerror="alert('XSS')"> `;
document.body.append($div);

☣️ L’ajout non contrôlé de contenu en provenance de l’utilisateur conduit à des failles (OWASP Top 10). ☣️

Exemple, ajouter des images (1/x)

Voir l’exemple

const cats = [
  { id: "k3", url: "https://cdn2.thecatapi.com/images/k3.jpg" },
  /* … */
  { id: "d4d", url: "https://cdn2.thecatapi.com/images/d4d.jpg" },
];

const $catsContainer = document.querySelector("#cats-container");
for (const cat of cats) {
  const $img = document.createElement("img");
  $img.setAttribute("src", cat.url);
  $img.setAttribute("alt", `Cat ${cat.id}`);
  $catsContainer.appendChild($img);
}

Variante avec fragment HTML pour ne rafraichir la page qu’une fois.

const $root = document.createDocumentFragment();
const $catsContainer = document.querySelector("#cats-container");
for (const cat of cats) {
  const $img = document.createElement("img");
  $img.setAttribute("src", cat.url);
  $img.setAttribute("alt", `Cat ${cat.id}`);
  $root.append($img);
}
$catsContainer.append($root);

Alternatives au DOM explicite

L’API DOM est complexe, à cause (notamment) de 20 ans d’évolution des standards avec maintien de la rétro compatibilité par les navigateurs.

  • Avoir de bonnes pratiques.
  • Outiller le développement.
  • Utiliser des objets/outils de plus haut niveau.

La manipulation explicite du DOM est fastidieuse 🥹 Des solutions permettent de l’éviter.

Templating

On peut utiliser des moteurs de templating plus ou moins riches pour la création d’éléments complexes :

https://mustache.github.io/, https://handlebarsjs.com/, https://ejs.co/, https://pugjs.org ou https://jinja.palletsprojects.com (Python)

On utilisera côté serveur, mais pas côté client.

Exemple mustache.js
<html>
  <body onload="renderHello()">
    <div id="target">Loading...</div>
    <script id="template" type="x-tmpl-mustache">
      <p> Hello {{ name }}! </p>
    </script>

    <script src="https://unpkg.com/mustache@latest"></script>
    <script src="render.js"></script>
  </body>
</html>
function renderHello() {
  const template = document.getElementById("template").innerHTML;
  const rendered = Mustache.render(template, { name: "Luke" });
  document.getElementById("target").innerHTML = rendered;
}

Frameworks clients

Les framework s’appuient sur des composants qui encapsulent contenu (HTML), présentation (CSS) et comportement (JS) via des composants standardisés (Web Components) ou propres (React DOM) :

https://react.dev/, https://preactjs.com/, https://angular.io/ ou https://vuejs.org/ sont les plus connus.

Hors du périmètre de LIFWEB.

Exemple React en JSX

JSX = JS + template HTML

// fichier JSX
const Index = (name) => {
  return <div>Hello {name}</div>;
};

// un élément Index ajouté à root
ReactDOM.render(<Index />, root);
// fichier JS obtenu après transpilation
const Index = (name) => {
  return React.createElement("div", null, "Hello ", name);
};
ReactDOM.render(React.createElement(Index, null), root);

Web Components

Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps. (Web Components)

Combinaison de trois technologies standardisées (javascript.info) :

  • Custom elements : <mon-composant>
  • Shadow DOM : rendu séparé du DOM
  • HTML Templates : éléments réutilisables
    • <template>
    • <slot>

Laborieux et bas niveau, on utilise plutôt :

Voie médiane n°1 : <template> HTML

Sans utiliser Shadow DOM et Custom elements

<!-- on écrit le HTML, sans passer par le DOM -->
<template id="alice-template">
  <figure>
    <img src="alice.svg" role="img" alt="Silhouette" />
    <figcaption>Silhouette of ...</figcaption>
  </figure>
</template>
// on instancie le template et on l'ajoute
const template = document.querySelector("#alice-template");
const alice = template.content.cloneNode(true);
container.appendChild(alice);

Voie médiane n°2 : lit-html

Une méthode sûre (pas de XSS) pour transformer les chaînes en objets DOM : lit-html

Exemple lit-html
// comme du templating, mais en JS pur
const aliceLit = (id, source) =>
  html`
    <figure>
      <img class="alice blue" id=${id} src=${source} />
      <figcaption>Silhouette of ...</figcaption>
    </figure>
  `;

// render = calcule le diff et màj le DOM
render(aliceLit(42, "alice.svg"), root);

Les événements

Les événements DOM sont déclenchés pour notifier au code des « changements intéressants » qui peuvent affecter l’exécution du code. Ces changements peuvent résulter d’interactions avec l’utilisateur, […], de changements dans l’état de l’environnement […], et d’autres causes. (MDN - Event reference)

Propagation des événements

Voir MDN et javascript.info.

  • Bubbling : les événements remontent le long de l’arbre DOM des feuilles (plus précis, les plus imbriqués) à la racine.
  • Capture : les parents peuvent capture les événements et ne pas les transmettre aux enfants.
    • c’est l’inverse du bubbling
Source www.w3.org - DOM Level 3 Events

Exemple de bubbling

<form onclick="alert('form')">
  FORM
  <div onclick="alert('div')">
    DIV
    <p onclick="alert('p')">P</p>
  </div>
</form>

(NB : ne respecte pas les bonnes pratiques)

Fonctionnement

De nombreux éléments DOM peuvent être paramétrés afin d’accepter (« d’écouter ») ces évènements et d’exécuter du code en réaction pour les traiter (« gérer »). (MDN - Event)

  • Un objet DOM (interface EventTarget) peut écouter (listen) certains événements.
  • On attache une fonction appelée handler ou listener à cet événement.
  • Le handler est appellé à chaque occurrence de l’événement.

MDN - Event handling (overview)

Attacher un handler : méthode recommandée

  • EventTarget.addEventListener(type, handler, opts).
  • EventTarget.removeEventListener(type, handler, opts).
    • 🥹 pénible, car il faut référencer le handler
function greet(...args) {
  console.log("greet:", args);
}

const $btn = document.querySelector("button");
$btn.addEventListener("click", greet);

Attacher un handler : méthode alternative

Attribut spécial onevent comme EventTarget.onclick :

  • onevent est un attribut
  • limitée à un seul handler
  • plus simple à utiliser
function greet(...args) {
  console.log("greet:", args);
}

const $btn = document.querySelector("button");
// ajout du handler
$btn.onclick = greet;
// suprression
$btn.onclick = null;

Attacher un handler : méthode inline HTML

☣️ À bannir (MDN) ☣️

<button onclick="console.log('Hello');">Press me</button>

Important ☠️ le handler est une fonction ☠️

  • element.onclick = handlerFunct()
    • n’est exécuté qu’une seule fois
    • ceci n’a pas de sens (sauf curryfication)
    • provoquera (généralement) une erreur
  • element.onclick = handlerFunct
    • L’appel handlerFunct() sera exécuté à chaque occurrence de l’événement "click".

On aura besoin de fermetures, de facto, on va faire de la programmation fonctionnelle.

this dans les handlers

When a function is used as an event handler, its this is set to the element on which the listener is placed

MDN

const $btn = document.querySelector("button");

function greet(event) {
  // affiche le texte du premier boutton de la page
  // et deux attributs de l'évenement "click"
  console.log(this.innerText, event.type, event.timeStamp);
}
$btn.addEventListener("click", greet);

☣️ this \neq currentTarget \neq target ☣️

Exemple, la plomberie du TP1

Extrait du fichier tp1.js :

/* Exercice 2 : 99 Bottles of Beer */
const $output2 = document.getElementById("output2");
const $input2 = document.getElementById("input2");

document.getElementById("eval2").onclick = () => {
  $output2.innerHTML = bottles($input2.value);
};

⚠️ Ne respecte pas le guide de style ⚠️

Exemple, ajouter des images (2/x)

Voir l’exemple. Ici, un handler par image.

const $imgs = $container.querySelectorAll("img");
for (const $img of $imgs) {
  $img.addEventListener("click", () => {
    for (const $other of $imgs) {
      $other.classList.remove("selected");
    }
    $img.classList.add("selected");
  });
}

Exemple, ajouter des images (3/x)

Variante, avec un handler unique pour le conteneur, qui distribue au bon enfant via Event.target, c’est la délégation d’événement.

const $imgs = $container.querySelectorAll("img");
$container.addEventListener("click", (event) => {
  const $this = event.target;
  for (const $other of $imgs) {
    $other.classList.remove("selected");
  }
  $this.classList.add("selected");
  console.debug("Handler activated on", $this);
});

Conclusion

Bonnes pratiques DOM

  • 👍 Privilégier .querySelector() et .querySelectorAll().
    • Stocker le nœud dans une variable.
    • Chaîner les .querySelector() si besoin.
    • Rechercher depuis le parent.
  • 👎 Éviter .innerHTML
    • Préférer .textContent à .innerText.
  • 👍 Utiliser .createDocumentFragment().
  • 👍 Préférer .addEventListener à .onevent.
    • Déléguer les événements via event.target.
  • 👍 Éviter les sélecteurs trop complexes.
    • Privilégier .class et #id à element
  • 👍 Privilégier les classes CSS aux styles en ligne
    • Utiliser .classList

🔧 Des outils intégrés aux IDE nous aideront.

Valeur de retour d’un handler

Est-il utile de renvoyer une valeur de retour dans un handler ?

const $btn = document.querySelector("button");

function greet(...args) {
  return "Bonjour";
}
$btn.addEventListener("click", greet);
// comment retrouver la valeur "Bonjour" ?

En général Non

  • Le traitement est asynchrone :
    • Impossible d’affecter en synchrone
    • La suite du traitement est passée en callback
  • Cas particulier element.onevent(event) :
    • La valeur de retour est utilisée : bloque le comportement par défaut si false
    • 👍 Privilégier event.preventDefault()

Mais return reste utile, car met fin (à coup sûr) au flot d’exécution (comme throw).

Outillage et programmation asynchrone seront l’objet du CM3.