-
Notifications
You must be signed in to change notification settings - Fork 2k
/
Copy pathtemplatizer.html
484 lines (454 loc) · 17.6 KB
/
templatizer.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
<!--
@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="../path.html">
<script>
/**
* The `Polymer.Templatizer` behavior adds methods to generate instances of
* templates that are each managed by an anonymous `Polymer.Base` instance.
*
* Example:
*
* // Get a template from somewhere, e.g. light DOM
* var template = Polymer.dom(this).querySelector('template');
* // Prepare the template
* this.templatize(template);
* // Instance the template with an initial data model
* var instance = this.stamp({myProp: 'initial'});
* // Insert the instance's DOM somewhere, e.g. light DOM
* Polymer.dom(this).appendChild(instance.root);
* // Changing a property on the instance will propagate to bindings
* // in the template
* instance.myProp = 'new value';
*
* Users of `Templatizer` may need to implement the following abstract
* API's to determine how properties and paths from the host should be
* forwarded into to instances:
*
* _forwardParentProp: function(prop, value)
* _forwardParentPath: function(path, value)
*
* Likewise, users may implement these additional abstract API's to determine
* how instance-specific properties that change on the instance should be
* forwarded out to the host, if necessary.
*
* _forwardInstanceProp: function(inst, prop, value)
* _forwardInstancePath: function(inst, path, value)
*
* In order to determine which properties are instance-specific and require
* custom forwarding via `_forwardInstanceProp`/`_forwardInstancePath`,
* define an `_instanceProps` map containing keys for each instance prop,
* for example:
*
* _instanceProps: {
* item: true,
* index: true
* }
*
* Any properties used in the template that are not defined in _instanceProp
* will be forwarded out to the host automatically.
*
* Users should also implement the following abstract function to show or
* hide any DOM generated using `stamp`:
*
* _showHideChildren: function(shouldHide)
*
* @polymerBehavior
*/
Polymer.Templatizer = {
properties: {
__hideTemplateChildren__: {
observer: '_showHideChildren'
}
},
// Extension point for overrides
_instanceProps: Polymer.nob,
_parentPropPrefix: '_parent_',
/**
* Prepares a template containing Polymer bindings by generating
* a constructor for an anonymous `Polymer.Base` subclass to serve as the
* binding context for the provided template.
*
* Use `this.stamp` to create instances of the template context containing
* a `root` fragment that may be stamped into the DOM.
*
* @method templatize
* @param {HTMLTemplateElement} template The template to process.
*/
templatize: function(template) {
this._templatized = template;
// TODO(sjmiles): supply _alternate_ content reference missing from root
// templates (not nested). `_content` exists to provide content sharing
// for nested templates.
if (!template._content) {
template._content = template.content;
}
// fast path if template's anonymous class has been memoized
if (template._content._ctor) {
this.ctor = template._content._ctor;
//console.log('Templatizer.templatize: using memoized archetype');
// forward parent properties to archetype
this._prepParentProperties(this.ctor.prototype, template);
return;
}
// `archetype` is the prototype of the anonymous
// class created by the templatizer
var archetype = Object.create(Polymer.Base);
// normally Annotations.parseAnnotations(template) but
// archetypes do special caching
this._customPrepAnnotations(archetype, template);
// forward parent properties to archetype
this._prepParentProperties(archetype, template);
// setup accessors
archetype._prepEffects();
this._customPrepEffects(archetype);
archetype._prepBehaviors();
archetype._prepPropertyInfo();
archetype._prepBindings();
// boilerplate code
archetype._notifyPathUp = this._notifyPathUpImpl;
archetype._scopeElementClass = this._scopeElementClassImpl;
archetype.listen = this._listenImpl;
archetype._showHideChildren = this._showHideChildrenImpl;
archetype.__setPropertyOrig = this.__setProperty;
archetype.__setProperty = this.__setPropertyImpl;
// boilerplate code
var _constructor = this._constructorImpl;
var ctor = function TemplateInstance(model, host) {
_constructor.call(this, model, host);
};
// standard references
ctor.prototype = archetype;
archetype.constructor = ctor;
// TODO(sjmiles): constructor cache?
template._content._ctor = ctor;
// TODO(sjmiles): choose less general name
this.ctor = ctor;
},
_getRootDataHost: function() {
return (this.dataHost && this.dataHost._rootDataHost) || this.dataHost;
},
_showHideChildrenImpl: function(hide) {
var c = this._children;
for (var i=0; i<c.length; i++) {
var 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;
}
},
__setPropertyImpl: function(property, value, fromAbove, node) {
if (node && node.__hideTemplateChildren__ && property == 'textContent') {
property = '__polymerTextContent__';
}
this.__setPropertyOrig(property, value, fromAbove, node);
},
_debounceTemplate: function(fn) {
Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', fn));
},
_flushTemplates: function() {
Polymer.dom.flush();
},
_customPrepEffects: function(archetype) {
var parentProps = archetype._parentProps;
for (var prop in parentProps) {
archetype._addPropertyEffect(prop, 'function',
this._createHostPropEffector(prop));
}
for (prop in this._instanceProps) {
archetype._addPropertyEffect(prop, 'function',
this._createInstancePropEffector(prop));
}
},
_customPrepAnnotations: function(archetype, template) {
archetype._template = template;
var c = template._content;
if (!c._notes) {
var rootDataHost = archetype._rootDataHost;
if (rootDataHost) {
Polymer.Annotations.prepElement = function() {
rootDataHost._prepElement();
}
}
c._notes = Polymer.Annotations.parseAnnotations(template);
Polymer.Annotations.prepElement = null;
this._processAnnotations(c._notes);
}
archetype._notes = c._notes;
archetype._parentProps = c._parentProps;
},
// Sets up accessors on the template to call abstract _forwardParentProp
// API that should be implemented by Templatizer users to get parent
// properties to their template instances. These accessors are memoized
// on the archetype and copied to instances.
_prepParentProperties: function(archetype, template) {
var parentProps = this._parentProps = archetype._parentProps;
if (this._forwardParentProp && parentProps) {
// Prototype setup (memoized on archetype)
var proto = archetype._parentPropProto;
var prop;
if (!proto) {
for (prop in this._instanceProps) {
delete parentProps[prop];
}
proto = archetype._parentPropProto = Object.create(null);
if (template != this) {
// Assumption: if `this` isn't the template being templatized,
// assume that the template is not a Poylmer.Base, so prep it
// for binding
Polymer.Bind.prepareModel(proto);
Polymer.Base.prepareModelNotifyPath(proto);
}
// Create accessors for each parent prop that forward the property
// to template instances through abstract _forwardParentProp API
// that should be implemented by Templatizer users
for (prop in parentProps) {
var parentProp = this._parentPropPrefix + prop;
// TODO(sorvell): remove reference Bind library functions here.
// Needed for effect optimization.
var effects = [{
kind: 'function',
effect: this._createForwardPropEffector(prop),
fn: Polymer.Bind._functionEffect
}, {
kind: 'notify',
fn: Polymer.Bind._notifyEffect,
effect: {event:
Polymer.CaseMap.camelToDashCase(parentProp) + '-changed'}
}];
proto._propertyEffects = proto._propertyEffects || {};
proto._propertyEffects[parentProp] = effects;
Polymer.Bind._createAccessors(proto, parentProp, effects);
}
}
// capture this reference for use below
var self = this;
// Instance setup
if (template != this) {
Polymer.Bind.prepareInstance(template);
template._forwardParentProp = function(source, value) {
self._forwardParentProp(source, value);
}
}
this._extendTemplate(template, proto);
template._pathEffector = function(path, value, fromAbove) {
return self._pathEffectorImpl(path, value, fromAbove);
}
}
},
_createForwardPropEffector: function(prop) {
return function(source, value) {
this._forwardParentProp(prop, value);
};
},
_createHostPropEffector: function(prop) {
var prefix = this._parentPropPrefix;
return function(source, value) {
this.dataHost._templatized[prefix + prop] = value;
};
},
_createInstancePropEffector: function(prop) {
return function(source, value, old, fromAbove) {
if (!fromAbove) {
this.dataHost._forwardInstanceProp(this, prop, value);
}
};
},
// Similar to Polymer.Base.extend, but retains any previously set instance
// values (_propertySetter back on instance once accessor is installed)
_extendTemplate: function(template, proto) {
var n$ = Object.getOwnPropertyNames(proto);
if (proto._propertySetter) {
// _propertySetter API may need to be copied onto the template,
// and it needs to come first to allow the property swizzle below
template._propertySetter = proto._propertySetter;
}
for (var i=0, n; (i<n$.length) && (n=n$[i]); i++) {
var val = template[n];
if (val && n == '_propertyEffects') {
// Merge property effects in
var pe = Polymer.Base.mixin({}, val);
template._propertyEffects = Polymer.Base.mixin(pe, proto._propertyEffects);
} else {
var pd = Object.getOwnPropertyDescriptor(proto, n);
Object.defineProperty(template, n, pd);
if (val !== undefined) {
template._propertySetter(n, val);
}
}
}
},
// Extension points for Templatizer sub-classes
/* eslint-disable no-unused-vars */
_showHideChildren: function(hidden) { },
_forwardInstancePath: function(inst, path, value) { },
_forwardInstanceProp: function(inst, prop, value) { },
// Defined-check rather than thunk used to avoid unnecessary work for these:
// _forwardParentPath: function(path, value) { },
// _forwardParentProp: function(prop, value) { },
/* eslint-enable no-unused-vars */
_notifyPathUpImpl: function(path, value) {
var dataHost = this.dataHost;
var root = Polymer.Path.root(path);
// Call extension point for Templatizer sub-classes
dataHost._forwardInstancePath.call(dataHost, this, path, value);
if (root in dataHost._parentProps) {
dataHost._templatized._notifyPath(dataHost._parentPropPrefix + path, value);
}
},
// Overrides Base notify-path module
_pathEffectorImpl: function(path, value, fromAbove) {
if (this._forwardParentPath) {
if (path.indexOf(this._parentPropPrefix) === 0) {
var subPath = path.substring(this._parentPropPrefix.length);
var model = Polymer.Path.root(subPath);
if (model in this._parentProps) {
this._forwardParentPath(subPath, value);
}
}
}
Polymer.Base._pathEffector.call(this._templatized, path, value, fromAbove);
},
_constructorImpl: function(model, host) {
this._rootDataHost = host._getRootDataHost();
this._setupConfigure(model);
this._registerHost(host);
this._beginHosting();
this.root = this.instanceTemplate(this._template);
this.root.__noContent = !this._notes._hasContent;
this.root.__styleScoped = true;
this._endHosting();
this._marshalAnnotatedNodes();
this._marshalInstanceEffects();
this._marshalAnnotatedListeners();
// each row is a document fragment which is lost when we appendChild,
// so we have to track each child individually
var children = [];
for (var n = this.root.firstChild; n; n=n.nextSibling) {
children.push(n);
n._templateInstance = this;
}
// Since archetype overrides Base/HTMLElement, Safari complains
// when accessing `children`
this._children = children;
// Ensure newly stamped nodes reflect host's hidden state
if (host.__hideTemplateChildren__) {
this._showHideChildren(true);
}
// ready self and children
this._tryReady();
},
// Decorate events with model (template instance)
_listenImpl: function(node, eventName, methodName) {
var model = this;
var host = this._rootDataHost;
var handler = host._createEventHandler(node, eventName, methodName);
var decorated = function(e) {
e.model = model;
handler(e);
};
host._listen(node, eventName, decorated);
},
_scopeElementClassImpl: function(node, value) {
var host = this._rootDataHost;
if (host) {
return host._scopeElementClass(node, value);
}
return value;
},
/**
* Creates an instance of the template previously processed via
* a call to `templatize`.
*
* The object returned is an anonymous subclass of Polymer.Base that
* has accessors generated to manage data in the template. The DOM for
* the instance is contained in a DocumentFragment called `root` on
* the instance returned and must be manually inserted into the DOM
* by the user.
*
* Note that a call to `templatize` must be called once before using
* `stamp`.
*
* @method stamp
* @param {Object=} model An object containing key/values to serve as the
* initial data configuration for the instance. Note that properties
* from the host used in the template are automatically copied into
* the model.
* @return {Polymer.Base} The Polymer.Base instance to manage the template
* instance.
*/
stamp: function(model) {
model = model || {};
if (this._parentProps) {
var templatized = this._templatized;
for (var prop in this._parentProps) {
if (model[prop] === undefined) {
model[prop] = templatized[this._parentPropPrefix + prop];
}
}
}
return new this.ctor(model, this);
},
/**
* 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:
*
* var model = modelForElement(el);
* if (model.index < 10) {
* model.set('item.checked', true);
* }
*
* @method modelForElement
* @param {HTMLElement} el Element for which to return a template model.
* @return {Object<Polymer.Base>} Model representing the binding scope for
* the element.
*/
modelForElement: function(el) {
var model;
while (el) {
// An element with a _templateInstance 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._templateInstance)) {
// Found an element stamped by another template; keep walking up
// from its dataHost
if (model.dataHost != this) {
el = model.dataHost;
} else {
return model;
}
} else {
// Still in a template scope, keep going up until
// a _templateInstance is found
el = el.parentNode;
}
}
}
};
</script>