:: Enseignements :: ESIPE :: E3INFO :: 2021-2022 :: Programmation Web avec JavaScript ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
Librarie Réactive
|
Le but de ce TP est d'écrire une version simplifié d'une librarie réactive de manipulation du DOM.
Voilà le code de la page HTML,
virtualdom.html, que l'on veut afficher
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8"/>
<script src="virtualdom.js" defer></script>
</head>
<body>
<div id = "my_node">
</div>
</body>
</html>
ainsi que le fichier
virtualdom.js
'use strict';
// Ecrire le reste du code ici !
render({
state: 0,
view: state =>
v("div", {}, [
v("h1", { class: "red" }, state),
v("button", { onclick: state => state - 1 }, "subtract"),
v("button", { onclick: state => state + 1 }, "add")
]),
node: document.getElementById("my_node")
});
Exercice 1 - Virtual DOMination
Une application réactive est une application qui change sont affichage en fonction du changement
de valeur d'un objet particulier qui correspond à l'état de l'application.
Par exemple, une application simple qui affiche une valeur avec un bouton + et -
pour incrémenter ou décrémenter la valeur auro pour état un simple entier
tandis que des application plus compliquées utiliseront des listes ou des objets comme état.
Une application réactive est composée de 3 valeurs différentes, l'état dont nous venons de parler,
la vue qui permet à partir de l'état de générer un arbre DOM qui sera affiché (une fonction donc)
et enfin un objet DOM de la page HTML qui va être remplacer par l'arbre DOM généré par la vue.
Donc de façon rudimentaire, l'application réactive qui incrémente et décrémente un entier peut se
décrire par l'object JavaScript suivant :
{
state: 0,
view: state => ... ,
node: document.getElementById("my_node")
}
Reste à savoir comment créer l'arbre DOM correspondant à l'état (les ... dans l'exemple, ci-dessus),
on se propose pour cela d'utiliser ce que l'on appel un
virtual DOM qui est un arbre
comme l'arbre DOM mais géré en JavaScript (contrairement à un arbre DOM qui est géré en C et
visible en JavaScript).
Voici un exemple de
virtual DOM
v("div", {}, [
v("h1", { class: "red" }, state),
v("button", { onclick: state => state - 1 }, "subtract"),
v("button", { onclick: state => state + 1 }, "add")
])
La fonction
v prend 3 paramètres, un nom d'élement (ici, div, h1 ou button), un ensemble
de propriété qui sont les propriétés de l'élément (par exemple la class de l'élement h1 ou la propriété
onclick) et enfin une valeur qui peut être soit un élément, soit un tableau d'élements
soit une valeur qui va être transformé en string pour être affiché.
Pour notre implantation, la fonction
v va renvoyer une instance de la classe
Element
ayant les attributs
name (le nom de l'élement),
properties un objet contenant les
propriétés de l'élements et
textOrChildren qui contient soit un élément, un tableau d'élements
ou une valeur.
De plus, la classe
Element va possèder une méthode
toDOM qui permet à partir
d'une élement du
virtual-dom de transformer celui-ci en élement réel du DOM
créer en utilisant les méthode habituelles
document.createElement(),
document.createTextNode(),
element.appendChild() et
element.append().
On peut noter que la fonction prise en paramètre par
onclick n'est pas la fonction classique
prise par la propriété
onclick sur l'arbre DOM, en effet, c'est fonction est ajouter
sur le
virtual dom pas usr le DOM généré.
La fonction classique
onclick enregistrer lors de la génération de l'arbre DOM
par la méthode
toDOM va appeler la fonction du
virtual dom avec en paramétre
l'état de l'application, puis stocker le résultat de l'appel dans le champs
state
et enfin appeler la fonction
render pour changer l'affichage graphique.
Cette astuce permet d'être sur que lorsque l'on clique sur un bouton, l'état de l'application
est mis à jour et que l'affichage graphique est raffraichi.
Si on met les différentes parties ensemble, on obtient le code suivant :
render({
state: 0,
view: state =>
v("div", {}, [
v("h1", { class: "red" }, state),
v("button", { onclick: state => state - 1 }, "subtract"),
v("button", { onclick: state => state + 1 }, "add")
]),
node: document.getElementById("my_node")
});
Lors de l'affichage de la page, la fonction
render prend l'objet correspondant à l'application,
récupère la valeur de l'état, appel la fonction
view pour créer l'arbre d'élements
(le
virtual dom, appel la méthode
toDOM sur l'arbre d'élement pour créer l'arbre DOM
correspondant puis remplace l'objet
node par le noeud racine de l'arbre DOM créé
(le remplacement se fait en demandant le
parentNode de
node puis en appelant la méthode
replaceChild() sur le parent (attention, les arguments de replaceChild() sont inversés !)).
Le code JavaScript doit utiliser la version JavaScript 2015 (classes, fonctions flèches, etc).
Les deux dernières questions optionnelles sont indépendantes.
-
Ecrire la classe Element et son constructeur. Ecrire la fonction v
qui renvoie une nouvelle instance de la classe Element à chaque appel.
-
Ecrire dans la classe Element la méthode toDOM et vérifier
avec un exemple sans gestion des évènements que l'arbre DOM généré est correct.
Note: Array.isArray permet de savoir si un objet est un tableau et
value instanceof Element permet de savoir si la variable value est un Element.
-
Ecrire la fonction render et vérifier que l'affichage de la page HTML
affiche bien l'arbre DOM générer.
-
Ajouter la gestion des évènements de tel sorte à ce que l'exemple ou l'on incrémente et décremente
une valeur ci-dessus fonctionne.
Note: value instanceof Function permet de savoir si la variable value est une fonction.
-
Optionnellement, au lieu de remplacer tout le DOM à chaque changement, on peut utiliser une technique
que l'on appel le DOM diffing pour le remplacer que les noeuds du DOM qui ont changé.
Le diff d'un arbre consiste à parcourir un element et le noeud du DOM correspondant en même temps
et modifier le noeud du DOM si le le name ou properties sont différents
puis parcourir les fils et faire le diff sur les fils.
De plus, si le nombre de fils de l'élement et du noeud du DOM ne sont pas identique,
il va falloir ajouter ou retirer des noeuds du DOM.
Donc au lieu de la méthode toDOM, créer une nouvelle méthode diffDOM dans Element
qui prend un noeud DOM et fait l'algorithme de DOM diffing décrit si dessus.
-
Optionnellement, le problème du design actuelle est qu'il n'y a pas de notion de composant
avec leur état propre, on a un seul valeur d'état (state) pour toute l'application,
donc les différentes partie de l'application (composant) ne se compose pas bien.
Par exemple, si on veut deux compteurs, il va falloir avoir deux champs counter1 et
counter2 en tant que state et dupliquer le code de visualisation des deux compteurs.
render({
state: { counter1: 0, counter2: 0 },
view: state =>
v("div"), {} [
v("div", {}, [
v("h1", { class: "red" }, state),
v("button", { onclick: state => { state..., counter1: state.counter1 - 1 } }, "subtract"),
v("button", { onclick: state => { state..., counter1: state.counter1 + 1 } }, "add")
]),
v("div", {}, [
v("h1", { class: "red" }, state),
v("button", { onclick: state => { state..., counter2: state.counter2 - 1 } }, "subtract"),
v("button", { onclick: state => { state..., counter2: state.counter2 + 1 } }, "add")
])
])
node: document.getElementById("my_node")
});
Donc c'est pas très beau.
On se propose de faire une nouvelle implantation avec une notion de composant et d'état d'un composant.
Pour cela, on va créer une classe Reagent qui prend le noeud du DOM à modifier à la création
et qui possède deux méthodes useState() et view().
useState(default_value) permet de créer une nouvelle case mémoire avec une valeur par défaut
et renvoie un tableau avec une fonction getter et une fonction setter sur la case mémoire.
Lorsque le setter est appelé, en plus de changer la valeur de la case mémoire, l'objet reagent appel
la fonction passée en second paramètre de view.
let reagent = new Reagent();
let counterComponent = () => {
let [getCounter, setCounter] = reagent.useState(0);
return () =>
v("div", {}, [
v("h1", { class: "red" }, getCounter()),
v("button", { onclick: () => setCounter(getCounter() - 1) }, "subtract"),
v("button", { onclick: () => setCounter(getCounter() + 1) }, "add")
]);
};
let counter1 = counterComponent();
let counter2 = counterComponent();
reagent.view(
document.getElementById("my_node"),
() => v("div", {}, [
counter1(),
counter2()
]));
Implanter la classe Reagent ainsi que ses méthodes useState et view
tel que le code ci-dessus fonctionne.
Note: la classe Element est plus exactement la même, donc il est plus simple de commenter
l'ancien code et d'en créer un nouveau.
© Université de Marne-la-Vallée