mirror of
https://github.com/Polymer/polymer.git
synced 2025-02-25 18:55:30 -06:00
These errors don't exist yet, but we need to suppress them in the files that will have them so that we're not broken when they're turned on.
2814 lines
101 KiB
JavaScript
2814 lines
101 KiB
JavaScript
/**
|
|
* @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 { 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 } from '../utils/settings.js';
|
|
|
|
// 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 ? 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 && 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 {!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 = 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 {!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 ? 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 {!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 = 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 (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 {!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 (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 {!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 = 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 (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 {
|
|
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 = 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;
|
|
}
|
|
|
|
// 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 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
|
|
* @extends {superClass}
|
|
* @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 {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 `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[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 ||
|
|
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 = get(this, path);
|
|
path = /** @type {string} */ (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 properties have side-effects when
|
|
* re-setting the same value (e.g. setting `<input>.value` resets the
|
|
* cursor position), so we do a dirty-check before setting the value.
|
|
* However, for better interop with non-Polymer custom elements that
|
|
* accept objects, we explicitly re-set object changes coming from the
|
|
* Polymer world (which may include deep object changes without the
|
|
* top reference changing), erring on the side of providing more
|
|
* information.
|
|
*
|
|
* Users may override this method to provide alternate approaches.
|
|
*
|
|
* @param {!Node} node The node to set a property on
|
|
* @param {string} prop The property to set
|
|
* @param {*} value The value to set
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_setUnmanagedPropertyToNode(node, prop, value) {
|
|
// It is a judgment call that resetting primitives is
|
|
// "bad" and resettings objects is also "good"; alternatively we could
|
|
// implement a whitelist of tag & property values that should never
|
|
// be reset (e.g. <input>.value && <select>.value)
|
|
if (value !== node[prop] || typeof value == 'object') {
|
|
node[prop] = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides the `PropertiesChanged` implementation to introduce special
|
|
* dirty check logic depending on the property & value being set:
|
|
*
|
|
* 1. Any value set to a path (e.g. 'obj.prop': 42 or 'obj.prop': {...})
|
|
* Stored in `__dataTemp`, dirty checked against `__dataTemp`
|
|
* 2. Object set to simple property (e.g. 'prop': {...})
|
|
* Stored in `__dataTemp` and `__data`, dirty checked against
|
|
* `__dataTemp` by default implementation of `_shouldPropertyChange`
|
|
* 3. Primitive value set to simple property (e.g. 'prop': 42)
|
|
* Stored in `__data`, dirty checked against `__data`
|
|
*
|
|
* The dirty-check is important to prevent cycles due to two-way
|
|
* notification, but paths and objects are only dirty checked against any
|
|
* previous value set during this turn via a "temporary cache" that is
|
|
* cleared when the last `_propertiesChanged` exits. This is so:
|
|
* a. any cached array paths (e.g. 'array.3.prop') may be invalidated
|
|
* due to array mutations like shift/unshift/splice; this is fine
|
|
* since path changes are dirty-checked at user entry points like `set`
|
|
* b. dirty-checking for objects only lasts one turn to allow the user
|
|
* to mutate the object in-place and re-set it with the same identity
|
|
* and have all sub-properties re-propagated in a subsequent turn.
|
|
*
|
|
* The temp cache is not necessarily sufficient to prevent invalid array
|
|
* paths, since a splice can happen during the same turn (with pathological
|
|
* user code); we could introduce a "fixup" for temporarily cached array
|
|
* paths if needed: https://github.com/Polymer/polymer/issues/4227
|
|
*
|
|
* @override
|
|
* @param {string} property Name of the property
|
|
* @param {*} value Value to set
|
|
* @param {boolean=} shouldNotify True if property should fire notification
|
|
* event (applies only for `notify: true` properties)
|
|
* @return {boolean} Returns true if the property changed
|
|
*/
|
|
_setPendingProperty(property, value, shouldNotify) {
|
|
let propIsPath = this.__dataHasPaths && isPath(property);
|
|
let prevProps = propIsPath ? this.__dataTemp : this.__data;
|
|
if (this._shouldPropertyChange(property, value, prevProps[property])) {
|
|
if (!this.__dataPending) {
|
|
this.__dataPending = {};
|
|
this.__dataOld = {};
|
|
}
|
|
// Ensure old is captured from the last turn
|
|
if (!(property in this.__dataOld)) {
|
|
this.__dataOld[property] = this.__data[property];
|
|
}
|
|
// Paths are stored in temporary cache (cleared at end of turn),
|
|
// which is used for dirty-checking, all others stored in __data
|
|
if (propIsPath) {
|
|
this.__dataTemp[property] = value;
|
|
} else {
|
|
this.__data[property] = value;
|
|
}
|
|
// All changes go into pending property bag, passed to _propertiesChanged
|
|
this.__dataPending[property] = value;
|
|
// Track properties that should notify separately
|
|
if (propIsPath || (this[TYPES.NOTIFY] && this[TYPES.NOTIFY][property])) {
|
|
this.__dataToNotify = this.__dataToNotify || {};
|
|
this.__dataToNotify[property] = shouldNotify;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Overrides base implementation to ensure all accessors set `shouldNotify`
|
|
* to true, for per-property notification tracking.
|
|
*
|
|
* @override
|
|
* @param {string} property Name of the property
|
|
* @param {*} value Value to set
|
|
* @return {void}
|
|
*/
|
|
_setProperty(property, value) {
|
|
if (this._setPendingProperty(property, value, true)) {
|
|
this._invalidateProperties();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides `PropertyAccessor`'s default async queuing of
|
|
* `_propertiesChanged`: if `__dataReady` is false (has not yet been
|
|
* manually flushed), the function no-ops; otherwise flushes
|
|
* `_propertiesChanged` synchronously.
|
|
*
|
|
* @override
|
|
* @return {void}
|
|
*/
|
|
_invalidateProperties() {
|
|
if (this.__dataReady) {
|
|
this._flushProperties();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enqueues the given client on a list of pending clients, whose
|
|
* pending property changes can later be flushed via a call to
|
|
* `_flushClients`.
|
|
*
|
|
* @param {Object} client PropertyEffects client to enqueue
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_enqueueClient(client) {
|
|
this.__dataPendingClients = this.__dataPendingClients || [];
|
|
if (client !== this) {
|
|
this.__dataPendingClients.push(client);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides superclass implementation.
|
|
*
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_flushProperties() {
|
|
this.__dataCounter++;
|
|
super._flushProperties();
|
|
this.__dataCounter--;
|
|
}
|
|
|
|
/**
|
|
* Flushes any clients previously enqueued via `_enqueueClient`, causing
|
|
* their `_flushProperties` method to run.
|
|
*
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_flushClients() {
|
|
if (!this.__dataClientsReady) {
|
|
this.__dataClientsReady = true;
|
|
this._readyClients();
|
|
// Override point where accessors are turned on; importantly,
|
|
// this is after clients have fully readied, providing a guarantee
|
|
// that any property effects occur only after all clients are ready.
|
|
this.__dataReady = true;
|
|
} else {
|
|
this.__enableOrFlushClients();
|
|
}
|
|
}
|
|
|
|
// NOTE: We ensure clients either enable or flush as appropriate. This
|
|
// handles two corner cases:
|
|
// (1) clients flush properly when connected/enabled before the host
|
|
// enables; e.g.
|
|
// (a) Templatize stamps with no properties and does not flush and
|
|
// (b) the instance is inserted into dom and
|
|
// (c) then the instance flushes.
|
|
// (2) clients enable properly when not connected/enabled when the host
|
|
// flushes; e.g.
|
|
// (a) a template is runtime stamped and not yet connected/enabled
|
|
// (b) a host sets a property, causing stamped dom to flush
|
|
// (c) the stamped dom enables.
|
|
__enableOrFlushClients() {
|
|
let clients = this.__dataPendingClients;
|
|
if (clients) {
|
|
this.__dataPendingClients = null;
|
|
for (let i=0; i < clients.length; i++) {
|
|
let client = clients[i];
|
|
if (!client.__dataEnabled) {
|
|
client._enableProperties();
|
|
} else if (client.__dataPending) {
|
|
client._flushProperties();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Perform any initial setup on client dom. Called before the first
|
|
* `_flushProperties` call on client dom and before any element
|
|
* observers are called.
|
|
*
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_readyClients() {
|
|
this.__enableOrFlushClients();
|
|
}
|
|
|
|
/**
|
|
* Sets a bag of property changes to this instance, and
|
|
* synchronously processes all effects of the properties as a batch.
|
|
*
|
|
* Property names must be simple properties, not paths. Batched
|
|
* path propagation is not supported.
|
|
*
|
|
* @param {Object} props Bag of one or more key-value pairs whose key is
|
|
* a property and value is the new value to set for that property.
|
|
* @param {boolean=} setReadOnly When true, any private values set in
|
|
* `props` will be set. By default, `setProperties` will not set
|
|
* `readOnly: true` root properties.
|
|
* @return {void}
|
|
* @public
|
|
*/
|
|
setProperties(props, setReadOnly) {
|
|
for (let path in props) {
|
|
if (setReadOnly || !this[TYPES.READ_ONLY] || !this[TYPES.READ_ONLY][path]) {
|
|
//TODO(kschaaf): explicitly disallow paths in setProperty?
|
|
// wildcard observers currently only pass the first changed path
|
|
// in the `info` object, and you could do some odd things batching
|
|
// paths, e.g. {'foo.bar': {...}, 'foo': null}
|
|
this._setPendingPropertyOrPath(path, props[path], true);
|
|
}
|
|
}
|
|
this._invalidateProperties();
|
|
}
|
|
|
|
/**
|
|
* Overrides `PropertyAccessors` so that property accessor
|
|
* side effects are not enabled until after client dom is fully ready.
|
|
* Also calls `_flushClients` callback to ensure client dom is enabled
|
|
* that was not enabled as a result of flushing properties.
|
|
*
|
|
* @override
|
|
* @return {void}
|
|
*/
|
|
ready() {
|
|
// It is important that `super.ready()` is not called here as it
|
|
// immediately turns on accessors. Instead, we wait until `readyClients`
|
|
// to enable accessors to provide a guarantee that clients are ready
|
|
// before processing any accessors side effects.
|
|
this._flushProperties();
|
|
// If no data was pending, `_flushProperties` will not `flushClients`
|
|
// so ensure this is done.
|
|
if (!this.__dataClientsReady) {
|
|
this._flushClients();
|
|
}
|
|
// Before ready, client notifications do not trigger _flushProperties.
|
|
// Therefore a flush is necessary here if data has been set.
|
|
if (this.__dataPending) {
|
|
this._flushProperties();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements `PropertyAccessors`'s properties changed callback.
|
|
*
|
|
* Runs each class of effects for the batch of changed properties in
|
|
* a specific order (compute, propagate, reflect, observe, notify).
|
|
*
|
|
* @param {!Object} currentProps Bag of all current accessor values
|
|
* @param {?Object} changedProps Bag of properties changed since the last
|
|
* call to `_propertiesChanged`
|
|
* @param {?Object} oldProps Bag of previous values for each property
|
|
* in `changedProps`
|
|
* @return {void}
|
|
*/
|
|
_propertiesChanged(currentProps, changedProps, oldProps) {
|
|
// ----------------------------
|
|
// let c = Object.getOwnPropertyNames(changedProps || {});
|
|
// window.debug && console.group(this.localName + '#' + this.id + ': ' + c);
|
|
// if (window.debug) { debugger; }
|
|
// ----------------------------
|
|
let hasPaths = this.__dataHasPaths;
|
|
this.__dataHasPaths = false;
|
|
// Compute properties
|
|
runComputedEffects(this, changedProps, oldProps, hasPaths);
|
|
// Clear notify properties prior to possible reentry (propagate, observe),
|
|
// but after computing effects have a chance to add to them
|
|
let notifyProps = this.__dataToNotify;
|
|
this.__dataToNotify = null;
|
|
// Propagate properties to clients
|
|
this._propagatePropertyChanges(changedProps, oldProps, hasPaths);
|
|
// Flush clients
|
|
this._flushClients();
|
|
// Reflect properties
|
|
runEffects(this, this[TYPES.REFLECT], changedProps, oldProps, hasPaths);
|
|
// Observe properties
|
|
runEffects(this, this[TYPES.OBSERVE], changedProps, oldProps, hasPaths);
|
|
// Notify properties to host
|
|
if (notifyProps) {
|
|
runNotifyEffects(this, notifyProps, changedProps, oldProps, hasPaths);
|
|
}
|
|
// Clear temporary cache at end of turn
|
|
if (this.__dataCounter == 1) {
|
|
this.__dataTemp = {};
|
|
}
|
|
// ----------------------------
|
|
// window.debug && console.groupEnd(this.localName + '#' + this.id + ': ' + c);
|
|
// ----------------------------
|
|
}
|
|
|
|
/**
|
|
* Called to propagate any property changes to stamped template nodes
|
|
* managed by this element.
|
|
*
|
|
* @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}
|
|
* @protected
|
|
*/
|
|
_propagatePropertyChanges(changedProps, oldProps, hasPaths) {
|
|
if (this[TYPES.PROPAGATE]) {
|
|
runEffects(this, this[TYPES.PROPAGATE], changedProps, oldProps, hasPaths);
|
|
}
|
|
let templateInfo = this.__templateInfo;
|
|
while (templateInfo) {
|
|
runEffects(this, templateInfo.propertyEffects, changedProps, oldProps,
|
|
hasPaths, templateInfo.nodeList);
|
|
templateInfo = templateInfo.nextTemplateInfo;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aliases one data path as another, such that path notifications from one
|
|
* are routed to the other.
|
|
*
|
|
* @param {string | !Array<string|number>} to Target path to link.
|
|
* @param {string | !Array<string|number>} from Source path to link.
|
|
* @return {void}
|
|
* @public
|
|
*/
|
|
linkPaths(to, from) {
|
|
to = normalize(to);
|
|
from = normalize(from);
|
|
this.__dataLinkedPaths = this.__dataLinkedPaths || {};
|
|
this.__dataLinkedPaths[to] = from;
|
|
}
|
|
|
|
/**
|
|
* Removes a data path alias previously established with `_linkPaths`.
|
|
*
|
|
* Note, the path to unlink should be the target (`to`) used when
|
|
* linking the paths.
|
|
*
|
|
* @param {string | !Array<string|number>} path Target path to unlink.
|
|
* @return {void}
|
|
* @public
|
|
*/
|
|
unlinkPaths(path) {
|
|
path = normalize(path);
|
|
if (this.__dataLinkedPaths) {
|
|
delete this.__dataLinkedPaths[path];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify that an array has changed.
|
|
*
|
|
* Example:
|
|
*
|
|
* this.items = [ {name: 'Jim'}, {name: 'Todd'}, {name: 'Bill'} ];
|
|
* ...
|
|
* this.items.splice(1, 1, {name: 'Sam'});
|
|
* this.items.push({name: 'Bob'});
|
|
* this.notifySplices('items', [
|
|
* { index: 1, removed: [{name: 'Todd'}], addedCount: 1, object: this.items, type: 'splice' },
|
|
* { index: 3, removed: [], addedCount: 1, object: this.items, type: 'splice'}
|
|
* ]);
|
|
*
|
|
* @param {string} path Path that should be notified.
|
|
* @param {Array} splices Array of splice records indicating ordered
|
|
* changes that occurred to the array. Each record should have the
|
|
* following fields:
|
|
* * index: index at which the change occurred
|
|
* * removed: array of items that were removed from this index
|
|
* * addedCount: number of new items added at this index
|
|
* * object: a reference to the array in question
|
|
* * type: the string literal 'splice'
|
|
*
|
|
* Note that splice records _must_ be normalized such that they are
|
|
* reported in index order (raw results from `Object.observe` are not
|
|
* ordered and must be normalized/merged before notifying).
|
|
* @return {void}
|
|
* @public
|
|
*/
|
|
notifySplices(path, splices) {
|
|
let info = {path: ''};
|
|
let array = /** @type {Array} */(get(this, path, info));
|
|
notifySplices(this, array, info.path, splices);
|
|
}
|
|
|
|
/**
|
|
* Convenience method for reading a value from a path.
|
|
*
|
|
* Note, if any part in the path is undefined, this method returns
|
|
* `undefined` (this method does not throw when dereferencing undefined
|
|
* paths).
|
|
*
|
|
* @param {(string|!Array<(string|number)>)} path Path to the value
|
|
* to read. The path may be specified as a string (e.g. `foo.bar.baz`)
|
|
* or an array of path parts (e.g. `['foo.bar', 'baz']`). Note that
|
|
* bracketed expressions are not supported; string-based path parts
|
|
* *must* be separated by dots. Note that when dereferencing array
|
|
* indices, the index may be used as a dotted part directly
|
|
* (e.g. `users.12.name` or `['users', 12, 'name']`).
|
|
* @param {Object=} root Root object from which the path is evaluated.
|
|
* @return {*} Value at the path, or `undefined` if any part of the path
|
|
* is undefined.
|
|
* @public
|
|
*/
|
|
get(path, root) {
|
|
return get(root || this, path);
|
|
}
|
|
|
|
/**
|
|
* Convenience method for setting a value to a path and notifying any
|
|
* elements bound to the same path.
|
|
*
|
|
* Note, if any part in the path except for the last is undefined,
|
|
* this method does nothing (this method does not throw when
|
|
* dereferencing undefined paths).
|
|
*
|
|
* @param {(string|!Array<(string|number)>)} path Path to the value
|
|
* to write. The path may be specified as a string (e.g. `'foo.bar.baz'`)
|
|
* or an array of path parts (e.g. `['foo.bar', 'baz']`). Note that
|
|
* bracketed expressions are not supported; string-based path parts
|
|
* *must* be separated by dots. Note that when dereferencing array
|
|
* indices, the index may be used as a dotted part directly
|
|
* (e.g. `'users.12.name'` or `['users', 12, 'name']`).
|
|
* @param {*} value Value to set at the specified path.
|
|
* @param {Object=} root Root object from which the path is evaluated.
|
|
* When specified, no notification will occur.
|
|
* @return {void}
|
|
* @public
|
|
*/
|
|
set(path, value, root) {
|
|
if (root) {
|
|
set(root, path, value);
|
|
} else {
|
|
if (!this[TYPES.READ_ONLY] || !this[TYPES.READ_ONLY][/** @type {string} */(path)]) {
|
|
if (this._setPendingPropertyOrPath(path, value, true)) {
|
|
this._invalidateProperties();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds items onto the end of the array at the path specified.
|
|
*
|
|
* The arguments after `path` and return value match that of
|
|
* `Array.prototype.push`.
|
|
*
|
|
* This method notifies other paths to the same array that a
|
|
* splice occurred to the array.
|
|
*
|
|
* @param {string | !Array<string|number>} path Path to array.
|
|
* @param {...*} items Items to push onto array
|
|
* @return {number} New length of the array.
|
|
* @public
|
|
*/
|
|
push(path, ...items) {
|
|
let info = {path: ''};
|
|
let array = /** @type {Array}*/(get(this, path, info));
|
|
let len = array.length;
|
|
let ret = array.push(...items);
|
|
if (items.length) {
|
|
notifySplice(this, array, info.path, len, items.length, []);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Removes an item from the end of array at the path specified.
|
|
*
|
|
* The arguments after `path` and return value match that of
|
|
* `Array.prototype.pop`.
|
|
*
|
|
* This method notifies other paths to the same array that a
|
|
* splice occurred to the array.
|
|
*
|
|
* @param {string | !Array<string|number>} path Path to array.
|
|
* @return {*} Item that was removed.
|
|
* @public
|
|
*/
|
|
pop(path) {
|
|
let info = {path: ''};
|
|
let array = /** @type {Array} */(get(this, path, info));
|
|
let hadLength = Boolean(array.length);
|
|
let ret = array.pop();
|
|
if (hadLength) {
|
|
notifySplice(this, array, info.path, array.length, 0, [ret]);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Starting from the start index specified, removes 0 or more items
|
|
* from the array and inserts 0 or more new items in their place.
|
|
*
|
|
* The arguments after `path` and return value match that of
|
|
* `Array.prototype.splice`.
|
|
*
|
|
* This method notifies other paths to the same array that a
|
|
* splice occurred to the array.
|
|
*
|
|
* @param {string | !Array<string|number>} path Path to array.
|
|
* @param {number} start Index from which to start removing/inserting.
|
|
* @param {number=} deleteCount Number of items to remove.
|
|
* @param {...*} items Items to insert into array.
|
|
* @return {Array} Array of removed items.
|
|
* @public
|
|
*/
|
|
splice(path, start, deleteCount, ...items) {
|
|
let info = {path : ''};
|
|
let array = /** @type {Array} */(get(this, path, info));
|
|
// Normalize fancy native splice handling of crazy start values
|
|
if (start < 0) {
|
|
start = array.length - Math.floor(-start);
|
|
} else if (start) {
|
|
start = Math.floor(start);
|
|
}
|
|
// array.splice does different things based on the number of arguments
|
|
// you pass in. Therefore, array.splice(0) and array.splice(0, undefined)
|
|
// do different things. In the former, the whole array is cleared. In the
|
|
// latter, no items are removed.
|
|
// This means that we need to detect whether 1. one of the arguments
|
|
// is actually passed in and then 2. determine how many arguments
|
|
// we should pass on to the native array.splice
|
|
//
|
|
let ret;
|
|
// Omit any additional arguments if they were not passed in
|
|
if (arguments.length === 2) {
|
|
ret = array.splice(start);
|
|
// Either start was undefined and the others were defined, but in this
|
|
// case we can safely pass on all arguments
|
|
//
|
|
// Note: this includes the case where none of the arguments were passed in,
|
|
// e.g. this.splice('array'). However, if both start and deleteCount
|
|
// are undefined, array.splice will not modify the array (as expected)
|
|
} else {
|
|
ret = array.splice(start, deleteCount, ...items);
|
|
}
|
|
// At the end, check whether any items were passed in (e.g. insertions)
|
|
// or if the return array contains items (e.g. deletions).
|
|
// Only notify if items were added or deleted.
|
|
if (items.length || ret.length) {
|
|
notifySplice(this, array, info.path, start, items.length, ret);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Removes an item from the beginning of array at the path specified.
|
|
*
|
|
* The arguments after `path` and return value match that of
|
|
* `Array.prototype.pop`.
|
|
*
|
|
* This method notifies other paths to the same array that a
|
|
* splice occurred to the array.
|
|
*
|
|
* @param {string | !Array<string|number>} path Path to array.
|
|
* @return {*} Item that was removed.
|
|
* @public
|
|
*/
|
|
shift(path) {
|
|
let info = {path: ''};
|
|
let array = /** @type {Array} */(get(this, path, info));
|
|
let hadLength = Boolean(array.length);
|
|
let ret = array.shift();
|
|
if (hadLength) {
|
|
notifySplice(this, array, info.path, 0, 0, [ret]);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Adds items onto the beginning of the array at the path specified.
|
|
*
|
|
* The arguments after `path` and return value match that of
|
|
* `Array.prototype.push`.
|
|
*
|
|
* This method notifies other paths to the same array that a
|
|
* splice occurred to the array.
|
|
*
|
|
* @param {string | !Array<string|number>} path Path to array.
|
|
* @param {...*} items Items to insert info array
|
|
* @return {number} New length of the array.
|
|
* @public
|
|
*/
|
|
unshift(path, ...items) {
|
|
let info = {path: ''};
|
|
let array = /** @type {Array} */(get(this, path, info));
|
|
let ret = array.unshift(...items);
|
|
if (items.length) {
|
|
notifySplice(this, array, info.path, 0, items.length, []);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Notify that a path has changed.
|
|
*
|
|
* Example:
|
|
*
|
|
* this.item.user.name = 'Bob';
|
|
* this.notifyPath('item.user.name');
|
|
*
|
|
* @param {string} path Path that should be notified.
|
|
* @param {*=} value Value at the path (optional).
|
|
* @return {void}
|
|
* @public
|
|
*/
|
|
notifyPath(path, value) {
|
|
/** @type {string} */
|
|
let propPath;
|
|
if (arguments.length == 1) {
|
|
// Get value if not supplied
|
|
let info = {path: ''};
|
|
value = get(this, path, info);
|
|
propPath = info.path;
|
|
} else if (Array.isArray(path)) {
|
|
// Normalize path if needed
|
|
propPath = normalize(path);
|
|
} else {
|
|
propPath = /** @type{string} */(path);
|
|
}
|
|
if (this._setPendingPropertyOrPath(propPath, value, true, true)) {
|
|
this._invalidateProperties();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Equivalent to static `createReadOnlyProperty` API but can be called on
|
|
* an instance to add effects at runtime. See that method for
|
|
* full API docs.
|
|
*
|
|
* @param {string} property Property name
|
|
* @param {boolean=} protectedSetter Creates a custom protected setter
|
|
* when `true`.
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_createReadOnlyProperty(property, protectedSetter) {
|
|
this._addPropertyEffect(property, TYPES.READ_ONLY);
|
|
if (protectedSetter) {
|
|
this['_set' + upper(property)] = /** @this {PropertyEffects} */function(value) {
|
|
this._setProperty(property, value);
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Equivalent to static `createPropertyObserver` API but can be called on
|
|
* an instance to add effects at runtime. See that method for
|
|
* full API docs.
|
|
*
|
|
* @param {string} property Property name
|
|
* @param {string|function(*,*)} method Function or name of observer method to call
|
|
* @param {boolean=} dynamicFn Whether the method name should be included as
|
|
* a dependency to the effect.
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_createPropertyObserver(property, method, dynamicFn) {
|
|
let info = { property, method, dynamicFn: Boolean(dynamicFn) };
|
|
this._addPropertyEffect(property, TYPES.OBSERVE, {
|
|
fn: runObserverEffect, info, trigger: {name: property}
|
|
});
|
|
if (dynamicFn) {
|
|
this._addPropertyEffect(/** @type {string} */(method), TYPES.OBSERVE, {
|
|
fn: runObserverEffect, info, trigger: {name: method}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Equivalent to static `createMethodObserver` API but can be called on
|
|
* an instance to add effects at runtime. See that method for
|
|
* full API docs.
|
|
*
|
|
* @param {string} expression Method expression
|
|
* @param {boolean|Object=} dynamicFn Boolean or object map indicating
|
|
* whether method names should be included as a dependency to the effect.
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_createMethodObserver(expression, dynamicFn) {
|
|
let sig = parseMethod(expression);
|
|
if (!sig) {
|
|
throw new Error("Malformed observer expression '" + expression + "'");
|
|
}
|
|
createMethodEffect(this, sig, TYPES.OBSERVE, runMethodEffect, null, dynamicFn);
|
|
}
|
|
|
|
/**
|
|
* Equivalent to static `createNotifyingProperty` API but can be called on
|
|
* an instance to add effects at runtime. See that method for
|
|
* full API docs.
|
|
*
|
|
* @param {string} property Property name
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_createNotifyingProperty(property) {
|
|
this._addPropertyEffect(property, TYPES.NOTIFY, {
|
|
fn: runNotifyEffect,
|
|
info: {
|
|
eventName: camelToDashCase(property) + '-changed',
|
|
property: property
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Equivalent to static `createReflectedProperty` API but can be called on
|
|
* an instance to add effects at runtime. See that method for
|
|
* full API docs.
|
|
*
|
|
* @param {string} property Property name
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_createReflectedProperty(property) {
|
|
let attr = this.constructor.attributeNameForProperty(property);
|
|
if (attr[0] === '-') {
|
|
console.warn('Property ' + property + ' cannot be reflected to attribute ' +
|
|
attr + ' because "-" is not a valid starting attribute name. Use a lowercase first letter for the property instead.');
|
|
} else {
|
|
this._addPropertyEffect(property, TYPES.REFLECT, {
|
|
fn: runReflectEffect,
|
|
info: {
|
|
attrName: attr
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Equivalent to static `createComputedProperty` API but can be called on
|
|
* an instance to add effects at runtime. See that method for
|
|
* full API docs.
|
|
*
|
|
* @param {string} property Name of computed property to set
|
|
* @param {string} expression Method expression
|
|
* @param {boolean|Object=} dynamicFn Boolean or object map indicating
|
|
* whether method names should be included as a dependency to the effect.
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_createComputedProperty(property, expression, dynamicFn) {
|
|
let sig = parseMethod(expression);
|
|
if (!sig) {
|
|
throw new Error("Malformed computed expression '" + expression + "'");
|
|
}
|
|
createMethodEffect(this, sig, TYPES.COMPUTE, runComputedEffect, property, dynamicFn);
|
|
}
|
|
|
|
/**
|
|
* Gather the argument values for a method specified in the provided array
|
|
* of argument metadata.
|
|
*
|
|
* The `path` and `value` arguments are used to fill in wildcard descriptor
|
|
* when the method is being called as a result of a path notification.
|
|
*
|
|
* @param {!Array<!MethodArg>} args Array of argument metadata
|
|
* @param {string} path Property/path name that triggered the method effect
|
|
* @param {Object} props Bag of current property changes
|
|
* @return {Array<*>} Array of argument values
|
|
* @private
|
|
*/
|
|
_marshalArgs(args, path, props) {
|
|
const data = this.__data;
|
|
let values = [];
|
|
for (let i=0, l=args.length; i<l; i++) {
|
|
let arg = args[i];
|
|
let name = arg.name;
|
|
let v;
|
|
if (arg.literal) {
|
|
v = arg.value;
|
|
} else {
|
|
if (arg.structured) {
|
|
v = get(data, name);
|
|
// when data is not stored e.g. `splices`
|
|
if (v === undefined) {
|
|
v = props[name];
|
|
}
|
|
} else {
|
|
v = data[name];
|
|
}
|
|
}
|
|
if (arg.wildcard) {
|
|
// Only send the actual path changed info if the change that
|
|
// caused the observer to run matched the wildcard
|
|
let baseChanged = (name.indexOf(path + '.') === 0);
|
|
let matches = (path.indexOf(name) === 0 && !baseChanged);
|
|
values[i] = {
|
|
path: matches ? path : name,
|
|
value: matches ? props[path] : v,
|
|
base: v
|
|
};
|
|
} else {
|
|
values[i] = v;
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
|
|
// -- static class methods ------------
|
|
|
|
/**
|
|
* Ensures an accessor exists for the specified property, and adds
|
|
* to a list of "property effects" that will run when the accessor for
|
|
* the specified property is set. Effects are grouped by "type", which
|
|
* roughly corresponds to a phase in effect processing. The effect
|
|
* metadata should be in the following form:
|
|
*
|
|
* {
|
|
* fn: effectFunction, // Reference to function to call to perform effect
|
|
* info: { ... } // Effect metadata passed to function
|
|
* trigger: { // Optional triggering metadata; if not provided
|
|
* name: string // the property is treated as a wildcard
|
|
* structured: boolean
|
|
* wildcard: boolean
|
|
* }
|
|
* }
|
|
*
|
|
* Effects are called from `_propertiesChanged` in the following order by
|
|
* type:
|
|
*
|
|
* 1. COMPUTE
|
|
* 2. PROPAGATE
|
|
* 3. REFLECT
|
|
* 4. OBSERVE
|
|
* 5. NOTIFY
|
|
*
|
|
* Effect functions are called with the following signature:
|
|
*
|
|
* effectFunction(inst, path, props, oldProps, info, hasPaths)
|
|
*
|
|
* @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
|
|
*/
|
|
static addPropertyEffect(property, type, effect) {
|
|
this.prototype._addPropertyEffect(property, type, effect);
|
|
}
|
|
|
|
/**
|
|
* Creates a single-property observer for the given property.
|
|
*
|
|
* @param {string} property Property name
|
|
* @param {string|function(*,*)} method Function or name of observer method to call
|
|
* @param {boolean=} dynamicFn Whether the method name should be included as
|
|
* a dependency to the effect.
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
static createPropertyObserver(property, method, dynamicFn) {
|
|
this.prototype._createPropertyObserver(property, method, dynamicFn);
|
|
}
|
|
|
|
/**
|
|
* Creates a multi-property "method observer" based on the provided
|
|
* expression, which should be a string in the form of a normal JavaScript
|
|
* function signature: `'methodName(arg1, [..., argn])'`. Each argument
|
|
* should correspond to a property or path in the context of this
|
|
* prototype (or instance), or may be a literal string or number.
|
|
*
|
|
* @param {string} expression Method expression
|
|
* @param {boolean|Object=} dynamicFn Boolean or object map indicating
|
|
* @return {void}
|
|
* whether method names should be included as a dependency to the effect.
|
|
* @protected
|
|
*/
|
|
static createMethodObserver(expression, dynamicFn) {
|
|
this.prototype._createMethodObserver(expression, dynamicFn);
|
|
}
|
|
|
|
/**
|
|
* Causes the setter for the given property to dispatch `<property>-changed`
|
|
* events to notify of changes to the property.
|
|
*
|
|
* @param {string} property Property name
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
static createNotifyingProperty(property) {
|
|
this.prototype._createNotifyingProperty(property);
|
|
}
|
|
|
|
/**
|
|
* Creates a read-only accessor for the given property.
|
|
*
|
|
* To set the property, use the protected `_setProperty` API.
|
|
* To create a custom protected setter (e.g. `_setMyProp()` for
|
|
* property `myProp`), pass `true` for `protectedSetter`.
|
|
*
|
|
* Note, if the property will have other property effects, this method
|
|
* should be called first, before adding other effects.
|
|
*
|
|
* @param {string} property Property name
|
|
* @param {boolean=} protectedSetter Creates a custom protected setter
|
|
* when `true`.
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
static createReadOnlyProperty(property, protectedSetter) {
|
|
this.prototype._createReadOnlyProperty(property, protectedSetter);
|
|
}
|
|
|
|
/**
|
|
* Causes the setter for the given property to reflect the property value
|
|
* to a (dash-cased) attribute of the same name.
|
|
*
|
|
* @param {string} property Property name
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
static createReflectedProperty(property) {
|
|
this.prototype._createReflectedProperty(property);
|
|
}
|
|
|
|
/**
|
|
* Creates a computed property whose value is set to the result of the
|
|
* method described by the given `expression` each time one or more
|
|
* arguments to the method changes. The expression should be a string
|
|
* in the form of a normal JavaScript function signature:
|
|
* `'methodName(arg1, [..., argn])'`
|
|
*
|
|
* @param {string} property Name of computed property to set
|
|
* @param {string} expression Method expression
|
|
* @param {boolean|Object=} dynamicFn Boolean or object map indicating whether
|
|
* method names should be included as a dependency to the effect.
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
static createComputedProperty(property, expression, dynamicFn) {
|
|
this.prototype._createComputedProperty(property, expression, dynamicFn);
|
|
}
|
|
|
|
/**
|
|
* Parses the provided template to ensure binding effects are created
|
|
* for them, and then ensures property accessors are created for any
|
|
* dependent properties in the template. Binding effects for bound
|
|
* templates are stored in a linked list on the instance so that
|
|
* templates can be efficiently stamped and unstamped.
|
|
*
|
|
* @param {!HTMLTemplateElement} template Template containing binding
|
|
* bindings
|
|
* @return {!TemplateInfo} Template metadata object
|
|
* @protected
|
|
*/
|
|
static bindTemplate(template) {
|
|
return this.prototype._bindTemplate(template);
|
|
}
|
|
|
|
// -- binding ----------------------------------------------
|
|
|
|
/**
|
|
* Equivalent to static `bindTemplate` API but can be called on
|
|
* an instance to add effects at runtime. See that method for
|
|
* full API docs.
|
|
*
|
|
* This method may be called on the prototype (for prototypical template
|
|
* binding, to avoid creating accessors every instance) once per prototype,
|
|
* and will be called with `runtimeBinding: true` by `_stampTemplate` to
|
|
* create and link an instance of the template metadata associated with a
|
|
* particular stamping.
|
|
*
|
|
* @param {!HTMLTemplateElement} template Template containing binding
|
|
* bindings
|
|
* @param {boolean=} instanceBinding When false (default), performs
|
|
* "prototypical" binding of the template and overwrites any previously
|
|
* bound template for the class. When true (as passed from
|
|
* `_stampTemplate`), the template info is instanced and linked into
|
|
* the list of bound templates.
|
|
* @return {!TemplateInfo} Template metadata object; for `runtimeBinding`,
|
|
* this is an instance of the prototypical template info
|
|
* @protected
|
|
*/
|
|
_bindTemplate(template, instanceBinding) {
|
|
let templateInfo = this.constructor._parseTemplate(template);
|
|
let wasPreBound = this.__templateInfo == templateInfo;
|
|
// Optimization: since this is called twice for proto-bound templates,
|
|
// don't attempt to recreate accessors if this template was pre-bound
|
|
if (!wasPreBound) {
|
|
for (let prop in templateInfo.propertyEffects) {
|
|
this._createPropertyAccessor(prop);
|
|
}
|
|
}
|
|
if (instanceBinding) {
|
|
// For instance-time binding, create instance of template metadata
|
|
// and link into list of templates if necessary
|
|
templateInfo = /** @type {!TemplateInfo} */(Object.create(templateInfo));
|
|
templateInfo.wasPreBound = wasPreBound;
|
|
if (!wasPreBound && this.__templateInfo) {
|
|
let last = this.__templateInfoLast || this.__templateInfo;
|
|
this.__templateInfoLast = last.nextTemplateInfo = templateInfo;
|
|
templateInfo.previousTemplateInfo = last;
|
|
return templateInfo;
|
|
}
|
|
}
|
|
return this.__templateInfo = templateInfo;
|
|
}
|
|
|
|
/**
|
|
* Adds a property effect to the given template metadata, which is run
|
|
* at the "propagate" stage of `_propertiesChanged` when the template
|
|
* has been bound to the element via `_bindTemplate`.
|
|
*
|
|
* The `effect` object should match the format in `_addPropertyEffect`.
|
|
*
|
|
* @param {Object} templateInfo Template metadata to add effect to
|
|
* @param {string} prop Property that should trigger the effect
|
|
* @param {Object=} effect Effect metadata object
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
static _addTemplatePropertyEffect(templateInfo, prop, effect) {
|
|
let hostProps = templateInfo.hostProps = templateInfo.hostProps || {};
|
|
hostProps[prop] = true;
|
|
let effects = templateInfo.propertyEffects = templateInfo.propertyEffects || {};
|
|
let propEffects = effects[prop] = effects[prop] || [];
|
|
propEffects.push(effect);
|
|
}
|
|
|
|
/**
|
|
* Stamps the provided template and performs instance-time setup for
|
|
* Polymer template features, including data bindings, declarative event
|
|
* listeners, and the `this.$` map of `id`'s to nodes. A document fragment
|
|
* is returned containing the stamped DOM, ready for insertion into the
|
|
* DOM.
|
|
*
|
|
* This method may be called more than once; however note that due to
|
|
* `shadycss` polyfill limitations, only styles from templates prepared
|
|
* using `ShadyCSS.prepareTemplate` will be correctly polyfilled (scoped
|
|
* to the shadow root and support CSS custom properties), and note that
|
|
* `ShadyCSS.prepareTemplate` may only be called once per element. As such,
|
|
* any styles required by in runtime-stamped templates must be included
|
|
* in the main element template.
|
|
*
|
|
* @param {!HTMLTemplateElement} template Template to stamp
|
|
* @return {!StampedTemplate} Cloned template content
|
|
* @override
|
|
* @protected
|
|
*/
|
|
_stampTemplate(template) {
|
|
// Ensures that created dom is `_enqueueClient`'d to this element so
|
|
// that it can be flushed on next call to `_flushProperties`
|
|
hostStack.beginHosting(this);
|
|
let dom = super._stampTemplate(template);
|
|
hostStack.endHosting(this);
|
|
let templateInfo = /** @type {!TemplateInfo} */(this._bindTemplate(template, true));
|
|
// Add template-instance-specific data to instanced templateInfo
|
|
templateInfo.nodeList = dom.nodeList;
|
|
// Capture child nodes to allow unstamping of non-prototypical templates
|
|
if (!templateInfo.wasPreBound) {
|
|
let nodes = templateInfo.childNodes = [];
|
|
for (let n=dom.firstChild; n; n=n.nextSibling) {
|
|
nodes.push(n);
|
|
}
|
|
}
|
|
dom.templateInfo = templateInfo;
|
|
// Setup compound storage, 2-way listeners, and dataHost for bindings
|
|
setupBindings(this, templateInfo);
|
|
// Flush properties into template nodes if already booted
|
|
if (this.__dataReady) {
|
|
runEffects(this, templateInfo.propertyEffects, this.__data, null,
|
|
false, templateInfo.nodeList);
|
|
}
|
|
return dom;
|
|
}
|
|
|
|
/**
|
|
* Removes and unbinds the nodes previously contained in the provided
|
|
* DocumentFragment returned from `_stampTemplate`.
|
|
*
|
|
* @param {!StampedTemplate} dom DocumentFragment previously returned
|
|
* from `_stampTemplate` associated with the nodes to be removed
|
|
* @return {void}
|
|
* @protected
|
|
*/
|
|
_removeBoundDom(dom) {
|
|
// Unlink template info
|
|
let templateInfo = dom.templateInfo;
|
|
if (templateInfo.previousTemplateInfo) {
|
|
templateInfo.previousTemplateInfo.nextTemplateInfo =
|
|
templateInfo.nextTemplateInfo;
|
|
}
|
|
if (templateInfo.nextTemplateInfo) {
|
|
templateInfo.nextTemplateInfo.previousTemplateInfo =
|
|
templateInfo.previousTemplateInfo;
|
|
}
|
|
if (this.__templateInfoLast == templateInfo) {
|
|
this.__templateInfoLast = templateInfo.previousTemplateInfo;
|
|
}
|
|
templateInfo.previousTemplateInfo = templateInfo.nextTemplateInfo = null;
|
|
// Remove stamped nodes
|
|
let nodes = templateInfo.childNodes;
|
|
for (let i=0; i<nodes.length; i++) {
|
|
let node = nodes[i];
|
|
node.parentNode.removeChild(node);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides default `TemplateStamp` implementation to add support for
|
|
* parsing bindings from `TextNode`'s' `textContent`. A `bindings`
|
|
* array is added to `nodeInfo` and populated with binding metadata
|
|
* with information capturing the binding target, and a `parts` array
|
|
* with one or more metadata objects capturing the source(s) of the
|
|
* binding.
|
|
*
|
|
* @override
|
|
* @param {Node} node Node to parse
|
|
* @param {TemplateInfo} templateInfo Template metadata for current template
|
|
* @param {NodeInfo} nodeInfo Node metadata for current template node
|
|
* @return {boolean} `true` if the visited node added node-specific
|
|
* metadata to `nodeInfo`
|
|
* @protected
|
|
* @suppress {missingProperties} Interfaces in closure do not inherit statics, but classes do
|
|
*/
|
|
static _parseTemplateNode(node, templateInfo, nodeInfo) {
|
|
let noted = super._parseTemplateNode(node, templateInfo, nodeInfo);
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
let parts = this._parseBindings(node.textContent, templateInfo);
|
|
if (parts) {
|
|
// Initialize the textContent with any literal parts
|
|
// NOTE: default to a space here so the textNode remains; some browsers
|
|
// (IE) omit an empty textNode following cloneNode/importNode.
|
|
node.textContent = literalFromParts(parts) || ' ';
|
|
addBinding(this, templateInfo, nodeInfo, 'text', 'textContent', parts);
|
|
noted = true;
|
|
}
|
|
}
|
|
return noted;
|
|
}
|
|
|
|
/**
|
|
* Overrides default `TemplateStamp` implementation to add support for
|
|
* parsing bindings from attributes. A `bindings`
|
|
* array is added to `nodeInfo` and populated with binding metadata
|
|
* with information capturing the binding target, and a `parts` array
|
|
* with one or more metadata objects capturing the source(s) of the
|
|
* binding.
|
|
*
|
|
* @override
|
|
* @param {Element} node Node to parse
|
|
* @param {TemplateInfo} templateInfo Template metadata for current template
|
|
* @param {NodeInfo} nodeInfo Node metadata for current template node
|
|
* @param {string} name Attribute name
|
|
* @param {string} value Attribute value
|
|
* @return {boolean} `true` if the visited node added node-specific
|
|
* metadata to `nodeInfo`
|
|
* @protected
|
|
* @suppress {missingProperties} Interfaces in closure do not inherit statics, but classes do
|
|
*/
|
|
static _parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, value) {
|
|
let parts = this._parseBindings(value, templateInfo);
|
|
if (parts) {
|
|
// Attribute or property
|
|
let origName = name;
|
|
let kind = 'property';
|
|
// The only way we see a capital letter here is if the attr has
|
|
// a capital letter in it per spec. In this case, to make sure
|
|
// this binding works, we go ahead and make the binding to the attribute.
|
|
if (capitalAttributeRegex.test(name)) {
|
|
kind = 'attribute';
|
|
} else if (name[name.length-1] == '$') {
|
|
name = name.slice(0, -1);
|
|
kind = 'attribute';
|
|
}
|
|
// Initialize attribute bindings with any literal parts
|
|
let literal = literalFromParts(parts);
|
|
if (literal && kind == 'attribute') {
|
|
// Ensure a ShadyCSS template scoped style is not removed
|
|
// when a class$ binding's initial literal value is set.
|
|
if (name == 'class' && node.hasAttribute('class')) {
|
|
literal += ' ' + node.getAttribute(name);
|
|
}
|
|
node.setAttribute(name, literal);
|
|
}
|
|
// Clear attribute before removing, since IE won't allow removing
|
|
// `value` attribute if it previously had a value (can't
|
|
// unconditionally set '' before removing since attributes with `$`
|
|
// can't be set using setAttribute)
|
|
if (node.localName === 'input' && origName === 'value') {
|
|
node.setAttribute(origName, '');
|
|
}
|
|
// Remove annotation
|
|
node.removeAttribute(origName);
|
|
// Case hackery: attributes are lower-case, but bind targets
|
|
// (properties) are case sensitive. Gambit is to map dash-case to
|
|
// camel-case: `foo-bar` becomes `fooBar`.
|
|
// Attribute bindings are excepted.
|
|
if (kind === 'property') {
|
|
name = dashToCamelCase(name);
|
|
}
|
|
addBinding(this, templateInfo, nodeInfo, kind, name, parts, literal);
|
|
return true;
|
|
} else {
|
|
return super._parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides default `TemplateStamp` implementation to add support for
|
|
* binding the properties that a nested template depends on to the template
|
|
* as `_host_<property>`.
|
|
*
|
|
* @override
|
|
* @param {Node} node Node to parse
|
|
* @param {TemplateInfo} templateInfo Template metadata for current template
|
|
* @param {NodeInfo} nodeInfo Node metadata for current template node
|
|
* @return {boolean} `true` if the visited node added node-specific
|
|
* metadata to `nodeInfo`
|
|
* @protected
|
|
* @suppress {missingProperties} Interfaces in closure do not inherit statics, but classes do
|
|
*/
|
|
static _parseTemplateNestedTemplate(node, templateInfo, nodeInfo) {
|
|
let noted = super._parseTemplateNestedTemplate(node, templateInfo, nodeInfo);
|
|
// Merge host props into outer template and add bindings
|
|
let hostProps = nodeInfo.templateInfo.hostProps;
|
|
let mode = '{';
|
|
for (let source in hostProps) {
|
|
let parts = [{ mode, source, dependencies: [source] }];
|
|
addBinding(this, templateInfo, nodeInfo, 'property', '_host_' + source, parts);
|
|
}
|
|
return noted;
|
|
}
|
|
|
|
/**
|
|
* Called to parse text in a template (either attribute values or
|
|
* textContent) into binding metadata.
|
|
*
|
|
* Any overrides of this method should return an array of binding part
|
|
* metadata representing one or more bindings found in the provided text
|
|
* and any "literal" text in between. Any non-literal parts will be passed
|
|
* to `_evaluateBinding` when any dependencies change. The only required
|
|
* fields of each "part" in the returned array are as follows:
|
|
*
|
|
* - `dependencies` - Array containing trigger metadata for each property
|
|
* that should trigger the binding to update
|
|
* - `literal` - String containing text if the part represents a literal;
|
|
* in this case no `dependencies` are needed
|
|
*
|
|
* Additional metadata for use by `_evaluateBinding` may be provided in
|
|
* each part object as needed.
|
|
*
|
|
* The default implementation handles the following types of bindings
|
|
* (one or more may be intermixed with literal strings):
|
|
* - Property binding: `[[prop]]`
|
|
* - Path binding: `[[object.prop]]`
|
|
* - Negated property or path bindings: `[[!prop]]` or `[[!object.prop]]`
|
|
* - Two-way property or path bindings (supports negation):
|
|
* `{{prop}}`, `{{object.prop}}`, `{{!prop}}` or `{{!object.prop}}`
|
|
* - Inline computed method (supports negation):
|
|
* `[[compute(a, 'literal', b)]]`, `[[!compute(a, 'literal', b)]]`
|
|
*
|
|
* The default implementation uses a regular expression for best
|
|
* performance. However, the regular expression uses a white-list of
|
|
* allowed characters in a data-binding, which causes problems for
|
|
* data-bindings that do use characters not in this white-list.
|
|
*
|
|
* Instead of updating the white-list with all allowed characters,
|
|
* there is a StrictBindingParser (see lib/mixins/strict-binding-parser)
|
|
* that uses a state machine instead. This state machine is able to handle
|
|
* all characters. However, it is slightly less performant, therefore we
|
|
* extracted it into a separate optional mixin.
|
|
*
|
|
* @param {string} text Text to parse from attribute or textContent
|
|
* @param {Object} templateInfo Current template metadata
|
|
* @return {Array<!BindingPart>} Array of binding part metadata
|
|
* @protected
|
|
*/
|
|
static _parseBindings(text, templateInfo) {
|
|
let parts = [];
|
|
let lastIndex = 0;
|
|
let m;
|
|
// Example: "literal1{{prop}}literal2[[!compute(foo,bar)]]final"
|
|
// Regex matches:
|
|
// Iteration 1: Iteration 2:
|
|
// m[1]: '{{' '[['
|
|
// m[2]: '' '!'
|
|
// m[3]: 'prop' 'compute(foo,bar)'
|
|
while ((m = bindingRegex.exec(text)) !== null) {
|
|
// Add literal part
|
|
if (m.index > lastIndex) {
|
|
parts.push({literal: text.slice(lastIndex, m.index)});
|
|
}
|
|
// Add binding part
|
|
let mode = m[1][0];
|
|
let negate = Boolean(m[2]);
|
|
let source = m[3].trim();
|
|
let customEvent = false, notifyEvent = '', colon = -1;
|
|
if (mode == '{' && (colon = source.indexOf('::')) > 0) {
|
|
notifyEvent = source.substring(colon + 2);
|
|
source = source.substring(0, colon);
|
|
customEvent = true;
|
|
}
|
|
let signature = parseMethod(source);
|
|
let dependencies = [];
|
|
if (signature) {
|
|
// Inline computed function
|
|
let {args, methodName} = signature;
|
|
for (let i=0; i<args.length; i++) {
|
|
let arg = args[i];
|
|
if (!arg.literal) {
|
|
dependencies.push(arg);
|
|
}
|
|
}
|
|
let dynamicFns = templateInfo.dynamicFns;
|
|
if (dynamicFns && dynamicFns[methodName] || signature.static) {
|
|
dependencies.push(methodName);
|
|
signature.dynamicFn = true;
|
|
}
|
|
} else {
|
|
// Property or path
|
|
dependencies.push(source);
|
|
}
|
|
parts.push({
|
|
source, mode, negate, customEvent, signature, dependencies,
|
|
event: notifyEvent
|
|
});
|
|
lastIndex = bindingRegex.lastIndex;
|
|
}
|
|
// Add a final literal part
|
|
if (lastIndex && lastIndex < text.length) {
|
|
let literal = text.substring(lastIndex);
|
|
if (literal) {
|
|
parts.push({
|
|
literal: literal
|
|
});
|
|
}
|
|
}
|
|
if (parts.length) {
|
|
return parts;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called to evaluate a previously parsed binding part based on a set of
|
|
* one or more changed dependencies.
|
|
*
|
|
* @param {this} inst Element that should be used as scope for
|
|
* binding dependencies
|
|
* @param {BindingPart} part Binding part metadata
|
|
* @param {string} path Property/path that triggered this effect
|
|
* @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 {*} Value the binding part evaluated to
|
|
* @protected
|
|
*/
|
|
static _evaluateBinding(inst, part, path, props, oldProps, hasPaths) {
|
|
let value;
|
|
if (part.signature) {
|
|
value = runMethodEffect(inst, path, props, oldProps, part.signature);
|
|
} else if (path != part.source) {
|
|
value = get(inst, part.source);
|
|
} else {
|
|
if (hasPaths && isPath(path)) {
|
|
value = get(inst, path);
|
|
} else {
|
|
value = inst.__data[path];
|
|
}
|
|
}
|
|
if (part.negate) {
|
|
value = !value;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
}
|
|
|
|
// make a typing for closure :P
|
|
PropertyEffectsType = PropertyEffects;
|
|
|
|
return PropertyEffects;
|
|
});
|
|
|
|
/**
|
|
* Helper api for enqueuing client dom created by a host element.
|
|
*
|
|
* By default elements are flushed via `_flushProperties` when
|
|
* `connectedCallback` is called. Elements attach their client dom to
|
|
* themselves at `ready` time which results from this first flush.
|
|
* This provides an ordering guarantee that the client dom an element
|
|
* creates is flushed before the element itself (i.e. client `ready`
|
|
* fires before host `ready`).
|
|
*
|
|
* However, if `_flushProperties` is called *before* an element is connected,
|
|
* as for example `Templatize` does, this ordering guarantee cannot be
|
|
* satisfied because no elements are connected. (Note: Bound elements that
|
|
* receive data do become enqueued clients and are properly ordered but
|
|
* unbound elements are not.)
|
|
*
|
|
* To maintain the desired "client before host" ordering guarantee for this
|
|
* case we rely on the "host stack. Client nodes registers themselves with
|
|
* the creating host element when created. This ensures that all client dom
|
|
* is readied in the proper order, maintaining the desired guarantee.
|
|
*
|
|
* @private
|
|
*/
|
|
class HostStack {
|
|
constructor() {
|
|
this.stack = [];
|
|
}
|
|
|
|
/**
|
|
* @param {*} inst Instance to add to hostStack
|
|
* @return {void}
|
|
*/
|
|
registerHost(inst) {
|
|
if (this.stack.length) {
|
|
let host = this.stack[this.stack.length-1];
|
|
host._enqueueClient(inst);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {*} inst Instance to begin hosting
|
|
* @return {void}
|
|
*/
|
|
beginHosting(inst) {
|
|
this.stack.push(inst);
|
|
}
|
|
|
|
/**
|
|
* @param {*} inst Instance to end hosting
|
|
* @return {void}
|
|
*/
|
|
endHosting(inst) {
|
|
let stackLen = this.stack.length;
|
|
if (stackLen && this.stack[stackLen-1] == inst) {
|
|
this.stack.pop();
|
|
}
|
|
}
|
|
}
|
|
const hostStack = new HostStack();
|