dev

Dorui est une librarie JavaScript qui propose une meilleure alternative pour créer des éléments DOM.

Elle est grandement inspirée d'UI-js, optimisée pour des interfaces plus complexes avec des éléments s'affichant de façon conditionnelle, avec des objets plus propres pour les éléments avec attributs, et du code plus joli utilisant des fonctions nommées après les éléments au lieu de se reposer sur type.

Le nom de la librairie fait référence à mon nom d'utilisateur, pour que vous vous rappeliez de moi.

Import

You know how this goes:

importArticle({
    type: 'script',
    article: 'u:dev:MediaWiki:Dorui.js'
});

mw.hook('doru.ui').add(function(ui) {
    // Votre code ici
    // `ui` est un alias de `window.dev.dorui`
    // Il est préférable de garder une référence à `ui`, pour faciliter la création d'éléments
    // Vous trouverez plus d'informations dans la section d'exemples.
});

Différences avec UI-js

Étant donné que Dorui est dérivé de UI-js, un bon début est de préciser en quoi il diffère de l'original.

Ajouts

// UI-js
var div = dev.ui({
    type: 'div'
});
var span = dev.ui({
    type: 'span',
    text: 'Hello, world!'
});

// Dorui
var div = ui.div();
var span = ui.span({
    text: 'Hello, world!'
});
  • Pour les cas (très) rares où il n'y a pas encore d'alias pour l'élément, vous pouvez demander à ce qu'il soit rajouté. En attendant, utilisez le premier paramètre de la fonction de base. Il s'agit de l'unique et très rare cas où vous allez utiliser la fonction exportée directement.
var marquee = ui('marquee', {
    text: 'I am classified as a carcinogenic'
});
// UI-js
var div = dev.ui({
    type: 'div',
    attr: {
        id: 'container'
    }
});

// Dorui
var div = ui.div({
    id: 'container'
});
  • Pour les cas où vous devez utiliser un nom d'attribut réservé (par exemple, text), vous pouvez toujours utiliser la méthode d'UI-js, bien que attr ait été renommé en attrs pour des raisons d'homogénéité.
var div = ui.div({
    attrs: {
        id: 'container',
        text: 'boohoo'
    }
});
// UI-js
var div = dev.ui({
    type: 'div',
    children: [
        {
            type: 'span',
            text: 'I am a child'
        }
    ]
});

// Dorui
var div = ui.div({
    child: ui.span({
        text: 'I am a child'
    })
});
  • Comme vous pouvez le constater dans l'exemple ci-dessus, les enfants ne sont plus des objets, mais des nœuds eux-mêmes. Dans ce cas, ils sont ce qui est retourné par les fonctions ui.balise().
  • child ne permet pas d'utiliser des chaînes de caractères, contrairement à children. Cela est dû au fait que la propriété text permet de gérer ce cas.
  • Vous ne devriez pas utiliser child et children en même temps. Ils seront simplement ajoutés dans l'ordre dans lequel ils sont déclarés, mais c'est un peu idiot.
var div = ui.div({
    style: {
        // Vous pouvez utiliser du camelCase ou du dashed-case
        backgroundColor: 'var(--custom-color)',
        '--custom-color': '#0ff'
    }
});
var button = ui.button({
    classes: {
        'wds-button': true,
        'wds-is-disbled': buttonData.disabled
    },
    props: {
        disabled: buttonData.disabled
    },
    text: buttonData.text
});

Différences

// UI-js
var div = dev.ui({
    type: 'div',
    children: [
        {
            type: 'span',
            text: 'Stop! You violated the law!',
            condition: shouldShowBanner
        }
    ]
});

// Dorui
var div = ui.div({
    children: [
        shouldShowBanner && ui.span({
            text: 'Stop! You violated the law!'
        })
    ]
    // Cela fonctionne aussi avec `child`:
    // child: shouldShowBanner && ui.span({
    //     text: 'Stop! You violated the law!'
    // })
});
  • Cela permet d'éviter de nombreuses répétitions de code lorsque vous avez également besoin des données utilisées dans la condition, dans l'élément que vous créez.
// UI-js
var div = dev.ui({
    type: 'div',
    children: [
        {
            type: 'span',
            text: banner && banner.message,
            condition: banner && banner.message
        }
    ]
});

// Dorui
var div = ui.div({
    child: banner && banner.message && ui.span({
        // Vous êtes certain que `banner` et `banner.message` sont truthy.
        // Vous pouvez donc vous passer de la vérification ici
        text: banner.message
    })
});
  • Les valeurs falsy sont formellement ignorées dans child et children.

Suppressions

// Avec JQuery
$('#mw-content-text').append(ui.div());

// Avec JavaScript vanilla
document.querySelector('#mw-content-text').appendChild(
    ui.div()
);

// Avec des null checks
var parent = document.querySelector('#mw-content-text');
if (parent !== null) {
    parent.appendChild(
        ui.div()
    );
}

// Ajouter en premier
var parent = document.getElementById('mw-content-text');

parent.insertBefore(
    ui.div(),
    parent.firstChild
);
var select = ui.select({
    children: [
        ui.option({ text: 'Option 1' }),
        ui.option({ text: 'Option 2' }),
    ],
    // Rappel: avoir `props` APRÈS `children`
    props: {
        // Par défaut, 'Option 2' est sélectionné
        selectedIndex: 1
    }
});
var checkbox = ui.input({
    type: 'checkbox',
    props: {
        checked: true
    }
});
var chatEntry = ui.li({
    attrs: {
        'data-name': username
    },
    text: content
});

Documentation

Dorui exporte une unique fonction, ui(tag, options), which que nous appellerons ui factory. Elle prend deux arguments.

tag
C'est une chaîne de caractères qui représente le nom de la balise HTML que vous voulez créer. Par exemple, div. Cette propriété doit être obligatoirement passée et ne doit pas être une chaîne de caractères vide.
options
C'est un objet qui représente les options qui vont configurer l'élément qui va être créé. Nous appelons cet objet Options, et il possède certaines propriétés qui ont une signification particulière. Toute propriété non reconnue sera mise en argument. Cet objet ne doit pas être undefined, ou quoi que ce soit qui ne soit pas un simple objet.

Heureusement, les utilisateurs ont rarement besoin d'utiliser la fonction factory directement, car elle est un peu bizarre à utiliser et vous devez toujours passer des options même si vous voulez un élément vierge. Au lieu de cela, ils utilisent des fonctions alias qui leur permettent de construire plus facilement des éléments communs.

Fonctions alias

Il y a plusieurs fonctions qui sont des alias de l'ui factory, mais liés à un nom de balise. Par exemple, ui.div(options). Appeler cette fonction directement est l'équivalent d'appeler ui('div', options).

Il y a encore une chose qui les rend encore plus pratique à utiliser, et c'est la gestion automatique de l'objet options si vous ne le mettez pas car elles sont moins strictes que la factory. Il est donc tout à fait possible d'utiliser ui.br() pour créer un retour à la ligne sans attributs ou enfants.

Voici une liste d'alias de balises HTML :


Et les balises SVG enregistrées qui ont une sémantique SVG spéciale :


L'ensemble des balises SVG prises en charge devrait s'étoffer à l'avenir.

L'objet Options

C'est le nerf de la guerre de la création d'éléments, sans laquelle la bibliothèque n'est qu'un appel à document.createElement glorifié.

Comment fonctionne-t-il ? Chaque propriété de l'objet est une paire composée d'une clé et d'une valeur. La clé est ce que le script interprétera comme ayant une signification particulière ou comme étant utilisé en tant qu'attribut. La valeur détermine ce qui est fait exactement.

Lorsque la clé n'est pas reconnue comme ayant une signification particulière, on suppose que la clé est un nom d'attribut et que la valeur est la valeur de l'attribut. La valeur sera simplement convertie en chaîne de caractères, donc un objet deviendra attribute="[object Object]" et une valeur booléenne comme false deviendra attribute="false".

Il y a un autre comportement de l'objet options que vous devez connaître, c'est que leurs effets se produisent dans l'ordre dans lequel ils sont déclarés dans l'objet. Vous avez pu l'entrapercevoir dans la section sur les suppressions par rapport à UI-js, où l'ordre de children et props.selectedIndex avait un impact pour la sélection de la valeur par défaut. La plupart du temps, cela ne changera pas grand chose, mais cela a vraiment de l'importance lors de la définition de comportements avec child, children, et text en simultané. Vous devriez seulement utiliser children au lieu de mélanger les trois pour des raisons de clarté, mais cela montre un des cas aux limites.

Voici la liste des clés qui ont une signification particulière :

attrs
Il doit s'agir d'un objet simple qui représente les valeurs qui seront attribuées à l'élément. Il s'agit d'une échappatoire aux clés à signification particulière, au cas où vous auriez besoin de définir un attribut avec le nom child ou events pour une raison ou une autre.
Toutefois ce n'est pas seulement une échappatoire, elle gère également les valeurs booléennes de façon spéciale. Au lieu d'être converties en chaîne de caractères comme les comportement par défauts, elles peuvent être utilisées comme des attributs conditionnels. Par exemple, si vous passez checked: false, l’élément n'aura pas attribut checked du tout. Cependant, si vous passez checked: true, il aura checked="checked".
Il s'agit seulement d'un exemple, pour l'attribut checked, vous devriez utiliser props, qui est la meilleure façon de gérer les valeurs d'input.
child
Il doit s'agir d'un nœud, tel un nœud textuel (bien que dans ce cas vous utiliseriez text), un document fragment (bien que dans ce cas vous utiliseriez children), ou un élément. En fait, utilisez seulement un élément. Vous ne devriez utiliser cette clé que lorsque vous n'avez pas besoin de plusieurs enfants et que le seul enfant est un élément et non du texte. Si une valeur falsy est passée (ex. 0, false, null, undefined, NaN), elle sera ignorée.
children
Il doit s'agir d'un tableau de nœuds ou de chaînes de caractères. Ils seront ajoutés dans l'ordre au parent. Les chaînes seront converties en nœuds textuels. Si une valeur falsy est trouvée dans le tableau, elle sera ignorée.
text
Il doit s'agir d'une chaîne de caractères et ajoute un nœud textuel à l'élément.
html
Il doit s'agir d'une chaîne de caractères. Il remplace complètement le contenu de l'élément par le code HTML donné. N'utilisez cette fonction que si vous devez absolument gérer de l'HTML externe au lieu de créer des nœuds vous-même.
classes
Il peut s'agir d'un tableau de chaînes ou d'un simple objet. S'il s'agit d'un tableau, il affecte ces classes à l'élément. S'il s'agit d'un objet, ses clés sont utilisées comme noms de classe, et la valeur détermine si la classe est ajoutée à l'élément ou non. Vous pouvez imaginer ce fonctionnement comme des classes conditionnelles, de la même façon que attrs peut avoir des attributs conditionnels.
events
Il doit s'agir d'un simple objet. Ses clés sont utilisées comme noms d'événements, et les valeurs doivent être des fonctions qui seront des callbacks passés à addEventListener.
style
Il doit s'agir d'un simple objet. Ses clés sont utilisées comme noms de propriétés CSS et ses valeurs comme... des valeurs. Vous pouvez utiliser des noms de propriétés en camelCase (par exemple, backgroundColor, borderTopRightRadius, WebkitTransform) ou en dashed-case. Les variables CSS sont aussi supportées.
props
Il doit s'agir d'un simple objet. Les propriétés personnalisées seront assignées directement à l'élément une fois créé.

Document fragments

Une fonction utilitaire supplémentaire est fournie pour créer des document fragments, ui.frag().

Il prend un seul argument, qui est un tableau d'enfants. Comme dans l'objet options, ce tableau peut contenir des nœuds normaux ou des chaînes qui seront converties en nœuds textuels.

Exemples

Avant de voir des exemples d'utilisation plus simples, nous allons nous attaquer au problème beaucoup plus compliqué de la structure des scripts.

Même si vous n'utilisez pas certaines méthodes, il est utile d'établir un contexte dans lequel les autres exemples pourraient se trouver et comment ils s'exécuteraient.

Structure générale

L'ajout de dépendances aux scripts en général nécessite une certaine réflexion sur la façon dont vous allez structurer votre programme pour qu'il reste ordonné

En général, le plus simple est le mieux, donc si vous n'avez besoin que d'une ou de quelques dépendances, une structure comme la suivante peut convenir à votre script.

(function() {
    var ui;

    function init(lib) {
        ui = lib;

        $('#my-tools-menu').append(
            ui.li({
                text: 'My script'
            })
        );
    }

    importArticle({
        type: 'script',
        article: 'u:dev:MediaWiki:Dorui.js'
    });

    mw.hook('doru.ui').add(init);
})();

C'est bien fait, et c'est la norme pour les scripts sur le Dev Wiki. Cependant, si vous voulez un exemple de code plus tendancieux pour un script qui pourrait avoir besoin d'une gestion des dépendances plus complexe, n'hésitez pas à jeter un coup d'œil à l'exemple suivant.

(function() {
    // Double exécutions
    if (window.MyScript && MyScript.loaded) return;

    var ui;

    window.MyScript = {
        loaded: true,

        // Liste des dépendances
        loading: [
            'dorui',
            'i18n-js',
            'i18n'
        ],

        // Callback pour chaque dépendance chargée
        onload: function(key, arg) {
            switch (key) {
                case 'i18n-js':
                    arg.loadMessages('MyScript').then(this.onload.bind(this, 'i18n'));
                    break;
                case 'i18n':
                    this.i18n = arg;
                    break;
                case 'dorui':
                    ui = arg;
                    break;
            }

            var index = this.loading.indexOf(key);
            if (index === -1) throw new Error('Unregistered dependency loaded: ' + key);

            this.loading.splice(index, 1);

            if (this.loading.length !== 0) return;

            this.init();
        },

        // Import des dépendances et lien à leurs hooks
        preload: function() {
            importArticles({
                type: 'script',
                articles: [
                    'u:dev:MediaWiki:Dorui.js',
                    'u:dev:MediaWiki:I18n-js/code.js'
                ]
            });

            mw.hook('dev.i18n').add(this.onload.bind(this, 'i18n-js'));
            mw.hook('doru.ui').add(this.onload.bind(this, 'dorui'));
        },

        // Fonction qui sera appelée quand toutes les dépendances auront été chargées
        init: function() {
            var menu = document.getElementById('my-tools-menu');
            if (menu === null) return;

            menu.appendChild(
                ui.li({
                    text: this.i18n.msg('tools-menu-label').plain()
                })
            );
        }
    };

    MyScript.preload();
})();

Bien sûr, c'est beaucoup plus complexe, mais cela permet de gérer un arbre de dépendance plus compliqué. C'est à vous de voir si le coût de cette complexité en vaut la peine ou non, ou si vous pouvez adapter certaines parties du code ci-dessus pour vous-même.

Créer des éléments simples

Pour la suite, nous allons assumer que vous avez la structure appropriée dans votre code et que la variable ui réfère à la fonction factory.

Vous devriez maintenant avoir une bonne idée de la façon dont fonctionne la création d'éléments. Ces exemples ne devraient donc pas vous paraître étranges, mais ils pourraient vous permettre de vous habituer à quoi ça ressemble,et à leur fonctionnement.

var span = ui.span({
    class: 'my-span-element',
    text: 'Howdy'
});

var div = ui.div({
    id: 'my-wrapper',
    child: span
});

var container = ui.div({
    id: 'my-container',
    style: {
        display: 'flex'
    },
    children: [
        div,
        ui.div({
            text: 'Badonk'
        })
    ]
});

Gérer les éléments d'input

Les éléments d'input font partie des plus compliqués en HTML, aux vues de leur diversité.

Heureusement, props vous permet de traiter les aspects les plus problématiques, telles que les différentes manières de définir les valeurs par défaut, etc.

// Équivalent à <input type="text" value="hello">
// Rappelez vous que type="text" est la valeur implicite du navigateur si omise
ui.input({
    type: 'text',
    props: {
        value: 'hello'
    }
});

// Équivalent à <textarea>hello</textarea>
// Oh mais dis donc, on peut utiliser la même méthode pour définir la valeur par défaut !
ui.textarea({
    props: {
        value: 'hello'
    }
});

// Équivalent à <input type="checkbox" checked="checked">
// L'attribut n'est jamais réellement défini, car la propriété checked est définie avec JS
// Mais la checkbox sera cochée
ui.input({
    type: 'checkbox',
    props: {
        checked: true
    }
});

Faire des boutons WDS

C'est une bonne chose de pouvoir créer des éléments, mais ils se démarqueront toujours en quelque sorte si vous ne les stylisez pas comme le reste du site.

Pour créer un bouton qui ressemble au bouton d'édition en haut de cette page, nous aurons besoin de WDSIcons, nous supposerons donc que vous l'avez importé.

var button = ui.button({
    class: 'wds-button',
    children: [
        dev.wds.icon('pencil-small'),
        ui.span({
            text: 'Edit'
        })
    ]
});

Qu'est-ce qu'il y a ? Vous voulez aussi le menu déroulant ? Eh bien, qu'est-ce que vous êtes difficile, d'accord, voilà :

var buttonGroup = ui.div({
    class: 'wds-button-group',
    children: [
        ui.button({
            class: 'wds-button',
            children: [
                dev.wds.icon('pencil-small'),
                ui.span({
                    text: 'Edit'
                })
            ]
        }),
        ui.div({
            class: 'wds-dropdown',
            children: [
                ui.div({
                    classes: ['wds-button', 'wds-dropdown__toggle'],
                    child: dev.wds.icon('dropdown-tiny')
                }),
                ui.div({
                    classes: ['wds-dropdown__content', 'wds-is-not-scrollable', 'wds-is-right-aligned'],
                    child: ui.ul({
                        classes: ['wds-list', 'wds-is-linked'],
                        children: [
                            ui.li({
                                child: ui.a({
                                    text: 'Ha-ha'
                                })
                            })
                        ]
                    })
                })
            ]
        })
    ]
});

Extraire des fonctions

Mais le code ci-dessus est super compliqué à réutiliser partout. Et si nous pouvions avoir un objet plus simple contenant uniquement les données pertinentes, et générer un élément à partir de celui-ci ? Comme ceci :

buildWDSButton({
    text: 'Click me',
    dropdown: [
        {
            text: 'Choice 1',
            href: '/wiki/User:Dorumin'
        },
        {
            text: 'Choice 2',
            href: mw.util.getUrl('User:Sophiedp')
        }
    ]
});

Vous pouvez facilement extraire la création d'éléments dans des fonctions, en utilisant des objets et des tableaux comme entrées. En fait, c'est encouragé, de sorte que vous pouvez garder votre interface utilisateur facile à comprendre sous la forme de petites fonctions, comme ceci :

function buildDropdownItem(data) {
    return ui.li({
        child: ui.a({
            href: data.href || '#',
            text: data.text
        })
    });
}

function buildDropdown(children) {
    return ui.div({
        class: 'wds-dropdown',
        children: [
            ui.div({
                classes: ['wds-button', 'wds-dropdown__toggle'],
                child: dev.wds.icon('dropdown-tiny')
            }),
            ui.div({
                classes: ['wds-dropdown__content', 'wds-is-not-scrollable', 'wds-is-right-aligned'],
                child: ui.ul({
                    classes: ['wds-list', 'wds-is-linked'],
                    children: children.map(buildDropdownItem)
                })
            })
        ]
    });
}

function buildWDSButton(data) {
    return ui.div({
        class: 'wds-button-group',
        children: [
            ui.button({
                class: 'wds-button',
                children: [
                    dev.wds.icon('pencil-small'),
                    ui.span({
                        text: data.text
                    })
                ]
            }),
            data.dropdown && buildDropdown(data.dropdown)
        ]
    });
}

N'est-ce pas plus facile à comprendre ?

Utiliser des modales

Pour afficher des modales avec ShowCustomModal, vous pouvez utiliser un code comme celui-ci :

mw.hook('dev.showCustomModal').add(function() {
    dev.showCustomModal('My modal', {
        id: 'MyModal',
        content: ui.ul({
            children: [
                ui.li({ text: 'Your pastimes consisted of the strange' }),
                ui.li({ text: 'And twisted and deranged' }),
                ui.li({ text: 'And I hate that little game you had called' }),
                ui.li({ text: 'Crying Lightning' }),
            ]
        })
    });
});

Utiliser des fragments

Fragments sont une fonctionnalité très spécifique qui pourraient selon certains ne pas avoir sa place dans cette librairie.

Tout d'abord, vous avez raison. Mais ils sont un moyen très pratique de regrouper facilement des enfants !

Normalement, avec Dorui, vous pouvez déjà le faire avec children, mais les fragments vous permettent de le faire même à l'intérieur de ce tableau.

Cela s'avère utile pour choisir d'inclure un ou plusieurs éléments en fonction d'une condition, sans avoir de conteneur :

var shouldRepeat = true;

var lyrics = ui.ul({
    children: [
        // vs. SAYU, No Straight Roads soundtrack
        ui.li({ text: 'One, two, three, four' }),
        ui.li({ text: 'Motion on the ocean floor!' }),
        ui.li({ text: 'Five, six, seven, eight' }),
        ui.li({ text: 'Double bubble, swim some more!' }),
        shouldRepeat && ui.frag([
            ui.li({ text: 'One, two, three, four' }),
            ui.li({ text: 'Motion on the ocean floor!' }),
        ])
    ]
});

Ils sont également utiles pour passer dans des fonctions qui attendent un seul nœud. Par exemple, si vous voulez afficher une modale avec plusieurs enfants, mais que vous ne voulez pas ajouter de conteneur, vous pouvez utiliser les fragments !

Revisitons l'exemple des modales et changeons le pour utiliser des fragments, et lister les éléments en paragraphes:

mw.hook('dev.showCustomModal').add(function() {
    dev.showCustomModal('My modal', {
        id: 'MyModal',
        content: ui.frag([
            ui.p({ text: 'Your pastimes consisted of the strange' }),
            ui.p({ text: 'And twisted and deranged' }),
            ui.p({ text: 'And I hate that little game you had called' }),
            ui.p({ text: 'Crying Lightning' }),
        ])
    });
});

Cela permet de se débarrasser d'un niveau d'indentation et d'un élément enveloppant inutile. Nous espérons que les fragments vous seront utiles, ne serait-ce que quelques fois.

Portage depuis UI-js

Le portage des scripts d'UI-js vers Dorui n'est généralement pas nécessaire, à moins que vous ne continuiez à travailler dessus et que vous pensiez que Dorui sera plus agréable à utiliser à long terme.

Quelques exemples de scripts qui ont été portés depuis UI-js vers Dorui peuvent être trouvés ici et ici.

Portage depuis HTML

Si vous avez un morceau d'HTML que vous voulez transformer en Dorui, vous n'avez pas besoin d'effectuer la transformation vous-même. Vous pouvez utiliser cet outil pour le convertir en arbre Dorui.

Ouvrir la modal HTML vers Dorui