Separating Transclusion Mechanisms for Inheritance and Data Binding

Review: Template Inheritance in the Current Specification

In the current specification, a super class doesn't define any hooks for subclasses.  Instead, it defines insertion points into which nodes from the original DOM ("light DOM") is inserted, and then subclasses use shadow element to replace elements that get distributed into superclass's insertion points.

Consider my-card element used as follows:
  <my-card>
  <span class="name">Ryosuke Niwa</span>
  <span class="email">rniwa@apple.com</span>
  </my-card>

Suppose this element's shadow DOM looks like this:
Name: <content select=".name"></content>
Email: <content select=".email"></content>

Then in the composed tree, the first span is distributed into the first content element and the second span is distributed into the second content element as follows:
  <my-card>
    <!-- shadow root begin -->
    Name: <content select=".name">
      <!-- distribution begin -->
      <span class="name">Ryosuke Niwa</span>
      <!-- distribution end -->
    </content>
    Email: <content select=".email">
      <!-- distribution begin -->
      <span class="email">rniwa@apple.com</span>
      <!-- distribution end -->
      </content>
    <!-- shadow root end -->
  </my-card>

If I had my-webkitten-card that always as "WebKitten" as a name that inherits from my-card, its shadow DOM may look like this:
  <shadow>
    <span class="name">WebKitten</span>
    <content></content>
    <span class="email">kitten@webkit.org</span>
  </shadow>

If I had an instance of my-webkitten-card as follows:
  <my-webkitten-card>
  <span class="name">Ryosuke Niwa</span>
  <span class="email">rniwa@webkit.org</span>
  </my-webkitten-card>

Then its composed tree will look like this:
  <my-webkitten-card>
    <!-- my-webkitten-card's shadow root begin -->
      <shadow>
      <!-- my-card's shadow root begin -->
      Name: <content select=".name">
        <!-- distribution begin -->
        <span class="name">WebKitten</span>
        <span class="name">Ryosuke Niwa</span>
        <!-- distribution end -->
      </content>
      Email: <content select=".email">
        <!-- distribution begin -->
        <span class="email">rniwa@webkit.org</span>
        <span class="email">kitten@webkit.org</span>
        <!-- distribution end -->
      </content>
      <!-- my-card's shadow root end -->
    </shadow>
    <!-- my-webkitten-card's shadow root end -->
  </my-webkitten-card>

Here, my-card's shadow DOM was inserted into where the shadow element existed in my-webkitten-card's shadow DOM, and the insertion points inside my-card's shadow DOM got nodes distributed from shadow element's children including nodes inside content element.  If we didn't have the content element inside my-webkitten-card with "name" and "email" classes, then we would only see WebKitten and kitten@webkit.org distributed into my-card's insertion points as in:

  <my-webkitten-card>
    <!-- my-webkitten-card's shadow root begin -->
    <shadow>
      <!-- my-card's shadow root begin -->
      Name:
      <content select=".name">
        <!-- distribution begin -->
          <span class="name">WebKitten</span>
        <!-- distribution end -->
      </content>
      Email:
      <content select=".email">
        <!-- distribution begin -->
          <span class="email">kitten@webkit.org</span>
        <!-- distribution end -->
      </content>
      <!-- my-card's shadow root end -->
    </shadow>
    <!-- my-webkitten-card's shadow root end -->
  </my-webkitten-card>

Separating Transclusion Mechanisms for Inheritance and Data Binding

The current model mixes data binding and inheritance if we consider distributing nodes from the "light DOM" as a form of data binding.  Namely, distributing nodes from my-card's or my-webkitten-card's light DOM is data binding where the data model is DOM whereas distributing nodes from my-webkitten-card's shadow element into my-card's insertion points is an inheritance hook.

Furthermore, the transclusion mechanism for inheritance happens backwards.  Instead of a superclass defining a transclusion points for its subclasses to use, the subclasses are overriding the meaning of insertion points in the superclass by injecting nodes.  This is how existing JS libraries and frameworks do template inheritance.

For example, the following two JS template libraries that support inheritance both allow superclass template to define "named blocks" that could be overridden by subclass templates:
http://paularmstrong.github.io/swig/docs/#inheritance
http://jlongster.github.io/nunjucks/templating.html#template-inheritance

An example from Nunjucks:
If we have a template parent.html that looks like this:

{% block header %}
This is the default content
{% endblock %}

<section class="left">
  {% block left %}{% endblock %}
</section>

<section class="right">
  {% block right %}
  This is more content
  {% endblock %}
</section>
And we render this template:

{% extends "parent.html" %}

{% block left %}
This is the left side!
{% endblock %}

{% block right %}
This is the right side!
{% endblock %}
The output would be:

This is the default content

<section class="left">
  This is the left side!
</section>

<section class="right">
  This is the right side!
</section>  

Alternative Approach to Inhertiance

Consider random-element which picks a random child node to show whenever a user clicks on the element.  This element may show the name of probability distribution it uses to pick a child in its shadow DOM.  The name of the probability distribution is in the definitions of subclasses of random-element, and not in the light DOM of this custom element.  If we wanted to use the current inheritance model (multiple generations of shadow DOM & shadow element), we have to either replace the entire shadow DOM in the subclass to show the name of the probability distribution that subclasses use or add an attribute, etc… to identify the element that contains the name of probability distribution inside subclasses' shadow element. The latter would be weird because there is nothing preventing from the user of random-element to put an element that matches the same selector as a child of random-element in the "light DOM".

Here, we propose an alternative approach.  We introduce a new "yield" element to be used in the superclass define a transclusion point as an inheritance hook.  We also introduce "transclude(id, template)" function on the element which, upon calling, would create a new shadow DOM on a "yield" element of the specified id and populates it with the specified template element's content.

Consider the earlier example of my-card and my-webkitten-card.  In this new model, the superclass my-card opts in to the transclusion by a subclass by adding two yield elements in its shadow DOM as follows:
  Name: <yield id="name"><content select=".name"></content></yield>
  Email: <yield id="email"><content select=".email"></content></yield>

The complete definition of my-card element, assuming the existence of class syntax in ES6 and the use of constructor (as opposed to created callback), will look like this:
  <template id=my-card-template>
  Name: <yield id="name"><content select=".name"></content></yield>
  Email: <yield id="email"><content select=".email"></content></yield>
  </template>
  <script>
  class MyCardElement : extends HTMLElement {
    function constructor() {
      var shadowRoot = this.createShadowRoot();
      shadowRoot.appendChild(document.getElementById('my-card-template').cloneNode(true));
    }
  }
  document.registerElement('my-card', MyCardElement);
  </script>

If we had an instance of my-card element (in light DOM) as follows:
  <my-card>
  <span class="name">R. Niwa</span>
  <span class="email">rniwa@apple.com</span>
  </my-card>

Then the composed tree would look like this:
  <my-card>
    <!-- shadow root begin -->
    Name:
    <yield id="name">
      <content select=".name">
        <!-- distribution begin -->
          <span class="name">R. Niwa</span>
        <!-- distribution end -->
      </content>
    </yield>
    Email:
    <yield id="email">
      <content select=".email">
        <!-- distribution begin -->
          <span class="email">rniwa@apple.com</span>
        <!-- distribution end -->
      </content>
    </yield>
    <!-- shadow root end -->
  </my-card>

Here, yield elements are behaving like div's because it hasn't been transcluded by a subclass.

Now recall that in the current inheritance model, MyWebKittenCardElement, a subclass of MyCardElement, could be defined as follows:
  <template id=my-webkitten-card-template>
  <shadow>
    <span class="name">WebKitten</span>
    <span class="email">kitten@webkit.org</span>
    <content></content>
  </shadow>
  </template>
  <script>
  class MyCardElement : extends HTMLElement {
    function constructor() {
      this.createShadowRoot().appendChild(document.getElementById('my-webkitten-card-templat').cloneNode(true));
    }
  }
  document.registerElement('my-webkitten-card', MyWebKittenCardElement);
  </script>

In the new model, we write it as follows:
  <template id=my-webkitten-name-template>
  <span>WebKitten</span><content select=".name"></content>
  </template>

  <template id=my-webkitten-email-template>
  <span>kitten@webkit.org</span><content select=".email"></content>
  </template>

  <script>
  class MyCardElement : extends HTMLElement {
    function constructor() {
      this.transclude('name', document.getElementById('my-webkitten-name-template').cloneNode(true));
      this.transclude('email', document.getElementById('my-webkitten-email-template').cloneNode(true));
    }
  }
  document.registerElement('my-webkitten-card', MyWebKittenCardElement);
  </script>


Now suppose we had an instance of my-webkitten-card as follows:
  <my-webkitten-card>
  <span class="name">R. Niwa</span>
  <span class="email">rniwa@apple.com</span>
  </my-webkitten-card>

Then we have the following composed tree:
  <my-webkitten-card>
    <!-- my-card's shadow root begin -->
    Name:
    <yield id="name">
      <!— transclusion begin -->
      <span>WebKitten</span>
      <content select=".name">
        <!-- distribution begin -->
          <span class="name">R. Niwa</span>
        <!-- distribution end -->
      </content>
      <!— transclusion end -->
    </yield>
    Email:
    <yield id="email">
      <!— transclusion begin -->
      <span>kitten@webkit.org</span>
      <content select=".email">
        <!-- distribution begin -->
          <span class="name">rniwa@apple.com</span>
        <!-- distribution end -->
      </content>
      <!— transclusion end -->
    </yield>
    <!-- my-webkitten-card's shadow root end -->
  </my-webkitten-card>

For implementors, this new model doesn't require multiple generations of shadow DOM for a single host.  Each element can have at most one shadow root, and its shadow DOM simply contain yield element that defines transclusion point.  Furthermore, transclusion is simply done as attaching a new shadow DOM to yield element.  If we wanted the same expressive power as the current inheritance model in grabbing light DOM's nodes, we can make insertion points pull nodes out of the parent shadow DOM's light DOM instead.

For authors, the new model separates the concerns of binding DOM data model to shadow DOM from defining inheritance hooks.  It addresses use cases where inheritance hooks for subclasses are separate from data source used by custom elements such as random-element showing the name of distribution, which is overridden by its subclasses.

- R. Niwa

Received on Thursday, 17 April 2014 09:43:01 UTC