Polymer 2 dynamically merging templates
Posted By: Anonymous
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"> _x000D_
<template>_x000D_
<style include="iron-flex iron-flex-alignment">_x000D_
_x000D_
paper-icon-item {_x000D_
--paper-item-min-height: var(--intworkspace-tree-margin,30px);_x000D_
--paper-item-icon-width : var(--intworkspace-tree-margin,30px);_x000D_
}_x000D_
_x000D_
paper-icon-item:focus::before,_x000D_
paper-icon-item:focus::after {_x000D_
color: inherit;_x000D_
opacity: 0;_x000D_
}_x000D_
_x000D_
.node {_x000D_
margin-left: var(--intworkspace-tree-margin,30px);;_x000D_
}_x000D_
</style>_x000D_
_x000D_
<slot id="labelView"></slot>_x000D_
_x000D_
<template id="nodeView">_x000D_
<div class="layout vertical">_x000D_
<paper-icon-item on-tap="nodeSelected">_x000D_
<iron-icon icon="expand-less" slot="item-icon" hidden$="[[!hasNodes(node)]]"></iron-icon>_x000D_
<!-- label goes here-->_x000D_
</paper-icon-item>_x000D_
_x000D_
<iron-collapse class="node" opened hidden$="[[!hasNodes(node)]]">_x000D_
<intworkspace-tree tree="[[node.nodes]]" embedded></intworkspace-tree>_x000D_
</iron-collapse>_x000D_
</div>_x000D_
</template>_x000D_
_x000D_
</template>_x000D_
..._x000D_
</dom-module>
_x000D_
_x000D_
_x000D_
and this usage:
<intworkspace-tree tree="{{testTree}}">_x000D_
<template><paper-item-body>[[node.name]]</paper-item-body> </template>_x000D_
</intworkspace-tree>_x000D_
_x000D_
_x000D_
_x000D_
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:
-
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.
-
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>_x000D_
<div>Template Contents</div>_x000D_
</template>_x000D_
<div>_x000D_
Template Test_x000D_
</div>_x000D_
<script>_x000D_
let template = document.querySelector("template");_x000D_
let clone = document.importNode(template.content,true);_x000D_
document.querySelector("div").appendChild(clone);_x000D_
</script>
_x000D_
_x000D_
_x000D_
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");_x000D_
newTemplate.content = ... // combine #labelView > template.content with #nodeView.content _x000D_
let nodeView = document.importNode(newTemplate.content,true);_x000D_
nodeView.tree=...
_x000D_
_x000D_
_x000D_
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.
Solution
After additional research I discovered three features of Polymer 2.0 that enabled me to produce a satisfactory solution:
-
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.
-
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.
-
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">_x000D_
<link rel="import" href="../iron-collapse/iron-collapse.html">_x000D_
<link rel="import" href="../paper-item/paper-icon-item.html">_x000D_
<link rel="import" href="../paper-item/paper-item-body.html">_x000D_
<link rel="import" href="../iron-flex-layout/iron-flex-layout-classes.html">_x000D_
<link rel="import" href="../iron-icons/iron-icons.html">_x000D_
<link rel="import" href="../iron-icon/iron-icon.html">_x000D_
_x000D_
_x000D_
<dom-module id="intworkspace-tree">_x000D_
<template>_x000D_
<!-- style includes don't work in stamped template, only in the shadowRoot -->_x000D_
<style include="iron-flex iron-flex-alignment">_x000D_
_x000D_
paper-icon-item {_x000D_
--paper-item-min-height: var(--intworkspace-tree-margin,30px);_x000D_
--paper-item-icon-width : var(--intworkspace-tree-margin,30px);_x000D_
}_x000D_
_x000D_
paper-icon-item:focus::before,_x000D_
paper-icon-item:focus::after {_x000D_
color: inherit;_x000D_
opacity: 0;_x000D_
}_x000D_
_x000D_
.node {_x000D_
margin-left: var(--intworkspace-tree-margin,30px);;_x000D_
}_x000D_
</style>_x000D_
_x000D_
<slot id="labelView"></slot>_x000D_
</template>_x000D_
_x000D_
<template id="nodeView">_x000D_
_x000D_
_x000D_
_x000D_
<template is="dom-repeat" items="{{tree}}" as="node" index-as="n">_x000D_
<div class="layout vertical">_x000D_
<!--<div>index: [[n]]</div>_x000D_
<div>name: [[node.name]]</div>-->_x000D_
<paper-icon-item on-tap="nodeSelected">_x000D_
<template is="dom-if" if="[[hasNodes(node)]]">_x000D_
<iron-icon icon="expand-more" slot="item-icon" hidden$="[[!hasNodes(node)]]"></iron-icon>_x000D_
</template>_x000D_
<!-- label goes here-->_x000D_
</paper-icon-item>_x000D_
<template is="dom-if" if="[[hasNodes(node)]]">_x000D_
<iron-collapse class="node" opened>_x000D_
<intworkspace-tree tree="[[node.nodes]]" node-template="[[nodeTemplate]]" embedded></intworkspace-tree>_x000D_
</iron-collapse>_x000D_
</template>_x000D_
</div>_x000D_
</template>_x000D_
</template>_x000D_
_x000D_
<script>_x000D_
class IntTree extends Polymer.TemplateStamp(Polymer.Element) {_x000D_
_x000D_
static get is() {_x000D_
return 'intworkspace-tree';_x000D_
}_x000D_
_x000D_
static get properties() {_x000D_
return {_x000D_
tree: {_x000D_
type: Array,_x000D_
value: []_x000D_
},_x000D_
nodeTemplate: {_x000D_
type: Object,_x000D_
}_x000D_
};_x000D_
}_x000D_
_x000D_
ready() {_x000D_
super.ready();_x000D_
if (!this.hasAttribute("embedded")) {_x000D_
let labelTemplate = this.$.labelView.assignedNodes().find((e) => {_x000D_
return e instanceof HTMLTemplateElement;_x000D_
});_x000D_
let nodeTemplate = document.importNode(Polymer.DomModule.import(IntTree.is, "#nodeView"), true);_x000D_
let repeatTemplate = nodeTemplate.content.querySelector("template[is='dom-repeat']");_x000D_
let iconItem = repeatTemplate.content.querySelector('paper-icon-item');_x000D_
iconItem.appendChild(labelTemplate.content);_x000D_
this.nodeTemplate = nodeTemplate;_x000D_
}_x000D_
let nodeInstance = this._stampTemplate(this.nodeTemplate);_x000D_
this.shadowRoot.appendChild(nodeInstance);_x000D_
}_x000D_
_x000D_
hasNodes(node) {_x000D_
return node.nodes != null && node.nodes.length > 0;_x000D_
}_x000D_
_x000D_
nodeSelected(e) {_x000D_
let collapse = e.currentTarget.parentNode.querySelector("iron-collapse");_x000D_
let nodeIcon = e.currentTarget.parentNode.querySelector("iron-icon");_x000D_
if (collapse && nodeIcon) {_x000D_
collapse.toggle();_x000D_
if (collapse.opened) {_x000D_
nodeIcon.icon = "expand-more";_x000D_
} else {_x000D_
nodeIcon.icon = "expand-less";_x000D_
}_x000D_
}_x000D_
}_x000D_
}_x000D_
_x000D_
window.customElements.define(IntTree.is, IntTree);_x000D_
</script>_x000D_
</dom-module>
_x000D_
_x000D_
_x000D_
Usage:
<intworkspace-tree tree="{{testTree}}">_x000D_
<template preserve-content><paper-item-body>[[node.name]]</paper-item-body></template>_x000D_
</intworkspace-tree>
_x000D_
_x000D_
_x000D_
Answered By: Anonymous
Disclaimer: This content is shared under creative common license cc-by-sa 3.0. It is generated from StackExchange Website Network.