| OLD | NEW |
| (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, ',').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(/,/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> |
| OLD | NEW |