新しいPolymerベースのWeb UIが間もなくリリースされます。皆さんもきっと楽しみにしていることと思います。新しいUIの構築に取り掛かるにあたり、特に、JSF UIはカスタマイズとプラガブル性の面でトップレベルでしたので、大きなチャレンジであることはわかっていました。ドキュメントレイアウトからコンテンツビューまでのすべてがXML定義で構築されており、実行時にメタモデルを利用できるようになり、既存のレイアウトを簡単にオーバーライドしたり、これに貢献したりすることができます。それはサーバ側のレンダリングに依存するので、このメタモデルを利用して貢献をすべてマージし、コンポーネントツリーを構築し、UIをHTMLでレンダリングすることができます。Web UIの場合、フレームワークはクライアント側です。当社のメタモデルについての知識はなく、DOMをフレームワークとして活用しています。つまり、基本的にAPIに依存しているリッチなウェブクライアントであり、できるだけシンプルでウェブ開発者に親しみやすいものにしたいと思っていました。

Webコンポーネントでは、最終的に相互運用可能なコンポーネントを作成する方法がありましたので、最初から構成に焦点を合わせることにしました。当社は、新しいWeb UIの構築に使用し、独自のカスタムUIの構築にも利用できるカスタム要素のセットとしてNuxeo UI Elementsを構築することから始めました。これにより、UIのカスタマイズをまったく新しいレベルまで高めることができます。当社は引き続きデフォルトのWeb UIをプラグイン可能にし、ユーザのUIを簡単にプラガブルにできるようにしたいと思っていましたが、同時にすべての操作を簡単にしておきたかったので、既成概念にとらわれずに簡単な解決方法を見つけ出すことが必要でした。

要素のオーバーライド

最も基本的な例として、アプリケーションに「<nuxeo-user-view-layout>」という要素があるとし、ユーザがレイアウトを再配置したり、新しいフィールドを導入したりするために、オーバーライドが実行できるようにしたいとしましょう。カスタム要素の再定義はオプションではありません。現在、カスタム要素の登録を解除する方法はなく、登録が重複するとエラーになります。この場合、最も簡単な解決策は、ユーザがnuxeo-user-view-layout.htmlファイルを上書きするようにすることです。これは、そのファイルをヴァルカナイズ処理から排除するだけで行うことができます。このアプローチに大きく依存していない限り、別々のファイルからいくつかの要素を読み込んでもパフォーマンスに大きな影響はなく、必要なカスタマイズが提供されます。シンプルで効果的です。

しかし、ここでもっと難しいシナリオを見てみましょう。一連のカスタムレイアウトをロードするために必要な各ドキュメントタイプについて考えてみましょう。デフォルトでは、現在のJSF UIは、少なくとも3つの異なるフォームレイアウト(ビュー、編集、作成)に依存しています。カスタム要素を使用しているので、これらのレイアウトをそれぞれ要素にすることができ、次のようなものになります:

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

しかし、Web UIは、事前に知ることができない多数のカスタムドキュメントタイプで動作する必要があることにも留意する必要があります。レジストリなしにどのような要素をロードし使用するか、どのように「推測」することができるか、それには、命名規則、「<nuxeo-(doctype)-(edit|view|create)-layout>」のような最も簡単なオプションから始めることができます。

既定のモード(ビュー、編集、作成)で、どのような要素/レイアウトを任意のドキュメントタイプに使用するかを分かっていますが、どのようにインポートできるでしょうか?ありがたいことに、Polymerは本当に便利な組み込みメソッドを提供しています。そのうちの1つがimportHrefです。

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

必要に応じて要素をロードするためにimportHrefの使い方を簡単に見てみましょう。提供されたドキュメントのビューレイアウトをロードする要素「<nuxeo-document-view>」があるとしましょう。要素のHTMLでは、レイアウトを追加するコンテナを定義します。

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

文書が変更されると:

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

必要に応じて動的なレイアウトが利用できます。これらの要素がヴァルカナイズ処理されていない限り、ユーザはオーバーライドすることができ、UIはカスタムレイアウトをロードします。

プラガブル要素

これまでは、ユーザが一部分をオーバーライドすることによってアプリケーションをカスタマイズできるようにしてきましたが、Nuxeo Platformのようなモジュール性の高くプラグイン可能なアプリケーションでは、不十分です。各アドオンがUIに徐々に貢献できるように、当社のUIもプラグイン可能にする必要があります。各アドオンが既存の要素を上書きする場合、最後にロードされたアドオンの変更のみが実行されます。現在のJSF UIでは、アクションカテゴリが新しいコンテンツを追加できるページで基本的に事前に定義されているため、これが実行できます。貢献はXMLを介して行われ、メタモデルのおかげでレンダリング時にすべて考慮されます。

既成概念にとらわれずに最も簡単な解決策を考えてみると、非常に似た概念がWeb Components、特にShadow DOMによって提供されていることがわかりました。V0では挿入ポイントとコンテンツセレクタ、V1ではスロットが導入されました。これらのShadow DOM改訂版では、カスタム要素にプレースホルダを定義して、ユーザが独自のカスタムマークアップを使用して埋め込むことができます。コンセプトは、実現したいものに非常に近いものです。主な違いは、標準のWebコンポーネントAPIでは、宣言するときにのみ要素にコンテンツを提供することができますが、当社のケースではどこでも(アドオンによって提供される個別のインポートであっても)提供することができる点です。これにより、当社独自のスロット要素を作成することができました。その使い方は

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

コンセプトは非常にシンプルです。あなたのアプリケーションや要素のどこにでも「<nuxeo-slot>」を宣言し、一意の「名前」を割り当てます。新しいコンテンツを投稿するには「<nuxeo-slot-content>」を使い、投稿するスロットの名前と一致するように「slot」属性を設定し、コンテンツの「<template>」を定義します。

最初はとっつきにくいかもしれませんが、スロットの実際の実装は驚くほど簡単です。必要最低限のバージョンを見てみましょう。

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

ご覧のように、スロットレジストリを保持して、それに貢献したコンテンツのリストだけでなく、すべてのスロットのインスタンスリストを保持できます。同じスロットの複数のインスタンスをサポートすることが実に簡単で便利だったからです。新しいコンテンツが当社のスロットの1つに割り当てられるたびに、「<template>」を取り、それをコンテナに追加します。

注記: 現在の実装はもう少し複雑になっています。たとえば、実際、インスタンス化されたテンプレートは、そのルートにではなくスロットの兄弟として追加されます。同じスロットコンテンツを複数のスロットに追加することもサポートしており、スロットコンテンツの順序付け、マージ、無効化のサポートを導入しました。当社の開発事業については、お気軽にnuxeo-ui-elementsリポジトリの実装、テスト、デモをご覧ください。

新しいWeb UIの構築に取り組む過程で、REST API を大幅に改善し、現在のUIフレームワークを解体してさらに使いやすくしました。Polymerシリーズで、当社の製品に関する最新情報をご覧ください。ご意見、ご提案、ご投稿をお待ちしております!