Une nouvelle version de notre UI Web basée sur Polymer est sur le point de sortir et vous avez probablement autant hâte que nous ! Lorsque nous avons décidé de concevoir une nouvelle UI, nous savions que le défi était de taille, surtout que notre UI JSF est exceptionnelle en termes de personnalisation et de connectivité. De l’organisation des documents aux visualisations de contenu, tout est conçu à partir de définitions XML et un meta-modèle est mis à disposition lors de l’exécution, vous permettant ainsi de passer outre et/ou de contribuer à n’importe quelle organisation existante. Puisqu’elle se base sur la génération des rendus côté serveur, elle peut utiliser ce meta-modèle pour fusionner toutes les contributions, créer l’arbre de composants et générer l’UI en HTML. Dans le cas de l’UI Web, le framework se trouve côté client : il n’a pas conscience de notre meta-modèle et s’appuie sur le framework DOM. C’est donc en fait un client Web riche basé sur nos API et nous voulions qu’il reste aussi simple et familier que possible pour les développeurs Web.
Les composants Web nous permettaient enfin de créer des composants interopérables et nous avons donc décidé dès le début de nous concentrer sur la composition. Nous avons commencé par concevoir des éléments d’UI Nuxeo, un ensemble d’éléments personnalisés que nous utilisons pour notre nouvelle UI Web et que vous pouvez également utiliser pour créer votre propre UI personnalisée. Cela nous permet d’atteindre untout autre niveau de personnalisation d’UI. Nous voulions que notre UI Web par défaut reste facile à connecter tout en vous proposant la même chose pour votre propre UI. Nous avons vraiment dû sortir des sentiers battus pour trouver les solutions les plus simples.
Contournement d’éléments
Commençons avec l’exemple le plus basique possible. Notre application dispose d’un élément <nuxeo-user-view-layout>
et nous souhaitons laisser aux utilisateurs la possibilité de le contourner s’ils veulent modifier son organisation ou ajouter de nouveaux champs. Redéfinir les éléments personnalisés n’est pas une très bonne option puisqu’il n’est actuellement pas possible de retirer un élément personnalisé et que la duplication provoque des erreurs. Dans ce cas, la solution la plus simple est de laisser l’utilisateur contourner notre fichier nuxeo-user-view-layout.html. C’est possible en l’excluant du processus de génération. Tant qu’on ne s’appuie pas trop sur cette approche, charger quelques éléments à partir de fichiers séparés n’aura pas un impact trop important sur les performances et nous permettra de bénéficier des options de personnalisation dont nous avons besoin. Simple et efficace.
Mais imaginons un scénario un peu plus complexe dans lequel plusieurs organisations personnalisées doivent être chargées pour chaque type de document. Par défaut, notre UI JSF se base sur un minimum de trois formulaires différents : affichage, édition et création. Puisque nous utilisons des éléments personnalisés, il est possible de transformer chaque organisation en élément avec un résultat ressemblant à :
<nuxeo-note-view-layout>, <nuxeo-note-edit-layout>, and <nuxeo-note-create-layout>
Mais il faut également garder en tête que notre UI Web doit fonctionner avec un grand nombre de types de documents personnalisés qu’il nous est impossible de prévoir à l’avance. Comment pouvons-nous donc “deviner” les éléments à charger et à utiliser sans registre ? On peut commencer par l’option la plus simple : une convention de désignation, avec quelque chose comme <nuxeo-(doctype)-(edit|view|create)-layout>
.
Nous savons maintenant quel élément / quelle organisation utiliser pour chaque type de document par défaut (affichage, édition, création), mais comment les importer ? Heureusement, Polymer offre un ensemble de méthodes intégrées très pratiques dont la méthode importHref :
// Dynamically imports an HTML document
importHref(href, onload, onerror, optAsync)
Jetons un œil à l’utilisation d’importHref pour charger nos éléments à la demande. Nous avons un élément <nuxeo-document-view>
dans lequel nous voulons charger la présentation pour le document fourni. Il est possible d’ajouter un conteneur dans le code HTML pour définir son organisation :
<div id="document-view">
<!-- Dynamic layout element will be inserted here -->
<!-- <nuxeo--view document=""> -->
</div>
Lorsque le document change :
_documentChanged: function() {
// determine our element name based on the convention
var layout = [
'nuxeo', Polymer.CaseMap.camelToDashCase(document.type), 'view-layout'
].join('-'):
// import the element
this.importHref(this.resolveUrl(layout + '.html'), function() {
// create the element
var el = document.createElement(layout);
// append it to the parent
this.$['document-view'].appendChild(el);
// setup data binding
el.document = this.document;
},
// error handling goes here
);
}
Et voilà : des présentations dynamiques à la demande. Tant que ces éléments ne sont pas générés, les utilisateurs peuvent simplement les contourner et notre UI chargera leurs organisations personnalisées.
Des éléments faciles à connecter
Nous avons jusqu’ici fait en sorte que les utilisateurs puissent personnaliser notre application en contournant certaines parties, mais pour une application hautement modulaire et facile à connecter comme Nuxeo Platform, ça ne suffit pas. Notre UI doit également être facile à connecter pour que chaque add-on puisse contribuer de manière progressive à l’UI. Si chaque add-on contournait un élément existant, seules les dernières modifications d’add-on chargées seraient utilisées. C’est possible avec notre UI JSF actuelle, grâce aux catégories d’action qui sont des emplacements prédéfinis sur la page sur lesquels vous pouvez ajouter du contenu. Les contributions se font en XML et, grâce à notre méta-modèle, ils sont tous pris en compte lors de la génération des rendus.
En réfléchissant différemment pour parvenir à la solution la plus simple possible, nous avons découvert que des concepts semblables étaient déjà proposés par les Web Components, en particulier Shadow DOM, qui a ajouté des points d’insertion et des sélecteurs de contenu en V0 et des slots en V1. Ces deux révisions de Shadow DOM vous permettent de définir des placeholders dans vos éléments personnalisés afin que les utilisateurs puissent les remplir avec leur propre markup personnalisé. Ça ressemble beaucoup à ce que nous essayons de faire. La principale différence est qu’avec les API Web Components standards, vous pouvez seulement ajouter du contenu à un élément lorsque vous le déclarez tandis que, dans notre cas, nous souhaitons pouvoir le faire n’importe où (même dans un import séparé rendu possible via un add-on). Cela nous a poussé à créer nos propres slots faciles à utiliser :
<!-- Our placeholder -->
<nuxeo-slot name=”MY_SLOT”></nuxeo-slot>
<!-- Our content contribution -->
<nuxeo-slot-content slot="MY_SLOT">
<template>Hello Slots!</template>
</nuxeo-slot-content>
Le concept est très simple : vous déclarez un <nuxeo-slot>
n’importe où dans votre application et/ou dans les éléments et vous lui assignez un “nom” unique. Lorsque vous souhaitez lui ajouter du contenu, vous pouvez utiliser <nuxeo-slot-content>
, définir l’attribut “slot” pour qu’il corresponde au nom du slot que vous souhaitez compléter et définir le <template>
pour votre contenu.
Même si ça peut être décourageant au premier abord, l’implémentation de nos slots est étonnamment simple. Jetons un œil à une version allégée de celle-ci :
<dom-module id="iron-slot">
<script>
(function() {
// GLOBAL SLOT REGISTRY
var REGISTRY = {};
function _getRegistry(slot) {
return REGISTRY[slot] = REGISTRY[slot] || {
nodes: [], // list of content for this slot
slots: [] // list of slot instances
};
}
/** Custom element to render nodes in a given slot **/
Polymer({
is: 'iron-slot',
behaviors: [Polymer.Templatizer],
properties: {
slot: {
type: String,
observer: '_register'
}
},
attached: function() {
this._updateContent();
},
_register: function(newSlot, oldSlot) {
_getRegistry(newSlot).slots.push(this);
},
_updateContent: function() {
// empty container
var container = Polymer.dom(this.root);
while (container.childNodes.length) {
container.removeChild(container.lastChild);
}
// render content
_getRegistry(this.slot).nodes.forEach((node) => {
// lookup the <template> in the slot content
var template = Polymer.dom(node).querySelector('template');
// templatize and stamp it
this.templatize(template);
var el = this.stamp({});
container.appendChild(el.root);
});
}
});
/** Custom element to render actions of a given category **/
Polymer({
is: 'iron-slot-content',
properties: {
slot: {
type: String,
observer: '_slotChanged'
}
},
_slotChanged: function() {
var registry = _getRegistry(slot);
registry.nodes.push(node);
registry.slots.forEach((c) => c._updateContent());
}
});
})();
</script>
</dom-module>
Comme vous pouvez le voir, nous conservons un registre de slots où se trouve le contenu ajouté sous forme de liste de toutes les instances du slot, et ce, juste parce ce qu’il était très simple et pratique de supporter de multiples instances d’un même slot. Lorsque du contenu est affecté à l’un de nos slots, le <template>
est horodaté et ajouté à notre conteneur.
Remarque : notre implémentation est un peu plus complexe. Par exemple, nous ajoutons les modèles instanciés au même niveau que le slot. Nous supportons également l’ajout du même contenu à plusieurs slots et nous avons ajouté le support de du tri, de la fusion et de la désactivation du contenu. Si vous souhaitez entrer dans les détails, n’hésitez pas à jeter un oeil à notre implémentation, nos tests et nos démos présents dans notre base documentaire nuxeo-ui-elements.
Nous avons travaillé d’arrache-pied pour concevoir notre nouvelle UI Web tout en améliorant considérablement nos API REST et nous avons essayé de déconstruire notre framework d’UI actuel pour qu’il soit plus facile à utiliser pour vous. Suivez notre série Polymer pour suivre l’évolution de notre travail. Tous les commentaires, suggestions et contributions sont les bienvenus !