Pluggable Polymer Applications


Tue 22 November 2016 Von Nelson Silva

Wir stehen kurz davor, eine neue Version unserer polymerunterstützten Web UI herauszubringen, auf die Sie sich wahrscheinlich schon genau so freuen wie wir. Als wir damit begonnen haben, eine neue Benutzeroberfläche zu erstellen, wussten wir, dass dies eine große Herausforderung darstellen würde, insbesondere deshalb, weil unsere JSF UI eine erstklassige Anpassbarkeit und Modularität besitzt. Alles von Dokumentenlayouts zu Content-Anzeigen wurde mit XML-Definitionen erstellt und ein Metamodell ist zur Laufzeit verfügbar, mit dem Sie jedes bestehende Layout einfach überschreiben oder ergänzen können. Da sie auf serverseitigem Rendering basiert, können mithilfe dieses Metamodells alle Beiträge zusammengeführt, der Komponentenbaum erstellt und die UI in HTML wiedergeben werden. Im Falle der Web UI ist das Framework clientseitig; es hat keine Kenntnis von unserem Metamodell und nutzt DOM als Framework. Damit handelt es sich praktisch um einen Rich Web Client, der auf unseren APIs basiert, und wir wollten dies für Webentwickler einfach gestalten und so wenig wie möglich ändern.

Mit Web Components hatten wir endlich eine Möglichkeit, herstellerunabhängige Komponenten zu schreiben, weshalb wir uns von Anfang an auf die Komposition konzentrierten. Wir begannen mit der Erstellung von Nuxeo UI-Elementen, einer Reihe von benutzerdefinierten Elementen, die wir zum Erstellen unserer neuen Web UI verwenden, und die auch Sie zum Erstellen Ihrer eigenen angepassten UI verwenden können. Dadurch stoßen wir bei der Anpassung von Benutzeroberflächen in eine völlig neue Dimension vor. Unsere Standard-Web-UI sollte weiterhin flexibel sein und auch Ihnen die Möglichkeit bieten, Ihre eigene Benutzeroberfläche auf einfache Weise flexibel zu gestalten, gleichzeitig jedoch so einfach wie möglich sein. Dazu mussten wir über den eigenen Tellerrand hinausschauen.

Überschreiben von Elementen

Wir beginnen mit einem ganz einfachen Beispiel. Nehmen wir einmal an, unsere Anwendung besitzt ein Element mit dem Namen <nuxeo-user-view-layout>, und wir möchten, dass Anwender dies überschreiben können, weil sie möglicherweise das Layout neu anordnen oder neue Felder hinzufügen möchten. Angepasste Elemente neu zu definieren ist keine Option, da es aktuell keine Möglichkeit gibt, die Registrierung eines angepassten Elements aufzuheben und eine doppelte Registrierung einen Fehler verursachen würde. In diesem Fall ist es am einfachsten, dem Anwender die Möglichkeit zu geben, unsere nuxeo-user-view-layout.html-Datei zu überschreiben. Dies erreicht man am einfachsten, indem man sie von der Vulkanisation ausschließt. So lange wir uns nicht zu sehr auf diesen Ansatz verlassen, hat das Laden einzelner Elemente aus unterschiedlichen Dateien keine großen Auswirkungen auf die Performance und gibt uns die erforderliche Anpassung. Es ist einfach und effektiv.

Werfen wir jetzt einen Blick auf ein komplizierteres Szenario. Gehen wir davon aus, dass wir für jeden unsere Dokumenttypen eine Reihe angepasster Layouts laden müssen. Unsere aktuelle JSF UI basiert standardmäßig auf mindestens drei verschiedenen Formularlayouts: Anzeigen, Bearbeiten und Erstellen. Da wir angepasste Elemente verwenden, können wir aus jedem dieser Elemente ein Layout machen und erhalten etwas in der Art:

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

Wir müssen allerdings auch berücksichtigen, dass unsere Web UI mit einer beliebigen Zahl von angepassten Dokumenttypen funktionieren muss, die wir im Vorfeld noch nicht kennen. Wie können wir also "erraten", welche Elemente wir ohne eine Art von Registrierung laden und verwenden müssen? Wir können mit der einfachsten Option beginnen: einer Namenskonvention, etwa <nuxeo-(doctype)-(edit|view|create)-layout>.

Jetzt wissen wir, welches Element/Layout wir für den jeweiligen Dokumenttyp in einem der Standardmodi 'Anzeigen'‚ 'Bearbeiten' und 'Erstellen' verwenden müssen. Aber wie können wir diese importieren? Glücklicherweise bietet Polymer eine Reihe wirklich hilfreicher, integrierter Methoden. Eine davon ist importHref:

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

Werfen wir einen kurzen Blick darauf, wie wir importHref verwenden können, um unsere Elemente bei Bedarf zu laden. Gehen wir davon aus, dass wir ein Element <nuxeo-document-view> haben, für das wir das Anzeigelayout für das bereitgestellte Dokument laden möchten. Im HTML des Elements legen wir einen Container fest, dem wir das Layout hinzufügen:

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

Sobald sich das Dokument ändert:

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

Und schon sind wir soweit - abrufbare dynamische Layouts. So lange diese Elemente nicht vulkanisiert sind, können Benutzer diese einfach überschreiben und unsere UI wird ihre individuell erstellten Layouts laden.

Elemente zum Einbinden

Bis jetzt haben wir uns darauf konzentriert, Benutzern die Möglichkeit zu geben, unsere Anwendung durch das Überschreiben einzelner Teile anzupassen. Für stark modulare und flexible Anwendungen wie die Nuxeo Platform reicht dies nicht aus. Unsere UI muss ebenfalls modular und flexibel sein, damit jedes Add-on schrittweise zur Erweiterung der UI beiträgt. Wenn jedes Add-on ein bestehendes Element überschreiben würde, dann würden nur die Änderungen des zuletzt geladenen Add-ons bestehen. In unserer derzeitigen JSF UI ist dies dank Aktionskategorien möglich, die eigentlich vordefinierte Stellen auf einer Seite sind, an denen das Hinzufügen neuer Inhalte möglich ist. Beiträge werden über XML hinzugefügt und dank unseres Metamodells während des Renderings berücksichtigt.

Um die einfachste mögliche Lösung bereitzustellen haben wir erneut konventionelle Denkmuster verlassen und herausgefunden, dass Web Components und insbesondere Shadow DOM bereits sehr ähnliche Konzepte anbieten, die Einfügepunkte und Content-Selektoren in V0 und Einschübe in V1 eingeführt haben. Diese beiden Shadow DOM-Überarbeitungen ermöglichen das Festlegen von Platzhaltern in Ihren angepassten Elementen, die Benutzer mit ihren eigenen benutzerdefinierten Markups füllen können. Das Konzept ähnelt dem, was wir erreichen möchten. Der größte Unterschied besteht darin, dass mit Standard-Web Components-APIs Content nur dann einem Element hinzugefügt werden kann, wenn dieser festgelegt ist, während wir dazu überall in der Lage sein möchten (sogar in separaten Importen, die durch ein Add-on zur Verfügung gestellt werden könnten). Dies führte dazu, dass wir unsere eigenen Einschubelemente erstellt haben, deren Verwendung so einfach ist:

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

Das Konzept ist äußerst einfach: Sie legen einen <nuxeo-slot> an einer beliebigen Stelle Ihrer Anwendung bzw. Elemente fest und weisen ihm einen eindeutigen "Namen" zu. Wenn Sie ihm neuen Content hinzufügen möchten, können Sie <nuxeo-slot-content> verwenden, das "slot"-Attribut so festlegen, dass es mit dem Einschub (Slot) übereinstimmt, dem Sie etwas hinzufügen möchten, und das <template> für Ihren Content festlegen.

Auch wenn es anfangs wie eine große Herausforderung aussehen mag, ist die eigentliche Implementierung unserer Einschübe überraschend einfach. Werfen wir einen Blick auf eine gekürzte Version davon:

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

Wie Sie sehen können, behalten wir ein Einschubregister, das eine Liste mit dem Content, den wir hinzufügen möchten, und eine Liste mit allen Einschüben enthält - einfach weil es wirklich einfach und hilfreich war, mehrere Instanzen des gleichen Einschubs zu unterstützen. Sobald einem unserer Einschübe neuer Content zugewiesen wird, nehmen wir das <template> und fügen es unserem Container hinzu.

Hinweis: Unsere derzeitige Implementierung ist etwas komplexer. Beispielsweise fügen wir die instanziierten Vorlagen als gleichgestellte Elemente dem Einschub und nicht seinem Stamm hinzu. Wir unterstützen auch das Hinzufügen des gleichen Contents auf mehrere Einschübe und haben die Unterstützung für das Sortieren, Zusammenführen und Deaktivieren von Einschub-Content eingeführt. Falls Sie sich näher mit unserer Arbeit auseinandersetzen möchten, werfen Sie gerne einen Blick auf unsere Implementierung, Tests und Demos in unserem nuxeo-ui-elements-Repository.

Wir haben mit viel Aufwand an der Erstellung unserer neuen Web UI gearbeitet und im weiteren Verlauf unsere REST APIs stark verbessert und versucht, unser aktuelles UI Framework auf seine wesentlichen Elemente zurückzuführen, um seine Verwendung für Sie zu vereinfachen. Folgen Sie unserer Polymer-Serie um stets auf dem neuesten Stand unserer Entwicklung zu bleiben. Wir freuen uns sehr über Ihre Kommentare, Vorschläge und Beiträge!


Etikettiert: Polymer, Nuxeo UI