Pluggable Polymer Applications


Tue 22 November 2016 By Nelson Silva

Another release of our new Polymer based Web UI is just around the corner and you are probably looking forward to it just as much as we are! When we set out to build a new UI we knew it would be a big challenge, especially since our JSF UI is top notch in terms of customization and pluggability. Everything from document layouts to content views are built with XML definitions and a metamodel is made available at runtime allowing you to easily override and/or make contributions to any existing layout. Since it relies on server side rendering it can leverage this metamodel to merge all contributions, build the component tree and render the UI in HTML. In case of the Web UI the framework is client side; it has no knowledge of our metamodel and leverages DOM as the framework. Thus it’s basically a rich web client relying on our APIs and we really wanted to keep it as simple and familiar to web developers as possible.

With web components we finally had a way to write interoperable components so, right from the start, we decided to focus on composition. We started by building Nuxeo UI Elements, a set of custom elements that we’re using to build our new Web UI and that you can leverage to build your own custom UI too. This allows us to take UI customization to a whole new level. We still wanted to make our default web UI pluggable and to allow you to easily make your own UI pluggable, but at the same time keep everything easy, which required us to think outside the box and look for the simplest possible solutions.

Overriding Elements

Starting with the most basic example, let’s say our application has an element called <nuxeo-user-view-layout> and we want to allow people to override it because they might want to rearrange the layout or introduce new fields. Redefining custom elements is not an option since there’s currently no way to unregister a custom element and duplicate registrations will result in error. In this case the simplest solution is to let the user override our nuxeo-user-view-layout.html file. This can be done by simply excluding it from vulcanization. As long as we don’t rely heavily on this approach loading a couple of elements from separate files won’t have a big impact on performance and will provide us the customization we need. It’s simple and effective.

But now let’s look at a more tricky scenario. Let’s say for each of our document types we need to load a set of custom layouts. By default our current JSF UI relies on at least three different form layouts: view, edit and create. Since we’re using custom elements we can make each of these layouts an element and we’ll end up with something like:

<nuxeo-note-view-layout>, <nuxeo-note-edit-layout>, and <nuxeo-note-create-layout>

But we also need to keep in mind that our Web UI needs to work with any number of custom document types, which we have no way of knowing beforehand. So how can we “guess” which elements to load and use without any sort of registry? We can start with the simplest option: a naming convention, something like <nuxeo-(doctype)-(edit|view|create)-layout>.

Now we know what element / layout to use for any given document type in any of the default modes: view, edit and create, but how can we import it? Thankfully Polymer provides a set of really helpful built-in methods, one of which is importHref:

// Dynamically imports an HTML document
importHref(href, onload, onerror, optAsync)

Let’s take a quick look at how we can use importHref to load our elements on demand. Let’s say we have an element <nuxeo-document-view> where we want to load the view layout for the provided document. In the element’s HTML we’d define a container to add the layout into:

<div id="document-view">
      <!-- Dynamic layout element will be inserted here -->
      <!-- <nuxeo--view document=""> -->
</div>

Whenever the document changes:

_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
  );
}

There we have it - dynamic layouts on demand. As long as these elements are not vulcanized users can simply override these and our UI will load their custom made layouts.

Pluggable Elements

So far we’ve focused on allowing users to customize our application by overriding some bits and pieces but for a highly modular and pluggable application like the Nuxeo Platform this is not enough. We need our UI to be pluggable too so that each addon can contribute to the UI incrementally. If each addon was to override an existing element then only the last loaded addon changes would be in place. In our current JSF UI this is possible, thanks to action categories which are essentially predefined places on the page where you can add new content. Contributions are done through XML and thanks to our metamodel they are all taken into account when rendering.

Thinking outside the box again to provide the simplest possible solution, we found that very similar concepts were already provided by Web Components, Shadow DOM in particular, which introduced insertion points and content selectors in V0 and slots in V1. Both of these Shadow DOM revisions allow you to define placeholders in your custom elements which users can fill with their own custom markup. It’s very similar in concept to what we want to achieve. The main difference is that with standard Web Components APIs you can only contribute content to an element when you declare it whereas in our case we’d like to be able to do so anywhere (even in a separate import which could be provided by an addon). That led us to the creation of our own slot elements whose usage is as simple as:

<!-- 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>

The concept is very simple: you declare a <nuxeo-slot> anywhere in your application and/or elements and assign a unique “name” to it. When you want to contribute new content to it you can use <nuxeo-slot-content>, set the “slot” attribute to match the name of the slot you want to contribute to, and define the <template>for you content.

Although it may look daunting at first the actual implementation of our slots is surprisingly simple. Let’s take a look at a stripped down version of it:

<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>

As you can see we keep a slot registry where we hold the list of content contributed to it as well as a list of all the slot’s instances - simply because it was really easy and useful to support multiple instances of the same slot. Whenever new content is assigned to one of our slots we take the <template> and stamp it appending it to our container.

Note: Our current implementation is a bit more complex. For instance, we actually append the instantiated templates as siblings of the slot and not to its root. We also support adding the same slot content to multiple slots and we introduced support for ordering, merging, and disabling slot contents. If you want to dig deeper into what we’re doing, feel free to take a peek at the implementation, tests and demos in our nuxeo-ui-elements repository.

We have been hard at work building our new web UI and in the process we’ve greatly improved our REST APIs and tried to deconstruct our current UI framework to make it easier for you to use. Follow our Polymer series to stay up to date with our work. Any comments, suggestions, or contributions are very welcome!


Tagged: Polymer, Nuxeo UI