Fonctions

Room est composé de 10 fonctions qui sont exportées dans la version module ECMAScript 6 et accessibles via l’objet Room dans la version non ESM.

Les 10 fonctions sont :

  1. h()
  2. elements()
  3. append()
  4. createData()
  5. getData()
  6. setData()
  7. createEffect()
  8. untrack()
  9. onError()
  10. map()

Dans la version ECMAScript de Room, il est possible d’importer les fonctions de Room individuellement comme par exemple avec ce code :

import {elements, createData} from "Room";

const {div} = elements();
createData(0);

Mais il est aussi possible d’importer un objet Room et d’utiliser les fonctions via cet objet comme par exemple ci-dessous :

import Room from "Room";

const {div} = Room.elements();
Room.createData(0);

Ceci permet d’écrire du code qui est le même (à l’exception de l’importation) entre la version ECMAScript et la version non ECMAScript puisque dans cette dernière, l‘utilisation des fonctions de Room ne peut se faire que via l’objet Room.

Cette possibilité a été utilisée dans les différents exemples proposés afin de faire abstraction de la version de Room utilisée.

Si vous développez uniquement des modules ECMAScript, utilisez directement les fonctions importées de Room permet d’avoir un code moins verbeux.

Fonction h()

Cette fonction permet de créer par défaut un élément HTML avec des attributs et des contenus mais peut aussi servir pour créer des éléments SVG ou MathML.

La signature de la fonction est la suivante :

h(tagName, ...contents)

Elle retourne un objet de la classe Element et plus précisément un HTMLElement, mais aussi éventuellement un SVGElement ou un MathMLElement comme indiqué plus loin.

La fonction h() est la fonction de base, mais il est bien plus pratique d’utiliser la fonction elements() de Room qui permet d’obtenir des fonctions dont le nom est directement un nom d’élément, l’écriture du code est ainsi plus concise et plus lisible.

Les paramètres attendus par la fonction sont :

  • tagName : Une chaîne de caractères contenant un nom d’élément HTML ("div", "p", "span", etc.), ou éventuellement un nom d’élément SVG ("svg", "path", etc.) ou MathML ("math", "mfrac", etc.).
  • contents : Des contenus pour l’élément qui peuvent être ses attributs et des éléments enfants.

La fonction commence par analyser la valeur de this.

Si la valeur de this n’est pas définie, null ou une chaîne de caractères vide, un élément HTML est créé avec la méthode createElement() en utilisant le paramètre tagName.

Si la valeur de this est une chaîne de caractères non vide, cette valeur est considérée comme étant un espace de nom (nameSpace) et un élément SVG ou MatHML est créé avec la méthode createElementNS() en utilisant ce nameSpace et le paramètre tagName.

S’il y en a, la fonction ajoute ensuite à l’élément les contenus référencés par le paramètre contents (attributs et enfants) en appelant la fonction append() de Room et retourne l’élément qui a été créé. Les types utilisables et les contraintes pour le paramètres contents sont donc identiques à ce qui est décrit dans la fonction append().

La fonction h() produit donc de base des éléments HTML mais peut aussi être utilisée pour produire des éléments SVG ou MathML. Pour cela il suffit de créer une nouvelle fonction via la fonction bind() de JavaScript en passant à cette dernière un unique paramètre qui doit être un nameSpace, celui de SVG ou celui de MathML.

Cette possibilité est présentée dans la page Composition avec SVG et MathML.

Fonction elements()

Cette fonction génère un objet Proxy de la fonction h() qui permet ensuite d’obtenir des fonctions dont les noms sont des noms d’éléments HTML et éventuellement des noms d’éléments SVG ou MathML.

La signature de la fonction est la suivante :

elements(nameSpace)

Le paramètre nameSpace n’est pas requis, il n’est pris en compte que s’il contient une chaîne de caractères non vide et est alors utilisé comme espace de nom passé, via this, à la fonction h().

La fonction elements() retourne un Proxy de la fonction h() et s’utilise typiquement avec l’affectation par décomposition de JavaScript pour obtenir des fonctions qui génèrent des objets de la classe Element dont le type est le nom de la fonction.

Un exemple :

// Récupération des fonctions div(), p() et my_component()
const {div, p, my_component} = Room.elements();
// Création d'un élément <div> de classe "container" et contenant
// un élément <p> et un composant web <my-component>
const container = div({class: "container"},
p("Hello World"),
my_component({text: "Composant de test"})
);

Il est à noter dans cet exemple le cas de la fonction my_component() qui génère bien un élément <my-component>, Room remplace automatiquement le caractère _ par un caractère -.

Des exemples d’utilisation de la fonction elements() pour générer des fonctions qui génèrent des éléments SVG ou MathML sont disponibles dans la page Composition avec SVG et MathML.

Fonction append()

Cette fonction permet d’ajouter à un élément des contenus qui peuvent être des attributs pour l’élément ou des éléments enfants.

La signature de la fonction est la suivante :

append(element, ...contents)

Le premier paramètre element est requis et doit être un objet de la classe Element, il est retourné par la fonction.

Les paramètres suivants non requis et référencés par contents sont les contenus à ajouter à l’élément, les objets littéraux étant considérés comme des descripteurs d’attributs, les autres types comme des éléments enfants.

Quelques exemples d’utilisation :

// Récupération des fonction div() et p()
const {div, p} = Room.elements();
// Création d'un élément <div>
const container = div();
// Création d'un élément <p>
const para = p();
// Ajout d'un attribut style et d'un texte à l'élément <p>
Room.append(para, {style: "color: red"}, "[Fin de page]");
// Ajout de l'élément <p> dans l'élément <div>
Room.append(container, para);
// Ajout de l'élément <div> à la fin de la page
Room.append(document.body, container);
// À partir de là, la fin de la page contient le texte [Fin de page] en rouge

En plus d’ajouter les contenus à l’élément, la fonction append() s’occupe de déterminer les dépendances des éléments aux données observables créées avec la fonction createData() et utilisées dans les attributs ou les éléments enfants. Le fonctionnement de cette recherche des dépendances est décrite dans la page la partie Réactivité de la page Fonctionnement.

Les attributs

Les attributs d’un élément sont décrits dans un objet littéral passé à la fonction append() comme un contenu.

Il peut y avoir plusieurs objets littéraux passés à la fonction append() et l’ordre dans lequel ils sont passés n’a pas d’importance, sinon qu’en cas de doublon d’un attribut, c’est la dernière valeur qui prime.

Les propriétés de l’objet correspondent aux noms des attributs, la valeur des propriétés deviennent les valeurs des attributs.

Les noms des attributs ne sont pas traités au niveau de la casse sauf si l’attribut concerne un gestionnaire d’évènement. Par contre, un traitement est réalisé pour transformer les caractères _ en -. Ceci permet par exemple d’écrire en JavaScript un attribut data-params sous la forme data_params et non sous la forme "data-params", le caractère - n’étant pas utilisable en JavaScript pour les identifiants.

La valeur d’un attribut peut être donnée par une donnée observable d’une primitive JavaScript ou par une fonction, c’est comme cela que vous pouvez rendre l’attribut réactif si la fonction consulte des données observables. Il est à noter que Room appelle la fonction avec un unique paramètre qui est l‘élément sur lequel l’attribut est appliqué.

Une valeur d’attribut à null ou undefined supprime l’attribut.

Les gestionnaires d’évènements

Les gestionnaires d’évènements sont renseignés dans les attributs.

Le nom de l’attribut doit commencer par on, par exemple onClick, onSubmit, onChange, etc. La casse n’a pas d’importance, Room transforme les noms en minuscule, ce qui n’est pas le cas avec les attributs ordinaires.

La valeur de l’attribut doit être une fonction qui reçoit un unique paramètre qui est un objet Event.

Si cette fonction retourne false, Room appelle automatiquement les méthodes preventDefault() et stopPropagation() de l’évènement. Cette facilité est par exemple intéressante pour un événement de type submit dans un formulaire.

Cette fonction n’est pas considérée par Room comme étant réactive, vous pouvez néanmoins rendre réactif l’attribut en utilisant comme valeur une donnée observable d’une fonction que vous modifiez ailleurs en fonction du besoin. Cette méthode permet également de supprimer un gestionnaire d’évènement en donnant la valeur null à l’attribut.

De plus, il n’est pas possible avec Room d’avoir pour un élément, plusieurs gestionnaires d’un même type d’évènement.

Il est tout a fait possible de définir des gestionnaires d’évènements personnalisés, il suffit juste de respecter la contrainte d’écriture avec on en début du nom de l’attribut, par exemple onHello pour un évènement personnalisé de type hello. Vous pouvez ensuite envoyer un évènement de type hello à l’élément avec la méthode dispatchEvent().

Room gère par ailleurs 4 évènements spéciaux qui ne sont pas des évènements standards :

  1. mount : L’élément est ajouté dans le DOM de la page.
  2. unmount : L’élément est supprimé du DOM de la page.
  3. pageShow : L’élément redevient visible car la page redevient visible.
  4. pageHide : L’élément devient invisible car la page devient invisible.

Ces évènements spéciaux s’utilisent comme les évènements standards dans les attributs donc en commençant le nom par on, par exemple onMount, onPageHide, etc. avec comme valeur une fonction.

Pour gérer les évènements spéciaux mount et unmount, Room met en place un MutationObserver sur l’élément <body> de la page, les modifications de l’élément <head> de la page ne sont donc pas pris en compte.

Pour les évènement spéciaux pageShow et pageHide, Room met en place un gestionnaire de l’évènement de type visibilityChange sur le document. Quand cet évènement intervient, suivant l’état de visibilité de la page, l’un ou l’autre des évènements spéciaux est envoyé aux éléments qui sont dans l’élément <body> de la page (l’élément <head> est ignoré) et qui ont un gestionnaire du type de l’évènement spécial.

Au chargement d’une page, l’évènement pageShow n’est pas envoyé mais l’évènement mount l’est dés qu’un élément est ajouté dans la page.
À contrario, l’évènement unmount n’est pas envoyé quand une page est déchargée ou rechargée mais l’évènement pageHide l’est.

Évènement mount

Cet événement se produit quand un élément devient connecté au DOM de la page (sa propriété isConnected devient true).

En définissant une fonction pour cet événement, cela donne l’opportunité de déclencher des traitements (comme charger des données, démarrer un timer, ouvrir une connexion, etc.) au moment du montage de l’élément dans le DOM.

Évènement unmount

Cet évènement se produit quand un élément n’est plus connecté au DOM de la page (sa propriété isConnected devient false).

En définissant une fonction pour cet événement, cela donne l’opportunité de contrebalancer des opérations réalisées au montage (par exemple arrêter un timer, fermer des connexions, etc.) mais aussi de sauvegarder des données.

Des exemples d’utilisation de cet événement sont disponibles dans les pages Liste de tâches à faire et Calcul de Pi.

Évènement pageShow

Cet évènement se produit quand la page devient visible.

En définissant une fonction pour cet événement, cela donne l’opportunité de déclencher des traitements comme relancer un traitement ou une vidéo mise en pause lors d’un évènement pageHide.

Évènement pageHide

Cet évènement se produit quand la page devient invisible.

En définissant une fonction pour cet événement, cela donne l’opportunité de déclencher des traitements, comme mettre en pause un traitement, une vidéo, etc.

C’est le dernier évènement traité en cas de déchargement d’une page (changement de page ou le navigateur quitte), d’un changement d’onglet dans le navigateur ou d’un changement d’application sur un mobile. C’est donc le bon moment pour par exemple faire une sauvegarde de données, ce qui évite d’en faire à chaque changement des données et donc économise les supports physiques et réduit la consommation d’énergie.

Des exemples d’utilisation de cet événement sont disponibles dans les pages Liste de tâches à faire et Calcul de Pi.

Les éléments enfants

Les éléments enfants sont tous les éléments du paramètre contents de la fonction append() qui ne sont pas des objets littéraux.

Ils peuvent être des 3 types suivants (les valeurs null et undefined sont ignorées) :

  • des textes ou des valeurs numériques (automatiquement converties en texte) et qui deviennent un élément Text,
  • des éléments HTML, SVG ou MathML,
  • des objets ayant une clé qui est le symbole standard Symbol.toPrimitive.

A cela s’ajoute :

  • des fonctions qui doivent retourner des données des 3 types précédents,
  • des tableaux qui peuvent contenir des données des 3 types ou des fonctions,
  • des expressions JavaScript qui donnent un des 3 types, des fonctions ou des tableaux.

Si une fonction ou une expression retourne un objet littéral ou si un tableau contient un objet littéral alors cet objet est considéré comme un descripteur d’attributs.

Les fonctions sont exécutées par Room en passant un seul paramètre qui est une référence à l’élément parent ou sa propriété isConnected est à false. Si la fonction consulte des données observables alors l’élément enfant devient réactif et la fonction est rappelée par Room si une des données consultées est modifiée. Mais dans ce cas, l’unique paramètre passé à la fonction est une référence à l’élément enfant ou sa propriété isConnected est alors à true.

Les fonctions peuvent retourner des fonctions qui elles même retournent des fonctions, mais à la fin de l’exécution, il faut une donnée des 3 types précédents ou un tableau.

Les fonctions peuvent être des fonctions asynchrones, mais dans ce cas, comme elle retourne une promesse (objet Promise), il faut utiliser le mot clé await pour attendre le résultat, sinon la promesse est transformée en un texte du genre [object Promise]!

Fonction createData()

Cette fonction permet de créer une donnée observable, c’est à dire que lire ou modifier cette donnée peut être suivi pour déclencher une action. C’est avec ce type de donnée que la réactivité est possible dans une interface utilisateur.

La signature de la fonction est la suivante :

createData(data)

Elle prend en entrée un seul paramètre qui peut être une primitive JavaScript (nombre, chaîne de caractère, booléen, null, etc.), un tableau ou un objet et retourne une donnée observable qui est en fait un objet Proxy JavaScript.

Un objet Proxy ne pouvant traiter que des objets ou des tableaux, une primitive JavaScript est transformé en un objet spécial contenant une seule propriété value dont la valeur initiale est la valeur de la primitive passée à la fonction. Ceci concerne aussi l es fonctions et les objets natifs (Date, Element, Map, Set, etc.) qui ne sont donc pas traités comme des objets ce qui veut dire que la lecture ou la modification d’une propriété de ces objets ne sont pas détectées.

L’objet spécial pour les primitive JavaScript contient en plus de la propriété value un symbole Symbole.toPrimitive qui retourne la valeur de la propriété value ce qui permet de simplifier l’utilisation de la donnée observable lors de sa lecture.

Les exemples suivants présentent quelques cas d’utilisation avec des primitives JavaScript :

const n1 = createData(1);
++n1.value;
console.log(n1); // Le Proxy de n1 dans la console
console.log(n1.value); // "2" dans la console

const n2 = createData(2);
n1.value = Math.pow(n2, 2) - 2 * n1;
console.log(n1.value); // "0" dans la console

const s = createData("Hello");
s.value += " World";
console.log(s.value); // "Hello World" dans la console
s.value += n1 + n2;
console.log(s.value); // "Hello World2" dans la console

const d = createData(new Date);
console.log(d.value.toLocaleTimeString()); // L'heure dans la console

Les objets littéraux et les tableaux sont traités comme des objets avec un suivi en profondeur, c’est à dire que la lecture ou la modification d’un sous-objet de l’objet, qui n’est pas un objet natif, est suivie. Par ailleurs, la lecture ou la modification d’une propriété de l’objet ou du tableau se fait naturellement et est suivie.

Les exemples suivants présentent quelques cas d’utilisation avec des objets littéraux et des tableaux :

const o = createData({a: 1, b: 2, c: 0});
o.c = o.a + o.b;
console.log(o.c); // "3" dans la console

o.d = {e: 4, f: 5, g: 0};
o.d.g = o.d["e"] + o.d.f;
console.log(o.d.g); // "9" dans la console

const t = createData([1, 2, 3]);
t.push(o);
console.log(t.length); // "4" dans la console
console.log(t[3].d["g"]); // "9" dans la console
o.c = 0;
console.log(t[3].c); // "0" dans la console
t[3].a = "Hello";
console.log(o.a); // "Hello" dans la console

Il est à noter dans cet exemple, que la donnée observable o est ajoutée à la donnée observable t et qu’une modification d’une propriété (c dans l’exemple) de la donnée o impacte la donnée observable t et inversement.

Il est également possible d’utiliser des objets qui sont des instances de vos classes.

Appeler la fonction createData() en lui donnant une donnée observable retournera la même donnée observable.

Fonction getData()

Cette fonction permet d’obtenir la valeur d’une donnée.

La signature de la fonction est la suivante :

getData(data)

Elle prend en entrée un unique paramètre.

Si la donnée passée en paramètre est une donnée observable d’un objet littéral, d’un tableau ou d’un objet non natif, la valeur retournée est une copie profonde sans Proxy de la donnée.

Si la donnée passée en paramètre est un donnée observable d’une primitive ou d’un objet natif, la donnée retournée est juste la valeur de la propriété value.

Si la donnée n’est pas observable, la fonction rend alors la même donnée qu’en entrée. Ceci est notamment intéressant dans un composant qui reçoit une donnée, en utilisant la fonction getData() la valeur est récupérée de la donnée qu’elle soit observable ou pas.

Il est possible d’appeler la fonction getData() avec un unique paramètre qui est une fonction, elle est alors appelée (sans paramètre) et c’est la donnée retournée par cette fonction qui sera traitée par la fonction getData().

Quelques exemples :

const d2 = createData({a: 1, b: {c: 2, d: 3}});
const d3 = getData(d2);
console.log(d2); // Un proxy dans la console
console.log(d3); // L'objet {a: 1, b: {c: 2, d: 3}} dans la console
d3.b.c = null;
console.log(d2.b.c); // "2" dans la console, d3 est une copie profonde de d2

Fonction setData()

Cette fonction permet de modifier une donnée.

La signature de la fonction est la suivante :

setData(data, value)

Elle prend en entrée 2 paramètres requis, la donnée et la valeur à attribuer à la donnée.

La donnée du premier paramètre ne sera modifiée que si le types des données sont identiques, le contenu d’un objet ne peut pas être remplacé par un tableau (et vice versa) par exemple.

Si la donnée du premier paramètre est une donnée observable d’un objet littéral, d’un tableau ou d’un objet non natif, les nouvelles données viennent remplacer les anciennes et seront observables (ajout d’un Proxy en profondeur).

Si la donnée du premier paramètre est un donnée observable d’une primitive ou d’un objet natif, la propriété value est modifiée si la donnée du deuxième paramètre est aussi une donnée observable d’une primitive ou d’un objet natif ou n’est pas une donnée observable.

Si la donnée du premier paramètre n’est pas une donnée observable, les nouvelles données viennent remplacer les anciennes par une copie profonde sans Proxy.

Quelques exemples :

const d4 = {a: 1, b: {c: 2, d: 3}};
setData(d4, {x: 1, y: 2});
console.log(d4); // L'objet {x: 1, y: 2} dans la console

const t1 = [1, 2, 3, 4];
setData(t1, [4, 3]);
console.log(t1); // Le tableau [4, 3] dans la console

const d5 = createData({a: 1, b: {c: 2, d: 3}});
setData(d5, {x: 1, y: 2})
console.log(d5); //Un Proxy d'un objet {x:1, y:2} dans la console

Fonction createEffect()

Cette fonction ajoute un effet qui est une fonction qui va être appelée à l’ajout de l’effet et dés qu’une donnée dont dépend l’effet est modifiée.

La signature de la fonction est la suivante :

createEffect(effect, ...dependencies)

Elle prend en entrée au minimum un paramètre requis qui doit être une fonction.

Elle exécute cette fonction immédiatement et détermine les dépendances aux données observables utilisées. Si aucune dépendance n’a été trouvée, la fonction ne sera plus jamais appelée. Par contre, si des dépendances ont été trouvées, la fonction est rappelée chaque fois qu’au moins une donnée observable est modifiée.

Il est possible d’indiquer des dépendances supplémentaires en ajoutant à l’appel de la fonction createEffect() et après le premier paramètre, des paramètres supplémentaires qui doivent être des données observables. Ceci peut être nécessaire par exemple si vous savez que le premier appel à la fonction ne consulte aucune donnée observable mais que les appels suivant le font.

Les appels suivants à la fonction sont réalisées après la modification de toutes les données et ne génère qu’un seul appel si par exemple 2 données observables sont modifiées dans le même traitement.

La fonction createEffect() retourne le résultat du premier appel à la fonction passée en paramètre. Si cette fonction est asynchrone, createEffect() retourne la promesse JavaScript (objet Promise) retournée par la fonction.

Des exemples d’utilisation de cette fonction sont disponibles dans les pages Composant utilisant une animation et Composant utilisant du chargement asynchrone.

Fonction untrack()

Cette fonction exécute la fonction passée en paramètre en suspendant la recherche des dépendances.

La signature de la fonction est la suivante :

untrack(func)

Elle prend en entrée un unique paramètre requis qui doit être une fonction.

Elle exécute cette fonction immédiatement en désactivant la recherche des dépendances pour le traitement en cours mais pas pour les traitements imbriqués suivants.

Les appels à untrack() peuvent être imbriqués, c’est à dire que l’appeler avec une fonction qui appelle aussi untrack() ne pose pas de problème.

La fonction untrack() retourne le résultat de l’appel à la fonction passée en paramètre. Si cette fonction est asynchrone, untrack() retourne la promesse JavaScript (objet Promise) retournée par la fonction.

Fonction onError()

Cette fonction permet de définir une fonction qui est appelée en cas d’erreur.

La signature de la fonction est la suivante :

onError(func)

Room utilise une fonction globale et unique appelée en cas d’erreur. Par défaut c’est la fonction console.error() qui est utilisée, la fonction onError() permet de remplacer cette fonction.

La fonction onError() attend en entrée une fonction qui reçoit en paramètre une erreur qui est un objet Error. Si la fonction onError() est appelée sans paramètre ou avec un paramètre qui n’est pas une fonction, elle ne fait rien. La fonction onError() ne retourne rien.

Exemple d’utilisation :

// Une fonction d'erreur affichant le message d'erreur
function errorFunc(error) {
alert(error.message);
}

// La mise en place de la fonction d'erreur
Room.onError(errorFunc);

Une erreur dans votre fonction d’erreur est interceptée et envoyée dans la console d’erreur.

Fonction map()

Cette fonction permet de synchroniser une donnée observable de type tableau ou objet littéral avec sa représentation en éléments dans un container.

La signature de la fonction est la suivante :

map(container, data, func)

Les paramètres attendus par la fonction maps() sont :

  • container : un objet de la classe Element qui va contenir la représentation des données,
  • data : une donnée observable d’un tableau ou d’un objet littéral,
  • func : une fonction qui doit retourner un objet de la classe Element représentant un élément des données.

La fonction map() retourne la valeur du paramètre container. Les éléments enfants du container sont initialement supprimés et il n’est pas possible d’ajouter d’autres éléments dans ce container, son contenu est entièrement géré par la fonction map().

La signature de la fonction attendue dans le troisième paramètre est la suivante :

func(value, key, data)

Les paramètres de cette fonction sont donc :

  • value : la valeur de l’élément correspondant à la clé key dans la donnée observable data,
  • key : la clé (pour un objet) ou l’indice (pour un tableau) de l’élément dans la donnée observable data,
  • data : La donnée observable passée à la fonction map().

Le paramètres key est en fait une donnée observable d’une primitive JavaScript créée par la fonction map() avec la fonction createData() et contenant un entier si la donnée data est un tableau et une chaine de caractères si la donnée data est un objet. Pour un tableau c’est donc l’indice de l’élément dans le tableau et pour un objet c’est la clé de l’objet donnant l’élément.

Cette fonction est appelée par la fonction map() dés qu’elle en a besoin et doit impérativement retourner un objet de la classe Element.

Attention si vous utilisez un tableau comme donnée et que vous supprimez un élément avec l’opérateur delete et non pas avec la méthode splice(), un trou est créé dans le tableau. C’est à dire que l’indice existe toujours mais avec la valeur associée undefined. Dans ce cas la fonction map() appelle la fonction du paramètre func avec un paramètre value qui est undefined, à vous alors de néanmoins rendre un objet de la classe Element.

Vous pouvez utiliser la fonction map() quand vous voulez représenter une liste d’éléments et que cette liste peut évoluer (ajout, suppression ou permutation d’éléments pour un tableau). Elle n’a pas d’intérêt si la liste n’a pas d’évolution, l’utilisation de la fonction map() pour un tableau ou entries() pour un objet est suffisante dans ce cas, même si le contenu des éléments eux évoluent.

Un exemple d’utilisation de cette fonction est disponible dans la page Liste de tâches à faire.

Dernière mise à jour :