Polymer 2 dynamically merging templates
Asked Answered
P

2

7

I am attempting to build generic web components that render JSON object collections, like a tree view and a multi-list view (moving items between two lists). I would like to copy the pattern used by iron-list where a template containing the individual item presentation is passed into the component for reuse.

For example, given this web component template:

<dom-module id="intworkspace-tree">  
  <template>
    <style include="iron-flex iron-flex-alignment">

      paper-icon-item {
        --paper-item-min-height: var(--intworkspace-tree-margin,30px);
        --paper-item-icon-width : var(--intworkspace-tree-margin,30px);
      }

      paper-icon-item:focus::before,
      paper-icon-item:focus::after {
        color: inherit;
        opacity: 0;
      }

     .node {
        margin-left: var(--intworkspace-tree-margin,30px);;
      }
    </style>

    <slot id="labelView"></slot>

    <template id="nodeView">
      <div class="layout vertical">
      <paper-icon-item on-tap="nodeSelected">
        <iron-icon icon="expand-less" slot="item-icon" hidden$="[[!hasNodes(node)]]"></iron-icon>
        <!-- label goes here-->
      </paper-icon-item>

      <iron-collapse class="node" opened hidden$="[[!hasNodes(node)]]">
        <intworkspace-tree tree="[[node.nodes]]" embedded></intworkspace-tree>
      </iron-collapse>
      </div>
  </template>

  </template>
	...
  </dom-module>

and this usage:

 <intworkspace-tree tree="{{testTree}}">
      <template><paper-item-body>[[node.name]]</paper-item-body>  </template>
  </intworkspace-tree>
  
I would like to render the JSON tree array in a hierachy that combines the web component's template along with template provided through the slot to render the opaque JSON objects. So far I have identified two methods of combining the templates:
  1. Utilize the Polymer.Templatize.templatize API to load the templates, create/stamp new instances, and use the DOM API to append them together and add them to the web component's shadow DOM.

  2. Access the templates contents, combine them together, create and import a new template, and then clone it as needed.

After much adversity I was able to successfully implement #1 but not #2 and that is motivation for my question. #2 is more appealing to me because it is easier for me to merge templates once rather than merging their resulting stamped instances and this approach seems to be the only way I can reuse nested templates like dom-repeat.

My main obstacle is that once Polymer or perhaps it's polyfill is loaded the templates become opaque and can only be utilized by Polymer templatize functionality. For instance, this code works fine without any Polymer imports:

<template>
  <div>Template Contents</div>
</template>
<div>
  Template Test
</div>
  <script>
  let template = document.querySelector("template");
  let clone = document.importNode(template.content,true);
  document.querySelector("div").appendChild(clone);
  </script>

Outside of Polymer the template.content DOMFragment has children and innerHTML is set. However once Polymer is used the template.content has no children and the innerHTML is empty. This prevents me from using the DOM API to create a new template that blends the available templates together, i.e.

let newTemplate = document.createElement("template");
newTemplate.content = ... // combine #labelView > template.content with #nodeView.content 
let nodeView = document.importNode(newTemplate.content,true);
nodeView.tree=...

Perhaps by design importing templates using the standard HTML mechanism didn't work for me. Is there another way to dynamically create/merge templates at runtime with Polymer? Again my main motivation is that I would like to re-use the dom-if and dom-repeat web components nested in a template without reimplementing all of their functionality.

Puga answered 22/5, 2017 at 2:51 Comment(0)
P
7

After additional research I discovered three features of Polymer 2.0 that enabled me to produce a satisfactory solution:

  1. Whenever Polymer processes DOM templates it memoizes them by default. This template caching prevents expense cloning operations and simplifies template binding. The Polymer 2.0 DOM templating documentation explains that the preserve-content attribute can be added to a template to bypass the optimization allowing the template to be manipulated using native DOM operations.

  2. The DOM templating documentation also describes multiple methods of obtaining a custom element's raw template. One option is to call the element's static template() method and another option is to use the Polymer.DomModule.import() function. This second method was of interest to me since it allows one to manage multiple templates beyond the default module template.

  3. The Polymer.TemplateStamp API class has an internal _stampTemplate() function that is used to stamp a template into the custom element's DOM. I would have preferred to have used the well documented Polymer.Templatize.templatize() function but it looks for properties and methods on the template itself which in my case was not a custom element with behaviors defined on it.

Putting these three features together I was able to prepare a dynamic reusable merged template utlizing nested dom-ifs and a dom-repeats as I desired.

Here is the functional result:

Component:

<link rel="import" href="../polymer/polymer-element.html">
<link rel="import" href="../iron-collapse/iron-collapse.html">
<link rel="import" href="../paper-item/paper-icon-item.html">
<link rel="import" href="../paper-item/paper-item-body.html">
<link rel="import" href="../iron-flex-layout/iron-flex-layout-classes.html">
<link rel="import" href="../iron-icons/iron-icons.html">
<link rel="import" href="../iron-icon/iron-icon.html">


<dom-module id="intworkspace-tree">
  <template>
    <!-- style includes don't work in stamped template, only in the shadowRoot -->
    <style include="iron-flex iron-flex-alignment">

    paper-icon-item {
      --paper-item-min-height: var(--intworkspace-tree-margin,30px);
      --paper-item-icon-width : var(--intworkspace-tree-margin,30px);
    }

    paper-icon-item:focus::before,
    paper-icon-item:focus::after {
      color: inherit;
      opacity: 0;
    }

   .node {
      margin-left: var(--intworkspace-tree-margin,30px);;
    }
  </style>

    <slot id="labelView"></slot>
  </template>

  <template id="nodeView">

 

    <template is="dom-repeat" items="{{tree}}" as="node" index-as="n">
        <div class="layout vertical">
          <!--<div>index: [[n]]</div>
          <div>name: [[node.name]]</div>-->
          <paper-icon-item on-tap="nodeSelected">
            <template is="dom-if" if="[[hasNodes(node)]]">
              <iron-icon icon="expand-more" slot="item-icon" hidden$="[[!hasNodes(node)]]"></iron-icon>
            </template>
            <!-- label goes here-->
          </paper-icon-item>
          <template is="dom-if" if="[[hasNodes(node)]]">
            <iron-collapse class="node" opened>
              <intworkspace-tree tree="[[node.nodes]]" node-template="[[nodeTemplate]]" embedded></intworkspace-tree>
            </iron-collapse>
          </template>
        </div>
    </template>
  </template>

  <script>
    class IntTree extends Polymer.TemplateStamp(Polymer.Element) {

      static get is() {
        return 'intworkspace-tree';
      }

      static get properties() {
        return {
          tree: {
            type: Array,
            value: []
          },
          nodeTemplate: {
            type: Object,
          }
        };
      }

      ready() {
        super.ready();
        if (!this.hasAttribute("embedded")) {
          let labelTemplate = this.$.labelView.assignedNodes().find((e) => {
            return e instanceof HTMLTemplateElement;
          });
          let nodeTemplate = document.importNode(Polymer.DomModule.import(IntTree.is, "#nodeView"), true);
          let repeatTemplate = nodeTemplate.content.querySelector("template[is='dom-repeat']");
          let iconItem = repeatTemplate.content.querySelector('paper-icon-item');
          iconItem.appendChild(labelTemplate.content);
          this.nodeTemplate = nodeTemplate;
        }
        let nodeInstance = this._stampTemplate(this.nodeTemplate);
        this.shadowRoot.appendChild(nodeInstance);
      }

      hasNodes(node) {
        return node.nodes != null && node.nodes.length > 0;
      }

      nodeSelected(e) {
        let collapse = e.currentTarget.parentNode.querySelector("iron-collapse");
        let nodeIcon = e.currentTarget.parentNode.querySelector("iron-icon");
        if (collapse && nodeIcon) {
          collapse.toggle();
          if (collapse.opened) {
            nodeIcon.icon = "expand-more";
          } else {
            nodeIcon.icon = "expand-less";
          }
        }
      }
    }

    window.customElements.define(IntTree.is, IntTree);
  </script>
</dom-module>

Usage:

<intworkspace-tree tree="{{testTree}}">
      <template preserve-content><paper-item-body>[[node.name]]</paper-item-body></template>
  </intworkspace-tree>
Puga answered 29/5, 2017 at 6:33 Comment(0)
P
0

I add an observation to Aaron's solution here because I don't have enough reputation to add a comment.

Note this line has a double import

let nodeTemplate = document.importNode(Polymer.DomModule.import(IntTree.is, "#nodeView"), true);

this is not necessary. In chrome and safari works for some reason, but not in FF.

So working with Polymer, just using DomModule import is enough

let nodeTemplate = Polymer.DomModule.import(IntTree.is, '#nodeView');

Hope this helps somebody

Pyszka answered 18/10, 2017 at 15:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.