1 | /**
|
2 | @license
|
3 | Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
4 | This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
5 | The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
6 | The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
7 | Code distributed by Google as part of the polymer project is also
|
8 | subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
9 | */
|
10 |
|
11 | /**
|
12 | * Module for preparing and stamping instances of templates that utilize
|
13 | * Polymer's data-binding and declarative event listener features.
|
14 | *
|
15 | * Example:
|
16 | *
|
17 | * // Get a template from somewhere, e.g. light DOM
|
18 | * let template = this.querySelector('template');
|
19 | * // Prepare the template
|
20 | * let TemplateClass = Templatize.templatize(template);
|
21 | * // Instance the template with an initial data model
|
22 | * let instance = new TemplateClass({myProp: 'initial'});
|
23 | * // Insert the instance's DOM somewhere, e.g. element's shadow DOM
|
24 | * this.shadowRoot.appendChild(instance.root);
|
25 | * // Changing a property on the instance will propagate to bindings
|
26 | * // in the template
|
27 | * instance.myProp = 'new value';
|
28 | *
|
29 | * The `options` dictionary passed to `templatize` allows for customizing
|
30 | * features of the generated template class, including how outer-scope host
|
31 | * properties should be forwarded into template instances, how any instance
|
32 | * properties added into the template's scope should be notified out to
|
33 | * the host, and whether the instance should be decorated as a "parent model"
|
34 | * of any event handlers.
|
35 | *
|
36 | * // Customize property forwarding and event model decoration
|
37 | * let TemplateClass = Templatize.templatize(template, this, {
|
38 | * parentModel: true,
|
39 | * forwardHostProp(property, value) {...},
|
40 | * instanceProps: {...},
|
41 | * notifyInstanceProp(instance, property, value) {...},
|
42 | * });
|
43 | *
|
44 | * @summary Module for preparing and stamping instances of templates
|
45 | * utilizing Polymer templating features.
|
46 | */
|
47 |
|
48 | import './boot.js';
|
49 |
|
50 | import { PropertyEffects } from '../mixins/property-effects.js';
|
51 | import { MutableData } from '../mixins/mutable-data.js';
|
52 | import { strictTemplatePolicy, legacyWarnings } from './settings.js';
|
53 | import { wrap } from './wrap.js';
|
54 |
|
55 | // Base class for HTMLTemplateElement extension that has property effects
|
56 | // machinery for propagating host properties to children. This is an ES5
|
57 | // class only because Babel (incorrectly) requires super() in the class
|
58 | // constructor even though no `this` is used and it returns an instance.
|
59 | let newInstance = null;
|
60 |
|
61 | /**
|
62 | * @constructor
|
63 | * @extends {HTMLTemplateElement}
|
64 | * @private
|
65 | */
|
66 | function HTMLTemplateElementExtension() { return newInstance; }
|
67 | HTMLTemplateElementExtension.prototype = Object.create(HTMLTemplateElement.prototype, {
|
68 | constructor: {
|
69 | value: HTMLTemplateElementExtension,
|
70 | writable: true
|
71 | }
|
72 | });
|
73 |
|
74 | /**
|
75 | * @constructor
|
76 | * @implements {Polymer_PropertyEffects}
|
77 | * @extends {HTMLTemplateElementExtension}
|
78 | * @private
|
79 | */
|
80 | const DataTemplate = PropertyEffects(HTMLTemplateElementExtension);
|
81 |
|
82 | /**
|
83 | * @constructor
|
84 | * @implements {Polymer_MutableData}
|
85 | * @extends {DataTemplate}
|
86 | * @private
|
87 | */
|
88 | const MutableDataTemplate = MutableData(DataTemplate);
|
89 |
|
90 | // Applies a DataTemplate subclass to a <template> instance
|
91 | function upgradeTemplate(template, constructor) {
|
92 | newInstance = template;
|
93 | Object.setPrototypeOf(template, constructor.prototype);
|
94 | new constructor();
|
95 | newInstance = null;
|
96 | }
|
97 |
|
98 | /**
|
99 | * Base class for TemplateInstance.
|
100 | * @constructor
|
101 | * @extends {HTMLElement}
|
102 | * @implements {Polymer_PropertyEffects}
|
103 | * @private
|
104 | */
|
105 | const templateInstanceBase = PropertyEffects(class {});
|
106 |
|
107 | export function showHideChildren(hide, children) {
|
108 | for (let i=0; i<children.length; i++) {
|
109 | let n = children[i];
|
110 | // Ignore non-changes
|
111 | if (Boolean(hide) != Boolean(n.__hideTemplateChildren__)) {
|
112 | // clear and restore text
|
113 | if (n.nodeType === Node.TEXT_NODE) {
|
114 | if (hide) {
|
115 | n.__polymerTextContent__ = n.textContent;
|
116 | n.textContent = '';
|
117 | } else {
|
118 | n.textContent = n.__polymerTextContent__;
|
119 | }
|
120 | // remove and replace slot
|
121 | } else if (n.localName === 'slot') {
|
122 | if (hide) {
|
123 | n.__polymerReplaced__ = document.createComment('hidden-slot');
|
124 | wrap(wrap(n).parentNode).replaceChild(n.__polymerReplaced__, n);
|
125 | } else {
|
126 | const replace = n.__polymerReplaced__;
|
127 | if (replace) {
|
128 | wrap(wrap(replace).parentNode).replaceChild(n, replace);
|
129 | }
|
130 | }
|
131 | }
|
132 | // hide and show nodes
|
133 | else if (n.style) {
|
134 | if (hide) {
|
135 | n.__polymerDisplay__ = n.style.display;
|
136 | n.style.display = 'none';
|
137 | } else {
|
138 | n.style.display = n.__polymerDisplay__;
|
139 | }
|
140 | }
|
141 | }
|
142 | n.__hideTemplateChildren__ = hide;
|
143 | if (n._showHideChildren) {
|
144 | n._showHideChildren(hide);
|
145 | }
|
146 | }
|
147 | }
|
148 |
|
149 | /**
|
150 | * @polymer
|
151 | * @customElement
|
152 | * @appliesMixin PropertyEffects
|
153 | * @unrestricted
|
154 | */
|
155 | class TemplateInstanceBase extends templateInstanceBase {
|
156 | constructor(props) {
|
157 | super();
|
158 | this._configureProperties(props);
|
159 | /** @type {!StampedTemplate} */
|
160 | this.root = this._stampTemplate(this.__dataHost);
|
161 | // Save list of stamped children
|
162 | let children = [];
|
163 | /** @suppress {invalidCasts} */
|
164 | this.children = /** @type {!NodeList} */ (children);
|
165 | // Polymer 1.x did not use `Polymer.dom` here so not bothering.
|
166 | for (let n = this.root.firstChild; n; n=n.nextSibling) {
|
167 | children.push(n);
|
168 | n.__templatizeInstance = this;
|
169 | }
|
170 | if (this.__templatizeOwner &&
|
171 | this.__templatizeOwner.__hideTemplateChildren__) {
|
172 | this._showHideChildren(true);
|
173 | }
|
174 | // Flush props only when props are passed if instance props exist
|
175 | // or when there isn't instance props.
|
176 | let options = this.__templatizeOptions;
|
177 | if ((props && options.instanceProps) || !options.instanceProps) {
|
178 | this._enableProperties();
|
179 | }
|
180 | }
|
181 | /**
|
182 | * Configure the given `props` by calling `_setPendingProperty`. Also
|
183 | * sets any properties stored in `__hostProps`.
|
184 | * @private
|
185 | * @param {Object} props Object of property name-value pairs to set.
|
186 | * @return {void}
|
187 | */
|
188 | _configureProperties(props) {
|
189 | let options = this.__templatizeOptions;
|
190 | if (options.forwardHostProp) {
|
191 | for (let hprop in this.__hostProps) {
|
192 | this._setPendingProperty(hprop, this.__dataHost['_host_' + hprop]);
|
193 | }
|
194 | }
|
195 | // Any instance props passed in the constructor will overwrite host props;
|
196 | // normally this would be a user error but we don't specifically filter them
|
197 | for (let iprop in props) {
|
198 | this._setPendingProperty(iprop, props[iprop]);
|
199 | }
|
200 | }
|
201 | /**
|
202 | * Forwards a host property to this instance. This method should be
|
203 | * called on instances from the `options.forwardHostProp` callback
|
204 | * to propagate changes of host properties to each instance.
|
205 | *
|
206 | * Note this method enqueues the change, which are flushed as a batch.
|
207 | *
|
208 | * @param {string} prop Property or path name
|
209 | * @param {*} value Value of the property to forward
|
210 | * @return {void}
|
211 | */
|
212 | forwardHostProp(prop, value) {
|
213 | if (this._setPendingPropertyOrPath(prop, value, false, true)) {
|
214 | this.__dataHost._enqueueClient(this);
|
215 | }
|
216 | }
|
217 |
|
218 | /**
|
219 | * Override point for adding custom or simulated event handling.
|
220 | *
|
221 | * @override
|
222 | * @param {!Node} node Node to add event listener to
|
223 | * @param {string} eventName Name of event
|
224 | * @param {function(!Event):void} handler Listener function to add
|
225 | * @return {void}
|
226 | */
|
227 | _addEventListenerToNode(node, eventName, handler) {
|
228 | if (this._methodHost && this.__templatizeOptions.parentModel) {
|
229 | // If this instance should be considered a parent model, decorate
|
230 | // events this template instance as `model`
|
231 | this._methodHost._addEventListenerToNode(node, eventName, (e) => {
|
232 | e.model = this;
|
233 | handler(e);
|
234 | });
|
235 | } else {
|
236 | // Otherwise delegate to the template's host (which could be)
|
237 | // another template instance
|
238 | let templateHost = this.__dataHost.__dataHost;
|
239 | if (templateHost) {
|
240 | templateHost._addEventListenerToNode(node, eventName, handler);
|
241 | }
|
242 | }
|
243 | }
|
244 | /**
|
245 | * Shows or hides the template instance top level child elements. For
|
246 | * text nodes, `textContent` is removed while "hidden" and replaced when
|
247 | * "shown."
|
248 | * @param {boolean} hide Set to true to hide the children;
|
249 | * set to false to show them.
|
250 | * @return {void}
|
251 | * @protected
|
252 | */
|
253 | _showHideChildren(hide) {
|
254 | showHideChildren(hide, this.children);
|
255 | }
|
256 | /**
|
257 | * Overrides default property-effects implementation to intercept
|
258 | * textContent bindings while children are "hidden" and cache in
|
259 | * private storage for later retrieval.
|
260 | *
|
261 | * @override
|
262 | * @param {!Node} node The node to set a property on
|
263 | * @param {string} prop The property to set
|
264 | * @param {*} value The value to set
|
265 | * @return {void}
|
266 | * @protected
|
267 | */
|
268 | _setUnmanagedPropertyToNode(node, prop, value) {
|
269 | if (node.__hideTemplateChildren__ &&
|
270 | node.nodeType == Node.TEXT_NODE && prop == 'textContent') {
|
271 | node.__polymerTextContent__ = value;
|
272 | } else {
|
273 | super._setUnmanagedPropertyToNode(node, prop, value);
|
274 | }
|
275 | }
|
276 | /**
|
277 | * Find the parent model of this template instance. The parent model
|
278 | * is either another templatize instance that had option `parentModel: true`,
|
279 | * or else the host element.
|
280 | *
|
281 | * @return {!Polymer_PropertyEffects} The parent model of this instance
|
282 | */
|
283 | get parentModel() {
|
284 | let model = this.__parentModel;
|
285 | if (!model) {
|
286 | let options;
|
287 | model = this;
|
288 | do {
|
289 | // A template instance's `__dataHost` is a <template>
|
290 | // `model.__dataHost.__dataHost` is the template's host
|
291 | model = model.__dataHost.__dataHost;
|
292 | } while ((options = model.__templatizeOptions) && !options.parentModel);
|
293 | this.__parentModel = model;
|
294 | }
|
295 | return model;
|
296 | }
|
297 |
|
298 | /**
|
299 | * Stub of HTMLElement's `dispatchEvent`, so that effects that may
|
300 | * dispatch events safely no-op.
|
301 | *
|
302 | * @param {Event} event Event to dispatch
|
303 | * @return {boolean} Always true.
|
304 | * @override
|
305 | */
|
306 | dispatchEvent(event) { // eslint-disable-line no-unused-vars
|
307 | return true;
|
308 | }
|
309 | }
|
310 |
|
311 | /** @type {!DataTemplate} */
|
312 | TemplateInstanceBase.prototype.__dataHost;
|
313 | /** @type {!TemplatizeOptions} */
|
314 | TemplateInstanceBase.prototype.__templatizeOptions;
|
315 | /** @type {!Polymer_PropertyEffects} */
|
316 | TemplateInstanceBase.prototype._methodHost;
|
317 | /** @type {!Object} */
|
318 | TemplateInstanceBase.prototype.__templatizeOwner;
|
319 | /** @type {!Object} */
|
320 | TemplateInstanceBase.prototype.__hostProps;
|
321 |
|
322 | /**
|
323 | * @constructor
|
324 | * @extends {TemplateInstanceBase}
|
325 | * @implements {Polymer_MutableData}
|
326 | * @private
|
327 | */
|
328 | const MutableTemplateInstanceBase = MutableData(
|
329 | // This cast shouldn't be neccessary, but Closure doesn't understand that
|
330 | // TemplateInstanceBase is a constructor function.
|
331 | /** @type {function(new:TemplateInstanceBase)} */ (TemplateInstanceBase));
|
332 |
|
333 | function findMethodHost(template) {
|
334 | // Technically this should be the owner of the outermost template.
|
335 | // In shadow dom, this is always getRootNode().host, but we can
|
336 | // approximate this via cooperation with our dataHost always setting
|
337 | // `_methodHost` as long as there were bindings (or id's) on this
|
338 | // instance causing it to get a dataHost.
|
339 | let templateHost = template.__dataHost;
|
340 | return templateHost && templateHost._methodHost || templateHost;
|
341 | }
|
342 |
|
343 | /* eslint-disable valid-jsdoc */
|
344 | /**
|
345 | * @suppress {missingProperties} class.prototype is not defined for some reason
|
346 | */
|
347 | function createTemplatizerClass(template, templateInfo, options) {
|
348 | /**
|
349 | * @constructor
|
350 | * @extends {TemplateInstanceBase}
|
351 | */
|
352 | let templatizerBase = options.mutableData ?
|
353 | MutableTemplateInstanceBase : TemplateInstanceBase;
|
354 |
|
355 | // Affordance for global mixins onto TemplatizeInstance
|
356 | if (templatize.mixin) {
|
357 | templatizerBase = templatize.mixin(templatizerBase);
|
358 | }
|
359 |
|
360 | /**
|
361 | * Anonymous class created by the templatize
|
362 | * @constructor
|
363 | * @private
|
364 | */
|
365 | let klass = class extends templatizerBase { };
|
366 | /** @override */
|
367 | klass.prototype.__templatizeOptions = options;
|
368 | klass.prototype._bindTemplate(template);
|
369 | addNotifyEffects(klass, template, templateInfo, options);
|
370 | return klass;
|
371 | }
|
372 |
|
373 | /**
|
374 | * Adds propagate effects from the template to the template instance for
|
375 | * properties that the host binds to the template using the `_host_` prefix.
|
376 | *
|
377 | * @suppress {missingProperties} class.prototype is not defined for some reason
|
378 | */
|
379 | function addPropagateEffects(target, templateInfo, options, methodHost) {
|
380 | let userForwardHostProp = options.forwardHostProp;
|
381 | if (userForwardHostProp && templateInfo.hasHostProps) {
|
382 | // Under the `removeNestedTemplates` optimization, a custom element like
|
383 | // `dom-if` or `dom-repeat` can itself be treated as the "template"; this
|
384 | // flag is used to switch between upgrading a `<template>` to be a property
|
385 | // effects client vs. adding the effects directly to the custom element
|
386 | const isTemplate = target.localName == 'template';
|
387 | // Provide data API and property effects on memoized template class
|
388 | let klass = templateInfo.templatizeTemplateClass;
|
389 | if (!klass) {
|
390 | if (isTemplate) {
|
391 | /**
|
392 | * @constructor
|
393 | * @extends {DataTemplate}
|
394 | */
|
395 | let templatizedBase =
|
396 | options.mutableData ? MutableDataTemplate : DataTemplate;
|
397 |
|
398 | // NOTE: due to https://github.com/google/closure-compiler/issues/2928,
|
399 | // combining the next two lines into one assignment causes a spurious
|
400 | // type error.
|
401 | /** @private */
|
402 | class TemplatizedTemplate extends templatizedBase {}
|
403 | klass = templateInfo.templatizeTemplateClass = TemplatizedTemplate;
|
404 | } else {
|
405 | /**
|
406 | * @constructor
|
407 | * @extends {PolymerElement}
|
408 | */
|
409 | const templatizedBase = target.constructor;
|
410 |
|
411 | // Create a cached subclass of the base custom element class onto which
|
412 | // to put the template-specific propagate effects
|
413 | // NOTE: due to https://github.com/google/closure-compiler/issues/2928,
|
414 | // combining the next two lines into one assignment causes a spurious
|
415 | // type error.
|
416 | /** @private */
|
417 | class TemplatizedTemplateExtension extends templatizedBase {}
|
418 | klass = templateInfo.templatizeTemplateClass =
|
419 | TemplatizedTemplateExtension;
|
420 | }
|
421 | // Add template - >instances effects
|
422 | // and host <- template effects
|
423 | let hostProps = templateInfo.hostProps;
|
424 | for (let prop in hostProps) {
|
425 | klass.prototype._addPropertyEffect('_host_' + prop,
|
426 | klass.prototype.PROPERTY_EFFECT_TYPES.PROPAGATE,
|
427 | {fn: createForwardHostPropEffect(prop, userForwardHostProp)});
|
428 | klass.prototype._createNotifyingProperty('_host_' + prop);
|
429 | }
|
430 | if (legacyWarnings && methodHost) {
|
431 | warnOnUndeclaredProperties(templateInfo, options, methodHost);
|
432 | }
|
433 | }
|
434 | // Mix any pre-bound data into __data; no need to flush this to
|
435 | // instances since they pull from the template at instance-time
|
436 | if (target.__dataProto) {
|
437 | // Note, generally `__dataProto` could be chained, but it's guaranteed
|
438 | // to not be since this is a vanilla template we just added effects to
|
439 | Object.assign(target.__data, target.__dataProto);
|
440 | }
|
441 | if (isTemplate) {
|
442 | upgradeTemplate(target, klass);
|
443 | // Clear any pending data for performance
|
444 | target.__dataTemp = {};
|
445 | target.__dataPending = null;
|
446 | target.__dataOld = null;
|
447 | target._enableProperties();
|
448 | } else {
|
449 | // Swizzle the cached subclass prototype onto the custom element
|
450 | Object.setPrototypeOf(target, klass.prototype);
|
451 | // Check for any pre-bound instance host properties, and do the
|
452 | // instance property delete/assign dance for those (directly into data;
|
453 | // not need to go through accessor since they are pulled at instance time)
|
454 | const hostProps = templateInfo.hostProps;
|
455 | for (let prop in hostProps) {
|
456 | prop = '_host_' + prop;
|
457 | if (prop in target) {
|
458 | const val = target[prop];
|
459 | delete target[prop];
|
460 | target.__data[prop] = val;
|
461 | }
|
462 | }
|
463 | }
|
464 | }
|
465 | }
|
466 | /* eslint-enable valid-jsdoc */
|
467 |
|
468 | function createForwardHostPropEffect(hostProp, userForwardHostProp) {
|
469 | return function forwardHostProp(template, prop, props) {
|
470 | userForwardHostProp.call(template.__templatizeOwner,
|
471 | prop.substring('_host_'.length), props[prop]);
|
472 | };
|
473 | }
|
474 |
|
475 | function addNotifyEffects(klass, template, templateInfo, options) {
|
476 | let hostProps = templateInfo.hostProps || {};
|
477 | for (let iprop in options.instanceProps) {
|
478 | delete hostProps[iprop];
|
479 | let userNotifyInstanceProp = options.notifyInstanceProp;
|
480 | if (userNotifyInstanceProp) {
|
481 | klass.prototype._addPropertyEffect(iprop,
|
482 | klass.prototype.PROPERTY_EFFECT_TYPES.NOTIFY,
|
483 | {fn: createNotifyInstancePropEffect(iprop, userNotifyInstanceProp)});
|
484 | }
|
485 | }
|
486 | if (options.forwardHostProp && template.__dataHost) {
|
487 | for (let hprop in hostProps) {
|
488 | // As we're iterating hostProps in this function, note whether
|
489 | // there were any, for an optimization in addPropagateEffects
|
490 | if (!templateInfo.hasHostProps) {
|
491 | templateInfo.hasHostProps = true;
|
492 | }
|
493 | klass.prototype._addPropertyEffect(hprop,
|
494 | klass.prototype.PROPERTY_EFFECT_TYPES.NOTIFY,
|
495 | {fn: createNotifyHostPropEffect()});
|
496 | }
|
497 | }
|
498 | }
|
499 |
|
500 | function createNotifyInstancePropEffect(instProp, userNotifyInstanceProp) {
|
501 | return function notifyInstanceProp(inst, prop, props) {
|
502 | userNotifyInstanceProp.call(inst.__templatizeOwner,
|
503 | inst, prop, props[prop]);
|
504 | };
|
505 | }
|
506 |
|
507 | function createNotifyHostPropEffect() {
|
508 | return function notifyHostProp(inst, prop, props) {
|
509 | inst.__dataHost._setPendingPropertyOrPath('_host_' + prop, props[prop], true, true);
|
510 | };
|
511 | }
|
512 |
|
513 |
|
514 | /**
|
515 | * Returns an anonymous `PropertyEffects` class bound to the
|
516 | * `<template>` provided. Instancing the class will result in the
|
517 | * template being stamped into a document fragment stored as the instance's
|
518 | * `root` property, after which it can be appended to the DOM.
|
519 | *
|
520 | * Templates may utilize all Polymer data-binding features as well as
|
521 | * declarative event listeners. Event listeners and inline computing
|
522 | * functions in the template will be called on the host of the template.
|
523 | *
|
524 | * The constructor returned takes a single argument dictionary of initial
|
525 | * property values to propagate into template bindings. Additionally
|
526 | * host properties can be forwarded in, and instance properties can be
|
527 | * notified out by providing optional callbacks in the `options` dictionary.
|
528 | *
|
529 | * Valid configuration in `options` are as follows:
|
530 | *
|
531 | * - `forwardHostProp(property, value)`: Called when a property referenced
|
532 | * in the template changed on the template's host. As this library does
|
533 | * not retain references to templates instanced by the user, it is the
|
534 | * templatize owner's responsibility to forward host property changes into
|
535 | * user-stamped instances. The `instance.forwardHostProp(property, value)`
|
536 | * method on the generated class should be called to forward host
|
537 | * properties into the template to prevent unnecessary property-changed
|
538 | * notifications. Any properties referenced in the template that are not
|
539 | * defined in `instanceProps` will be notified up to the template's host
|
540 | * automatically.
|
541 | * - `instanceProps`: Dictionary of property names that will be added
|
542 | * to the instance by the templatize owner. These properties shadow any
|
543 | * host properties, and changes within the template to these properties
|
544 | * will result in `notifyInstanceProp` being called.
|
545 | * - `mutableData`: When `true`, the generated class will skip strict
|
546 | * dirty-checking for objects and arrays (always consider them to be
|
547 | * "dirty").
|
548 | * - `notifyInstanceProp(instance, property, value)`: Called when
|
549 | * an instance property changes. Users may choose to call `notifyPath`
|
550 | * on e.g. the owner to notify the change.
|
551 | * - `parentModel`: When `true`, events handled by declarative event listeners
|
552 | * (`on-event="handler"`) will be decorated with a `model` property pointing
|
553 | * to the template instance that stamped it. It will also be returned
|
554 | * from `instance.parentModel` in cases where template instance nesting
|
555 | * causes an inner model to shadow an outer model.
|
556 | *
|
557 | * All callbacks are called bound to the `owner`. Any context
|
558 | * needed for the callbacks (such as references to `instances` stamped)
|
559 | * should be stored on the `owner` such that they can be retrieved via
|
560 | * `this`.
|
561 | *
|
562 | * When `options.forwardHostProp` is declared as an option, any properties
|
563 | * referenced in the template will be automatically forwarded from the host of
|
564 | * the `<template>` to instances, with the exception of any properties listed in
|
565 | * the `options.instanceProps` object. `instanceProps` are assumed to be
|
566 | * managed by the owner of the instances, either passed into the constructor
|
567 | * or set after the fact. Note, any properties passed into the constructor will
|
568 | * always be set to the instance (regardless of whether they would normally
|
569 | * be forwarded from the host).
|
570 | *
|
571 | * Note that `templatize()` can be run only once for a given `<template>`.
|
572 | * Further calls will result in an error. Also, there is a special
|
573 | * behavior if the template was duplicated through a mechanism such as
|
574 | * `<dom-repeat>` or `<test-fixture>`. In this case, all calls to
|
575 | * `templatize()` return the same class for all duplicates of a template.
|
576 | * The class returned from `templatize()` is generated only once using
|
577 | * the `options` from the first call. This means that any `options`
|
578 | * provided to subsequent calls will be ignored. Therefore, it is very
|
579 | * important not to close over any variables inside the callbacks. Also,
|
580 | * arrow functions must be avoided because they bind the outer `this`.
|
581 | * Inside the callbacks, any contextual information can be accessed
|
582 | * through `this`, which points to the `owner`.
|
583 | *
|
584 | * @param {!HTMLTemplateElement} template Template to templatize
|
585 | * @param {Polymer_PropertyEffects=} owner Owner of the template instances;
|
586 | * any optional callbacks will be bound to this owner.
|
587 | * @param {Object=} options Options dictionary (see summary for details)
|
588 | * @return {function(new:TemplateInstanceBase, Object=)} Generated class bound
|
589 | * to the template provided
|
590 | * @suppress {invalidCasts}
|
591 | */
|
592 | export function templatize(template, owner, options) {
|
593 | // Under strictTemplatePolicy, the templatized element must be owned
|
594 | // by a (trusted) Polymer element, indicated by existence of _methodHost;
|
595 | // e.g. for dom-if & dom-repeat in main document, _methodHost is null
|
596 | if (strictTemplatePolicy && !findMethodHost(template)) {
|
597 | throw new Error('strictTemplatePolicy: template owner not trusted');
|
598 | }
|
599 | options = /** @type {!TemplatizeOptions} */(options || {});
|
600 | if (template.__templatizeOwner) {
|
601 | throw new Error('A <template> can only be templatized once');
|
602 | }
|
603 | template.__templatizeOwner = owner;
|
604 | const ctor = owner ? owner.constructor : TemplateInstanceBase;
|
605 | let templateInfo = ctor._parseTemplate(template);
|
606 | // Get memoized base class for the prototypical template, which
|
607 | // includes property effects for binding template & forwarding
|
608 | /**
|
609 | * @constructor
|
610 | * @extends {TemplateInstanceBase}
|
611 | */
|
612 | let baseClass = templateInfo.templatizeInstanceClass;
|
613 | if (!baseClass) {
|
614 | baseClass = createTemplatizerClass(template, templateInfo, options);
|
615 | templateInfo.templatizeInstanceClass = baseClass;
|
616 | }
|
617 | const methodHost = findMethodHost(template);
|
618 | // Host property forwarding must be installed onto template instance
|
619 | addPropagateEffects(template, templateInfo, options, methodHost);
|
620 | // Subclass base class and add reference for this specific template
|
621 | /** @private */
|
622 | let klass = class TemplateInstance extends baseClass {};
|
623 | /** @override */
|
624 | klass.prototype._methodHost = methodHost;
|
625 | /** @override */
|
626 | klass.prototype.__dataHost = /** @type {!DataTemplate} */ (template);
|
627 | /** @override */
|
628 | klass.prototype.__templatizeOwner = /** @type {!Object} */ (owner);
|
629 | /** @override */
|
630 | klass.prototype.__hostProps = templateInfo.hostProps;
|
631 | klass = /** @type {function(new:TemplateInstanceBase)} */(klass); //eslint-disable-line no-self-assign
|
632 | return klass;
|
633 | }
|
634 |
|
635 | function warnOnUndeclaredProperties(templateInfo, options, methodHost) {
|
636 | const declaredProps = methodHost.constructor._properties;
|
637 | const {propertyEffects} = templateInfo;
|
638 | const {instanceProps} = options;
|
639 | for (let prop in propertyEffects) {
|
640 | // Ensure properties with template effects are declared on the outermost
|
641 | // host (`methodHost`), unless they are instance props or static functions
|
642 | if (!declaredProps[prop] && !(instanceProps && instanceProps[prop])) {
|
643 | const effects = propertyEffects[prop];
|
644 | for (let i=0; i<effects.length; i++) {
|
645 | const {part} = effects[i].info;
|
646 | if (!(part.signature && part.signature.static)) {
|
647 | console.warn(`Property '${prop}' used in template but not ` +
|
648 | `declared in 'properties'; attribute will not be observed.`);
|
649 | break;
|
650 | }
|
651 | }
|
652 | }
|
653 | }
|
654 | }
|
655 |
|
656 | /**
|
657 | * Returns the template "model" associated with a given element, which
|
658 | * serves as the binding scope for the template instance the element is
|
659 | * contained in. A template model is an instance of
|
660 | * `TemplateInstanceBase`, and should be used to manipulate data
|
661 | * associated with this template instance.
|
662 | *
|
663 | * Example:
|
664 | *
|
665 | * let model = modelForElement(el);
|
666 | * if (model.index < 10) {
|
667 | * model.set('item.checked', true);
|
668 | * }
|
669 | *
|
670 | * @param {HTMLElement} template The model will be returned for
|
671 | * elements stamped from this template (accepts either an HTMLTemplateElement)
|
672 | * or a `<dom-if>`/`<dom-repeat>` element when using `removeNestedTemplates`
|
673 | * optimization.
|
674 | * @param {Node=} node Node for which to return a template model.
|
675 | * @return {TemplateInstanceBase} Template instance representing the
|
676 | * binding scope for the element
|
677 | */
|
678 | export function modelForElement(template, node) {
|
679 | let model;
|
680 | while (node) {
|
681 | // An element with a __templatizeInstance marks the top boundary
|
682 | // of a scope; walk up until we find one, and then ensure that
|
683 | // its __dataHost matches `this`, meaning this dom-repeat stamped it
|
684 | if ((model = node.__dataHost ? node : node.__templatizeInstance)) {
|
685 | // Found an element stamped by another template; keep walking up
|
686 | // from its __dataHost
|
687 | if (model.__dataHost != template) {
|
688 | node = model.__dataHost;
|
689 | } else {
|
690 | return model;
|
691 | }
|
692 | } else {
|
693 | // Still in a template scope, keep going up until
|
694 | // a __templatizeInstance is found
|
695 | node = wrap(node).parentNode;
|
696 | }
|
697 | }
|
698 | return null;
|
699 | }
|
700 |
|
701 | export { TemplateInstanceBase };
|