@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
JavaScript
/**
* @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, ',').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(/,/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