Files
polymer/lib/utils/templatize.html
Daniel Freedman 155e3ffc4e move src -> lib
2017-02-28 10:37:04 -08:00

447 lines
18 KiB
HTML

<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="boot.html">
<link rel="import" href="../mixins/property-effects.html">
<script>
(function() {
'use strict';
// Base class for HTMLTemplateElement extension that has property effects
// machinery for propagating host properties to children. This is an ES5
// class only because Babel (incorrectly) requires super() in the class
// constructor even though no `this` is used and it returns an instance.
let newInstance = null;
function HTMLTemplateElementExtension() { return newInstance; }
HTMLTemplateElementExtension.prototype = Object.create(HTMLTemplateElement.prototype, {
constructor: {
value: HTMLTemplateElementExtension,
writable: true
}
});
const DataTemplate = Polymer.PropertyEffects(HTMLTemplateElementExtension);
// Applies a DataTemplate subclass to a <template> instance
function upgradeTemplate(template, constructor) {
newInstance = template;
Object.setPrototypeOf(template, constructor.prototype);
new constructor();
newInstance = null;
}
// Base class for TemplateInstance's
class TemplateInstanceBase extends Polymer.PropertyEffects(class{}) {
constructor(props) {
super();
this._configureProperties(props);
this.root = this._stampTemplate(this.__dataHost);
// Save list of stamped children
let children = this.children = [];
for (let n = this.root.firstChild; n; n=n.nextSibling) {
children.push(n);
n.__templatizeInstance = this;
}
if (this.__templatizeOwner.__hideTemplateChildren__) {
this._showHideChildren(true);
}
// Flush props only when props are passed if instance props exist
// or when there isn't instance props.
let options = this.__templatizeOptions;
if ((props && options.instanceProps) || !options.instanceProps) {
this._flushProperties();
}
}
/**
* @private
*/
_configureProperties(props) {
let options = this.__templatizeOptions;
if (props) {
for (let iprop in options.instanceProps) {
if (iprop in props) {
this._setPendingProperty(iprop, props[iprop]);
}
}
}
for (let hprop in this.__hostProps) {
this._setPendingProperty(hprop, this.__dataHost['_host_' + hprop]);
}
}
/**
* Forwards a host property to this instance. This method should be
* called on instances from the `options.forwardHostProp` to propagate
* changes of host properties to each instance.
*
* Note this method enqueues the change, which are flushed as a batch.
*
* @param {string} prop Property or path name
* @param {*} value Value of the property to forward
*/
forwardHostProp(prop, value) {
if (this._setPendingPropertyOrPath(prop, value, false, true)) {
this.__dataHost._enqueueClient(this);
}
}
/**
* @override
*/
_addEventListenerToNode(node, eventName, handler) {
if (this._methodHost && this.__templatizeOptions.parentModel) {
// If this instance should be considered a parent model, decorate
// events this template instance as `model`
this._methodHost._addEventListenerToNode(node, eventName, (e) => {
e.model = this;
handler(e);
});
} else {
// Otherwise delegate to the template's host (which could be)
// another template instance
let templateHost = this.__dataHost.__dataHost;
if (templateHost) {
templateHost._addEventListenerToNode(node, eventName, handler);
}
}
}
/**
* @protected
*/
_showHideChildren(hide) {
let c = this.children;
for (let i=0; i<c.length; i++) {
let n = c[i];
// Ignore non-changes
if (Boolean(hide) != Boolean(n.__hideTemplateChildren__)) {
if (n.nodeType === Node.TEXT_NODE) {
if (hide) {
n.__polymerTextContent__ = n.textContent;
n.textContent = '';
} else {
n.textContent = n.__polymerTextContent__;
}
} else if (n.style) {
if (hide) {
n.__polymerDisplay__ = n.style.display;
n.style.display = 'none';
} else {
n.style.display = n.__polymerDisplay__;
}
}
}
n.__hideTemplateChildren__ = hide;
if (n._showHideChildren) {
n._showHideChildren(hide);
}
}
}
/**
* Overrides default property-effects implementation to intercept
* textContent bindings while children are "hidden" and cache in
* private storage for later retrieval.
*
* @override
*/
_setUnmanagedPropertyToNode(node, prop, value) {
if (node.__hideTemplateChildren__ &&
node.nodeType == Node.TEXT_NODE && prop == 'textContent') {
node.__polymerTextContent__ = value;
} else {
super._setUnmanagedPropertyToNode(node, prop, value);
}
}
/**
* Find the parent model of this template instance. The parent model
* is either another templatize instance that had option `parentModel: true`,
* or else the host element.
*
* @return {Polymer.PropertyEffectsInterface} The parent model of this instance
*/
get parentModel() {
let model = this.__parentModel;
if (!model) {
let options;
model = this
do {
// A template instance's `__dataHost` is a <template>
// `model.__dataHost.__dataHost` is the template's host
model = model.__dataHost.__dataHost;
} while ((options = model.__templatizeOptions) && !options.parentModel)
this.__parentModel = model;
}
return model;
}
}
function findMethodHost(template) {
// Technically this should be the owner of the outermost template.
// In shadow dom, this is always getRootNode().host, but we can
// approximate this via cooperation with our dataHost always setting
// `_methodHost` as long as there were bindings (or id's) on this
// instance causing it to get a dataHost.
let templateHost = template.__dataHost;
return templateHost && templateHost._methodHost || templateHost;
}
function createTemplatizerClass(template, options) {
// Anonymous class created by the templatize
/**
* @unrestricted
*/
let klass = class extends TemplateInstanceBase { }
klass.prototype.__templatizeOptions = options;
klass.prototype._bindTemplate(template);
addNotifyEffects(klass, template, options);
return klass;
}
function addPropagateEffects(template, options) {
let userForwardHostProp = options.forwardHostProp;
if (userForwardHostProp) {
// Provide data API and property effects on memoized template class
let klass = template._content.__templatizeTemplateClass;
if (!klass) {
klass = template._content.__templatizeTemplateClass =
class TemplatizedTemplate extends DataTemplate {}
// Add template - >instances effects
// and host <- template effects
let hostProps = template._content._hostProps;
for (let prop in hostProps) {
klass.prototype._addPropertyEffect('_host_' + prop,
klass.prototype.PROPERTY_EFFECT_TYPES.PROPAGATE,
{fn: createForwardHostPropEffect(prop, userForwardHostProp)});
klass.prototype._createNotifyingProperty('_host_' + prop);
}
}
upgradeTemplate(template, klass);
// Mix any pre-bound data into __data; no need to flush this to
// instances since they pull from the template at instance-time
if (template.__dataProto) {
Polymer.mixin(template.__data, template.__dataProto);
}
// Clear any pending data for performance
template.__dataTemp = {};
template.__dataPending = null;
template.__dataOld = null;
template._flushProperties();
}
}
function createForwardHostPropEffect(hostProp, userForwardHostProp) {
return function forwardHostProp(template, prop, props) {
userForwardHostProp.call(template.__templatizeOwner,
prop.substring('_host_'.length), props[prop]);
}
}
function addNotifyEffects(klass, template, options) {
let hostProps = template._content._hostProps || {};
for (let iprop in options.instanceProps) {
delete hostProps[iprop];
let userNotifyInstanceProp = options.notifyInstanceProp;
if (userNotifyInstanceProp) {
klass.prototype._addPropertyEffect(iprop,
klass.prototype.PROPERTY_EFFECT_TYPES.NOTIFY,
{fn: createNotifyInstancePropEffect(iprop, userNotifyInstanceProp)});
}
}
if (options.forwardHostProp && template.__dataHost) {
for (let hprop in hostProps) {
klass.prototype._addPropertyEffect(hprop,
klass.prototype.PROPERTY_EFFECT_TYPES.NOTIFY,
{fn: createNotifyHostPropEffect()})
}
}
}
function createNotifyInstancePropEffect(instProp, userNotifyInstanceProp) {
return function notifyInstanceProp(inst, prop, props) {
userNotifyInstanceProp.call(inst.__templatizeOwner,
inst, prop, props[prop]);
}
}
function createNotifyHostPropEffect() {
return function notifyHostProp(inst, prop, props) {
inst.__dataHost._setPendingPropertyOrPath('_host_' + prop, props[prop], true, true);
}
}
/**
* Module for preparing and stamping instances of templates that utilize
* Polymer's data-binding and declarative event listener features.
*
* Example:
*
* // Get a template from somewhere, e.g. light DOM
* let template = this.querySelector('template');
* // Prepare the template
* let TemplateClass = Polymer.Tempaltize.templatize(template);
* // Instance the template with an initial data model
* let instance = new TemplateClass({myProp: 'initial'});
* // Insert the instance's DOM somewhere, e.g. element's shadow DOM
* this.shadowRoot.appendChild(instance.root);
* // Changing a property on the instance will propagate to bindings
* // in the template
* instance.myProp = 'new value';
*
* The `options` dictionary passed to `templatize` allows for customizing
* features of the generated template class, including how outer-scope host
* properties should be forwarded into template instances, how any instance
* properties added into the template's scope should be notified out to
* the host, and whether the instance should be decorated as a "parent model"
* of any event handlers.
*
* // Customze property forwarding and event model decoration
* let TemplateClass = Polymer.Tempaltize.templatize(template, this, {
* parentModel: true,
* instanceProps: {...},
* forwardHostProp(property, value) {...},
* notifyInstanceProp(instance, property, value) {...},
* });
*
*
* @namespace
* @memberof Polymer
* @summary Module for preparing and stamping instances of templates
* utilizing Polymer templating features.
*/
const Templatize = {
/**
* Returns an anonymous `Polymer.PropertyEffects` class bound to the
* `<template>` provided. Instancing the class will result in the
* template being stamped into document fragment stored as the instance's
* `root` property, after which it can be appended to the DOM.
*
* Templates may utilize all Polymer data-binding features as well as
* declarative event listeners. Event listeners and inline computing
* functions in the template will be called on the host of the template.
*
* The constructor returned takes a single argument dictionary of initial
* property values to propagate into template bindings. Additionally
* host properties can be forwarded in, and instance properties can be
* notified out by providing optional callbacks in the `options` dictionary.
*
* Valid configuration in `options` are as follows:
*
* - `forwardHostProp(property, value)`: Called when a property referenced
* in the template changed on the template's host. As this library does
* not retain references to templates instanced by the user, it is the
* templatize owner's responsibility to forward host property changes into
* user-stamped instances. The `instance.forwardHostProp(property, value)`
* method on the generated class should be called to forward host
* properties into the template to prevent unnecessary property-changed
* notifications. Any properties referenced in the template that are not
* defined in `instanceProps` will be notified up to the template's host
* automatically.
* - `instanceProps`: Dictionary of property names that will be added
* to the instance by the templatize owner. These properties shadow any
* host properties, and changes within the template to these properties
* will result in `notifyInstanceProperties` to be called.
* - `notifyInstanceProperties(instance, property, value)`: Called when
* an instance property changes. Users may choose to call `notifyPath`
* on e.g. the owner to notify the change.
* - `parentModel`: When `true`, events handled by declarative event listeners
* (`on-event="handler"`) will be decorated with a `model` property pointing
* to the template instance that stamped it. It will also be returned
* from `instance.parentModel` in cases where template instance nesting
* causes an inner model to shadow an outer model.
*
* Note that the class returned from `templatize` is generated only once
* for a given `<template>` using `options` from the first call for that
* template, and the cached class is returned for all subsequent calls to
* `templatize` for that template. As such, `options` callbacks should not
* close over owner-specific properties since only the first `options` is
* used; rather, callbacks are called bound to the `owner`, and so context
* needed from the callbacks (such as references to `instances` stamped)
* should be stored on the `owner` such that they can be retrieved via `this`.
*
* @memberof Polymer.Templatize
* @param {HTMLTemplateElement} template Template to templatize
* @param {*} owner Owner of the template instances; any optional callbacks
* will be bound to this owner.
* @param {*} options Options dictionary (see summary for details)
* @return {TemplateInstanceBase} Generated class bound to the template
* provided
*/
templatize(template, owner, options) {
if (template.__templatizeOwner) {
throw new Error('A <template> can only be templatized once');
}
template.__templatizeOwner = owner;
// Ensure template has _content
template._content = template._content || template.content;
// Get memoized base class for the prototypical template, which
// includes property effects for binding template & forwarding
let baseClass = template._content.__templatizeInstanceClass;
if (!baseClass) {
baseClass = template._content.__templatizeInstanceClass =
createTemplatizerClass(template, options);
}
// Host property forwarding must be installed onto template instance
addPropagateEffects(template, options);
// Subclass base class and add reference for this specific template
let klass = class TemplateInstance extends baseClass {};
klass.prototype._methodHost = findMethodHost(template);
klass.prototype.__dataHost = template;
klass.prototype.__templatizeOwner = owner;
klass.prototype.__hostProps = template._content._hostProps;
return klass;
},
/**
* Returns the template "model" associated with a given element, which
* serves as the binding scope for the template instance the element is
* contained in. A template model is an instance of `Polymer.Base`, and
* should be used to manipulate data associated with this template instance.
*
* Example:
*
* let model = modelForElement(el);
* if (model.index < 10) {
* model.set('item.checked', true);
* }
*
* @memberof Polymer.Templatize
* @method modelForElement
* @param {HTMLElement} el Element for which to return a template model.
* @return {TemplateInstanceBase} Template instance representing the
* binding scope for the element
*/
modelForElement(host, el) {
let model;
while (el) {
// An element with a __templatizeInstance marks the top boundary
// of a scope; walk up until we find one, and then ensure that
// its __dataHost matches `this`, meaning this dom-repeat stamped it
if ((model = el.__templatizeInstance)) {
// Found an element stamped by another template; keep walking up
// from its __dataHost
if (model.__dataHost != host) {
el = model.__dataHost;
} else {
return model;
}
} else {
// Still in a template scope, keep going up until
// a __templatizeInstance is found
el = el.parentNode;
}
}
return null;
}
}
Polymer.Templatize = Templatize;
})();
</script>