Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(294)

Side by Side Diff: appengine/config_service/ui/bower_components/polymer/lib/mixins/property-effects.html

Issue 2923973003: Added base template for config ui. (Closed)
Patch Set: Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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.g ithub.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/CONTRI BUTORS.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/PATEN TS.txt
9 -->
10
11
12 <link rel="import" href="../utils/boot.html">
13 <link rel="import" href="../utils/mixin.html">
14 <link rel="import" href="../utils/path.html">
15 <!-- for notify, reflect -->
16 <link rel="import" href="../utils/case-map.html">
17 <link rel="import" href="property-accessors.html">
18 <!-- for annotated effects -->
19 <link rel="import" href="template-stamp.html">
20
21
22 <script>
23 (function() {
24
25 'use strict';
26
27 /** @const {Object} */
28 const CaseMap = Polymer.CaseMap;
29
30 // Monotonically increasing unique ID used for de-duping effects triggered
31 // from multiple properties in the same turn
32 let dedupeId = 0;
33
34 // Property effect types; effects are stored on the prototype using these keys
35 const TYPES = {
36 COMPUTE: '__computeEffects',
37 REFLECT: '__reflectEffects',
38 NOTIFY: '__notifyEffects',
39 PROPAGATE: '__propagateEffects',
40 OBSERVE: '__observeEffects',
41 READ_ONLY: '__readOnly'
42 }
43
44 /**
45 * Ensures that the model has an own-property map of effects for the given typ e.
46 * The model may be a prototype or an instance.
47 *
48 * Property effects are stored as arrays of effects by property in a map,
49 * by named type on the model. e.g.
50 *
51 * __computeEffects: {
52 * foo: [ ... ],
53 * bar: [ ... ]
54 * }
55 *
56 * If the model does not yet have an effect map for the type, one is created
57 * and returned. If it does, but it is not an own property (i.e. the
58 * prototype had effects), the the map is deeply cloned and the copy is
59 * set on the model and returned, ready for new effects to be added.
60 *
61 * @param {Object} model Prototype or instance
62 * @param {string} type Property effect type
63 * @return {Object} The own-property map of effects for the given type
64 * @private
65 */
66 function ensureOwnEffectMap(model, type) {
67 let effects = model[type];
68 if (!effects) {
69 effects = model[type] = {};
70 } else if (!model.hasOwnProperty(type)) {
71 effects = model[type] = Object.create(model[type]);
72 for (let p in effects) {
73 let protoFx = effects[p];
74 let instFx = effects[p] = Array(protoFx.length);
75 for (let i=0; i<protoFx.length; i++) {
76 instFx[i] = protoFx[i];
77 }
78 }
79 }
80 return effects;
81 }
82
83 // -- effects ----------------------------------------------
84
85 /**
86 * Runs all effects of a given type for the given set of property changes
87 * on an instance.
88 *
89 * @param {Object} inst The instance with effects to run
90 * @param {Object} effects Object map of property-to-Array of effects
91 * @param {Object} props Bag of current property changes
92 * @param {Object=} oldProps Bag of previous values for changed properties
93 * @param {boolean=} hasPaths True with `props` contains one or more paths
94 * @param {*=} extraArgs Additional metadata to pass to effect function
95 * @return {boolean} True if an effect ran for this property
96 * @private
97 */
98 function runEffects(inst, effects, props, oldProps, hasPaths, extraArgs) {
99 if (effects) {
100 let ran = false;
101 let id = dedupeId++;
102 for (let prop in props) {
103 if (runEffectsForProperty(inst, effects, id, prop, props, oldProps, hasP aths, extraArgs)) {
104 ran = true;
105 }
106 }
107 return ran;
108 }
109 return false;
110 }
111
112 /**
113 * Runs a list of effects for a given property.
114 *
115 * @param {Object} inst The instance with effects to run
116 * @param {Object} effects Object map of property-to-Array of effects
117 * @param {number} dedupeId Counter used for de-duping effects
118 * @param {string} prop Name of changed property
119 * @param {*} props Changed properties
120 * @param {*} oldProps Old properties
121 * @param {boolean=} hasPaths True with `props` contains one or more paths
122 * @param {*=} extraArgs Additional metadata to pass to effect function
123 * @return {boolean} True if an effect ran for this property
124 * @private
125 */
126 function runEffectsForProperty(inst, effects, dedupeId, prop, props, oldProps, hasPaths, extraArgs) {
127 let ran = false;
128 let rootProperty = hasPaths ? Polymer.Path.root(prop) : prop;
129 let fxs = effects[rootProperty];
130 if (fxs) {
131 for (let i=0, l=fxs.length, fx; (i<l) && (fx=fxs[i]); i++) {
132 if ((!fx.info || fx.info.lastRun !== dedupeId) &&
133 (!hasPaths || pathMatchesTrigger(prop, fx.trigger))) {
134 if (fx.info) {
135 fx.info.lastRun = dedupeId;
136 }
137 fx.fn(inst, prop, props, oldProps, fx.info, hasPaths, extraArgs);
138 ran = true;
139 }
140 }
141 }
142 return ran;
143 }
144
145 /**
146 * Determines whether a property/path that has changed matches the trigger
147 * criteria for an effect. A trigger is a descriptor with the following
148 * structure, which matches the descriptors returned from `parseArg`.
149 * e.g. for `foo.bar.*`:
150 * ```
151 * trigger: {
152 * name: 'a.b',
153 * structured: true,
154 * wildcard: true
155 * }
156 * ```
157 * If no trigger is given, the path is deemed to match.
158 *
159 * @param {string} path Path or property that changed
160 * @param {Object} trigger Descriptor
161 * @return {boolean} Whether the path matched the trigger
162 */
163 function pathMatchesTrigger(path, trigger) {
164 if (trigger) {
165 let triggerPath = trigger.name;
166 return (triggerPath == path) ||
167 (trigger.structured && Polymer.Path.isAncestor(triggerPath, path)) ||
168 (trigger.wildcard && Polymer.Path.isDescendant(triggerPath, path));
169 } else {
170 return true;
171 }
172 }
173
174 /**
175 * Implements the "observer" effect.
176 *
177 * Calls the method with `info.methodName` on the instance, passing the
178 * new and old values.
179 *
180 * @param {Object} inst The instance the effect will be run on
181 * @param {string} property Name of property
182 * @param {Object} props Bag of current property changes
183 * @param {Object} oldProps Bag of previous values for changed properties
184 * @param {Object} info Effect metadata
185 * @private
186 */
187 function runObserverEffect(inst, property, props, oldProps, info) {
188 let fn = inst[info.methodName];
189 let changedProp = info.property;
190 if (fn) {
191 fn.call(inst, inst.__data[changedProp], oldProps[changedProp]);
192 } else if (!info.dynamicFn) {
193 console.warn('observer method `' + info.methodName + '` not defined');
194 }
195 }
196
197 /**
198 * Runs "notify" effects for a set of changed properties.
199 *
200 * This method differs from the generic `runEffects` method in that it
201 * will dispatch path notification events in the case that the property
202 * changed was a path and the root property for that path didn't have a
203 * "notify" effect. This is to maintain 1.0 behavior that did not require
204 * `notify: true` to ensure object sub-property notifications were
205 * sent.
206 *
207 * @param {Element} inst The instance with effects to run
208 * @param {Object} notifyProps Bag of properties to notify
209 * @param {Object} props Bag of current property changes
210 * @param {Object} oldProps Bag of previous values for changed properties
211 * @param {boolean} hasPaths True with `props` contains one or more paths
212 * @private
213 */
214 function runNotifyEffects(inst, notifyProps, props, oldProps, hasPaths) {
215 // Notify
216 let fxs = inst.__notifyEffects;
217 let notified;
218 let id = dedupeId++;
219 // Try normal notify effects; if none, fall back to try path notification
220 for (let prop in notifyProps) {
221 if (notifyProps[prop]) {
222 if (fxs && runEffectsForProperty(inst, fxs, id, prop, props, oldProps, h asPaths)) {
223 notified = true;
224 } else if (hasPaths && notifyPath(inst, prop, props)) {
225 notified = true;
226 }
227 }
228 }
229 // Flush host if we actually notified and host was batching
230 // And the host has already initialized clients; this prevents
231 // an issue with a host observing data changes before clients are ready.
232 let host;
233 if (notified && (host = inst.__dataHost) && host._invalidateProperties) {
234 host._invalidateProperties();
235 }
236 }
237
238 /**
239 * Dispatches {property}-changed events with path information in the detail
240 * object to indicate a sub-path of the property was changed.
241 *
242 * @param {Element} inst The element from which to fire the event
243 * @param {string} path The path that was changed
244 * @param {Object} props Bag of current property changes
245 * @return {boolean} Returns true if the path was notified
246 * @private
247 */
248 function notifyPath(inst, path, props) {
249 let rootProperty = Polymer.Path.root(path);
250 if (rootProperty !== path) {
251 let eventName = Polymer.CaseMap.camelToDashCase(rootProperty) + '-changed' ;
252 dispatchNotifyEvent(inst, eventName, props[path], path);
253 return true;
254 }
255 }
256
257 /**
258 * Dispatches {property}-changed events to indicate a property (or path)
259 * changed.
260 *
261 * @param {Element} inst The element from which to fire the event
262 * @param {string} eventName The name of the event to send ('{property}-change d')
263 * @param {*} value The value of the changed property
264 * @param {string | null | undefined} path If a sub-path of this property chan ged, the path
265 * that changed (optional).
266 * @private
267 */
268 function dispatchNotifyEvent(inst, eventName, value, path) {
269 let detail = {
270 value: value,
271 queueProperty: true
272 };
273 if (path) {
274 detail.path = path;
275 }
276 inst.dispatchEvent(new CustomEvent(eventName, { detail }));
277 }
278
279 /**
280 * Implements the "notify" effect.
281 *
282 * Dispatches a non-bubbling event named `info.eventName` on the instance
283 * with a detail object containing the new `value`.
284 *
285 * @param {Element} inst The instance the effect will be run on
286 * @param {string} property Name of property
287 * @param {Object} props Bag of current property changes
288 * @param {Object} oldProps Bag of previous values for changed properties
289 * @param {Object} info Effect metadata
290 * @param {boolean} hasPaths True with `props` contains one or more paths
291 * @private
292 */
293 function runNotifyEffect(inst, property, props, oldProps, info, hasPaths) {
294 let rootProperty = hasPaths ? Polymer.Path.root(property) : property;
295 let path = rootProperty != property ? property : null;
296 let value = path ? Polymer.Path.get(inst, path) : inst.__data[property];
297 if (path && value === undefined) {
298 value = props[property]; // specifically for .splices
299 }
300 dispatchNotifyEvent(inst, info.eventName, value, path);
301 }
302
303 /**
304 * Handler function for 2-way notification events. Receives context
305 * information captured in the `addNotifyListener` closure from the
306 * `__notifyListeners` metadata.
307 *
308 * Sets the value of the notified property to the host property or path. If
309 * the event contained path information, translate that path to the host
310 * scope's name for that path first.
311 *
312 * @param {Event} event Notification event (e.g. '<property>-changed')
313 * @param {Object} inst Host element instance handling the notification event
314 * @param {string} fromProp Child element property that was bound
315 * @param {string} toPath Host property/path that was bound
316 * @param {boolean} negate Whether the binding was negated
317 * @private
318 */
319 function handleNotification(event, inst, fromProp, toPath, negate) {
320 let value;
321 let detail = event.detail;
322 let fromPath = detail && detail.path;
323 if (fromPath) {
324 toPath = Polymer.Path.translate(fromProp, toPath, fromPath);
325 value = detail && detail.value;
326 } else {
327 value = event.target[fromProp];
328 }
329 value = negate ? !value : value;
330 if (!inst.__readOnly || !inst.__readOnly[toPath]) {
331 if (inst._setPendingPropertyOrPath(toPath, value, true, Boolean(fromPath))
332 && (!detail || !detail.queueProperty)) {
333 inst._invalidateProperties();
334 }
335 }
336 }
337
338 /**
339 * Implements the "reflect" effect.
340 *
341 * Sets the attribute named `info.attrName` to the given property value.
342 *
343 * @param {Object} inst The instance the effect will be run on
344 * @param {string} property Name of property
345 * @param {Object} props Bag of current property changes
346 * @param {Object} oldProps Bag of previous values for changed properties
347 * @param {Object} info Effect metadata
348 * @private
349 */
350 function runReflectEffect(inst, property, props, oldProps, info) {
351 let value = inst.__data[property];
352 if (Polymer.sanitizeDOMValue) {
353 value = Polymer.sanitizeDOMValue(value, info.attrName, 'attribute', inst);
354 }
355 inst._propertyToAttribute(property, info.attrName, value);
356 }
357
358 /**
359 * Runs "computed" effects for a set of changed properties.
360 *
361 * This method differs from the generic `runEffects` method in that it
362 * continues to run computed effects based on the output of each pass until
363 * there are no more newly computed properties. This ensures that all
364 * properties that will be computed by the initial set of changes are
365 * computed before other effects (binding propagation, observers, and notify)
366 * run.
367 *
368 * @param {Element} inst The instance the effect will be run on
369 * @param {Object} changedProps Bag of changed properties
370 * @param {Object} oldProps Bag of previous values for changed properties
371 * @param {boolean} hasPaths True with `props` contains one or more paths
372 * @private
373 */
374 function runComputedEffects(inst, changedProps, oldProps, hasPaths) {
375 let computeEffects = inst.__computeEffects;
376 if (computeEffects) {
377 let inputProps = changedProps;
378 while (runEffects(inst, computeEffects, inputProps, oldProps, hasPaths)) {
379 Object.assign(oldProps, inst.__dataOld);
380 Object.assign(changedProps, inst.__dataPending);
381 inputProps = inst.__dataPending;
382 inst.__dataPending = null;
383 }
384 }
385 }
386
387 /**
388 * Implements the "computed property" effect by running the method with the
389 * values of the arguments specified in the `info` object and setting the
390 * return value to the computed property specified.
391 *
392 * @param {Object} inst The instance the effect will be run on
393 * @param {string} property Name of property
394 * @param {Object} props Bag of current property changes
395 * @param {Object} oldProps Bag of previous values for changed properties
396 * @param {Object} info Effect metadata
397 * @private
398 */
399 function runComputedEffect(inst, property, props, oldProps, info) {
400 let result = runMethodEffect(inst, property, props, oldProps, info);
401 let computedProp = info.methodInfo;
402 if (inst.__dataHasAccessor && inst.__dataHasAccessor[computedProp]) {
403 inst._setPendingProperty(computedProp, result, true);
404 } else {
405 inst[computedProp] = result;
406 }
407 }
408
409 /**
410 * Computes path changes based on path links set up using the `linkPaths`
411 * API.
412 *
413 * @param {Element} inst The instance whose props are changing
414 * @param {string} path Path that has changed
415 * @param {*} value Value of changed path
416 * @private
417 */
418 function computeLinkedPaths(inst, path, value) {
419 let links = inst.__dataLinkedPaths;
420 if (links) {
421 let link;
422 for (let a in links) {
423 let b = links[a];
424 if (Polymer.Path.isDescendant(a, path)) {
425 link = Polymer.Path.translate(a, b, path);
426 inst._setPendingPropertyOrPath(link, value, true, true);
427 } else if (Polymer.Path.isDescendant(b, path)) {
428 link = Polymer.Path.translate(b, a, path);
429 inst._setPendingPropertyOrPath(link, value, true, true);
430 }
431 }
432 }
433 }
434
435 // -- bindings ----------------------------------------------
436
437 /**
438 * Adds binding metadata to the current `nodeInfo`, and binding effects
439 * for all part dependencies to `templateInfo`.
440 *
441 * @param {Function} constructor Class that `_parseTemplate` is currently
442 * running on
443 * @param {Object} templateInfo Template metadata for current template
444 * @param {Object} nodeInfo Node metadata for current template node
445 * @param {string} kind Binding kind, either 'property', 'attribute', or 'text '
446 * @param {string} target Target property name
447 * @param {Array<Object>} parts Array of binding part metadata
448 * @param {string} literal Literal text surrounding binding parts (specified
449 * only for 'property' bindings, since these must be initialized as part
450 * of boot-up)
451 * @private
452 */
453 function addBinding(constructor, templateInfo, nodeInfo, kind, target, parts, literal) {
454 // Create binding metadata and add to nodeInfo
455 nodeInfo.bindings = nodeInfo.bindings || [];
456 let binding = { kind, target, parts, literal, isCompound: (parts.length !== 1) };
457 nodeInfo.bindings.push(binding);
458 // Add listener info to binding metadata
459 if (shouldAddListener(binding)) {
460 let {event, negate} = binding.parts[0];
461 binding.listenerEvent = event || (CaseMap.camelToDashCase(target) + '-chan ged');
462 binding.listenerNegate = negate;
463 }
464 // Add "propagate" property effects to templateInfo
465 let index = templateInfo.nodeInfoList.length;
466 for (let i=0; i<binding.parts.length; i++) {
467 let part = binding.parts[i];
468 part.compoundIndex = i;
469 addEffectForBindingPart(constructor, templateInfo, binding, part, index);
470 }
471 }
472
473 /**
474 * Adds property effects to the given `templateInfo` for the given binding
475 * part.
476 *
477 * @param {Function} constructor Class that `_parseTemplate` is currently
478 * running on
479 * @param {Object} templateInfo Template metadata for current template
480 * @param {Object} binding Binding metadata
481 * @param {Object} part Binding part metadata
482 * @param {number} index Index into `nodeInfoList` for this node
483 */
484 function addEffectForBindingPart(constructor, templateInfo, binding, part, ind ex) {
485 if (!part.literal) {
486 if (binding.kind === 'attribute' && binding.target[0] === '-') {
487 console.warn('Cannot set attribute ' + binding.target +
488 ' because "-" is not a valid attribute starting character');
489 } else {
490 let dependencies = part.dependencies;
491 let info = { index, binding, part, evaluator: constructor };
492 for (let j=0; j<dependencies.length; j++) {
493 let trigger = dependencies[j];
494 if (typeof trigger == 'string') {
495 trigger = parseArg(trigger);
496 trigger.wildcard = true;
497 }
498 constructor._addTemplatePropertyEffect(templateInfo, trigger.rootPrope rty, {
499 fn: runBindingEffect,
500 info, trigger
501 });
502 }
503 }
504 }
505 }
506
507 /**
508 * Implements the "binding" (property/path binding) effect.
509 *
510 * Note that binding syntax is overridable via `_parseBindings` and
511 * `_evaluateBinding`. This method will call `_evaluateBinding` for any
512 * non-literal parts returned from `_parseBindings`. However,
513 * there is no support for _path_ bindings via custom binding parts,
514 * as this is specific to Polymer's path binding syntax.
515 *
516 * @param {Element} inst The instance the effect will be run on
517 * @param {string} path Name of property
518 * @param {Object} props Bag of current property changes
519 * @param {Object} oldProps Bag of previous values for changed properties
520 * @param {Object} info Effect metadata
521 * @param {boolean} hasPaths True with `props` contains one or more paths
522 * @param {Array} nodeList List of nodes associated with `nodeInfoList` templa te
523 * metadata
524 * @private
525 */
526 function runBindingEffect(inst, path, props, oldProps, info, hasPaths, nodeLis t) {
527 let node = nodeList[info.index];
528 let binding = info.binding;
529 let part = info.part;
530 // Subpath notification: transform path and set to client
531 // e.g.: foo="{{obj.sub}}", path: 'obj.sub.prop', set 'foo.prop'=obj.sub.pro p
532 if (hasPaths && part.source && (path.length > part.source.length) &&
533 (binding.kind == 'property') && !binding.isCompound &&
534 node.__dataHasAccessor && node.__dataHasAccessor[binding.target]) {
535 let value = props[path];
536 path = Polymer.Path.translate(part.source, binding.target, path);
537 if (node._setPendingPropertyOrPath(path, value, false, true)) {
538 inst._enqueueClient(node);
539 }
540 } else {
541 let value = info.evaluator._evaluateBinding(inst, part, path, props, oldPr ops, hasPaths);
542 // Propagate value to child
543 applyBindingValue(inst, node, binding, part, value);
544 }
545 }
546
547 /**
548 * Sets the value for an "binding" (binding) effect to a node,
549 * either as a property or attribute.
550 *
551 * @param {Object} inst The instance owning the binding effect
552 * @param {Node} node Target node for binding
553 * @param {Object} binding Binding metadata
554 * @param {Object} part Binding part metadata
555 * @param {*} value Value to set
556 * @private
557 */
558 function applyBindingValue(inst, node, binding, part, value) {
559 value = computeBindingValue(node, value, binding, part);
560 if (Polymer.sanitizeDOMValue) {
561 value = Polymer.sanitizeDOMValue(value, binding.target, binding.kind, node );
562 }
563 if (binding.kind == 'attribute') {
564 // Attribute binding
565 inst._valueToNodeAttribute(node, value, binding.target);
566 } else {
567 // Property binding
568 let prop = binding.target;
569 if (node.__dataHasAccessor && node.__dataHasAccessor[prop]) {
570 if (!node.__readOnly || !node.__readOnly[prop]) {
571 if (node._setPendingProperty(prop, value)) {
572 inst._enqueueClient(node);
573 }
574 }
575 } else {
576 inst._setUnmanagedPropertyToNode(node, prop, value);
577 }
578 }
579 }
580
581 /**
582 * Transforms an "binding" effect value based on compound & negation
583 * effect metadata, as well as handling for special-case properties
584 *
585 * @param {Node} node Node the value will be set to
586 * @param {*} value Value to set
587 * @param {Object} binding Binding metadata
588 * @param {Object} part Binding part metadata
589 * @return {*} Transformed value to set
590 * @private
591 */
592 function computeBindingValue(node, value, binding, part) {
593 if (binding.isCompound) {
594 let storage = node.__dataCompoundStorage[binding.target];
595 storage[part.compoundIndex] = value;
596 value = storage.join('');
597 }
598 if (binding.kind !== 'attribute') {
599 // Some browsers serialize `undefined` to `"undefined"`
600 if (binding.target === 'textContent' ||
601 (node.localName == 'input' && binding.target == 'value')) {
602 value = value == undefined ? '' : value;
603 }
604 }
605 return value;
606 }
607
608 /**
609 * Returns true if a binding's metadata meets all the requirements to allow
610 * 2-way binding, and therefore a `<property>-changed` event listener should b e
611 * added:
612 * - used curly braces
613 * - is a property (not attribute) binding
614 * - is not a textContent binding
615 * - is not compound
616 *
617 * @param {Object} binding Binding metadata
618 * @return {boolean} True if 2-way listener should be added
619 * @private
620 */
621 function shouldAddListener(binding) {
622 return binding.target &&
623 binding.kind != 'attribute' &&
624 binding.kind != 'text' &&
625 !binding.isCompound &&
626 binding.parts[0].mode === '{';
627 }
628
629 /**
630 * Setup compound binding storage structures, notify listeners, and dataHost
631 * references onto the bound nodeList.
632 *
633 * @param {Object} inst Instance that bas been previously bound
634 * @param {Object} templateInfo Template metadata
635 * @private
636 */
637 function setupBindings(inst, templateInfo) {
638 // Setup compound storage, dataHost, and notify listeners
639 let {nodeList, nodeInfoList} = templateInfo;
640 if (nodeInfoList.length) {
641 for (let i=0; i < nodeInfoList.length; i++) {
642 let info = nodeInfoList[i];
643 let node = nodeList[i];
644 let bindings = info.bindings;
645 if (bindings) {
646 for (let i=0; i<bindings.length; i++) {
647 let binding = bindings[i];
648 setupCompoundStorage(node, binding);
649 addNotifyListener(node, inst, binding);
650 }
651 }
652 node.__dataHost = inst;
653 }
654 }
655 }
656
657 /**
658 * Initializes `__dataCompoundStorage` local storage on a bound node with
659 * initial literal data for compound bindings, and sets the joined
660 * literal parts to the bound property.
661 *
662 * When changes to compound parts occur, they are first set into the compound
663 * storage array for that property, and then the array is joined to result in
664 * the final value set to the property/attribute.
665 *
666 * @param {Node} node Bound node to initialize
667 * @param {Object} binding Binding metadata
668 * @private
669 */
670 function setupCompoundStorage(node, binding) {
671 if (binding.isCompound) {
672 // Create compound storage map
673 let storage = node.__dataCompoundStorage ||
674 (node.__dataCompoundStorage = {});
675 let parts = binding.parts;
676 // Copy literals from parts into storage for this binding
677 let literals = new Array(parts.length);
678 for (let j=0; j<parts.length; j++) {
679 literals[j] = parts[j].literal;
680 }
681 let target = binding.target;
682 storage[target] = literals;
683 // Configure properties with their literal parts
684 if (binding.literal && binding.kind == 'property') {
685 node[target] = binding.literal;
686 }
687 }
688 }
689
690 /**
691 * Adds a 2-way binding notification event listener to the node specified
692 *
693 * @param {Object} node Child element to add listener to
694 * @param {Object} inst Host element instance to handle notification event
695 * @param {Object} binding Binding metadata
696 * @private
697 */
698 function addNotifyListener(node, inst, binding) {
699 if (binding.listenerEvent) {
700 let part = binding.parts[0];
701 node.addEventListener(binding.listenerEvent, function(e) {
702 handleNotification(e, inst, binding.target, part.source, part.negate);
703 });
704 }
705 }
706
707 // -- for method-based effects (complexObserver & computed) --------------
708
709 /**
710 * Adds property effects for each argument in the method signature (and
711 * optionally, for the method name if `dynamic` is true) that calls the
712 * provided effect function.
713 *
714 * @param {Element | Object} model Prototype or instance
715 * @param {Object} sig Method signature metadata
716 * @param {string} type Type of property effect to add
717 * @param {Function} effectFn Function to run when arguments change
718 * @param {*=} methodInfo Effect-specific information to be included in
719 * method effect metadata
720 * @param {boolean|Object=} dynamicFn Boolean or object map indicating whether
721 * method names should be included as a dependency to the effect. Note,
722 * defaults to true if the signature is static (sig.static is true).
723 * @private
724 */
725 function createMethodEffect(model, sig, type, effectFn, methodInfo, dynamicFn) {
726 dynamicFn = sig.static || (dynamicFn &&
727 (typeof dynamicFn !== 'object' || dynamicFn[sig.methodName]));
728 let info = {
729 methodName: sig.methodName,
730 args: sig.args,
731 methodInfo,
732 dynamicFn
733 };
734 for (let i=0, arg; (i<sig.args.length) && (arg=sig.args[i]); i++) {
735 if (!arg.literal) {
736 model._addPropertyEffect(arg.rootProperty, type, {
737 fn: effectFn, info: info, trigger: arg
738 });
739 }
740 }
741 if (dynamicFn) {
742 model._addPropertyEffect(sig.methodName, type, {
743 fn: effectFn, info: info
744 });
745 }
746 }
747
748 /**
749 * Calls a method with arguments marshaled from properties on the instance
750 * based on the method signature contained in the effect metadata.
751 *
752 * Multi-property observers, computed properties, and inline computing
753 * functions call this function to invoke the method, then use the return
754 * value accordingly.
755 *
756 * @param {Object} inst The instance the effect will be run on
757 * @param {string} property Name of property
758 * @param {Object} props Bag of current property changes
759 * @param {Object} oldProps Bag of previous values for changed properties
760 * @param {Object} info Effect metadata
761 * @return {*} Returns the return value from the method invocation
762 * @private
763 */
764 function runMethodEffect(inst, property, props, oldProps, info) {
765 // Instances can optionally have a _methodHost which allows redirecting wher e
766 // to find methods. Currently used by `templatize`.
767 let context = inst._methodHost || inst;
768 let fn = context[info.methodName];
769 if (fn) {
770 let args = marshalArgs(inst.__data, info.args, property, props);
771 return fn.apply(context, args);
772 } else if (!info.dynamicFn) {
773 console.warn('method `' + info.methodName + '` not defined');
774 }
775 }
776
777 const emptyArray = [];
778
779 // Regular expressions used for binding
780 const IDENT = '(?:' + '[a-zA-Z_$][\\w.:$\\-*]*' + ')';
781 const NUMBER = '(?:' + '[-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?' + ')';
782 const SQUOTE_STRING = '(?:' + '\'(?:[^\'\\\\]|\\\\.)*\'' + ')';
783 const DQUOTE_STRING = '(?:' + '"(?:[^"\\\\]|\\\\.)*"' + ')';
784 const STRING = '(?:' + SQUOTE_STRING + '|' + DQUOTE_STRING + ')';
785 const ARGUMENT = '(?:' + IDENT + '|' + NUMBER + '|' + STRING + '\\s*' + ')';
786 const ARGUMENTS = '(?:' + ARGUMENT + '(?:,\\s*' + ARGUMENT + ')*' + ')';
787 const ARGUMENT_LIST = '(?:' + '\\(\\s*' +
788 '(?:' + ARGUMENTS + '?' + ')' +
789 '\\)\\s*' + ')';
790 const BINDING = '(' + IDENT + '\\s*' + ARGUMENT_LIST + '?' + ')'; // Group 3
791 const OPEN_BRACKET = '(\\[\\[|{{)' + '\\s*';
792 const CLOSE_BRACKET = '(?:]]|}})';
793 const NEGATE = '(?:(!)\\s*)?'; // Group 2
794 const EXPRESSION = OPEN_BRACKET + NEGATE + BINDING + CLOSE_BRACKET;
795 const bindingRegex = new RegExp(EXPRESSION, "g");
796
797 function literalFromParts(parts) {
798 let s = '';
799 for (let i=0; i<parts.length; i++) {
800 let literal = parts[i].literal;
801 s += literal || '';
802 }
803 return s;
804 }
805
806 /**
807 * Parses an expression string for a method signature, and returns a metadata
808 * describing the method in terms of `methodName`, `static` (whether all the
809 * arguments are literals), and an array of `args`
810 *
811 * @param {string} expression The expression to parse
812 * @return {?Object} The method metadata object if a method expression was
813 * found, otherwise `undefined`
814 * @private
815 */
816 function parseMethod(expression) {
817 // tries to match valid javascript property names
818 let m = expression.match(/([^\s]+?)\(([\s\S]*)\)/);
819 if (m) {
820 let methodName = m[1];
821 let sig = { methodName, static: true };
822 if (m[2].trim()) {
823 // replace escaped commas with comma entity, split on un-escaped commas
824 let args = m[2].replace(/\\,/g, '&comma;').split(',');
825 return parseArgs(args, sig);
826 } else {
827 sig.args = emptyArray;
828 return sig;
829 }
830 }
831 return null;
832 }
833
834 /**
835 * Parses an array of arguments and sets the `args` property of the supplied
836 * signature metadata object. Sets the `static` property to false if any
837 * argument is a non-literal.
838 *
839 * @param {Array<string>} argList Array of argument names
840 * @param {Object} sig Method signature metadata object
841 * @return {Object} The updated signature metadata object
842 * @private
843 */
844 function parseArgs(argList, sig) {
845 sig.args = argList.map(function(rawArg) {
846 let arg = parseArg(rawArg);
847 if (!arg.literal) {
848 sig.static = false;
849 }
850 return arg;
851 }, this);
852 return sig;
853 }
854
855 /**
856 * Parses an individual argument, and returns an argument metadata object
857 * with the following fields:
858 *
859 * {
860 * value: 'prop', // property/path or literal value
861 * literal: false, // whether argument is a literal
862 * structured: false, // whether the property is a path
863 * rootProperty: 'prop', // the root property of the path
864 * wildcard: false // whether the argument was a wildcard '.*' path
865 * }
866 *
867 * @param {string} rawArg The string value of the argument
868 * @return {Object} Argument metadata object
869 * @private
870 */
871 function parseArg(rawArg) {
872 // clean up whitespace
873 let arg = rawArg.trim()
874 // replace comma entity with comma
875 .replace(/&comma;/g, ',')
876 // repair extra escape sequences; note only commas strictly need
877 // escaping, but we allow any other char to be escaped since its
878 // likely users will do this
879 .replace(/\\(.)/g, '\$1')
880 ;
881 // basic argument descriptor
882 let a = {
883 name: arg
884 };
885 // detect literal value (must be String or Number)
886 let fc = arg[0];
887 if (fc === '-') {
888 fc = arg[1];
889 }
890 if (fc >= '0' && fc <= '9') {
891 fc = '#';
892 }
893 switch(fc) {
894 case "'":
895 case '"':
896 a.value = arg.slice(1, -1);
897 a.literal = true;
898 break;
899 case '#':
900 a.value = Number(arg);
901 a.literal = true;
902 break;
903 }
904 // if not literal, look for structured path
905 if (!a.literal) {
906 a.rootProperty = Polymer.Path.root(arg);
907 // detect structured path (has dots)
908 a.structured = Polymer.Path.isPath(arg);
909 if (a.structured) {
910 a.wildcard = (arg.slice(-2) == '.*');
911 if (a.wildcard) {
912 a.name = arg.slice(0, -2);
913 }
914 }
915 }
916 return a;
917 }
918
919 /**
920 * Gather the argument values for a method specified in the provided array
921 * of argument metadata.
922 *
923 * The `path` and `value` arguments are used to fill in wildcard descriptor
924 * when the method is being called as a result of a path notification.
925 *
926 * @param {Object} data Instance data storage object to read properties from
927 * @param {Array<Object>} args Array of argument metadata
928 * @param {string} path Property/path name that triggered the method effect
929 * @param {Object} props Bag of current property changes
930 * @return {Array<*>} Array of argument values
931 * @private
932 */
933 function marshalArgs(data, args, path, props) {
934 let values = [];
935 for (let i=0, l=args.length; i<l; i++) {
936 let arg = args[i];
937 let name = arg.name;
938 let v;
939 if (arg.literal) {
940 v = arg.value;
941 } else {
942 if (arg.structured) {
943 v = Polymer.Path.get(data, name);
944 // when data is not stored e.g. `splices`
945 if (v === undefined) {
946 v = props[name];
947 }
948 } else {
949 v = data[name];
950 }
951 }
952 if (arg.wildcard) {
953 // Only send the actual path changed info if the change that
954 // caused the observer to run matched the wildcard
955 let baseChanged = (name.indexOf(path + '.') === 0);
956 let matches = (path.indexOf(name) === 0 && !baseChanged);
957 values[i] = {
958 path: matches ? path : name,
959 value: matches ? props[path] : v,
960 base: v
961 };
962 } else {
963 values[i] = v;
964 }
965 }
966 return values;
967 }
968
969 // data api
970
971 /**
972 * Sends array splice notifications (`.splices` and `.length`)
973 *
974 * Note: this implementation only accepts normalized paths
975 *
976 * @param {Element} inst Instance to send notifications to
977 * @param {Array} array The array the mutations occurred on
978 * @param {string} path The path to the array that was mutated
979 * @param {Array} splices Array of splice records
980 * @private
981 */
982 function notifySplices(inst, array, path, splices) {
983 let splicesPath = path + '.splices';
984 inst.notifyPath(splicesPath, { indexSplices: splices });
985 inst.notifyPath(path + '.length', array.length);
986 // Null here to allow potentially large splice records to be GC'ed.
987 inst.__data[splicesPath] = {indexSplices: null};
988 }
989
990 /**
991 * Creates a splice record and sends an array splice notification for
992 * the described mutation
993 *
994 * Note: this implementation only accepts normalized paths
995 *
996 * @param {Element} inst Instance to send notifications to
997 * @param {Array} array The array the mutations occurred on
998 * @param {string} path The path to the array that was mutated
999 * @param {number} index Index at which the array mutation occurred
1000 * @param {number} addedCount Number of added items
1001 * @param {Array} removed Array of removed items
1002 * @private
1003 */
1004 function notifySplice(inst, array, path, index, addedCount, removed) {
1005 notifySplices(inst, array, path, [{
1006 index: index,
1007 addedCount: addedCount,
1008 removed: removed,
1009 object: array,
1010 type: 'splice'
1011 }]);
1012 }
1013
1014 /**
1015 * Returns an upper-cased version of the string.
1016 *
1017 * @param {string} name String to uppercase
1018 * @return {string} Uppercased string
1019 * @private
1020 */
1021 function upper(name) {
1022 return name[0].toUpperCase() + name.substring(1);
1023 }
1024
1025 /**
1026 * Element class mixin that provides meta-programming for Polymer's template
1027 * binding and data observation (collectively, "property effects") system.
1028 *
1029 * This mixin uses provides the following key static methods for adding
1030 * property effects to an element class:
1031 * - `addPropertyEffect`
1032 * - `createPropertyObserver`
1033 * - `createMethodObserver`
1034 * - `createNotifyingProperty`
1035 * - `createReadOnlyProperty`
1036 * - `createReflectedProperty`
1037 * - `createComputedProperty`
1038 * - `bindTemplate`
1039 *
1040 * Each method creates one or more property accessors, along with metadata
1041 * used by this mixin's implementation of `_propertiesChanged` to perform
1042 * the property effects.
1043 *
1044 * Underscored versions of the above methods also exist on the element
1045 * prototype for adding property effects on instances at runtime.
1046 *
1047 * Note that this mixin overrides several `PropertyAccessors` methods, in
1048 * many cases to maintain guarantees provided by the Polymer 1.x features;
1049 * notably it changes property accessors to be synchronous by default
1050 * whereas the default when using `PropertyAccessors` standalone is to be
1051 * async by default.
1052 *
1053 * @polymerMixin
1054 * @mixes Polymer.TemplateStamp
1055 * @mixes Polymer.PropertyAccessors
1056 * @memberof Polymer
1057 * @summary Element class mixin that provides meta-programming for Polymer's
1058 * template binding and data observation system.
1059 */
1060 Polymer.PropertyEffects = Polymer.dedupingMixin(superClass => {
1061
1062 /**
1063 * @constructor
1064 * @extends {superClass}
1065 * @implements {Polymer_PropertyAccessors}
1066 * @implements {Polymer_TemplateStamp}
1067 */
1068 const propertyEffectsBase = Polymer.TemplateStamp(Polymer.PropertyAccessors( superClass));
1069
1070 /**
1071 * @polymerMixinClass
1072 * @unrestricted
1073 * @implements {Polymer_PropertyEffects}
1074 */
1075 class PropertyEffects extends propertyEffectsBase {
1076
1077 get PROPERTY_EFFECT_TYPES() {
1078 return TYPES;
1079 }
1080
1081 /**
1082 * Overrides `Polymer.PropertyAccessors` implementation to initialize
1083 * additional property-effect related properties.
1084 *
1085 * @override
1086 */
1087 _initializeProperties() {
1088 super._initializeProperties();
1089 hostStack.registerHost(this);
1090 this.__dataClientsReady = false;
1091 this.__dataPendingClients = null;
1092 this.__dataToNotify = null;
1093 this.__dataLinkedPaths = null;
1094 this.__dataHasPaths = false;
1095 // May be set on instance prior to upgrade
1096 this.__dataCompoundStorage = this.__dataCompoundStorage || null;
1097 this.__dataHost = this.__dataHost || null;
1098 this.__dataTemp = {};
1099 }
1100
1101 /**
1102 * Overrides `Polymer.PropertyAccessors` implementation to provide a
1103 * more efficient implementation of initializing properties from
1104 * the prototype on the instance.
1105 *
1106 * @override
1107 */
1108 _initializeProtoProperties(props) {
1109 this.__data = Object.create(props);
1110 this.__dataPending = Object.create(props);
1111 this.__dataOld = {};
1112 }
1113
1114 /**
1115 * Overrides `Polymer.PropertyAccessors` implementation to avoid setting
1116 * `_setProperty`'s `shouldNotify: true`.
1117 *
1118 * @override
1119 */
1120 _initializeInstanceProperties(props) {
1121 let readOnly = this.__readOnly;
1122 for (let prop in props) {
1123 if (!readOnly || !readOnly[prop]) {
1124 this.__dataPending = this.__dataPending || {};
1125 this.__dataOld = this.__dataOld || {};
1126 this.__data[prop] = this.__dataPending[prop] = props[prop];
1127 }
1128 }
1129 }
1130
1131 // Prototype setup ----------------------------------------
1132
1133 /**
1134 * Equivalent to static `addPropertyEffect` API but can be called on
1135 * an instance to add effects at runtime. See that method for
1136 * full API docs.
1137 *
1138 * @param {string} property Property that should trigger the effect
1139 * @param {string} type Effect type, from this.PROPERTY_EFFECT_TYPES
1140 * @param {Object=} effect Effect metadata object
1141 * @protected
1142 */
1143 _addPropertyEffect(property, type, effect) {
1144 this._createPropertyAccessor(property, type == TYPES.READ_ONLY);
1145 // effects are accumulated into arrays per property based on type
1146 let effects = ensureOwnEffectMap(this, type)[property];
1147 if (!effects) {
1148 effects = this[type][property] = [];
1149 }
1150 effects.push(effect);
1151 }
1152
1153 /**
1154 * Removes the given property effect.
1155 *
1156 * @param {string} property Property the effect was associated with
1157 * @param {string} type Effect type, from this.PROPERTY_EFFECT_TYPES
1158 * @param {Object=} effect Effect metadata object to remove
1159 */
1160 _removePropertyEffect(property, type, effect) {
1161 let effects = ensureOwnEffectMap(this, type)[property];
1162 let idx = effects.indexOf(effect);
1163 if (idx >= 0) {
1164 effects.splice(idx, 1);
1165 }
1166 }
1167
1168 /**
1169 * Returns whether the current prototype/instance has a property effect
1170 * of a certain type.
1171 *
1172 * @param {string} property Property name
1173 * @param {string=} type Effect type, from this.PROPERTY_EFFECT_TYPES
1174 * @return {boolean} True if the prototype/instance has an effect of this type
1175 * @protected
1176 */
1177 _hasPropertyEffect(property, type) {
1178 let effects = this[type];
1179 return Boolean(effects && effects[property]);
1180 }
1181
1182 /**
1183 * Returns whether the current prototype/instance has a "read only"
1184 * accessor for the given property.
1185 *
1186 * @param {string} property Property name
1187 * @return {boolean} True if the prototype/instance has an effect of this type
1188 * @protected
1189 */
1190 _hasReadOnlyEffect(property) {
1191 return this._hasPropertyEffect(property, TYPES.READ_ONLY);
1192 }
1193
1194 /**
1195 * Returns whether the current prototype/instance has a "notify"
1196 * property effect for the given property.
1197 *
1198 * @param {string} property Property name
1199 * @return {boolean} True if the prototype/instance has an effect of this type
1200 * @protected
1201 */
1202 _hasNotifyEffect(property) {
1203 return this._hasPropertyEffect(property, TYPES.NOTIFY);
1204 }
1205
1206 /**
1207 * Returns whether the current prototype/instance has a "reflect to attrib ute"
1208 * property effect for the given property.
1209 *
1210 * @param {string} property Property name
1211 * @return {boolean} True if the prototype/instance has an effect of this type
1212 * @protected
1213 */
1214 _hasReflectEffect(property) {
1215 return this._hasPropertyEffect(property, TYPES.REFLECT);
1216 }
1217
1218 /**
1219 * Returns whether the current prototype/instance has a "computed"
1220 * property effect for the given property.
1221 *
1222 * @param {string} property Property name
1223 * @return {boolean} True if the prototype/instance has an effect of this type
1224 * @protected
1225 */
1226 _hasComputedEffect(property) {
1227 return this._hasPropertyEffect(property, TYPES.COMPUTE);
1228 }
1229
1230 // Runtime ----------------------------------------
1231
1232 /**
1233 * Sets a pending property or path. If the root property of the path in
1234 * question had no accessor, the path is set, otherwise it is enqueued
1235 * via `_setPendingProperty`.
1236 *
1237 * This function isolates relatively expensive functionality necessary
1238 * for the public API (`set`, `setProperties`, `notifyPath`, and property
1239 * change listeners via {{...}} bindings), such that it is only done
1240 * when paths enter the system, and not at every propagation step. It
1241 * also sets a `__dataHasPaths` flag on the instance which is used to
1242 * fast-path slower path-matching code in the property effects host paths.
1243 *
1244 * `path` can be a path string or array of path parts as accepted by the
1245 * public API.
1246 *
1247 * @param {string | !Array<number|string>} path Path to set
1248 * @param {*} value Value to set
1249 * @param {boolean=} shouldNotify Set to true if this change should
1250 * cause a property notification event dispatch
1251 * @param {boolean=} isPathNotification If the path being set is a path
1252 * notification of an already changed value, as opposed to a request
1253 * to set and notify the change. In the latter `false` case, a dirty
1254 * check is performed and then the value is set to the path before
1255 * enqueuing the pending property change.
1256 * @return {boolean} Returns true if the property/path was enqueued in
1257 * the pending changes bag.
1258 * @protected
1259 */
1260 _setPendingPropertyOrPath(path, value, shouldNotify, isPathNotification) {
1261 if (isPathNotification ||
1262 Polymer.Path.root(Array.isArray(path) ? path[0] : path) !== path) {
1263 // Dirty check changes being set to a path against the actual object,
1264 // since this is the entry point for paths into the system; from here
1265 // the only dirty checks are against the `__dataTemp` cache to prevent
1266 // duplicate work in the same turn only. Note, if this was a notificat ion
1267 // of a change already set to a path (isPathNotification: true),
1268 // we always let the change through and skip the `set` since it was
1269 // already dirty checked at the point of entry and the underlying
1270 // object has already been updated
1271 if (!isPathNotification) {
1272 let old = Polymer.Path.get(this, path);
1273 path = /** @type {string} */ (Polymer.Path.set(this, path, value));
1274 // Use property-accessor's simpler dirty check
1275 if (!path || !super._shouldPropertyChange(path, value, old)) {
1276 return false;
1277 }
1278 }
1279 this.__dataHasPaths = true;
1280 if (this._setPendingProperty(path, value, shouldNotify)) {
1281 computeLinkedPaths(this, path, value);
1282 return true;
1283 }
1284 } else {
1285 if (this.__dataHasAccessor && this.__dataHasAccessor[path]) {
1286 return this._setPendingProperty(path, value, shouldNotify);
1287 } else {
1288 this[path] = value;
1289 }
1290 }
1291 return false;
1292 }
1293
1294 /**
1295 * Applies a value to a non-Polymer element/node's property.
1296 *
1297 * The implementation makes a best-effort at binding interop:
1298 * Some native element properties have side-effects when
1299 * re-setting the same value (e.g. setting `<input>.value` resets the
1300 * cursor position), so we do a dirty-check before setting the value.
1301 * However, for better interop with non-Polymer custom elements that
1302 * accept objects, we explicitly re-set object changes coming from the
1303 * Polymer world (which may include deep object changes without the
1304 * top reference changing), erring on the side of providing more
1305 * information.
1306 *
1307 * Users may override this method to provide alternate approaches.
1308 *
1309 * @param {Node} node The node to set a property on
1310 * @param {string} prop The property to set
1311 * @param {*} value The value to set
1312 * @protected
1313 */
1314 _setUnmanagedPropertyToNode(node, prop, value) {
1315 // It is a judgment call that resetting primitives is
1316 // "bad" and resettings objects is also "good"; alternatively we could
1317 // implement a whitelist of tag & property values that should never
1318 // be reset (e.g. <input>.value && <select>.value)
1319 if (value !== node[prop] || typeof value == 'object') {
1320 node[prop] = value;
1321 }
1322 }
1323
1324 /**
1325 * Overrides the `PropertyAccessors` implementation to introduce special
1326 * dirty check logic depending on the property & value being set:
1327 *
1328 * 1. Any value set to a path (e.g. 'obj.prop': 42 or 'obj.prop': {...})
1329 * Stored in `__dataTemp`, dirty checked against `__dataTemp`
1330 * 2. Object set to simple property (e.g. 'prop': {...})
1331 * Stored in `__dataTemp` and `__data`, dirty checked against
1332 * `__dataTemp` by default implementation of `_shouldPropertyChange`
1333 * 3. Primitive value set to simple property (e.g. 'prop': 42)
1334 * Stored in `__data`, dirty checked against `__data`
1335 *
1336 * The dirty-check is important to prevent cycles due to two-way
1337 * notification, but paths and objects are only dirty checked against any
1338 * previous value set during this turn via a "temporary cache" that is
1339 * cleared when the last `_propertiesChaged` exits. This is so:
1340 * a. any cached array paths (e.g. 'array.3.prop') may be invalidated
1341 * due to array mutations like shift/unshift/splice; this is fine
1342 * since path changes are dirty-checked at user entry points like `set`
1343 * b. dirty-checking for objects only lasts one turn to allow the user
1344 * to mutate the object in-place and re-set it with the same identity
1345 * and have all sub-properties re-propagated in a subsequent turn.
1346 *
1347 * The temp cache is not necessarily sufficient to prevent invalid array
1348 * paths, since a splice can happen during the same turn (with pathologica l
1349 * user code); we could introduce a "fixup" for temporarily cached array
1350 * paths if needed: https://github.com/Polymer/polymer/issues/4227
1351 *
1352 * @override
1353 */
1354 _setPendingProperty(property, value, shouldNotify) {
1355 let isPath = this.__dataHasPaths && Polymer.Path.isPath(property);
1356 let prevProps = isPath ? this.__dataTemp : this.__data;
1357 if (this._shouldPropertyChange(property, value, prevProps[property])) {
1358 if (!this.__dataPending) {
1359 this.__dataPending = {};
1360 this.__dataOld = {};
1361 }
1362 // Ensure old is captured from the last turn
1363 if (!(property in this.__dataOld)) {
1364 this.__dataOld[property] = this.__data[property];
1365 }
1366 // Paths are stored in temporary cache (cleared at end of turn),
1367 // which is used for dirty-checking, all others stored in __data
1368 if (isPath) {
1369 this.__dataTemp[property] = value;
1370 } else {
1371 this.__data[property] = value;
1372 }
1373 // All changes go into pending property bag, passed to _propertiesChan ged
1374 this.__dataPending[property] = value;
1375 // Track properties that should notify separately
1376 if (isPath || (this.__notifyEffects && this.__notifyEffects[property]) ) {
1377 this.__dataToNotify = this.__dataToNotify || {};
1378 this.__dataToNotify[property] = shouldNotify;
1379 }
1380 return true;
1381 }
1382 }
1383
1384 /**
1385 * Overrides base implementation to ensure all accessors set `shouldNotify `
1386 * to true, for per-property notification tracking.
1387 *
1388 * @override
1389 */
1390 _setProperty(property, value) {
1391 if (this._setPendingProperty(property, value, true)) {
1392 this._invalidateProperties();
1393 }
1394 }
1395
1396 /**
1397 * Overrides `PropertyAccessor`'s default async queuing of
1398 * `_propertiesChanged`: if `__dataReady` is false (has not yet been
1399 * manually flushed), the function no-ops; otherwise flushes
1400 * `_propertiesChanged` synchronously.
1401 *
1402 * @override
1403 */
1404 _invalidateProperties() {
1405 if (this.__dataReady) {
1406 this._flushProperties();
1407 }
1408 }
1409
1410 /**
1411 * Enqueues the given client on a list of pending clients, whose
1412 * pending property changes can later be flushed via a call to
1413 * `_flushClients`.
1414 *
1415 * @param {Object} client PropertyEffects client to enqueue
1416 * @protected
1417 */
1418 _enqueueClient(client) {
1419 this.__dataPendingClients = this.__dataPendingClients || [];
1420 if (client !== this) {
1421 this.__dataPendingClients.push(client);
1422 }
1423 }
1424
1425 /**
1426 * Flushes any clients previously enqueued via `_enqueueClient`, causing
1427 * their `_flushProperties` method to run.
1428 *
1429 * @protected
1430 */
1431 _flushClients() {
1432 if (!this.__dataClientsReady) {
1433 this.__dataClientsReady = true;
1434 this._readyClients();
1435 // Override point where accessors are turned on; importantly,
1436 // this is after clients have fully readied, providing a guarantee
1437 // that any property effects occur only after all clients are ready.
1438 this.__dataReady = true;
1439 } else {
1440 this.__enableOrFlushClients();
1441 }
1442 }
1443
1444 // NOTE: We ensure clients either enable or flush as appropriate. This
1445 // handles two corner cases:
1446 // (1) clients flush properly when connected/enabled before the host
1447 // enables; e.g.
1448 // (a) Templatize stamps with no properties and does not flush and
1449 // (b) the instance is inserted into dom and
1450 // (c) then the instance flushes.
1451 // (2) clients enable properly when not connected/enabled when the host
1452 // flushes; e.g.
1453 // (a) a template is runtime stamped and not yet connected/enabled
1454 // (b) a host sets a property, causing stamped dom to flush
1455 // (c) the stamped dom enables.
1456 __enableOrFlushClients() {
1457 let clients = this.__dataPendingClients;
1458 if (clients) {
1459 this.__dataPendingClients = null;
1460 for (let i=0; i < clients.length; i++) {
1461 let client = clients[i];
1462 if (!client.__dataEnabled) {
1463 client._enableProperties();
1464 } else if (client.__dataPending) {
1465 client._flushProperties();
1466 }
1467 }
1468 }
1469 }
1470
1471 /**
1472 * Perform any initial setup on client dom. Called before the first
1473 * `_flushProperties` call on client dom and before any element
1474 * observers are called.
1475 *
1476 * @protected
1477 */
1478 _readyClients() {
1479 this.__enableOrFlushClients();
1480 }
1481
1482 /**
1483 * Sets a bag of property changes to this instance, and
1484 * synchronously processes all effects of the properties as a batch.
1485 *
1486 * Property names must be simple properties, not paths. Batched
1487 * path propagation is not supported.
1488 *
1489 * @param {Object} props Bag of one or more key-value pairs whose key is
1490 * a property and value is the new value to set for that property.
1491 * @param {boolean=} setReadOnly When true, any private values set in
1492 * `props` will be set. By default, `setProperties` will not set
1493 * `readOnly: true` root properties.
1494 * @public
1495 */
1496 setProperties(props, setReadOnly) {
1497 for (let path in props) {
1498 if (setReadOnly || !this.__readOnly || !this.__readOnly[path]) {
1499 //TODO(kschaaf): explicitly disallow paths in setProperty?
1500 // wildcard observers currently only pass the first changed path
1501 // in the `info` object, and you could do some odd things batching
1502 // paths, e.g. {'foo.bar': {...}, 'foo': null}
1503 this._setPendingPropertyOrPath(path, props[path], true);
1504 }
1505 }
1506 this._invalidateProperties();
1507 }
1508
1509 /**
1510 * Overrides `PropertyAccessors` so that property accessor
1511 * side effects are not enabled until after client dom is fully ready.
1512 * Also calls `_flushClients` callback to ensure client dom is enabled
1513 * that was not enabled as a result of flushing properties.
1514 *
1515 * @override
1516 */
1517 ready() {
1518 // It is important that `super.ready()` is not called here as it
1519 // immediately turns on accessors. Instead, we wait until `readyClients`
1520 // to enable accessors to provide a guarantee that clients are ready
1521 // before processing any accessors side effects.
1522 this._flushProperties();
1523 // If no data was pending, `_flushProperties` will not `flushClients`
1524 // so ensure this is done.
1525 if (!this.__dataClientsReady) {
1526 this._flushClients();
1527 }
1528 // Before ready, client notifications do not trigger _flushProperties.
1529 // Therefore a flush is necessary here if data has been set.
1530 if (this.__dataPending) {
1531 this._flushProperties();
1532 }
1533 }
1534
1535 /**
1536 * Implements `PropertyAccessors`'s properties changed callback.
1537 *
1538 * Runs each class of effects for the batch of changed properties in
1539 * a specific order (compute, propagate, reflect, observe, notify).
1540 *
1541 * @override
1542 */
1543 _propertiesChanged(currentProps, changedProps, oldProps) {
1544 // ----------------------------
1545 // let c = Object.getOwnPropertyNames(changedProps || {});
1546 // window.debug && console.group(this.localName + '#' + this.id + ': ' + c);
1547 // if (window.debug) { debugger; }
1548 // ----------------------------
1549 let hasPaths = this.__dataHasPaths;
1550 this.__dataHasPaths = false;
1551 // Compute properties
1552 runComputedEffects(this, changedProps, oldProps, hasPaths);
1553 // Clear notify properties prior to possible reentry (propagate, observe ),
1554 // but after computing effects have a chance to add to them
1555 let notifyProps = this.__dataToNotify;
1556 this.__dataToNotify = null;
1557 // Propagate properties to clients
1558 this._propagatePropertyChanges(changedProps, oldProps, hasPaths);
1559 // Flush clients
1560 this._flushClients();
1561 // Reflect properties
1562 runEffects(this, this.__reflectEffects, changedProps, oldProps, hasPaths );
1563 // Observe properties
1564 runEffects(this, this.__observeEffects, changedProps, oldProps, hasPaths );
1565 // Notify properties to host
1566 if (notifyProps) {
1567 runNotifyEffects(this, notifyProps, changedProps, oldProps, hasPaths);
1568 }
1569 // Clear temporary cache at end of turn
1570 if (this.__dataCounter == 1) {
1571 this.__dataTemp = {};
1572 }
1573 // ----------------------------
1574 // window.debug && console.groupEnd(this.localName + '#' + this.id + ': ' + c);
1575 // ----------------------------
1576 }
1577
1578 /**
1579 * Called to propagate any property changes to stamped template nodes
1580 * managed by this element.
1581 *
1582 * @param {Object} changedProps Bag of changed properties
1583 * @param {Object} oldProps Bag of previous values for changed properties
1584 * @param {boolean} hasPaths True with `props` contains one or more paths
1585 * @protected
1586 */
1587 _propagatePropertyChanges(changedProps, oldProps, hasPaths) {
1588 if (this.__propagateEffects) {
1589 runEffects(this, this.__propagateEffects, changedProps, oldProps, hasP aths);
1590 }
1591 let templateInfo = this.__templateInfo;
1592 while (templateInfo) {
1593 runEffects(this, templateInfo.propertyEffects, changedProps, oldProps,
1594 hasPaths, templateInfo.nodeList);
1595 templateInfo = templateInfo.nextTemplateInfo;
1596 }
1597 }
1598
1599 /**
1600 * Aliases one data path as another, such that path notifications from one
1601 * are routed to the other.
1602 *
1603 * @param {string | !Array<string|number>} to Target path to link.
1604 * @param {string | !Array<string|number>} from Source path to link.
1605 * @public
1606 */
1607 linkPaths(to, from) {
1608 to = Polymer.Path.normalize(to);
1609 from = Polymer.Path.normalize(from);
1610 this.__dataLinkedPaths = this.__dataLinkedPaths || {};
1611 this.__dataLinkedPaths[to] = from;
1612 }
1613
1614 /**
1615 * Removes a data path alias previously established with `_linkPaths`.
1616 *
1617 * Note, the path to unlink should be the target (`to`) used when
1618 * linking the paths.
1619 *
1620 * @param {string | !Array<string|number>} path Target path to unlink.
1621 * @public
1622 */
1623 unlinkPaths(path) {
1624 path = Polymer.Path.normalize(path);
1625 if (this.__dataLinkedPaths) {
1626 delete this.__dataLinkedPaths[path];
1627 }
1628 }
1629
1630 /**
1631 * Notify that an array has changed.
1632 *
1633 * Example:
1634 *
1635 * this.items = [ {name: 'Jim'}, {name: 'Todd'}, {name: 'Bill'} ];
1636 * ...
1637 * this.items.splice(1, 1, {name: 'Sam'});
1638 * this.items.push({name: 'Bob'});
1639 * this.notifySplices('items', [
1640 * { index: 1, removed: [{name: 'Todd'}], addedCount: 1, obect: this .items, type: 'splice' },
1641 * { index: 3, removed: [], addedCount: 1, object: this.items, type: 'splice'}
1642 * ]);
1643 *
1644 * @param {string} path Path that should be notified.
1645 * @param {Array} splices Array of splice records indicating ordered
1646 * changes that occurred to the array. Each record should have the
1647 * following fields:
1648 * * index: index at which the change occurred
1649 * * removed: array of items that were removed from this index
1650 * * addedCount: number of new items added at this index
1651 * * object: a reference to the array in question
1652 * * type: the string literal 'splice'
1653 *
1654 * Note that splice records _must_ be normalized such that they are
1655 * reported in index order (raw results from `Object.observe` are not
1656 * ordered and must be normalized/merged before notifying).
1657 * @public
1658 */
1659 notifySplices(path, splices) {
1660 let info = {};
1661 let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
1662 notifySplices(this, array, info.path, splices);
1663 }
1664
1665 /**
1666 * Convenience method for reading a value from a path.
1667 *
1668 * Note, if any part in the path is undefined, this method returns
1669 * `undefined` (this method does not throw when dereferencing undefined
1670 * paths).
1671 *
1672 * @param {(string|!Array<(string|number)>)} path Path to the value
1673 * to read. The path may be specified as a string (e.g. `foo.bar.baz`)
1674 * or an array of path parts (e.g. `['foo.bar', 'baz']`). Note that
1675 * bracketed expressions are not supported; string-based path parts
1676 * *must* be separated by dots. Note that when dereferencing array
1677 * indices, the index may be used as a dotted part directly
1678 * (e.g. `users.12.name` or `['users', 12, 'name']`).
1679 * @param {Object=} root Root object from which the path is evaluated.
1680 * @return {*} Value at the path, or `undefined` if any part of the path
1681 * is undefined.
1682 * @public
1683 */
1684 get(path, root) {
1685 return Polymer.Path.get(root || this, path);
1686 }
1687
1688 /**
1689 * Convenience method for setting a value to a path and notifying any
1690 * elements bound to the same path.
1691 *
1692 * Note, if any part in the path except for the last is undefined,
1693 * this method does nothing (this method does not throw when
1694 * dereferencing undefined paths).
1695 *
1696 * @param {(string|!Array<(string|number)>)} path Path to the value
1697 * to write. The path may be specified as a string (e.g. `'foo.bar.baz' `)
1698 * or an array of path parts (e.g. `['foo.bar', 'baz']`). Note that
1699 * bracketed expressions are not supported; string-based path parts
1700 * *must* be separated by dots. Note that when dereferencing array
1701 * indices, the index may be used as a dotted part directly
1702 * (e.g. `'users.12.name'` or `['users', 12, 'name']`).
1703 * @param {*} value Value to set at the specified path.
1704 * @param {Object=} root Root object from which the path is evaluated.
1705 * When specified, no notification will occur.
1706 * @public
1707 */
1708 set(path, value, root) {
1709 if (root) {
1710 Polymer.Path.set(root, path, value);
1711 } else {
1712 if (!this.__readOnly || !this.__readOnly[/** @type {string} */(path)]) {
1713 if (this._setPendingPropertyOrPath(path, value, true)) {
1714 this._invalidateProperties();
1715 }
1716 }
1717 }
1718 }
1719
1720 /**
1721 * Adds items onto the end of the array at the path specified.
1722 *
1723 * The arguments after `path` and return value match that of
1724 * `Array.prototype.push`.
1725 *
1726 * This method notifies other paths to the same array that a
1727 * splice occurred to the array.
1728 *
1729 * @param {string} path Path to array.
1730 * @param {...*} items Items to push onto array
1731 * @return {number} New length of the array.
1732 * @public
1733 */
1734 push(path, ...items) {
1735 let info = {};
1736 let array = /** @type {Array}*/(Polymer.Path.get(this, path, info));
1737 let len = array.length;
1738 let ret = array.push(...items);
1739 if (items.length) {
1740 notifySplice(this, array, info.path, len, items.length, []);
1741 }
1742 return ret;
1743 }
1744
1745 /**
1746 * Removes an item from the end of array at the path specified.
1747 *
1748 * The arguments after `path` and return value match that of
1749 * `Array.prototype.pop`.
1750 *
1751 * This method notifies other paths to the same array that a
1752 * splice occurred to the array.
1753 *
1754 * @param {string} path Path to array.
1755 * @return {*} Item that was removed.
1756 * @public
1757 */
1758 pop(path) {
1759 let info = {};
1760 let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
1761 let hadLength = Boolean(array.length);
1762 let ret = array.pop();
1763 if (hadLength) {
1764 notifySplice(this, array, info.path, array.length, 0, [ret]);
1765 }
1766 return ret;
1767 }
1768
1769 /**
1770 * Starting from the start index specified, removes 0 or more items
1771 * from the array and inserts 0 or more new items in their place.
1772 *
1773 * The arguments after `path` and return value match that of
1774 * `Array.prototype.splice`.
1775 *
1776 * This method notifies other paths to the same array that a
1777 * splice occurred to the array.
1778 *
1779 * @param {string} path Path to array.
1780 * @param {number} start Index from which to start removing/inserting.
1781 * @param {number} deleteCount Number of items to remove.
1782 * @param {...*} items Items to insert into array.
1783 * @return {Array} Array of removed items.
1784 * @public
1785 */
1786 splice(path, start, deleteCount, ...items) {
1787 let info = {};
1788 let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
1789 // Normalize fancy native splice handling of crazy start values
1790 if (start < 0) {
1791 start = array.length - Math.floor(-start);
1792 } else {
1793 start = Math.floor(start);
1794 }
1795 if (!start) {
1796 start = 0;
1797 }
1798 let ret = array.splice(start, deleteCount, ...items);
1799 if (items.length || ret.length) {
1800 notifySplice(this, array, info.path, start, items.length, ret);
1801 }
1802 return ret;
1803 }
1804
1805 /**
1806 * Removes an item from the beginning of array at the path specified.
1807 *
1808 * The arguments after `path` and return value match that of
1809 * `Array.prototype.pop`.
1810 *
1811 * This method notifies other paths to the same array that a
1812 * splice occurred to the array.
1813 *
1814 * @param {string} path Path to array.
1815 * @return {*} Item that was removed.
1816 * @public
1817 */
1818 shift(path) {
1819 let info = {};
1820 let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
1821 let hadLength = Boolean(array.length);
1822 let ret = array.shift();
1823 if (hadLength) {
1824 notifySplice(this, array, info.path, 0, 0, [ret]);
1825 }
1826 return ret;
1827 }
1828
1829 /**
1830 * Adds items onto the beginning of the array at the path specified.
1831 *
1832 * The arguments after `path` and return value match that of
1833 * `Array.prototype.push`.
1834 *
1835 * This method notifies other paths to the same array that a
1836 * splice occurred to the array.
1837 *
1838 * @param {string} path Path to array.
1839 * @param {...*} items Items to insert info array
1840 * @return {number} New length of the array.
1841 * @public
1842 */
1843 unshift(path, ...items) {
1844 let info = {};
1845 let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
1846 let ret = array.unshift(...items);
1847 if (items.length) {
1848 notifySplice(this, array, info.path, 0, items.length, []);
1849 }
1850 return ret;
1851 }
1852
1853 /**
1854 * Notify that a path has changed.
1855 *
1856 * Example:
1857 *
1858 * this.item.user.name = 'Bob';
1859 * this.notifyPath('item.user.name');
1860 *
1861 * @param {string} path Path that should be notified.
1862 * @param {*=} value Value at the path (optional).
1863 * @public
1864 */
1865 notifyPath(path, value) {
1866 /** @type {string} */
1867 let propPath;
1868 if (arguments.length == 1) {
1869 // Get value if not supplied
1870 let info = {};
1871 value = Polymer.Path.get(this, path, info);
1872 propPath = info.path;
1873 } else if (Array.isArray(path)) {
1874 // Normalize path if needed
1875 propPath = Polymer.Path.normalize(path);
1876 } else {
1877 propPath = /** @type{string} */(path);
1878 }
1879 if (this._setPendingPropertyOrPath(propPath, value, true, true)) {
1880 this._invalidateProperties();
1881 }
1882 }
1883
1884 /**
1885 * Equivalent to static `createReadOnlyProperty` API but can be called on
1886 * an instance to add effects at runtime. See that method for
1887 * full API docs.
1888 *
1889 * @param {string} property Property name
1890 * @param {boolean=} protectedSetter Creates a custom protected setter
1891 * when `true`.
1892 * @protected
1893 */
1894 _createReadOnlyProperty(property, protectedSetter) {
1895 this._addPropertyEffect(property, TYPES.READ_ONLY);
1896 if (protectedSetter) {
1897 this['_set' + upper(property)] = function(value) {
1898 this._setProperty(property, value);
1899 }
1900 }
1901 }
1902
1903 /**
1904 * Equivalent to static `createPropertyObserver` API but can be called on
1905 * an instance to add effects at runtime. See that method for
1906 * full API docs.
1907 *
1908 * @param {string} property Property name
1909 * @param {string} methodName Name of observer method to call
1910 * @param {boolean=} dynamicFn Whether the method name should be included as
1911 * a dependency to the effect.
1912 * @protected
1913 */
1914 _createPropertyObserver(property, methodName, dynamicFn) {
1915 let info = { property, methodName, dynamicFn };
1916 this._addPropertyEffect(property, TYPES.OBSERVE, {
1917 fn: runObserverEffect, info, trigger: {name: property}
1918 });
1919 if (dynamicFn) {
1920 this._addPropertyEffect(methodName, TYPES.OBSERVE, {
1921 fn: runObserverEffect, info, trigger: {name: methodName}
1922 });
1923 }
1924 }
1925
1926 /**
1927 * Equivalent to static `createMethodObserver` API but can be called on
1928 * an instance to add effects at runtime. See that method for
1929 * full API docs.
1930 *
1931 * @param {string} expression Method expression
1932 * @param {boolean|Object=} dynamicFn Boolean or object map indicating
1933 * whether method names should be included as a dependency to the effect .
1934 * @protected
1935 */
1936 _createMethodObserver(expression, dynamicFn) {
1937 let sig = parseMethod(expression);
1938 if (!sig) {
1939 throw new Error("Malformed observer expression '" + expression + "'");
1940 }
1941 createMethodEffect(this, sig, TYPES.OBSERVE, runMethodEffect, null, dyna micFn);
1942 }
1943
1944 /**
1945 * Equivalent to static `createNotifyingProperty` API but can be called on
1946 * an instance to add effects at runtime. See that method for
1947 * full API docs.
1948 *
1949 * @param {string} property Property name
1950 * @protected
1951 */
1952 _createNotifyingProperty(property) {
1953 this._addPropertyEffect(property, TYPES.NOTIFY, {
1954 fn: runNotifyEffect,
1955 info: {
1956 eventName: CaseMap.camelToDashCase(property) + '-changed',
1957 property: property
1958 }
1959 });
1960 }
1961
1962 /**
1963 * Equivalent to static `createReflectedProperty` API but can be called on
1964 * an instance to add effects at runtime. See that method for
1965 * full API docs.
1966 *
1967 * @param {string} property Property name
1968 * @protected
1969 */
1970 _createReflectedProperty(property) {
1971 let attr = CaseMap.camelToDashCase(property);
1972 if (attr[0] === '-') {
1973 console.warn('Property ' + property + ' cannot be reflected to attribu te ' +
1974 attr + ' because "-" is not a valid starting attribute name. Use a l owercase first letter for the property thisead.');
1975 } else {
1976 this._addPropertyEffect(property, TYPES.REFLECT, {
1977 fn: runReflectEffect,
1978 info: {
1979 attrName: attr
1980 }
1981 });
1982 }
1983 }
1984
1985 /**
1986 * Equivalent to static `createComputedProperty` API but can be called on
1987 * an instance to add effects at runtime. See that method for
1988 * full API docs.
1989 *
1990 * @param {string} property Name of computed property to set
1991 * @param {string} expression Method expression
1992 * @param {boolean|Object=} dynamicFn Boolean or object map indicating
1993 * whether method names should be included as a dependency to the effect .
1994 * @protected
1995 */
1996 _createComputedProperty(property, expression, dynamicFn) {
1997 let sig = parseMethod(expression);
1998 if (!sig) {
1999 throw new Error("Malformed computed expression '" + expression + "'");
2000 }
2001 createMethodEffect(this, sig, TYPES.COMPUTE, runComputedEffect, property , dynamicFn);
2002 }
2003
2004 // -- static class methods ------------
2005
2006 /**
2007 * Ensures an accessor exists for the specified property, and adds
2008 * to a list of "property effects" that will run when the accessor for
2009 * the specified property is set. Effects are grouped by "type", which
2010 * roughly corresponds to a phase in effect processing. The effect
2011 * metadata should be in the following form:
2012 *
2013 * {
2014 * fn: effectFunction, // Reference to function to call to perform eff ect
2015 * info: { ... } // Effect metadata passed to function
2016 * trigger: { // Optional triggering metadata; if not provide d
2017 * name: string // the property is treated as a wildcard
2018 * structured: boolean
2019 * wildcard: boolean
2020 * }
2021 * }
2022 *
2023 * Effects are called from `_propertiesChanged` in the following order by
2024 * type:
2025 *
2026 * 1. COMPUTE
2027 * 2. PROPAGATE
2028 * 3. REFLECT
2029 * 4. OBSERVE
2030 * 5. NOTIFY
2031 *
2032 * Effect functions are called with the following signature:
2033 *
2034 * effectFunction(inst, path, props, oldProps, info, hasPaths)
2035 *
2036 * @param {string} property Property that should trigger the effect
2037 * @param {string} type Effect type, from this.PROPERTY_EFFECT_TYPES
2038 * @param {Object=} effect Effect metadata object
2039 * @protected
2040 */
2041 static addPropertyEffect(property, type, effect) {
2042 this.prototype._addPropertyEffect(property, type, effect);
2043 }
2044
2045 /**
2046 * Creates a single-property observer for the given property.
2047 *
2048 * @param {string} property Property name
2049 * @param {string} methodName Name of observer method to call
2050 * @param {boolean=} dynamicFn Whether the method name should be included as
2051 * a dependency to the effect.
2052 * @protected
2053 */
2054 static createPropertyObserver(property, methodName, dynamicFn) {
2055 this.prototype._createPropertyObserver(property, methodName, dynamicFn);
2056 }
2057
2058 /**
2059 * Creates a multi-property "method observer" based on the provided
2060 * expression, which should be a string in the form of a normal Javascript
2061 * function signature: `'methodName(arg1, [..., argn])'`. Each argument
2062 * should correspond to a property or path in the context of this
2063 * prototype (or instance), or may be a literal string or number.
2064 *
2065 * @param {string} expression Method expression
2066 * @param {boolean|Object=} dynamicFn Boolean or object map indicating
2067 * whether method names should be included as a dependency to the effect .
2068 * @protected
2069 */
2070 static createMethodObserver(expression, dynamicFn) {
2071 this.prototype._createMethodObserver(expression, dynamicFn);
2072 }
2073
2074 /**
2075 * Causes the setter for the given property to dispatch `<property>-change d`
2076 * events to notify of changes to the property.
2077 *
2078 * @param {string} property Property name
2079 * @protected
2080 */
2081 static createNotifyingProperty(property) {
2082 this.prototype._createNotifyingProperty(property);
2083 }
2084
2085 /**
2086 * Creates a read-only accessor for the given property.
2087 *
2088 * To set the property, use the protected `_setProperty` API.
2089 * To create a custom protected setter (e.g. `_setMyProp()` for
2090 * property `myProp`), pass `true` for `protectedSetter`.
2091 *
2092 * Note, if the property will have other property effects, this method
2093 * should be called first, before adding other effects.
2094 *
2095 * @param {string} property Property name
2096 * @param {boolean=} protectedSetter Creates a custom protected setter
2097 * when `true`.
2098 * @protected
2099 */
2100 static createReadOnlyProperty(property, protectedSetter) {
2101 this.prototype._createReadOnlyProperty(property, protectedSetter);
2102 }
2103
2104 /**
2105 * Causes the setter for the given property to reflect the property value
2106 * to a (dash-cased) attribute of the same name.
2107 *
2108 * @param {string} property Property name
2109 * @protected
2110 */
2111 static createReflectedProperty(property) {
2112 this.prototype._createReflectedProperty(property);
2113 }
2114
2115 /**
2116 * Creates a computed property whose value is set to the result of the
2117 * method described by the given `expression` each time one or more
2118 * arguments to the method changes. The expression should be a string
2119 * in the form of a normal Javascript function signature:
2120 * `'methodName(arg1, [..., argn])'`
2121 *
2122 * @param {string} property Name of computed property to set
2123 * @param {string} expression Method expression
2124 * @param {boolean|Object=} dynamicFn Boolean or object map indicating whe ther
2125 * method names should be included as a dependency to the effect.
2126 * @protected
2127 */
2128 static createComputedProperty(property, expression, dynamicFn) {
2129 this.prototype._createComputedProperty(property, expression, dynamicFn);
2130 }
2131
2132 /**
2133 * Parses the provided template to ensure binding effects are created
2134 * for them, and then ensures property accessors are created for any
2135 * dependent properties in the template. Binding effects for bound
2136 * templates are stored in a linked list on the instance so that
2137 * templates can be efficiently stamped and unstamped.
2138 *
2139 * @param {HTMLTemplateElement} template Template containing binding
2140 * bindings
2141 * @return {Object} Template metadata object
2142 * @protected
2143 */
2144 static bindTemplate(template) {
2145 return this.prototype._bindTemplate(template);
2146 }
2147
2148 // -- binding ----------------------------------------------
2149
2150 /**
2151 * Equivalent to static `bindTemplate` API but can be called on
2152 * an instance to add effects at runtime. See that method for
2153 * full API docs.
2154 *
2155 * This method may be called on the prototype (for prototypical template
2156 * binding, to avoid creating accessors every instance) once per prototype ,
2157 * and will be called with `runtimeBinding: true` by `_stampTemplate` to
2158 * create and link an instance of the template metadata associated with a
2159 * particular stamping.
2160 *
2161 * @param {HTMLTemplateElement} template Template containing binding
2162 * bindings
2163 * @param {boolean=} instanceBinding When false (default), performs
2164 * "prototypical" binding of the template and overwrites any previously
2165 * bound template for the class. When true (as passed from
2166 * `_stampTemplate`), the template info is instanced and linked into
2167 * the list of bound templates.
2168 * @return {Object} Template metadata object; for `runtimeBinding`,
2169 * this is an instance of the prototypical template info
2170 * @protected
2171 */
2172 _bindTemplate(template, instanceBinding) {
2173 let templateInfo = this.constructor._parseTemplate(template);
2174 let wasPreBound = this.__templateInfo == templateInfo;
2175 // Optimization: since this is called twice for proto-bound templates,
2176 // don't attempt to recreate accessors if this template was pre-bound
2177 if (!wasPreBound) {
2178 for (let prop in templateInfo.propertyEffects) {
2179 this._createPropertyAccessor(prop);
2180 }
2181 }
2182 if (instanceBinding) {
2183 // For instance-time binding, create instance of template metadata
2184 // and link into list of templates if necessary
2185 templateInfo = Object.create(templateInfo);
2186 templateInfo.wasPreBound = wasPreBound;
2187 if (!wasPreBound && this.__templateInfo) {
2188 let last = this.__templateInfoLast || this.__templateInfo;
2189 this.__templateInfoLast = last.nextTemplateInfo = templateInfo;
2190 templateInfo.previousTemplateInfo = last;
2191 return templateInfo;
2192 }
2193 }
2194 return this.__templateInfo = templateInfo;
2195 }
2196
2197 /**
2198 * Adds a property effect to the given template metadata, which is run
2199 * at the "propagate" stage of `_propertiesChanged` when the template
2200 * has been bound to the element via `_bindTemplate`.
2201 *
2202 * The `effect` object should match the format in `_addPropertyEffect`.
2203 *
2204 * @param {Object} templateInfo Template metadata to add effect to
2205 * @param {string} prop Property that should trigger the effect
2206 * @param {Object=} effect Effect metadata object
2207 * @protected
2208 */
2209 static _addTemplatePropertyEffect(templateInfo, prop, effect) {
2210 let hostProps = templateInfo.hostProps = templateInfo.hostProps || {};
2211 hostProps[prop] = true;
2212 let effects = templateInfo.propertyEffects = templateInfo.propertyEffect s || {};
2213 let propEffects = effects[prop] = effects[prop] || [];
2214 propEffects.push(effect);
2215 }
2216
2217 /**
2218 * Stamps the provided template and performs instance-time setup for
2219 * Polymer template features, including data bindings, declarative event
2220 * listeners, and the `this.$` map of `id`'s to nodes. A document fragmen t
2221 * is returned containing the stamped DOM, ready for insertion into the
2222 * DOM.
2223 *
2224 * This method may be called more than once; however note that due to
2225 * `shadycss` polyfill limitations, only styles from templates prepared
2226 * using `ShadyCSS.prepareTemplate` will be correctly polyfilled (scoped
2227 * to the shadow root and support CSS custom properties), and note that
2228 * `ShadyCSS.prepareTemplate` may only be called once per element. As such ,
2229 * any styles required by in runtime-stamped templates must be included
2230 * in the main element template.
2231 *
2232 * @param {HTMLTemplateElement} template Template to stamp
2233 * @return {DocumentFragment} Cloned template content
2234 * @protected
2235 */
2236 _stampTemplate(template) {
2237 // Ensures that created dom is `_enqueueClient`'d to this element so
2238 // that it can be flushed on next call to `_flushProperties`
2239 hostStack.beginHosting(this);
2240 let dom = super._stampTemplate(template);
2241 hostStack.endHosting(this);
2242 let templateInfo = this._bindTemplate(template, true);
2243 // Add template-instance-specific data to instanced templateInfo
2244 templateInfo.nodeList = dom.nodeList;
2245 // Capture child nodes to allow unstamping of non-prototypical templates
2246 if (!templateInfo.wasPreBound) {
2247 let nodes = templateInfo.childNodes = [];
2248 for (let n=dom.firstChild; n; n=n.nextSibling) {
2249 nodes.push(n);
2250 }
2251 }
2252 dom.templateInfo = templateInfo;
2253 // Setup compound storage, 2-way listeners, and dataHost for bindings
2254 setupBindings(this, templateInfo);
2255 // Flush properties into template nodes if already booted
2256 if (this.__dataReady) {
2257 runEffects(this, templateInfo.propertyEffects, this.__data, null,
2258 false, templateInfo.nodeList);
2259 }
2260 return dom;
2261 }
2262
2263 /**
2264 * Removes and unbinds the nodes previously contained in the provided
2265 * DocumentFragment returned from `_stampTemplate`.
2266 *
2267 * @param {DocumentFragment} dom DocumentFragment previously returned
2268 * from `_stampTemplate` associated with the nodes to be removed
2269 * @protected
2270 */
2271 _removeBoundDom(dom) {
2272 // Unlink template info
2273 let templateInfo = dom.templateInfo;
2274 if (templateInfo.previousTemplateInfo) {
2275 templateInfo.previousTemplateInfo.nextTemplateInfo =
2276 templateInfo.nextTemplateInfo;
2277 }
2278 if (templateInfo.nextTemplateInfo) {
2279 templateInfo.nextTemplateInfo.previousTemplateInfo =
2280 templateInfo.previousTemplateInfo;
2281 }
2282 if (this.__templateInfoLast == templateInfo) {
2283 this.__templateInfoLast = templateInfo.previousTemplateInfo;
2284 }
2285 templateInfo.previousTemplateInfo = templateInfo.nextTemplateInfo = null ;
2286 // Remove stamped nodes
2287 let nodes = templateInfo.childNodes;
2288 for (let i=0; i<nodes.length; i++) {
2289 let node = nodes[i];
2290 node.parentNode.removeChild(node);
2291 }
2292 }
2293
2294 /**
2295 * Overrides default `TemplateStamp` implementation to add support for
2296 * parsing bindings from `TextNode`'s' `textContent`. A `bindings`
2297 * array is added to `nodeInfo` and populated with binding metadata
2298 * with information capturing the binding target, and a `parts` array
2299 * with one or more metadata objects capturing the source(s) of the
2300 * binding.
2301 *
2302 * @override
2303 * @param {Node} node Node to parse
2304 * @param {Object} templateInfo Template metadata for current template
2305 * @param {Object} nodeInfo Node metadata for current template node
2306 * @return {boolean} `true` if the visited node added node-specific
2307 * metadata to `nodeInfo`
2308 * @protected
2309 */
2310 static _parseTemplateNode(node, templateInfo, nodeInfo) {
2311 let noted = super._parseTemplateNode(node, templateInfo, nodeInfo);
2312 if (node.nodeType === Node.TEXT_NODE) {
2313 let parts = this._parseBindings(node.textContent, templateInfo);
2314 if (parts) {
2315 // Initialize the textContent with any literal parts
2316 // NOTE: default to a space here so the textNode remains; some brows ers
2317 // (IE) evacipate an empty textNode following cloneNode/importNode.
2318 node.textContent = literalFromParts(parts) || ' ';
2319 addBinding(this, templateInfo, nodeInfo, 'text', 'textContent', part s);
2320 noted = true;
2321 }
2322 }
2323 return noted;
2324 }
2325
2326 /**
2327 * Overrides default `TemplateStamp` implementation to add support for
2328 * parsing bindings from attributes. A `bindings`
2329 * array is added to `nodeInfo` and populated with binding metadata
2330 * with information capturing the binding target, and a `parts` array
2331 * with one or more metadata objects capturing the source(s) of the
2332 * binding.
2333 *
2334 * @override
2335 * @param {Node} node Node to parse
2336 * @param {Object} templateInfo Template metadata for current template
2337 * @param {Object} nodeInfo Node metadata for current template node
2338 * @return {boolean} `true` if the visited node added node-specific
2339 * metadata to `nodeInfo`
2340 * @protected
2341 */
2342 static _parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, val ue) {
2343 let parts = this._parseBindings(value, templateInfo);
2344 if (parts) {
2345 // Attribute or property
2346 let origName = name;
2347 let kind = 'property';
2348 if (name[name.length-1] == '$') {
2349 name = name.slice(0, -1);
2350 kind = 'attribute';
2351 }
2352 // Initialize attribute bindings with any literal parts
2353 let literal = literalFromParts(parts);
2354 if (literal && kind == 'attribute') {
2355 node.setAttribute(name, literal);
2356 }
2357 // Clear attribute before removing, since IE won't allow removing
2358 // `value` attribute if it previously had a value (can't
2359 // unconditionally set '' before removing since attributes with `$`
2360 // can't be set using setAttribute)
2361 if (node.localName === 'input' && origName === 'value') {
2362 node.setAttribute(origName, '');
2363 }
2364 // Remove annotation
2365 node.removeAttribute(origName);
2366 // Case hackery: attributes are lower-case, but bind targets
2367 // (properties) are case sensitive. Gambit is to map dash-case to
2368 // camel-case: `foo-bar` becomes `fooBar`.
2369 // Attribute bindings are excepted.
2370 if (kind === 'property') {
2371 name = Polymer.CaseMap.dashToCamelCase(name);
2372 }
2373 addBinding(this, templateInfo, nodeInfo, kind, name, parts, literal);
2374 return true;
2375 } else {
2376 return super._parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, value);
2377 }
2378 }
2379
2380 /**
2381 * Overrides default `TemplateStamp` implementation to add support for
2382 * binding the properties that a nested template depends on to the templat e
2383 * as `_host_<property>`.
2384 *
2385 * @override
2386 * @param {Node} node Node to parse
2387 * @param {Object} templateInfo Template metadata for current template
2388 * @param {Object} nodeInfo Node metadata for current template node
2389 * @return {boolean} `true` if the visited node added node-specific
2390 * metadata to `nodeInfo`
2391 * @protected
2392 */
2393 static _parseTemplateNestedTemplate(node, templateInfo, nodeInfo) {
2394 let noted = super._parseTemplateNestedTemplate(node, templateInfo, nodeI nfo);
2395 // Merge host props into outer template and add bindings
2396 let hostProps = nodeInfo.templateInfo.hostProps;
2397 let mode = '{';
2398 for (let source in hostProps) {
2399 let parts = [{ mode, source, dependencies: [source] }];
2400 addBinding(this, templateInfo, nodeInfo, 'property', '_host_' + source , parts);
2401 }
2402 return noted;
2403 }
2404
2405 /**
2406 * Called to parse text in a template (either attribute values or
2407 * textContent) into binding metadata.
2408 *
2409 * Any overrides of this method should return an array of binding part
2410 * metadata representing one or more bindings found in the provided text
2411 * and any "literal" text in between. Any non-literal parts will be passe d
2412 * to `_evaluateBinding` when any dependencies change. The only required
2413 * fields of each "part" in the returned array are as follows:
2414 *
2415 * - `dependencies` - Array containing trigger metadata for each property
2416 * that should trigger the binding to update
2417 * - `literal` - String containing text if the part represents a literal;
2418 * in this case no `dependencies` are needed
2419 *
2420 * Additional metadata for use by `_evaluateBinding` may be provided in
2421 * each part object as needed.
2422 *
2423 * The default implementation handles the following types of bindings
2424 * (one or more may be intermixed with literal strings):
2425 * - Property binding: `[[prop]]`
2426 * - Path binding: `[[object.prop]]`
2427 * - Negated property or path bindings: `[[!prop]]` or `[[!object.prop]]`
2428 * - Two-way property or path bindings (supports negation):
2429 * `{{prop}}`, `{{object.prop}}`, `{{!prop}}` or `{{!object.prop}}`
2430 * - Inline computed method (supports negation):
2431 * `[[compute(a, 'literal', b)]]`, `[[!compute(a, 'literal', b)]]`
2432 *
2433 * @param {string} text Text to parse from attribute or textContent
2434 * @param {Object} templateInfo Current template metadata
2435 * @return {Array<Object>} Array of binding part metadata
2436 * @protected
2437 */
2438 static _parseBindings(text, templateInfo) {
2439 let parts = [];
2440 let lastIndex = 0;
2441 let m;
2442 // Example: "literal1{{prop}}literal2[[!compute(foo,bar)]]final"
2443 // Regex matches:
2444 // Iteration 1: Iteration 2:
2445 // m[1]: '{{' '[['
2446 // m[2]: '' '!'
2447 // m[3]: 'prop' 'compute(foo,bar)'
2448 while ((m = bindingRegex.exec(text)) !== null) {
2449 // Add literal part
2450 if (m.index > lastIndex) {
2451 parts.push({literal: text.slice(lastIndex, m.index)});
2452 }
2453 // Add binding part
2454 let mode = m[1][0];
2455 let negate = Boolean(m[2]);
2456 let source = m[3].trim();
2457 let customEvent, notifyEvent, colon;
2458 if (mode == '{' && (colon = source.indexOf('::')) > 0) {
2459 notifyEvent = source.substring(colon + 2);
2460 source = source.substring(0, colon);
2461 customEvent = true;
2462 }
2463 let signature = parseMethod(source);
2464 let dependencies = [];
2465 if (signature) {
2466 // Inline computed function
2467 let {args, methodName} = signature;
2468 for (let i=0; i<args.length; i++) {
2469 let arg = args[i];
2470 if (!arg.literal) {
2471 dependencies.push(arg);
2472 }
2473 }
2474 let dynamicFns = templateInfo.dynamicFns;
2475 if (dynamicFns && dynamicFns[methodName] || signature.static) {
2476 dependencies.push(methodName);
2477 signature.dynamicFn = true;
2478 }
2479 } else {
2480 // Property or path
2481 dependencies.push(source);
2482 }
2483 parts.push({
2484 source, mode, negate, customEvent, signature, dependencies,
2485 event: notifyEvent
2486 });
2487 lastIndex = bindingRegex.lastIndex;
2488 }
2489 // Add a final literal part
2490 if (lastIndex && lastIndex < text.length) {
2491 let literal = text.substring(lastIndex);
2492 if (literal) {
2493 parts.push({
2494 literal: literal
2495 });
2496 }
2497 }
2498 if (parts.length) {
2499 return parts;
2500 }
2501 }
2502
2503 /**
2504 * Called to evaluate a previously parsed binding part based on a set of
2505 * one or more changed dependencies.
2506 *
2507 * @param {HTMLElement} inst Element that should be used as scope for
2508 * binding dependencies
2509 * @param {Object} part Binding part metadata
2510 * @param {string} path Property/path that triggered this effect
2511 * @param {Object} props Bag of current property changes
2512 * @param {Object} oldProps Bag of previous values for changed properties
2513 * @param {boolean} hasPaths True with `props` contains one or more paths
2514 * @return {*} Value the binding part evaluated to
2515 * @protected
2516 */
2517 static _evaluateBinding(inst, part, path, props, oldProps, hasPaths) {
2518 let value;
2519 if (part.signature) {
2520 value = runMethodEffect(inst, path, props, oldProps, part.signature);
2521 } else if (path != part.source) {
2522 value = Polymer.Path.get(inst, part.source);
2523 } else {
2524 if (hasPaths && Polymer.Path.isPath(path)) {
2525 value = Polymer.Path.get(inst, path);
2526 } else {
2527 value = inst.__data[path];
2528 }
2529 }
2530 if (part.negate) {
2531 value = !value;
2532 }
2533 return value;
2534 }
2535
2536 }
2537
2538 return PropertyEffects;
2539 });
2540
2541 /**
2542 * Helper api for enqueing client dom created by a host element.
2543 *
2544 * By default elements are flushed via `_flushProperties` when
2545 * `connectedCallback` is called. Elements attach their client dom to
2546 * themselves at `ready` time which results from this first flush.
2547 * This provides an ordering guarantee that the client dom an element
2548 * creates is flushed before the element itself (i.e. client `ready`
2549 * fires before host `ready`).
2550 *
2551 * However, if `_flushProperties` is called *before* an element is connected,
2552 * as for example `Templatize` does, this ordering guarantee cannot be
2553 * satisfied because no elements are connected. (Note: Bound elements that
2554 * receive data do become enqueued clients and are properly ordered but
2555 * unbound elements are not.)
2556 *
2557 * To maintain the desired "client before host" ordering guarantee for this
2558 * case we rely on the "host stack. Client nodes registers themselves with
2559 * the creating host element when created. This ensures that all client dom
2560 * is readied in the proper order, maintaining the desired guarantee.
2561 *
2562 * @private
2563 */
2564 let hostStack = {
2565
2566 stack: [],
2567
2568 registerHost(inst) {
2569 if (this.stack.length) {
2570 let host = this.stack[this.stack.length-1];
2571 host._enqueueClient(inst);
2572 }
2573 },
2574
2575 beginHosting(inst) {
2576 this.stack.push(inst);
2577 },
2578
2579 endHosting(inst) {
2580 let stackLen = this.stack.length;
2581 if (stackLen && this.stack[stackLen-1] == inst) {
2582 this.stack.pop();
2583 }
2584 }
2585
2586 }
2587
2588 })();
2589 </script>
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698