@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,363 lines (1,295 loc) • 109 kB
HTML
<!--
@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
-->
<link rel="import" href="../utils/boot.html">
<link rel="import" href="../utils/mixin.html">
<link rel="import" href="../utils/path.html">
<!-- for notify, reflect -->
<link rel="import" href="../utils/case-map.html">
<link rel="import" href="property-accessors.html">
<!-- for annotated effects -->
<link rel="import" href="template-stamp.html">
<script>
(function() {
'use strict';
/** @const {Object} */
const CaseMap = Polymer.CaseMap;
// Monotonically increasing unique ID used for de-duping effects triggered
// from multiple properties in the same turn
let dedupeId = 0;
/**
* 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 {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
let PropertyEffectsType; //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
* @return {Object} The own-property map of effects for the given type
* @private
*/
function ensureOwnEffectMap(model, type) {
let effects = model[type];
if (!effects) {
effects = model[type] = {};
} else if (!model.hasOwnProperty(type)) {
effects = model[type] = Object.create(model[type]);
for (let p in effects) {
let protoFx = effects[p];
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 {!PropertyEffectsType} 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;
let id = dedupeId++;
for (let prop in props) {
if (runEffectsForProperty(inst, effects, id, prop, props, oldProps, hasPaths, extraArgs)) {
ran = true;
}
}
return ran;
}
return false;
}
/**
* Runs a list of effects for a given property.
*
* @param {!PropertyEffectsType} 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 ? Polymer.Path.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 = trigger.name;
return (triggerPath == path) ||
(trigger.structured && Polymer.Path.isAncestor(triggerPath, path)) ||
(trigger.wildcard && Polymer.Path.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 {!PropertyEffectsType} 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 {!PropertyEffectsType} 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 {!PropertyEffectsType} 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 = Polymer.Path.root(path);
if (rootProperty !== path) {
let eventName = Polymer.CaseMap.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 {!PropertyEffectsType} 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;
}
/** @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 {!PropertyEffectsType} 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 ? Polymer.Path.root(property) : property;
let path = rootProperty != property ? property : null;
let value = path ? Polymer.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 {!PropertyEffectsType} 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 = Polymer.Path.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 {!PropertyEffectsType} 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 (Polymer.sanitizeDOMValue) {
value = Polymer.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 {!PropertyEffectsType} 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) {
let inputProps = changedProps;
while (runEffects(inst, computeEffects, inputProps, oldProps, hasPaths)) {
Object.assign(oldProps, inst.__dataOld);
Object.assign(changedProps, inst.__dataPending);
inputProps = inst.__dataPending;
inst.__dataPending = null;
}
}
}
/**
* 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 {!PropertyEffectsType} 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 runComputedEffect(inst, property, props, oldProps, info) {
let result = runMethodEffect(inst, property, props, oldProps, info);
let computedProp = info.methodInfo;
if (inst.__dataHasAccessor && inst.__dataHasAccessor[computedProp]) {
inst._setPendingProperty(computedProp, result, true);
} else {
inst[computedProp] = result;
}
}
/**
* Computes path changes based on path links set up using the `linkPaths`
* API.
*
* @param {!PropertyEffectsType} inst The instance whose props are changing
* @param {string | !Array<(string|number)>} 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 (Polymer.Path.isDescendant(a, path)) {
link = Polymer.Path.translate(a, b, path);
inst._setPendingPropertyOrPath(link, value, true, true);
} else if (Polymer.Path.isDescendant(b, path)) {
link = Polymer.Path.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 || (CaseMap.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 {!PropertyEffectsType} 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 = Polymer.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
applyBindingValue(inst, node, binding, part, value);
}
}
/**
* Sets the value for an "binding" (binding) effect to a node,
* either as a property or attribute.
*
* @param {!PropertyEffectsType} 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 (Polymer.sanitizeDOMValue) {
value = Polymer.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 {
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 {!PropertyEffectsType} 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);
}
}
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') {
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 {!PropertyEffectsType} 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 {void}
* @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
});
}
}
/**
* 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 {!PropertyEffectsType} 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 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 = Polymer.Path.root(arg);
// detect structured path (has dots)
a.structured = Polymer.Path.isPath(arg);
if (a.structured) {
a.wildcard = (arg.slice(-2) == '.*');
if (a.wildcard) {
a.name = arg.slice(0, -2);
}
}
}
return a;
}
// data api
/**
* Sends array splice notifications (`.splices` and `.length`)
*
* Note: this implementation only accepts normalized paths
*
* @param {!PropertyEffectsType} 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) {
let splicesPath = path + '.splices';
inst.notifyPath(splicesPath, { indexSplices: splices });
inst.notifyPath(path + '.length', array.length);
// Null here to allow potentially large splice records to be GC'ed.
inst.__data[splicesPath] = {indexSplices: null};
}
/**
* Creates a splice record and sends an array splice notification for
* the described mutation
*
* Note: this implementation only accepts normalized paths
*
* @param {!PropertyEffectsType} 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 Polymer.TemplateStamp
* @appliesMixin Polymer.PropertyAccessors
* @memberof Polymer
* @summary Element class mixin that provides meta-programming for Polymer's
* template binding and data observation system.
*/
Polymer.PropertyEffects = Polymer.dedupingMixin(superClass => {
/**
* @constructor
* @extends {superClass}
* @implements {Polymer_PropertyAccessors}
* @implements {Polymer_TemplateStamp}
* @unrestricted
* @private
*/
const propertyEffectsBase = Polymer.TemplateStamp(Polymer.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 {number} */
// NOTE: used to track re-entrant calls to `_flushProperties`
// path changes dirty check against `__dataTemp` only during one "turn"
// and are cleared when `__dataCounter` returns to 0.
this.__dataCounter = 0;
/** @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} */
this.__dataPending;
/** @type {!Object} */
this.__dataOld;
/** @type {Object} */
this.__computeEffects;
/** @type {Object} */
this.__reflectEffects;
/** @type {Object} */
this.__notifyEffects;
/** @type {Object} */
this.__propagateEffects;
/** @type {Object} */
this.__observeEffects;
/** @type {Object} */
this.__readOnly;
/** @type {!TemplateInfo} */
this.__templateInfo;
}
get PROPERTY_EFFECT_TYPES() {
return TYPES;
}
/**
* @return {void}
*/
_initializeProperties() {
super._initializeProperties();
hostStack.registerHost(this);
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;
}
/**
* Overrides `Polymer.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 `Polymer.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[prop] = this.__dataPending[prop] = props[prop];
}
}
}
// Prototype setup ----------------------------------------
/**
* Equivalent to static `addPropertyEffect` API but can be called on
* an instance to add effects at runtime. See that method for
* full API docs.
*
* @param {string} property Property that should trigger the effect
* @param {string} type Effect type, from this.PROPERTY_EFFECT_TYPES
* @param {Object=} effect Effect metadata object
* @return {void}
* @protected
*/
_addPropertyEffect(property, type, effect) {
this._createPropertyAccessor(property, type == TYPES.READ_ONLY);
// effects are accumulated into arrays per property based on type
let effects = ensureOwnEffectMap(this, type)[property];
if (!effects) {
effects = this[type][property] = [];
}
effects.push(effect);
}
/**
* Removes the given property effect.
*
* @param {string} property Property the effect was associated with
* @param {string} type Effect type, from this.PROPERTY_EFFECT_TYPES
* @param {Object=} effect Effect metadata object to remove
* @return {void}
*/
_removePropertyEffect(property, type, effect) {
let effects = ensureOwnEffectMap(this, type)[property];
let idx = effects.indexOf(effect);
if (idx >= 0) {
effects.splice(idx, 1);
}
}
/**
* Returns whether the current prototype/instance has a property effect
* of a certain type.
*
* @param {string} property Property name
* @param {string=} type Effect type, from this.PROPERTY_EFFECT_TYPES
* @return {boolean} True if the prototype/instance has an effect of this type
* @protected
*/
_hasPropertyEffect(property, type) {
let effects = this[type];
return Boolean(effects && effects[property]);
}
/**
* Returns whether the current prototype/instance has a "read only"
* accessor for the given property.
*
* @param {string} property Property name
* @return {boolean} True if the prototype/instance has an effect of this type
* @protected
*/
_hasReadOnlyEffect(property) {
return this._hasPropertyEffect(property, TYPES.READ_ONLY);
}
/**
* Returns whether the current prototype/instance has a "notify"
* property effect for the given property.
*
* @param {string} property Property name
* @return {boolean} True if the prototype/instance has an effect of this type
* @protected
*/
_hasNotifyEffect(property) {
return this._hasPropertyEffect(property, TYPES.NOTIFY);
}
/**
* Returns whether the current prototype/instance has a "reflect to attribute"
* property effect for the given property.
*
* @param {string} property Property name
* @return {boolean} True if the prototype/instance has an effect of this type
* @protected
*/
_hasReflectEffect(property) {
return this._hasPropertyEffect(property, TYPES.REFLECT);
}
/**
* Returns whether the current prototype/instance has a "computed"
* property effect for the given property.
*
* @param {string} property Property name
* @return {boolean} True if the prototype/instance has an effect of this type
* @protected
*/
_hasComputedEffect(property) {
return this._hasPropertyEffect(property, TYPES.COMPUTE);
}
// Runtime ----------------------------------------
/**
* Sets a pending property or path. If the root property of the path in
* question had no accessor, the path is set, otherwise it is enqueued
* via `_setPendingProperty`.
*
* This function isolates relatively expensive functionality necessary
* for the public API (`set`, `setProperties`, `notifyPath`, and property
* change listeners via {{...}} bindings), such that it is only done
* when paths enter the system, and not at every propagation step. It
* also sets a `__dataHasPaths` flag on the instance which is used to
* fast-path slower path-matching code in the property effects host paths.
*
* `path` can be a path string or array of path parts as accepted by the
* public API.
*
* @param {string | !Array<number|string>} path Path to set
* @param {*} value Value to set
* @param {boolean=} shouldNotify Set to true if this change should
* cause a property notification event dispatch
* @param {boolean=} isPathNotification If the path being set is a path
* notification of an already changed value, as opposed to a request
* to set and notify the change. In the latter `false` case, a dirty
* check is performed and then the value is set to the path before
* enqueuing the pending property change.
* @return {boolean} Returns true if the property/path was enqueued in
* the pending changes bag.
* @protected
*/
_setPendingPropertyOrPath(path, value, shouldNotify, isPathNotification) {
if (isPathNotification ||
Polymer.Path.root(Array.isArray(path) ? path[0] : path) !== path) {
// Dirty check changes being set to a path against the actual object,
// since this is the entry point for paths into the system; from here
// the only dirty checks are against the `__dataTemp` cache to prevent
// duplicate work in the same turn only. Note, if this was a notification
// of a change already set to a path (isPathNotification: true),
// we always let the change through and skip the `set` since it was
// already dirty checked at the point of entry and the underlying
// object has already been updated
if (!isPathNotification) {
let old = Polymer.Path.get(this, path);
path = /** @type {string} */ (Polymer.Path.set(this, path, value));
// Use property-accessor's simpler dirty check
if (!path || !super._shouldPropertyChange(path, value, old)) {
return false;
}
}
this.__dataHasPaths = true;
if (this._setPendingProperty(/**@type{string}*/(path), value, shouldNotify)) {
computeLinkedPaths(this, path, value);
return true;
}
} else {
if (this.__dataHasAccessor && this.__dataHasAccessor[path]) {
return this._setPendingProperty(/**@type{string}*/(path), value, shouldNotify);
} else {
this[path] = value;
}
}
return false;
}
/**
* Applies a value to a non-Polymer element/node's property.
*
* The implementation makes a best-effort at binding interop:
* Some native element propert