@polymer/polymer
Version:
The Polymer library makes it easy to create your own web components. Give your element some markup and properties, and then use it on a site. Polymer provides features like dynamic templates and data binding to reduce the amount of boilerplate you need to
702 lines (670 loc) • 27.1 kB
JavaScript
/**
@license
Copyright (c) 2017 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
*/
/**
* 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 = Templatize.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.
*
* // Customize property forwarding and event model decoration
* let TemplateClass = Templatize.templatize(template, this, {
* parentModel: true,
* forwardHostProp(property, value) {...},
* instanceProps: {...},
* notifyInstanceProp(instance, property, value) {...},
* });
*
* @summary Module for preparing and stamping instances of templates
* utilizing Polymer templating features.
*/
import './boot.js';
import { PropertyEffects } from '../mixins/property-effects.js';
import { MutableData } from '../mixins/mutable-data.js';
import { strictTemplatePolicy, legacyWarnings } from './settings.js';
import { wrap } from './wrap.js';
// 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;
/**
* @constructor
* @extends {HTMLTemplateElement}
* @private
*/
function HTMLTemplateElementExtension() { return newInstance; }
HTMLTemplateElementExtension.prototype = Object.create(HTMLTemplateElement.prototype, {
constructor: {
value: HTMLTemplateElementExtension,
writable: true
}
});
/**
* @constructor
* @implements {Polymer_PropertyEffects}
* @extends {HTMLTemplateElementExtension}
* @private
*/
const DataTemplate = PropertyEffects(HTMLTemplateElementExtension);
/**
* @constructor
* @implements {Polymer_MutableData}
* @extends {DataTemplate}
* @private
*/
const MutableDataTemplate = MutableData(DataTemplate);
// 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.
* @constructor
* @extends {HTMLElement}
* @implements {Polymer_PropertyEffects}
* @private
*/
const templateInstanceBase = PropertyEffects(class {});
export function showHideChildren(hide, children) {
for (let i=0; i<children.length; i++) {
let n = children[i];
// Ignore non-changes
if (Boolean(hide) != Boolean(n.__hideTemplateChildren__)) {
// clear and restore text
if (n.nodeType === Node.TEXT_NODE) {
if (hide) {
n.__polymerTextContent__ = n.textContent;
n.textContent = '';
} else {
n.textContent = n.__polymerTextContent__;
}
// remove and replace slot
} else if (n.localName === 'slot') {
if (hide) {
n.__polymerReplaced__ = document.createComment('hidden-slot');
wrap(wrap(n).parentNode).replaceChild(n.__polymerReplaced__, n);
} else {
const replace = n.__polymerReplaced__;
if (replace) {
wrap(wrap(replace).parentNode).replaceChild(n, replace);
}
}
}
// hide and show nodes
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);
}
}
}
/**
* @polymer
* @customElement
* @appliesMixin PropertyEffects
* @unrestricted
*/
class TemplateInstanceBase extends templateInstanceBase {
constructor(props) {
super();
this._configureProperties(props);
/** @type {!StampedTemplate} */
this.root = this._stampTemplate(this.__dataHost);
// Save list of stamped children
let children = [];
/** @suppress {invalidCasts} */
this.children = /** @type {!NodeList} */ (children);
// Polymer 1.x did not use `Polymer.dom` here so not bothering.
for (let n = this.root.firstChild; n; n=n.nextSibling) {
children.push(n);
n.__templatizeInstance = this;
}
if (this.__templatizeOwner &&
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._enableProperties();
}
}
/**
* Configure the given `props` by calling `_setPendingProperty`. Also
* sets any properties stored in `__hostProps`.
* @private
* @param {Object} props Object of property name-value pairs to set.
* @return {void}
*/
_configureProperties(props) {
let options = this.__templatizeOptions;
if (options.forwardHostProp) {
for (let hprop in this.__hostProps) {
this._setPendingProperty(hprop, this.__dataHost['_host_' + hprop]);
}
}
// Any instance props passed in the constructor will overwrite host props;
// normally this would be a user error but we don't specifically filter them
for (let iprop in props) {
this._setPendingProperty(iprop, props[iprop]);
}
}
/**
* Forwards a host property to this instance. This method should be
* called on instances from the `options.forwardHostProp` callback
* 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
* @return {void}
*/
forwardHostProp(prop, value) {
if (this._setPendingPropertyOrPath(prop, value, false, true)) {
this.__dataHost._enqueueClient(this);
}
}
/**
* Override point for adding custom or simulated event handling.
*
* @override
* @param {!Node} node Node to add event listener to
* @param {string} eventName Name of event
* @param {function(!Event):void} handler Listener function to add
* @return {void}
*/
_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);
}
}
}
/**
* Shows or hides the template instance top level child elements. For
* text nodes, `textContent` is removed while "hidden" and replaced when
* "shown."
* @param {boolean} hide Set to true to hide the children;
* set to false to show them.
* @return {void}
* @protected
*/
_showHideChildren(hide) {
showHideChildren(hide, this.children);
}
/**
* Overrides default property-effects implementation to intercept
* textContent bindings while children are "hidden" and cache in
* private storage for later retrieval.
*
* @override
* @param {!Node} node The node to set a property on
* @param {string} prop The property to set
* @param {*} value The value to set
* @return {void}
* @protected
*/
_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_PropertyEffects} 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;
}
/**
* Stub of HTMLElement's `dispatchEvent`, so that effects that may
* dispatch events safely no-op.
*
* @param {Event} event Event to dispatch
* @return {boolean} Always true.
* @override
*/
dispatchEvent(event) { // eslint-disable-line no-unused-vars
return true;
}
}
/** @type {!DataTemplate} */
TemplateInstanceBase.prototype.__dataHost;
/** @type {!TemplatizeOptions} */
TemplateInstanceBase.prototype.__templatizeOptions;
/** @type {!Polymer_PropertyEffects} */
TemplateInstanceBase.prototype._methodHost;
/** @type {!Object} */
TemplateInstanceBase.prototype.__templatizeOwner;
/** @type {!Object} */
TemplateInstanceBase.prototype.__hostProps;
/**
* @constructor
* @extends {TemplateInstanceBase}
* @implements {Polymer_MutableData}
* @private
*/
const MutableTemplateInstanceBase = MutableData(
// This cast shouldn't be neccessary, but Closure doesn't understand that
// TemplateInstanceBase is a constructor function.
/** @type {function(new:TemplateInstanceBase)} */ (TemplateInstanceBase));
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;
}
/* eslint-disable valid-jsdoc */
/**
* @suppress {missingProperties} class.prototype is not defined for some reason
*/
function createTemplatizerClass(template, templateInfo, options) {
/**
* @constructor
* @extends {TemplateInstanceBase}
*/
let templatizerBase = options.mutableData ?
MutableTemplateInstanceBase : TemplateInstanceBase;
// Affordance for global mixins onto TemplatizeInstance
if (templatize.mixin) {
templatizerBase = templatize.mixin(templatizerBase);
}
/**
* Anonymous class created by the templatize
* @constructor
* @private
*/
let klass = class extends templatizerBase { };
/** @override */
klass.prototype.__templatizeOptions = options;
klass.prototype._bindTemplate(template);
addNotifyEffects(klass, template, templateInfo, options);
return klass;
}
/**
* Adds propagate effects from the template to the template instance for
* properties that the host binds to the template using the `_host_` prefix.
*
* @suppress {missingProperties} class.prototype is not defined for some reason
*/
function addPropagateEffects(target, templateInfo, options, methodHost) {
let userForwardHostProp = options.forwardHostProp;
if (userForwardHostProp && templateInfo.hasHostProps) {
// Under the `removeNestedTemplates` optimization, a custom element like
// `dom-if` or `dom-repeat` can itself be treated as the "template"; this
// flag is used to switch between upgrading a `<template>` to be a property
// effects client vs. adding the effects directly to the custom element
const isTemplate = target.localName == 'template';
// Provide data API and property effects on memoized template class
let klass = templateInfo.templatizeTemplateClass;
if (!klass) {
if (isTemplate) {
/**
* @constructor
* @extends {DataTemplate}
*/
let templatizedBase =
options.mutableData ? MutableDataTemplate : DataTemplate;
// NOTE: due to https://github.com/google/closure-compiler/issues/2928,
// combining the next two lines into one assignment causes a spurious
// type error.
/** @private */
class TemplatizedTemplate extends templatizedBase {}
klass = templateInfo.templatizeTemplateClass = TemplatizedTemplate;
} else {
/**
* @constructor
* @extends {PolymerElement}
*/
const templatizedBase = target.constructor;
// Create a cached subclass of the base custom element class onto which
// to put the template-specific propagate effects
// NOTE: due to https://github.com/google/closure-compiler/issues/2928,
// combining the next two lines into one assignment causes a spurious
// type error.
/** @private */
class TemplatizedTemplateExtension extends templatizedBase {}
klass = templateInfo.templatizeTemplateClass =
TemplatizedTemplateExtension;
}
// Add template - >instances effects
// and host <- template effects
let hostProps = templateInfo.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);
}
if (legacyWarnings && methodHost) {
warnOnUndeclaredProperties(templateInfo, options, methodHost);
}
}
// Mix any pre-bound data into __data; no need to flush this to
// instances since they pull from the template at instance-time
if (target.__dataProto) {
// Note, generally `__dataProto` could be chained, but it's guaranteed
// to not be since this is a vanilla template we just added effects to
Object.assign(target.__data, target.__dataProto);
}
if (isTemplate) {
upgradeTemplate(target, klass);
// Clear any pending data for performance
target.__dataTemp = {};
target.__dataPending = null;
target.__dataOld = null;
target._enableProperties();
} else {
// Swizzle the cached subclass prototype onto the custom element
Object.setPrototypeOf(target, klass.prototype);
// Check for any pre-bound instance host properties, and do the
// instance property delete/assign dance for those (directly into data;
// not need to go through accessor since they are pulled at instance time)
const hostProps = templateInfo.hostProps;
for (let prop in hostProps) {
prop = '_host_' + prop;
if (prop in target) {
const val = target[prop];
delete target[prop];
target.__data[prop] = val;
}
}
}
}
}
/* eslint-enable valid-jsdoc */
function createForwardHostPropEffect(hostProp, userForwardHostProp) {
return function forwardHostProp(template, prop, props) {
userForwardHostProp.call(template.__templatizeOwner,
prop.substring('_host_'.length), props[prop]);
};
}
function addNotifyEffects(klass, template, templateInfo, options) {
let hostProps = templateInfo.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) {
// As we're iterating hostProps in this function, note whether
// there were any, for an optimization in addPropagateEffects
if (!templateInfo.hasHostProps) {
templateInfo.hasHostProps = true;
}
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);
};
}
/**
* Returns an anonymous `PropertyEffects` class bound to the
* `<template>` provided. Instancing the class will result in the
* template being stamped into a 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 `notifyInstanceProp` being called.
* - `mutableData`: When `true`, the generated class will skip strict
* dirty-checking for objects and arrays (always consider them to be
* "dirty").
* - `notifyInstanceProp(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.
*
* All callbacks are called bound to the `owner`. Any context
* needed for the callbacks (such as references to `instances` stamped)
* should be stored on the `owner` such that they can be retrieved via
* `this`.
*
* When `options.forwardHostProp` is declared as an option, any properties
* referenced in the template will be automatically forwarded from the host of
* the `<template>` to instances, with the exception of any properties listed in
* the `options.instanceProps` object. `instanceProps` are assumed to be
* managed by the owner of the instances, either passed into the constructor
* or set after the fact. Note, any properties passed into the constructor will
* always be set to the instance (regardless of whether they would normally
* be forwarded from the host).
*
* Note that `templatize()` can be run only once for a given `<template>`.
* Further calls will result in an error. Also, there is a special
* behavior if the template was duplicated through a mechanism such as
* `<dom-repeat>` or `<test-fixture>`. In this case, all calls to
* `templatize()` return the same class for all duplicates of a template.
* The class returned from `templatize()` is generated only once using
* the `options` from the first call. This means that any `options`
* provided to subsequent calls will be ignored. Therefore, it is very
* important not to close over any variables inside the callbacks. Also,
* arrow functions must be avoided because they bind the outer `this`.
* Inside the callbacks, any contextual information can be accessed
* through `this`, which points to the `owner`.
*
* @param {!HTMLTemplateElement} template Template to templatize
* @param {Polymer_PropertyEffects=} owner Owner of the template instances;
* any optional callbacks will be bound to this owner.
* @param {Object=} options Options dictionary (see summary for details)
* @return {function(new:TemplateInstanceBase, Object=)} Generated class bound
* to the template provided
* @suppress {invalidCasts}
*/
export function templatize(template, owner, options) {
// Under strictTemplatePolicy, the templatized element must be owned
// by a (trusted) Polymer element, indicated by existence of _methodHost;
// e.g. for dom-if & dom-repeat in main document, _methodHost is null
if (strictTemplatePolicy && !findMethodHost(template)) {
throw new Error('strictTemplatePolicy: template owner not trusted');
}
options = /** @type {!TemplatizeOptions} */(options || {});
if (template.__templatizeOwner) {
throw new Error('A <template> can only be templatized once');
}
template.__templatizeOwner = owner;
const ctor = owner ? owner.constructor : TemplateInstanceBase;
let templateInfo = ctor._parseTemplate(template);
// Get memoized base class for the prototypical template, which
// includes property effects for binding template & forwarding
/**
* @constructor
* @extends {TemplateInstanceBase}
*/
let baseClass = templateInfo.templatizeInstanceClass;
if (!baseClass) {
baseClass = createTemplatizerClass(template, templateInfo, options);
templateInfo.templatizeInstanceClass = baseClass;
}
const methodHost = findMethodHost(template);
// Host property forwarding must be installed onto template instance
addPropagateEffects(template, templateInfo, options, methodHost);
// Subclass base class and add reference for this specific template
/** @private */
let klass = class TemplateInstance extends baseClass {};
/** @override */
klass.prototype._methodHost = methodHost;
/** @override */
klass.prototype.__dataHost = /** @type {!DataTemplate} */ (template);
/** @override */
klass.prototype.__templatizeOwner = /** @type {!Object} */ (owner);
/** @override */
klass.prototype.__hostProps = templateInfo.hostProps;
klass = /** @type {function(new:TemplateInstanceBase)} */(klass); //eslint-disable-line no-self-assign
return klass;
}
function warnOnUndeclaredProperties(templateInfo, options, methodHost) {
const declaredProps = methodHost.constructor._properties;
const {propertyEffects} = templateInfo;
const {instanceProps} = options;
for (let prop in propertyEffects) {
// Ensure properties with template effects are declared on the outermost
// host (`methodHost`), unless they are instance props or static functions
if (!declaredProps[prop] && !(instanceProps && instanceProps[prop])) {
const effects = propertyEffects[prop];
for (let i=0; i<effects.length; i++) {
const {part} = effects[i].info;
if (!(part.signature && part.signature.static)) {
console.warn(`Property '${prop}' used in template but not ` +
`declared in 'properties'; attribute will not be observed.`);
break;
}
}
}
}
}
/**
* 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
* `TemplateInstanceBase`, 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);
* }
*
* @param {HTMLElement} template The model will be returned for
* elements stamped from this template (accepts either an HTMLTemplateElement)
* or a `<dom-if>`/`<dom-repeat>` element when using `removeNestedTemplates`
* optimization.
* @param {Node=} node Node for which to return a template model.
* @return {TemplateInstanceBase} Template instance representing the
* binding scope for the element
*/
export function modelForElement(template, node) {
let model;
while (node) {
// 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 = node.__dataHost ? node : node.__templatizeInstance)) {
// Found an element stamped by another template; keep walking up
// from its __dataHost
if (model.__dataHost != template) {
node = model.__dataHost;
} else {
return model;
}
} else {
// Still in a template scope, keep going up until
// a __templatizeInstance is found
node = wrap(node).parentNode;
}
}
return null;
}
export { TemplateInstanceBase };