JsTemplates

Petit exercice de création d'un outil simple de templating en HTML et javascript.

Attention: Code encore en phase de développement et sans aucune garantie qu'il puisse fonctionner. À n'utiliser que si vous comprenez ce que vous faites !

Présentation

Tout a commencé au boulot alors que je faisais une étude de librairies graphiques Angular pour un client. J'ai été proprement effaré par ce qu'on est obligé d'écrire dans le HTML — mais c'est la même chose avec React ou Vue.js. Ce gloubiboulga amorphe est absolument illisible et d'une fragilité indécente. Avec ce genres d'outils, vous êtes pieds et poings liés à la librairie / technos que vous choisissez. J'avais contourné le problème quelques mois plus tôt en développant un petit utilitaire pour un autre client en pur HTML / CSS / Javascript ; un collègue l'avait ensuite intégré en tant que composant Angular sans difficulté. Le client était content et moi aussi car le code était adaptable ailleurs. On évite ainsi un effet silo. Ici le problème était tout autre : pas moyen de s'échapper. Cela m'a néanmoins obligé à me poser la question : comment est-ce que j'écrirai moi un système de « templating » simple, basé sur les technologies webs standards ? jstemplates est le résultat de quelques heures de reflexions rapides sur le sujet.

Fonctionnalités

jstemplates se base sur deux fonctionnalités du HTML 5 :

Tout est pour le moment réduit à sa plus simple expression :

  • L'affichage de données est une simple création de noeud texte. jstemplates pourra par la suite gérer des formats ou des masques d'affichage sans problèmes.
  • La logique est réduite elle aussi à sa plus simple expression : la possibilité de faire des boucles et des tests conditionnels basiques.

Malgré sa simplicité (c'est de toute façon le but recherché), je pense que l'outil peut répondre à beaucoup de besoins. Il fait avant tout office de POC mais je compte bien l'utiliser prochainement pour développer un client web pour le gestionnaire de bibliothèque Calibre, ce qui me permettra de le faire évoluer en le confrontant à des besoins concrêts.

Exemple d'utilisation

<DOCTYPE HTML>
<html>
    <head>
        <title>Template</title>
        <meta charset="utf-8">
    </head>
    <body>
<!-- template pour l'affichage d'un bouquin-->
<template id="book">
    <article class="book">
        <h2 data-prop="title">title</h2>
        <p data-prop="meta" data-logic="if" data-test="meta !== undefined">meta</p>
        <div data-prop="content">content</div>
    </article>
</template>
<!-- Template pour l'affichage d'une liste de bouquins -->
<template id="books">
    <section data-logic="loop" data-tpl="book" data-prop="books"></section>
</template>
<!-- main -->
<script type="module" src="main.js"></script>
    </body>
</html>
Page HTML avec définition des templates
import { TemplateManager } from './js/jstemplates.js';

// display one book
const renderBook = () => {
    const tpl = TemplateManager.get('book');
    tpl.render({title: 'Book 1', content: 'This is Book 1'});
};

// display books
const renderBooks = () => {
    const tpl = TemplateManager.get('books');
    tpl.render({books:[
        {title: 'Book 1', meta: 'yes', content: 'This is book 1'},
        {title: 'Book 2', content: 'This is book 2'},
        {title: 'Book 3', content: 'This is book 3'}
    ]});
};

// run
renderBooks();
Code javascript d'utilisation des templates.

Code

/**
 * Objet de gestion d'un élément template
 */
class Template {
    /**
     * 
     * @param {String} id Identifiant de l'élément template
     */
    constructor (id) {
        /**
         * Élement template.
         * @type {HTMLTemplateElement}
         */
        this.template = document.getElementById(id);
    }
    
    dispose () {
        // do something ?
    }

    /**
     * Get the template id
     * @type {String}
     */
    get id () {
        return this.template.id;
    }
    
    /**
     * Is it a valid template ?
     * @type {Boolean}
     */
    get valid () {
        return this.template && this.template.content;
    }

    /**
     * Render data
     * @param {Object} data Data to display
     * @param {HTMLElement} [parent] Parent element (document.body by default)
     * @return {HTMLElement} The element
     */
    render (data, parent) {
        /**
         * @type {HTMLElement}
         */
        const element = this.template.content.cloneNode(true);
        const propElements = element.querySelectorAll('[data-prop]');
        propElements.forEach((propElement) => {
            const key = propElement.dataset.prop;
            if (propElement.dataset.logic) {
                this.renderLogic(data[key], propElement);
            } else if (data[key]) {
                this.renderContent(data[key], propElement);
            }
        });
        (parent || document.body).appendChild(element);

        return element;
    }

    renderLogic(data, element) {
        switch (element.dataset.logic) {
        case 'loop':
            this.renderLoop(data, element);
            break;
        case 'if':
            this.renderIf(data, element);
            break;
        }
    }

    /**
     * @param {Object} data Data to display
     * @param {HTMLElement} element Element with the for… loop
     */
    renderLoop(data, element) {
        const tpl = new Template(element.dataset.tpl);
        data.forEach((d) => { tpl.render(d, element); });
    }

    /**
     * Process a conditional statement.
     * @param {Object} data Data to display
     * @param {HTMLElement} element Element with the conditional statement
     */
    renderIf (data, element) {
        const prop = element.dataset.prop;
        const test = element.dataset.test.replace(prop, 'data');
        if (eval(test)) { // eslint-disable-line no-eval
            this.renderContent(data, element);
        } else {
            element.parentNode.removeChild(element);
        }
    }

    renderContent (data, element) {
        switch (element.nodeName.toLowerCase()) {
        case 'a':
            this.renderLink(data, element);
            break;
        case 'img':
            this.renderImage(data, element);
            break;
        default:
            element.textContent = data;
            break;
        }
    }

    /**
     * Render an image element.
     * @param {Object} data Data to render. Can be a string (src) or an object with 
     * the attributes of the image
     * @param {HTMLImageElement} element Element to render
     */
    renderImage (data, element) {
        if (typeof data === 'object') {
            Object.keys(data).forEach((key) => {
                element[key] = data[key];
            });
        } else {
            element.src = data;
        }
    }

    /**
     * Render an anchor element.
     * @param {Object} data Data to render. Can be a string (href and text) or 
     * an object with the attributes of the anchor and it's text content.
     * @param {HTMLAnchorElement} element Element to render
     */
    renderLink (data, element) {
        if (typeof data === 'object') {
            if (data.text) {
                element.textContent = data.text;
                delete data.text;    
            } else {
                element.textContent = data.href;
            }
            Object.keys(data).forEach((key) => {
                element[key] = data[key];
            });
        } else {
            element.textContent = data;
            element.href = data;
        }
    }
}

let instances = {};

class TemplateManager {
    /**
     * @param {String} id Template id
     * @return {Template} Template instance
     */
    static get (id) {
        if (!instances[id]) {
            instances[id] = new Template(id);
        }
        return instances[id];
    }
    /**
     * @param {String} id Template id to delete
     */
    static del (id) {
        if (instances[id]) {
            instances[id].dispose();
            delete instances[id];
        }
    }
    /**
     * @param {Template} template Template instance to add.
     */
    static add (template) {
        instances[template.id] = template;
    }

    static clear () {
        instances = {};
    }
}

export { Template, TemplateManager };
Module templates.

Licence

jstemplates - templating simple en HTML et javascript Copyright (C) 2019 Rui Nibau

Ce programme est un logiciel libre : vous pouvez le redistribuer ou le modifier selon les termes de la GNU General Public License tels que publiés par la Free Software Foundation : à votre choix, soit la version 3 de la licence, soit une version ultérieure quelle qu'elle soit.

Ce programme est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE ; sans même la garantie implicite de QUALITÉ MARCHANDE ou D'ADÉQUATION À UNE UTILISATION PARTICULIÈRE. Pour plus de détails, reportez-vous à la GNU General Public License.

Vous devez avoir reçu une copie de la GNU General Public License avec ce programme. Si ce n'est pas le cas, consultez <http://www.gnu.org/licenses/>

Ressources et références

A faire...

Historique

2019-02-01
  • add première écriture du module.