UNPKG

@polymer/polymer

Version:

The Polymer library makes it easy to create your own web components. Give your element some markup and properties, and then use it on a site. Polymer provides features like dynamic templates and data binding to reduce the amount of boilerplate you need to

1,437 lines (1,377 loc) 121 kB
/** * @fileoverview * @suppress {checkPrototypalTypes} * @license Copyright (c) 2017 The Polymer Project Authors. All rights reserved. * This code may only be used under the BSD style license found at * http://polymer.github.io/LICENSE.txt The complete set of authors may be found * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by * Google as part of the polymer project is also subject to an additional IP * rights grant found at http://polymer.github.io/PATENTS.txt */ import '../utils/boot.js'; import { wrap } from '../utils/wrap.js'; import { dedupingMixin } from '../utils/mixin.js'; import { root, isAncestor, isDescendant, get, translate, isPath, set, normalize } from '../utils/path.js'; /* for notify, reflect */ import { camelToDashCase, dashToCamelCase } from '../utils/case-map.js'; import { PropertyAccessors } from './property-accessors.js'; /* for annotated effects */ import { TemplateStamp } from './template-stamp.js'; import { sanitizeDOMValue, legacyUndefined, orderedComputed, removeNestedTemplates, fastDomIf } from '../utils/settings.js'; // Monotonically increasing unique ID used for de-duping effects triggered // from multiple properties in the same turn let dedupeId = 0; const NOOP = []; /** * Property effect types; effects are stored on the prototype using these keys * @enum {string} */ const TYPES = { COMPUTE: '__computeEffects', REFLECT: '__reflectEffects', NOTIFY: '__notifyEffects', PROPAGATE: '__propagateEffects', OBSERVE: '__observeEffects', READ_ONLY: '__readOnly' }; const COMPUTE_INFO = '__computeInfo'; /** @const {!RegExp} */ const capitalAttributeRegex = /[A-Z]/; /** * @typedef {{ * name: (string | undefined), * structured: (boolean | undefined), * wildcard: (boolean | undefined) * }} */ let DataTrigger; //eslint-disable-line no-unused-vars /** * @typedef {{ * info: ?, * trigger: (!DataTrigger | undefined), * fn: (!Function | undefined) * }} */ let DataEffect; //eslint-disable-line no-unused-vars /** * Ensures that the model has an own-property map of effects for the given type. * The model may be a prototype or an instance. * * Property effects are stored as arrays of effects by property in a map, * by named type on the model. e.g. * * __computeEffects: { * foo: [ ... ], * bar: [ ... ] * } * * If the model does not yet have an effect map for the type, one is created * and returned. If it does, but it is not an own property (i.e. the * prototype had effects), the the map is deeply cloned and the copy is * set on the model and returned, ready for new effects to be added. * * @param {Object} model Prototype or instance * @param {string} type Property effect type * @param {boolean=} cloneArrays Clone any arrays assigned to the map when * extending a superclass map onto this subclass * @return {Object} The own-property map of effects for the given type * @private */ function ensureOwnEffectMap(model, type, cloneArrays) { let effects = model[type]; if (!effects) { effects = model[type] = {}; } else if (!model.hasOwnProperty(type)) { effects = model[type] = Object.create(model[type]); if (cloneArrays) { for (let p in effects) { let protoFx = effects[p]; // Perf optimization over Array.slice let instFx = effects[p] = Array(protoFx.length); for (let i=0; i<protoFx.length; i++) { instFx[i] = protoFx[i]; } } } } return effects; } // -- effects ---------------------------------------------- /** * Runs all effects of a given type for the given set of property changes * on an instance. * * @param {!Polymer_PropertyEffects} inst The instance with effects to run * @param {?Object} effects Object map of property-to-Array of effects * @param {?Object} props Bag of current property changes * @param {?Object=} oldProps Bag of previous values for changed properties * @param {boolean=} hasPaths True with `props` contains one or more paths * @param {*=} extraArgs Additional metadata to pass to effect function * @return {boolean} True if an effect ran for this property * @private */ function runEffects(inst, effects, props, oldProps, hasPaths, extraArgs) { if (effects) { let ran = false; const id = dedupeId++; for (let prop in props) { // Inline `runEffectsForProperty` for perf. let rootProperty = hasPaths ? root(prop) : prop; let fxs = effects[rootProperty]; if (fxs) { for (let i=0, l=fxs.length, fx; (i<l) && (fx=fxs[i]); i++) { if ((!fx.info || fx.info.lastRun !== id) && (!hasPaths || pathMatchesTrigger(prop, fx.trigger))) { if (fx.info) { fx.info.lastRun = id; } fx.fn(inst, prop, props, oldProps, fx.info, hasPaths, extraArgs); ran = true; } } } } return ran; } return false; } /** * Runs a list of effects for a given property. * * @param {!Polymer_PropertyEffects} inst The instance with effects to run * @param {!Object} effects Object map of property-to-Array of effects * @param {number} dedupeId Counter used for de-duping effects * @param {string} prop Name of changed property * @param {*} props Changed properties * @param {*} oldProps Old properties * @param {boolean=} hasPaths True with `props` contains one or more paths * @param {*=} extraArgs Additional metadata to pass to effect function * @return {boolean} True if an effect ran for this property * @private */ function runEffectsForProperty(inst, effects, dedupeId, prop, props, oldProps, hasPaths, extraArgs) { let ran = false; let rootProperty = hasPaths ? root(prop) : prop; let fxs = effects[rootProperty]; if (fxs) { for (let i=0, l=fxs.length, fx; (i<l) && (fx=fxs[i]); i++) { if ((!fx.info || fx.info.lastRun !== dedupeId) && (!hasPaths || pathMatchesTrigger(prop, fx.trigger))) { if (fx.info) { fx.info.lastRun = dedupeId; } fx.fn(inst, prop, props, oldProps, fx.info, hasPaths, extraArgs); ran = true; } } } return ran; } /** * Determines whether a property/path that has changed matches the trigger * criteria for an effect. A trigger is a descriptor with the following * structure, which matches the descriptors returned from `parseArg`. * e.g. for `foo.bar.*`: * ``` * trigger: { * name: 'a.b', * structured: true, * wildcard: true * } * ``` * If no trigger is given, the path is deemed to match. * * @param {string} path Path or property that changed * @param {?DataTrigger} trigger Descriptor * @return {boolean} Whether the path matched the trigger */ function pathMatchesTrigger(path, trigger) { if (trigger) { let triggerPath = /** @type {string} */ (trigger.name); return (triggerPath == path) || !!(trigger.structured && isAncestor(triggerPath, path)) || !!(trigger.wildcard && isDescendant(triggerPath, path)); } else { return true; } } /** * Implements the "observer" effect. * * Calls the method with `info.methodName` on the instance, passing the * new and old values. * * @param {!Polymer_PropertyEffects} inst The instance the effect will be run on * @param {string} property Name of property * @param {Object} props Bag of current property changes * @param {Object} oldProps Bag of previous values for changed properties * @param {?} info Effect metadata * @return {void} * @private */ function runObserverEffect(inst, property, props, oldProps, info) { let fn = typeof info.method === "string" ? inst[info.method] : info.method; let changedProp = info.property; if (fn) { fn.call(inst, inst.__data[changedProp], oldProps[changedProp]); } else if (!info.dynamicFn) { console.warn('observer method `' + info.method + '` not defined'); } } /** * Runs "notify" effects for a set of changed properties. * * This method differs from the generic `runEffects` method in that it * will dispatch path notification events in the case that the property * changed was a path and the root property for that path didn't have a * "notify" effect. This is to maintain 1.0 behavior that did not require * `notify: true` to ensure object sub-property notifications were * sent. * * @param {!Polymer_PropertyEffects} inst The instance with effects to run * @param {Object} notifyProps Bag of properties to notify * @param {Object} props Bag of current property changes * @param {Object} oldProps Bag of previous values for changed properties * @param {boolean} hasPaths True with `props` contains one or more paths * @return {void} * @private */ function runNotifyEffects(inst, notifyProps, props, oldProps, hasPaths) { // Notify let fxs = inst[TYPES.NOTIFY]; let notified; let id = dedupeId++; // Try normal notify effects; if none, fall back to try path notification for (let prop in notifyProps) { if (notifyProps[prop]) { if (fxs && runEffectsForProperty(inst, fxs, id, prop, props, oldProps, hasPaths)) { notified = true; } else if (hasPaths && notifyPath(inst, prop, props)) { notified = true; } } } // Flush host if we actually notified and host was batching // And the host has already initialized clients; this prevents // an issue with a host observing data changes before clients are ready. let host; if (notified && (host = inst.__dataHost) && host._invalidateProperties) { host._invalidateProperties(); } } /** * Dispatches {property}-changed events with path information in the detail * object to indicate a sub-path of the property was changed. * * @param {!Polymer_PropertyEffects} inst The element from which to fire the * event * @param {string} path The path that was changed * @param {Object} props Bag of current property changes * @return {boolean} Returns true if the path was notified * @private */ function notifyPath(inst, path, props) { let rootProperty = root(path); if (rootProperty !== path) { let eventName = camelToDashCase(rootProperty) + '-changed'; dispatchNotifyEvent(inst, eventName, props[path], path); return true; } return false; } /** * Dispatches {property}-changed events to indicate a property (or path) * changed. * * @param {!Polymer_PropertyEffects} inst The element from which to fire the * event * @param {string} eventName The name of the event to send * ('{property}-changed') * @param {*} value The value of the changed property * @param {string | null | undefined} path If a sub-path of this property * changed, the path that changed (optional). * @return {void} * @private * @suppress {invalidCasts} */ function dispatchNotifyEvent(inst, eventName, value, path) { let detail = { value: value, queueProperty: true }; if (path) { detail.path = path; } // As a performance optimization, we could elide the wrap here since notifying // events are non-bubbling and shouldn't need retargeting. However, a very // small number of internal tests failed in obscure ways, which may indicate // user code relied on timing differences resulting from ShadyDOM flushing // as a result of the wrapped `dispatchEvent`. wrap(/** @type {!HTMLElement} */(inst)).dispatchEvent(new CustomEvent(eventName, { detail })); } /** * Implements the "notify" effect. * * Dispatches a non-bubbling event named `info.eventName` on the instance * with a detail object containing the new `value`. * * @param {!Polymer_PropertyEffects} inst The instance the effect will be run on * @param {string} property Name of property * @param {Object} props Bag of current property changes * @param {Object} oldProps Bag of previous values for changed properties * @param {?} info Effect metadata * @param {boolean} hasPaths True with `props` contains one or more paths * @return {void} * @private */ function runNotifyEffect(inst, property, props, oldProps, info, hasPaths) { let rootProperty = hasPaths ? root(property) : property; let path = rootProperty != property ? property : null; let value = path ? get(inst, path) : inst.__data[property]; if (path && value === undefined) { value = props[property]; // specifically for .splices } dispatchNotifyEvent(inst, info.eventName, value, path); } /** * Handler function for 2-way notification events. Receives context * information captured in the `addNotifyListener` closure from the * `__notifyListeners` metadata. * * Sets the value of the notified property to the host property or path. If * the event contained path information, translate that path to the host * scope's name for that path first. * * @param {CustomEvent} event Notification event (e.g. '<property>-changed') * @param {!Polymer_PropertyEffects} inst Host element instance handling the * notification event * @param {string} fromProp Child element property that was bound * @param {string} toPath Host property/path that was bound * @param {boolean} negate Whether the binding was negated * @return {void} * @private */ function handleNotification(event, inst, fromProp, toPath, negate) { let value; let detail = /** @type {Object} */(event.detail); let fromPath = detail && detail.path; if (fromPath) { toPath = translate(fromProp, toPath, fromPath); value = detail && detail.value; } else { value = event.currentTarget[fromProp]; } value = negate ? !value : value; if (!inst[TYPES.READ_ONLY] || !inst[TYPES.READ_ONLY][toPath]) { if (inst._setPendingPropertyOrPath(toPath, value, true, Boolean(fromPath)) && (!detail || !detail.queueProperty)) { inst._invalidateProperties(); } } } /** * Implements the "reflect" effect. * * Sets the attribute named `info.attrName` to the given property value. * * @param {!Polymer_PropertyEffects} inst The instance the effect will be run on * @param {string} property Name of property * @param {Object} props Bag of current property changes * @param {Object} oldProps Bag of previous values for changed properties * @param {?} info Effect metadata * @return {void} * @private */ function runReflectEffect(inst, property, props, oldProps, info) { let value = inst.__data[property]; if (sanitizeDOMValue) { value = sanitizeDOMValue(value, info.attrName, 'attribute', /** @type {Node} */(inst)); } inst._propertyToAttribute(property, info.attrName, value); } /** * Runs "computed" effects for a set of changed properties. * * This method differs from the generic `runEffects` method in that it * continues to run computed effects based on the output of each pass until * there are no more newly computed properties. This ensures that all * properties that will be computed by the initial set of changes are * computed before other effects (binding propagation, observers, and notify) * run. * * @param {!Polymer_PropertyEffects} inst The instance the effect will be run on * @param {?Object} changedProps Bag of changed properties * @param {?Object} oldProps Bag of previous values for changed properties * @param {boolean} hasPaths True with `props` contains one or more paths * @return {void} * @private */ function runComputedEffects(inst, changedProps, oldProps, hasPaths) { let computeEffects = inst[TYPES.COMPUTE]; if (computeEffects) { if (orderedComputed) { // Runs computed effects in efficient order by keeping a topologically- // sorted queue of compute effects to run, and inserting subsequently // invalidated effects as they are run dedupeId++; const order = getComputedOrder(inst); const queue = []; for (let p in changedProps) { enqueueEffectsFor(p, computeEffects, queue, order, hasPaths); } let info; while ((info = queue.shift())) { if (runComputedEffect(inst, '', changedProps, oldProps, info)) { enqueueEffectsFor(info.methodInfo, computeEffects, queue, order, hasPaths); } } Object.assign(/** @type {!Object} */ (oldProps), inst.__dataOld); Object.assign(/** @type {!Object} */ (changedProps), inst.__dataPending); inst.__dataPending = null; } else { // Original Polymer 2.x computed effects order, which continues running // effects until no further computed properties have been invalidated let inputProps = changedProps; while (runEffects(inst, computeEffects, inputProps, oldProps, hasPaths)) { Object.assign(/** @type {!Object} */ (oldProps), inst.__dataOld); Object.assign(/** @type {!Object} */ (changedProps), inst.__dataPending); inputProps = inst.__dataPending; inst.__dataPending = null; } } } } /** * Inserts a computed effect into a queue, given the specified order. Performs * the insert using a binary search. * * Used by `orderedComputed: true` computed property algorithm. * * @param {Object} info Property effects metadata * @param {Array<Object>} queue Ordered queue of effects * @param {Map<string,number>} order Map of computed property name->topological * sort order */ const insertEffect = (info, queue, order) => { let start = 0; let end = queue.length - 1; let idx = -1; while (start <= end) { const mid = (start + end) >> 1; // Note `methodInfo` is where the computed property name is stored in // the effect metadata const cmp = order.get(queue[mid].methodInfo) - order.get(info.methodInfo); if (cmp < 0) { start = mid + 1; } else if (cmp > 0) { end = mid - 1; } else { idx = mid; break; } } if (idx < 0) { idx = end + 1; } queue.splice(idx, 0, info); }; /** * Inserts all downstream computed effects invalidated by the specified property * into the topologically-sorted queue of effects to be run. * * Used by `orderedComputed: true` computed property algorithm. * * @param {string} prop Property name * @param {Object} computeEffects Computed effects for this element * @param {Array<Object>} queue Topologically-sorted queue of computed effects * to be run * @param {Map<string,number>} order Map of computed property name->topological * sort order * @param {boolean} hasPaths True with `changedProps` contains one or more paths */ const enqueueEffectsFor = (prop, computeEffects, queue, order, hasPaths) => { const rootProperty = hasPaths ? root(prop) : prop; const fxs = computeEffects[rootProperty]; if (fxs) { for (let i=0; i<fxs.length; i++) { const fx = fxs[i]; if ((fx.info.lastRun !== dedupeId) && (!hasPaths || pathMatchesTrigger(prop, fx.trigger))) { fx.info.lastRun = dedupeId; insertEffect(fx.info, queue, order); } } } }; /** * Generates and retrieves a memoized map of computed property name to its * topologically-sorted order. * * The map is generated by first assigning a "dependency count" to each property * (defined as number properties it depends on, including its method for * "dynamic functions"). Any properties that have no dependencies are added to * the `ready` queue, which are properties whose order can be added to the final * order map. Properties are popped off the `ready` queue one by one and a.) added as * the next property in the order map, and b.) each property that it is a * dependency for has its dep count decremented (and if that property's dep * count goes to zero, it is added to the `ready` queue), until all properties * have been visited and ordered. * * Used by `orderedComputed: true` computed property algorithm. * * @param {!Polymer_PropertyEffects} inst The instance to retrieve the computed * effect order for. * @return {Map<string,number>} Map of computed property name->topological sort * order */ function getComputedOrder(inst) { let ordered = inst.constructor.__orderedComputedDeps; if (!ordered) { ordered = new Map(); const effects = inst[TYPES.COMPUTE]; let {counts, ready, total} = dependencyCounts(inst); let curr; while ((curr = ready.shift())) { ordered.set(curr, ordered.size); const computedByCurr = effects[curr]; if (computedByCurr) { computedByCurr.forEach(fx => { // Note `methodInfo` is where the computed property name is stored const computedProp = fx.info.methodInfo; --total; if (--counts[computedProp] === 0) { ready.push(computedProp); } }); } } if (total !== 0) { const el = /** @type {HTMLElement} */ (inst); console.warn(`Computed graph for ${el.localName} incomplete; circular?`); } inst.constructor.__orderedComputedDeps = ordered; } return ordered; } /** * Generates a map of property-to-dependency count (`counts`, where "dependency * count" is the number of dependencies a given property has assuming it is a * computed property, otherwise 0). It also returns a pre-populated list of * `ready` properties that have no dependencies and a `total` count, which is * used for error-checking the graph. * * Used by `orderedComputed: true` computed property algorithm. * * @param {!Polymer_PropertyEffects} inst The instance to generate dependency * counts for. * @return {!Object} Object containing `counts` map (property-to-dependency * count) and pre-populated `ready` array of properties that had zero * dependencies. */ function dependencyCounts(inst) { const infoForComputed = inst[COMPUTE_INFO]; const counts = {}; const computedDeps = inst[TYPES.COMPUTE]; const ready = []; let total = 0; // Count dependencies for each computed property for (let p in infoForComputed) { const info = infoForComputed[p]; // Be sure to add the method name itself in case of "dynamic functions" total += counts[p] = info.args.filter(a => !a.literal).length + (info.dynamicFn ? 1 : 0); } // Build list of ready properties (that aren't themselves computed) for (let p in computedDeps) { if (!infoForComputed[p]) { ready.push(p); } } return {counts, ready, total}; } /** * Implements the "computed property" effect by running the method with the * values of the arguments specified in the `info` object and setting the * return value to the computed property specified. * * @param {!Polymer_PropertyEffects} inst The instance the effect will be run on * @param {string} property Name of property * @param {?Object} changedProps Bag of current property changes * @param {?Object} oldProps Bag of previous values for changed properties * @param {?} info Effect metadata * @return {boolean} True when the property being computed changed * @private */ function runComputedEffect(inst, property, changedProps, oldProps, info) { // Dirty check dependencies and run if any invalid let result = runMethodEffect(inst, property, changedProps, oldProps, info); // Abort if method returns a no-op result if (result === NOOP) { return false; } let computedProp = info.methodInfo; if (inst.__dataHasAccessor && inst.__dataHasAccessor[computedProp]) { return inst._setPendingProperty(computedProp, result, true); } else { inst[computedProp] = result; return false; } } /** * Computes path changes based on path links set up using the `linkPaths` * API. * * @param {!Polymer_PropertyEffects} inst The instance whose props are changing * @param {string} path Path that has changed * @param {*} value Value of changed path * @return {void} * @private */ function computeLinkedPaths(inst, path, value) { let links = inst.__dataLinkedPaths; if (links) { let link; for (let a in links) { let b = links[a]; if (isDescendant(a, path)) { link = translate(a, b, path); inst._setPendingPropertyOrPath(link, value, true, true); } else if (isDescendant(b, path)) { link = translate(b, a, path); inst._setPendingPropertyOrPath(link, value, true, true); } } } } // -- bindings ---------------------------------------------- /** * Adds binding metadata to the current `nodeInfo`, and binding effects * for all part dependencies to `templateInfo`. * * @param {Function} constructor Class that `_parseTemplate` is currently * running on * @param {TemplateInfo} templateInfo Template metadata for current template * @param {NodeInfo} nodeInfo Node metadata for current template node * @param {string} kind Binding kind, either 'property', 'attribute', or 'text' * @param {string} target Target property name * @param {!Array<!BindingPart>} parts Array of binding part metadata * @param {string=} literal Literal text surrounding binding parts (specified * only for 'property' bindings, since these must be initialized as part * of boot-up) * @return {void} * @private */ function addBinding(constructor, templateInfo, nodeInfo, kind, target, parts, literal) { // Create binding metadata and add to nodeInfo nodeInfo.bindings = nodeInfo.bindings || []; let /** Binding */ binding = { kind, target, parts, literal, isCompound: (parts.length !== 1) }; nodeInfo.bindings.push(binding); // Add listener info to binding metadata if (shouldAddListener(binding)) { let {event, negate} = binding.parts[0]; binding.listenerEvent = event || (camelToDashCase(target) + '-changed'); binding.listenerNegate = negate; } // Add "propagate" property effects to templateInfo let index = templateInfo.nodeInfoList.length; for (let i=0; i<binding.parts.length; i++) { let part = binding.parts[i]; part.compoundIndex = i; addEffectForBindingPart(constructor, templateInfo, binding, part, index); } } /** * Adds property effects to the given `templateInfo` for the given binding * part. * * @param {Function} constructor Class that `_parseTemplate` is currently * running on * @param {TemplateInfo} templateInfo Template metadata for current template * @param {!Binding} binding Binding metadata * @param {!BindingPart} part Binding part metadata * @param {number} index Index into `nodeInfoList` for this node * @return {void} */ function addEffectForBindingPart(constructor, templateInfo, binding, part, index) { if (!part.literal) { if (binding.kind === 'attribute' && binding.target[0] === '-') { console.warn('Cannot set attribute ' + binding.target + ' because "-" is not a valid attribute starting character'); } else { let dependencies = part.dependencies; let info = { index, binding, part, evaluator: constructor }; for (let j=0; j<dependencies.length; j++) { let trigger = dependencies[j]; if (typeof trigger == 'string') { trigger = parseArg(trigger); trigger.wildcard = true; } constructor._addTemplatePropertyEffect(templateInfo, trigger.rootProperty, { fn: runBindingEffect, info, trigger }); } } } } /** * Implements the "binding" (property/path binding) effect. * * Note that binding syntax is overridable via `_parseBindings` and * `_evaluateBinding`. This method will call `_evaluateBinding` for any * non-literal parts returned from `_parseBindings`. However, * there is no support for _path_ bindings via custom binding parts, * as this is specific to Polymer's path binding syntax. * * @param {!Polymer_PropertyEffects} inst The instance the effect will be run on * @param {string} path Name of property * @param {Object} props Bag of current property changes * @param {Object} oldProps Bag of previous values for changed properties * @param {?} info Effect metadata * @param {boolean} hasPaths True with `props` contains one or more paths * @param {Array} nodeList List of nodes associated with `nodeInfoList` template * metadata * @return {void} * @private */ function runBindingEffect(inst, path, props, oldProps, info, hasPaths, nodeList) { let node = nodeList[info.index]; let binding = info.binding; let part = info.part; // Subpath notification: transform path and set to client // e.g.: foo="{{obj.sub}}", path: 'obj.sub.prop', set 'foo.prop'=obj.sub.prop if (hasPaths && part.source && (path.length > part.source.length) && (binding.kind == 'property') && !binding.isCompound && node.__isPropertyEffectsClient && node.__dataHasAccessor && node.__dataHasAccessor[binding.target]) { let value = props[path]; path = translate(part.source, binding.target, path); if (node._setPendingPropertyOrPath(path, value, false, true)) { inst._enqueueClient(node); } } else { let value = info.evaluator._evaluateBinding(inst, part, path, props, oldProps, hasPaths); // Propagate value to child // Abort if value is a no-op result if (value !== NOOP) { applyBindingValue(inst, node, binding, part, value); } } } /** * Sets the value for an "binding" (binding) effect to a node, * either as a property or attribute. * * @param {!Polymer_PropertyEffects} inst The instance owning the binding effect * @param {Node} node Target node for binding * @param {!Binding} binding Binding metadata * @param {!BindingPart} part Binding part metadata * @param {*} value Value to set * @return {void} * @private */ function applyBindingValue(inst, node, binding, part, value) { value = computeBindingValue(node, value, binding, part); if (sanitizeDOMValue) { value = sanitizeDOMValue(value, binding.target, binding.kind, node); } if (binding.kind == 'attribute') { // Attribute binding inst._valueToNodeAttribute(/** @type {Element} */(node), value, binding.target); } else { // Property binding let prop = binding.target; if (node.__isPropertyEffectsClient && node.__dataHasAccessor && node.__dataHasAccessor[prop]) { if (!node[TYPES.READ_ONLY] || !node[TYPES.READ_ONLY][prop]) { if (node._setPendingProperty(prop, value)) { inst._enqueueClient(node); } } } else { // In legacy no-batching mode, bindings applied before dataReady are // equivalent to the "apply config" phase, which only set managed props inst._setUnmanagedPropertyToNode(node, prop, value); } } } /** * Transforms an "binding" effect value based on compound & negation * effect metadata, as well as handling for special-case properties * * @param {Node} node Node the value will be set to * @param {*} value Value to set * @param {!Binding} binding Binding metadata * @param {!BindingPart} part Binding part metadata * @return {*} Transformed value to set * @private */ function computeBindingValue(node, value, binding, part) { if (binding.isCompound) { let storage = node.__dataCompoundStorage[binding.target]; storage[part.compoundIndex] = value; value = storage.join(''); } if (binding.kind !== 'attribute') { // Some browsers serialize `undefined` to `"undefined"` if (binding.target === 'textContent' || (binding.target === 'value' && (node.localName === 'input' || node.localName === 'textarea'))) { value = value == undefined ? '' : value; } } return value; } /** * Returns true if a binding's metadata meets all the requirements to allow * 2-way binding, and therefore a `<property>-changed` event listener should be * added: * - used curly braces * - is a property (not attribute) binding * - is not a textContent binding * - is not compound * * @param {!Binding} binding Binding metadata * @return {boolean} True if 2-way listener should be added * @private */ function shouldAddListener(binding) { return Boolean(binding.target) && binding.kind != 'attribute' && binding.kind != 'text' && !binding.isCompound && binding.parts[0].mode === '{'; } /** * Setup compound binding storage structures, notify listeners, and dataHost * references onto the bound nodeList. * * @param {!Polymer_PropertyEffects} inst Instance that bas been previously * bound * @param {TemplateInfo} templateInfo Template metadata * @return {void} * @private */ function setupBindings(inst, templateInfo) { // Setup compound storage, dataHost, and notify listeners let {nodeList, nodeInfoList} = templateInfo; if (nodeInfoList.length) { for (let i=0; i < nodeInfoList.length; i++) { let info = nodeInfoList[i]; let node = nodeList[i]; let bindings = info.bindings; if (bindings) { for (let i=0; i<bindings.length; i++) { let binding = bindings[i]; setupCompoundStorage(node, binding); addNotifyListener(node, inst, binding); } } // This ensures all bound elements have a host set, regardless // of whether they upgrade synchronous to creation node.__dataHost = inst; } } } /** * Initializes `__dataCompoundStorage` local storage on a bound node with * initial literal data for compound bindings, and sets the joined * literal parts to the bound property. * * When changes to compound parts occur, they are first set into the compound * storage array for that property, and then the array is joined to result in * the final value set to the property/attribute. * * @param {Node} node Bound node to initialize * @param {Binding} binding Binding metadata * @return {void} * @private */ function setupCompoundStorage(node, binding) { if (binding.isCompound) { // Create compound storage map let storage = node.__dataCompoundStorage || (node.__dataCompoundStorage = {}); let parts = binding.parts; // Copy literals from parts into storage for this binding let literals = new Array(parts.length); for (let j=0; j<parts.length; j++) { literals[j] = parts[j].literal; } let target = binding.target; storage[target] = literals; // Configure properties with their literal parts if (binding.literal && binding.kind == 'property') { // Note, className needs style scoping so this needs wrapping. // We may also want to consider doing this for `textContent` and // `innerHTML`. if (target === 'className') { node = wrap(node); } node[target] = binding.literal; } } } /** * Adds a 2-way binding notification event listener to the node specified * * @param {Object} node Child element to add listener to * @param {!Polymer_PropertyEffects} inst Host element instance to handle * notification event * @param {Binding} binding Binding metadata * @return {void} * @private */ function addNotifyListener(node, inst, binding) { if (binding.listenerEvent) { let part = binding.parts[0]; node.addEventListener(binding.listenerEvent, function(e) { handleNotification(e, inst, binding.target, part.source, part.negate); }); } } // -- for method-based effects (complexObserver & computed) -------------- /** * Adds property effects for each argument in the method signature (and * optionally, for the method name if `dynamic` is true) that calls the * provided effect function. * * @param {Element | Object} model Prototype or instance * @param {!MethodSignature} sig Method signature metadata * @param {string} type Type of property effect to add * @param {Function} effectFn Function to run when arguments change * @param {*=} methodInfo Effect-specific information to be included in * method effect metadata * @param {boolean|Object=} dynamicFn Boolean or object map indicating whether * method names should be included as a dependency to the effect. Note, * defaults to true if the signature is static (sig.static is true). * @return {!Object} Effect metadata for this method effect * @private */ function createMethodEffect(model, sig, type, effectFn, methodInfo, dynamicFn) { dynamicFn = sig.static || (dynamicFn && (typeof dynamicFn !== 'object' || dynamicFn[sig.methodName])); let info = { methodName: sig.methodName, args: sig.args, methodInfo, dynamicFn }; for (let i=0, arg; (i<sig.args.length) && (arg=sig.args[i]); i++) { if (!arg.literal) { model._addPropertyEffect(arg.rootProperty, type, { fn: effectFn, info: info, trigger: arg }); } } if (dynamicFn) { model._addPropertyEffect(sig.methodName, type, { fn: effectFn, info: info }); } return info; } /** * Calls a method with arguments marshaled from properties on the instance * based on the method signature contained in the effect metadata. * * Multi-property observers, computed properties, and inline computing * functions call this function to invoke the method, then use the return * value accordingly. * * @param {!Polymer_PropertyEffects} inst The instance the effect will be run on * @param {string} property Name of property * @param {Object} props Bag of current property changes * @param {Object} oldProps Bag of previous values for changed properties * @param {?} info Effect metadata * @return {*} Returns the return value from the method invocation * @private */ function runMethodEffect(inst, property, props, oldProps, info) { // Instances can optionally have a _methodHost which allows redirecting where // to find methods. Currently used by `templatize`. let context = inst._methodHost || inst; let fn = context[info.methodName]; if (fn) { let args = inst._marshalArgs(info.args, property, props); return args === NOOP ? NOOP : fn.apply(context, args); } else if (!info.dynamicFn) { console.warn('method `' + info.methodName + '` not defined'); } } const emptyArray = []; // Regular expressions used for binding const IDENT = '(?:' + '[a-zA-Z_$][\\w.:$\\-*]*' + ')'; const NUMBER = '(?:' + '[-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?' + ')'; const SQUOTE_STRING = '(?:' + '\'(?:[^\'\\\\]|\\\\.)*\'' + ')'; const DQUOTE_STRING = '(?:' + '"(?:[^"\\\\]|\\\\.)*"' + ')'; const STRING = '(?:' + SQUOTE_STRING + '|' + DQUOTE_STRING + ')'; const ARGUMENT = '(?:(' + IDENT + '|' + NUMBER + '|' + STRING + ')\\s*' + ')'; const ARGUMENTS = '(?:' + ARGUMENT + '(?:,\\s*' + ARGUMENT + ')*' + ')'; const ARGUMENT_LIST = '(?:' + '\\(\\s*' + '(?:' + ARGUMENTS + '?' + ')' + '\\)\\s*' + ')'; const BINDING = '(' + IDENT + '\\s*' + ARGUMENT_LIST + '?' + ')'; // Group 3 const OPEN_BRACKET = '(\\[\\[|{{)' + '\\s*'; const CLOSE_BRACKET = '(?:]]|}})'; const NEGATE = '(?:(!)\\s*)?'; // Group 2 const EXPRESSION = OPEN_BRACKET + NEGATE + BINDING + CLOSE_BRACKET; const bindingRegex = new RegExp(EXPRESSION, "g"); /** * Create a string from binding parts of all the literal parts * * @param {!Array<BindingPart>} parts All parts to stringify * @return {string} String made from the literal parts */ function literalFromParts(parts) { let s = ''; for (let i=0; i<parts.length; i++) { let literal = parts[i].literal; s += literal || ''; } return s; } /** * Parses an expression string for a method signature, and returns a metadata * describing the method in terms of `methodName`, `static` (whether all the * arguments are literals), and an array of `args` * * @param {string} expression The expression to parse * @return {?MethodSignature} The method metadata object if a method expression was * found, otherwise `undefined` * @private */ function parseMethod(expression) { // tries to match valid javascript property names let m = expression.match(/([^\s]+?)\(([\s\S]*)\)/); if (m) { let methodName = m[1]; let sig = { methodName, static: true, args: emptyArray }; if (m[2].trim()) { // replace escaped commas with comma entity, split on un-escaped commas let args = m[2].replace(/\\,/g, '&comma;').split(','); return parseArgs(args, sig); } else { return sig; } } return null; } /** * Parses an array of arguments and sets the `args` property of the supplied * signature metadata object. Sets the `static` property to false if any * argument is a non-literal. * * @param {!Array<string>} argList Array of argument names * @param {!MethodSignature} sig Method signature metadata object * @return {!MethodSignature} The updated signature metadata object * @private */ function parseArgs(argList, sig) { sig.args = argList.map(function(rawArg) { let arg = parseArg(rawArg); if (!arg.literal) { sig.static = false; } return arg; }, this); return sig; } /** * Parses an individual argument, and returns an argument metadata object * with the following fields: * * { * value: 'prop', // property/path or literal value * literal: false, // whether argument is a literal * structured: false, // whether the property is a path * rootProperty: 'prop', // the root property of the path * wildcard: false // whether the argument was a wildcard '.*' path * } * * @param {string} rawArg The string value of the argument * @return {!MethodArg} Argument metadata object * @private */ function parseArg(rawArg) { // clean up whitespace let arg = rawArg.trim() // replace comma entity with comma .replace(/&comma;/g, ',') // repair extra escape sequences; note only commas strictly need // escaping, but we allow any other char to be escaped since its // likely users will do this .replace(/\\(.)/g, '$1') ; // basic argument descriptor let a = { name: arg, value: '', literal: false }; // detect literal value (must be String or Number) let fc = arg[0]; if (fc === '-') { fc = arg[1]; } if (fc >= '0' && fc <= '9') { fc = '#'; } switch(fc) { case "'": case '"': a.value = arg.slice(1, -1); a.literal = true; break; case '#': a.value = Number(arg); a.literal = true; break; } // if not literal, look for structured path if (!a.literal) { a.rootProperty = root(arg); // detect structured path (has dots) a.structured = isPath(arg); if (a.structured) { a.wildcard = (arg.slice(-2) == '.*'); if (a.wildcard) { a.name = arg.slice(0, -2); } } } return a; } function getArgValue(data, props, path) { let value = get(data, path); // when data is not stored e.g. `splices`, get the value from changedProps // TODO(kschaaf): Note, this can cause a rare issue where the wildcard // info.value could pull a stale value out of changedProps during a reentrant // change that sets the value back to undefined. // https://github.com/Polymer/polymer/issues/5479 if (value === undefined) { value = props[path]; } return value; } // data api /** * Sends array splice notifications (`.splices` and `.length`) * * Note: this implementation only accepts normalized paths * * @param {!Polymer_PropertyEffects} inst Instance to send notifications to * @param {Array} array The array the mutations occurred on * @param {string} path The path to the array that was mutated * @param {Array} splices Array of splice records * @return {void} * @private */ function notifySplices(inst, array, path, splices) { const splicesData = { indexSplices: splices }; // Legacy behavior stored splices in `__data__` so it was *not* ephemeral. // To match this behavior, we store splices directly on the array. if (legacyUndefined && !inst._overrideLegacyUndefined) { array.splices = splicesData; } inst.notifyPath(path + '.splices', splicesData); inst.notifyPath(path + '.length', array.length); // Clear splice data only when it's stored on the array. if (legacyUndefined && !inst._overrideLegacyUndefined) { splicesData.indexSplices = []; } } /** * Creates a splice record and sends an array splice notification for * the described mutation * * Note: this implementation only accepts normalized paths * * @param {!Polymer_PropertyEffects} inst Instance to send notifications to * @param {Array} array The array the mutations occurred on * @param {string} path The path to the array that was mutated * @param {number} index Index at which the array mutation occurred * @param {number} addedCount Number of added items * @param {Array} removed Array of removed items * @return {void} * @private */ function notifySplice(inst, array, path, index, addedCount, removed) { notifySplices(inst, array, path, [{ index: index, addedCount: addedCount, removed: removed, object: array, type: 'splice' }]); } /** * Returns an upper-cased version of the string. * * @param {string} name String to uppercase * @return {string} Uppercased string * @private */ function upper(name) { return name[0].toUpperCase() + name.substring(1); } /** * Element class mixin that provides meta-programming for Polymer's template * binding and data observation (collectively, "property effects") system. * * This mixin uses provides the following key static methods for adding * property effects to an element class: * - `addPropertyEffect` * - `createPropertyObserver` * - `createMethodObserver` * - `createNotifyingProperty` * - `createReadOnlyProperty` * - `createReflectedProperty` * - `createComputedProperty` * - `bindTemplate` * * Each method creates one or more property accessors, along with metadata * used by this mixin's implementation of `_propertiesChanged` to perform * the property effects. * * Underscored versions of the above methods also exist on the element * prototype for adding property effects on instances at runtime. * * Note that this mixin overrides several `PropertyAccessors` methods, in * many cases to maintain guarantees provided by the Polymer 1.x features; * notably it changes property accessors to be synchronous by default * whereas the default when using `PropertyAccessors` standalone is to be * async by default. * * @mixinFunction * @polymer * @appliesMixin TemplateStamp * @appliesMixin PropertyAccessors * @summary Element class mixin that provides meta-programming for Polymer's * template binding and data observation system. */ export const PropertyEffects = dedupingMixin(superClass => { /** * @constructor * @implements {Polymer_PropertyAccessors} * @implements {Polymer_TemplateStamp} * @unrestricted * @private */ const propertyEffectsBase = TemplateStamp(PropertyAccessors(superClass)); /** * @polymer * @mixinClass * @implements {Polymer_PropertyEffects} * @extends {propertyEffectsBase} * @unrestricted */ class PropertyEffects extends propertyEffectsBase { constructor() { super(); /** @type {boolean} */ // Used to identify users of this mixin, ala instanceof this.__isPropertyEffectsClient = true; /** @type {boolean} */ this.__dataClientsReady; /** @type {Array} */ this.__dataPendingClients; /** @type {Object} */ this.__dataToNotify; /** @type {Object} */ this.__dataLinkedPaths; /** @type {boolean} */ this.__dataHasPaths; /** @type {Object} */ this.__dataCompoundStorage; /** @type {Polymer_PropertyEffects} */ this.__dataHost; /** @type {!Object} */ this.__dataTemp; /** @type {boolean} */ this.__dataClientsInitialized; /** @type {!Object} */ this.__data; /** @type {!Object|null} */ this.__dataPending; /** @type {!Object} */ this.__dataOld; /** @type {Object} */ this.__computeEffects; /** @type {Object} */ this.__computeInfo; /** @type {Object} */ this.__reflectEffects; /** @type {Object} */ this.__notifyEffects; /** @type {Object} */ this.__propagateEffects; /** @type {Object} */ this.__observeEffects; /** @type {Object} */ this.__readOnly; /** @type {!TemplateInfo} */ this.__templateInfo; /** @type {boolean} */ this._overrideLegacyUndefined; } get PROPERTY_EFFECT_TYPES() { return TYPES; } /** * @override * @return {void} */ _initializeProperties() { super._initializeProperties(); this._registerHost(); this.__dataClientsReady = false; this.__dataPendingClients = null; this.__dataToNotify = null; this.__dataLinkedPaths = null; this.__dataHasPaths = false; // May be set on instance prior to upgrade this.__dataCompoundStorage = this.__dataCompoundStorage || null; this.__dataHost = this.__dataHost || null; this.__dataTemp = {}; this.__dataClientsInitialized = false; } _registerHost() { if (hostStack.length) { let host = hostStack[hostStack.length-1]; host._enqueueClient(this); // This ensures even non-bound elements have a host set, as // long as they upgrade synchronously this.__dataHost = host; } } /** * Overrides `PropertyAccessors` implementation to provide a * more efficient implementation of initializing properties from * the prototype on the instance. * * @override * @param {Object} props Properties to initialize on the prototype * @return {void} */ _initializeProtoProperties(props) { this.__data = Object.create(props); this.__dataPending = Object.create(props); this.__dataOld = {}; } /** * Overrides `PropertyAccessors` implementation to avoid setting * `_setProperty`'s `shouldNotify: true`. * * @override * @param {Object} props Properties to initialize on the instance * @return {void} */ _initializeInstanceProperties(props) { let readOnly = this[TYPES.READ_ONLY]; for (let prop in props) { if (!readOnly || !readOnly[prop]) { this.__dataPending = this.__dataPending || {}; this.__dataOld = this.__dataOld || {}; this.__data