2018-04-13 16:40:26 -07:00
|
|
|
/**
|
2018-12-19 16:15:06 -08:00
|
|
|
* @suppress {checkPrototypalTypes}
|
2016-02-19 10:23:22 -08:00
|
|
|
@license
|
2017-03-03 16:54:36 -08:00
|
|
|
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
2016-02-19 10:23:22 -08:00
|
|
|
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
|
2018-04-13 16:40:26 -07:00
|
|
|
*/
|
2018-04-15 18:38:09 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
import '../utils/boot.js';
|
|
|
|
|
|
|
|
|
|
import { dedupingMixin } from '../utils/mixin.js';
|
2018-08-21 10:53:42 -07:00
|
|
|
import { root, isAncestor, isDescendant, get, translate, isPath, set, normalize } from '../utils/path.js';
|
2018-04-15 18:38:09 -07:00
|
|
|
/* for notify, reflect */
|
2018-08-16 14:29:39 -07:00
|
|
|
import { camelToDashCase, dashToCamelCase } from '../utils/case-map.js';
|
2018-04-13 16:40:26 -07:00
|
|
|
import { PropertyAccessors } from './property-accessors.js';
|
2018-04-15 18:38:09 -07:00
|
|
|
/* for annotated effects */
|
2018-04-13 16:40:26 -07:00
|
|
|
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];
|
2016-07-07 10:50:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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;
|
2016-07-07 10:50:34 -07:00
|
|
|
}
|
2016-08-16 23:45:41 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
return ran;
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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;
|
2018-08-21 10:53:42 -07:00
|
|
|
let rootProperty = hasPaths ? root(prop) : prop;
|
2018-04-13 16:40:26 -07:00
|
|
|
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;
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
fx.fn(inst, prop, props, oldProps, fx.info, hasPaths, extraArgs);
|
|
|
|
|
ran = true;
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
|
|
|
|
}
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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;
|
2017-02-10 11:32:57 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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');
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2017-02-23 20:39:20 -08:00
|
|
|
}
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
// 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();
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) {
|
2018-08-21 10:53:42 -07:00
|
|
|
let rootProperty = root(path);
|
2018-04-13 16:40:26 -07:00
|
|
|
if (rootProperty !== path) {
|
2018-08-16 14:29:39 -07:00
|
|
|
let eventName = camelToDashCase(rootProperty) + '-changed';
|
2018-04-13 16:40:26 -07:00
|
|
|
dispatchNotifyEvent(inst, eventName, props[path], path);
|
|
|
|
|
return true;
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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;
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
/** @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) {
|
2018-08-21 10:53:42 -07:00
|
|
|
let rootProperty = hasPaths ? root(property) : property;
|
2018-04-13 16:40:26 -07:00
|
|
|
let path = rootProperty != property ? property : null;
|
2018-08-21 10:53:42 -07:00
|
|
|
let value = path ? get(inst, path) : inst.__data[property];
|
2018-04-13 16:40:26 -07:00
|
|
|
if (path && value === undefined) {
|
|
|
|
|
value = props[property]; // specifically for .splices
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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 {
|
2018-08-02 13:14:36 +02:00
|
|
|
value = event.currentTarget[fromProp];
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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();
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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));
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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;
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
2017-04-07 19:14:55 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -- 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];
|
2018-08-16 14:29:39 -07:00
|
|
|
binding.listenerEvent = event || (camelToDashCase(target) + '-changed');
|
2018-04-13 16:40:26 -07:00
|
|
|
binding.listenerNegate = negate;
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
// 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');
|
2016-09-01 12:44:48 -07:00
|
|
|
} else {
|
2018-04-13 16:40:26 -07:00
|
|
|
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;
|
2017-01-17 15:07:30 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
constructor._addTemplatePropertyEffect(templateInfo, trigger.rootProperty, {
|
|
|
|
|
fn: runBindingEffect,
|
|
|
|
|
info, trigger
|
|
|
|
|
});
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
} else {
|
|
|
|
|
let value = info.evaluator._evaluateBinding(inst, part, path, props, oldProps, hasPaths);
|
|
|
|
|
// Propagate value to child
|
|
|
|
|
applyBindingValue(inst, node, binding, part, value);
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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);
|
2016-10-27 09:51:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
} else {
|
|
|
|
|
inst._setUnmanagedPropertyToNode(node, prop, value);
|
2016-10-27 09:51:41 -07:00
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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('');
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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;
|
2016-10-27 09:51:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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);
|
|
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
node.__dataHost = inst;
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
});
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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) {
|
2018-08-21 10:53:42 -07:00
|
|
|
let args = inst._marshalArgs(info.args, property, props);
|
2018-04-13 16:40:26 -07:00
|
|
|
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;
|
2017-03-06 15:04:34 -08:00
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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;
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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];
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
if (fc >= '0' && fc <= '9') {
|
|
|
|
|
fc = '#';
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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) {
|
2018-08-21 10:53:42 -07:00
|
|
|
a.rootProperty = root(arg);
|
2018-04-13 16:40:26 -07:00
|
|
|
// detect structured path (has dots)
|
2018-08-21 10:53:42 -07:00
|
|
|
a.structured = isPath(arg);
|
2018-04-13 16:40:26 -07:00
|
|
|
if (a.structured) {
|
|
|
|
|
a.wildcard = (arg.slice(-2) == '.*');
|
|
|
|
|
if (a.wildcard) {
|
|
|
|
|
a.name = arg.slice(0, -2);
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
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
|
2018-04-23 17:33:52 -07:00
|
|
|
* @appliesMixin TemplateStamp
|
|
|
|
|
* @appliesMixin PropertyAccessors
|
2018-04-13 16:40:26 -07:00
|
|
|
* @summary Element class mixin that provides meta-programming for Polymer's
|
|
|
|
|
* template binding and data observation system.
|
|
|
|
|
*/
|
|
|
|
|
export const PropertyEffects = dedupingMixin(superClass => {
|
2016-08-16 23:45:41 -07:00
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
2018-04-13 16:40:26 -07:00
|
|
|
* @constructor
|
|
|
|
|
* @extends {superClass}
|
|
|
|
|
* @implements {Polymer_PropertyAccessors}
|
|
|
|
|
* @implements {Polymer_TemplateStamp}
|
|
|
|
|
* @unrestricted
|
2018-07-20 21:40:34 -07:00
|
|
|
* @private
|
2016-10-27 09:51:41 -07:00
|
|
|
*/
|
2018-04-13 16:40:26 -07:00
|
|
|
const propertyEffectsBase = TemplateStamp(PropertyAccessors(superClass));
|
2016-09-01 12:44:48 -07:00
|
|
|
|
2017-02-21 23:23:40 -08:00
|
|
|
/**
|
2017-04-26 14:58:43 -07:00
|
|
|
* @polymer
|
2018-04-13 16:40:26 -07:00
|
|
|
* @mixinClass
|
|
|
|
|
* @implements {Polymer_PropertyEffects}
|
|
|
|
|
* @extends {propertyEffectsBase}
|
|
|
|
|
* @unrestricted
|
2017-02-21 23:23:40 -08:00
|
|
|
*/
|
2018-04-13 16:40:26 -07:00
|
|
|
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;
|
|
|
|
|
}
|
2016-08-16 23:45:41 -07:00
|
|
|
|
2017-03-29 15:52:01 -07:00
|
|
|
/**
|
2018-04-13 16:40:26 -07:00
|
|
|
* @return {void}
|
2017-03-29 15:52:01 -07:00
|
|
|
*/
|
2018-04-13 16:40:26 -07:00
|
|
|
_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;
|
|
|
|
|
}
|
2017-01-12 15:55:12 -08:00
|
|
|
|
|
|
|
|
/**
|
2018-04-23 17:33:52 -07:00
|
|
|
* Overrides `PropertyAccessors` implementation to provide a
|
2018-04-13 16:40:26 -07:00
|
|
|
* more efficient implementation of initializing properties from
|
|
|
|
|
* the prototype on the instance.
|
|
|
|
|
*
|
|
|
|
|
* @override
|
|
|
|
|
* @param {Object} props Properties to initialize on the prototype
|
|
|
|
|
* @return {void}
|
2017-01-12 15:55:12 -08:00
|
|
|
*/
|
2018-04-13 16:40:26 -07:00
|
|
|
_initializeProtoProperties(props) {
|
|
|
|
|
this.__data = Object.create(props);
|
|
|
|
|
this.__dataPending = Object.create(props);
|
|
|
|
|
this.__dataOld = {};
|
|
|
|
|
}
|
2017-02-14 16:59:19 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
2018-04-23 17:33:52 -07:00
|
|
|
* Overrides `PropertyAccessors` implementation to avoid setting
|
2018-04-13 16:40:26 -07:00
|
|
|
* `_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];
|
2017-04-07 19:14:55 -07:00
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2017-04-07 19:14:55 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
// Prototype setup ----------------------------------------
|
2016-08-09 20:43:30 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2017-04-06 02:33:03 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
2016-08-17 16:29:27 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2016-08-17 16:29:27 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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]);
|
|
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2016-07-07 10:50:34 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
// Runtime ----------------------------------------
|
2017-02-08 23:20:36 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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 ||
|
2018-08-21 10:53:42 -07:00
|
|
|
root(Array.isArray(path) ? path[0] : path) !== path) {
|
2018-04-13 16:40:26 -07:00
|
|
|
// 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) {
|
2018-08-21 10:53:42 -07:00
|
|
|
let old = get(this, path);
|
|
|
|
|
path = /** @type {string} */ (set(this, path, value));
|
2018-04-13 16:40:26 -07:00
|
|
|
// Use property-accessor's simpler dirty check
|
|
|
|
|
if (!path || !super._shouldPropertyChange(path, value, old)) {
|
|
|
|
|
return false;
|
2017-02-23 19:10:43 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
this.__dataHasPaths = true;
|
|
|
|
|
if (this._setPendingProperty(/**@type{string}*/(path), value, shouldNotify)) {
|
|
|
|
|
computeLinkedPaths(this, path, value);
|
2016-12-19 11:13:56 -08:00
|
|
|
return true;
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
} else {
|
|
|
|
|
if (this.__dataHasAccessor && this.__dataHasAccessor[path]) {
|
|
|
|
|
return this._setPendingProperty(/**@type{string}*/(path), value, shouldNotify);
|
|
|
|
|
} else {
|
|
|
|
|
this[path] = value;
|
2017-02-23 19:10:43 -08:00
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
return false;
|
|
|
|
|
}
|
2017-02-23 19:10:43 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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;
|
2016-07-07 10:50:34 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2016-07-07 10:50:34 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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) {
|
2018-08-21 10:53:42 -07:00
|
|
|
let propIsPath = this.__dataHasPaths && isPath(property);
|
|
|
|
|
let prevProps = propIsPath ? this.__dataTemp : this.__data;
|
2018-04-13 16:40:26 -07:00
|
|
|
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
|
2018-08-21 10:53:42 -07:00
|
|
|
if (propIsPath) {
|
2018-04-13 16:40:26 -07:00
|
|
|
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
|
2018-08-21 10:53:42 -07:00
|
|
|
if (propIsPath || (this[TYPES.NOTIFY] && this[TYPES.NOTIFY][property])) {
|
2018-04-13 16:40:26 -07:00
|
|
|
this.__dataToNotify = this.__dataToNotify || {};
|
|
|
|
|
this.__dataToNotify[property] = shouldNotify;
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
return true;
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
return false;
|
|
|
|
|
}
|
2016-12-09 11:44:22 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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();
|
2017-11-16 17:12:53 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2017-11-16 17:12:53 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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();
|
2017-05-12 13:06:50 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2017-05-12 13:06:50 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2016-12-09 11:44:22 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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 {
|
2017-05-22 17:51:15 -07:00
|
|
|
this.__enableOrFlushClients();
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2017-05-22 17:51:15 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
// 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();
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2017-02-24 00:47:15 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
2017-02-28 10:31:23 -08:00
|
|
|
}
|
2016-09-08 11:47:49 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
this._invalidateProperties();
|
|
|
|
|
}
|
2016-07-07 10:50:34 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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) {
|
2016-12-09 11:44:22 -08:00
|
|
|
this._flushClients();
|
2016-08-17 01:10:38 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
// Before ready, client notifications do not trigger _flushProperties.
|
|
|
|
|
// Therefore a flush is necessary here if data has been set.
|
|
|
|
|
if (this.__dataPending) {
|
|
|
|
|
this._flushProperties();
|
2017-04-06 02:33:03 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2017-04-06 02:33:03 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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
|
2018-07-26 18:16:48 -07:00
|
|
|
* @param {?Object} changedProps Bag of properties changed since the last
|
2018-04-13 16:40:26 -07:00
|
|
|
* call to `_propertiesChanged`
|
2018-07-26 18:16:48 -07:00
|
|
|
* @param {?Object} oldProps Bag of previous values for each property
|
2018-04-13 16:40:26 -07:00
|
|
|
* 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 = {};
|
2016-08-15 19:17:29 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
// ----------------------------
|
|
|
|
|
// window.debug && console.groupEnd(this.localName + '#' + this.id + ': ' + c);
|
|
|
|
|
// ----------------------------
|
|
|
|
|
}
|
2016-08-15 19:17:29 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
2016-08-15 19:17:29 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
let templateInfo = this.__templateInfo;
|
|
|
|
|
while (templateInfo) {
|
|
|
|
|
runEffects(this, templateInfo.propertyEffects, changedProps, oldProps,
|
|
|
|
|
hasPaths, templateInfo.nodeList);
|
|
|
|
|
templateInfo = templateInfo.nextTemplateInfo;
|
2016-08-15 19:17:29 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2016-08-31 19:09:55 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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];
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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: ''};
|
2018-08-21 10:53:42 -07:00
|
|
|
let array = /** @type {Array} */(get(this, path, info));
|
2018-04-13 16:40:26 -07:00
|
|
|
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) {
|
2018-08-21 10:53:42 -07:00
|
|
|
return get(root || this, path);
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) {
|
2018-08-21 10:53:42 -07:00
|
|
|
set(root, path, value);
|
2018-04-13 16:40:26 -07:00
|
|
|
} else {
|
|
|
|
|
if (!this[TYPES.READ_ONLY] || !this[TYPES.READ_ONLY][/** @type {string} */(path)]) {
|
|
|
|
|
if (this._setPendingPropertyOrPath(path, value, true)) {
|
|
|
|
|
this._invalidateProperties();
|
2017-01-17 13:02:54 -08:00
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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: ''};
|
2018-08-21 10:53:42 -07:00
|
|
|
let array = /** @type {Array}*/(get(this, path, info));
|
2018-04-13 16:40:26 -07:00
|
|
|
let len = array.length;
|
|
|
|
|
let ret = array.push(...items);
|
|
|
|
|
if (items.length) {
|
|
|
|
|
notifySplice(this, array, info.path, len, items.length, []);
|
|
|
|
|
}
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
2016-08-15 19:17:29 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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: ''};
|
2018-08-21 10:53:42 -07:00
|
|
|
let array = /** @type {Array} */(get(this, path, info));
|
2018-04-13 16:40:26 -07:00
|
|
|
let hadLength = Boolean(array.length);
|
|
|
|
|
let ret = array.pop();
|
|
|
|
|
if (hadLength) {
|
|
|
|
|
notifySplice(this, array, info.path, array.length, 0, [ret]);
|
|
|
|
|
}
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
2016-08-15 19:17:29 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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.
|
2018-10-05 02:57:02 -07:00
|
|
|
* @param {number=} deleteCount Number of items to remove.
|
2018-04-13 16:40:26 -07:00
|
|
|
* @param {...*} items Items to insert into array.
|
|
|
|
|
* @return {Array} Array of removed items.
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
|
|
|
|
splice(path, start, deleteCount, ...items) {
|
|
|
|
|
let info = {path : ''};
|
2018-08-21 10:53:42 -07:00
|
|
|
let array = /** @type {Array} */(get(this, path, info));
|
2018-04-13 16:40:26 -07:00
|
|
|
// 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);
|
2016-08-15 19:17:29 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
// 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);
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
return ret;
|
|
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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: ''};
|
2018-08-21 10:53:42 -07:00
|
|
|
let array = /** @type {Array} */(get(this, path, info));
|
2018-04-13 16:40:26 -07:00
|
|
|
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: ''};
|
2018-08-21 10:53:42 -07:00
|
|
|
let array = /** @type {Array} */(get(this, path, info));
|
2018-04-13 16:40:26 -07:00
|
|
|
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
|
2017-06-29 16:47:17 -07:00
|
|
|
let info = {path: ''};
|
2018-08-21 10:53:42 -07:00
|
|
|
value = get(this, path, info);
|
2018-04-13 16:40:26 -07:00
|
|
|
propPath = info.path;
|
|
|
|
|
} else if (Array.isArray(path)) {
|
|
|
|
|
// Normalize path if needed
|
|
|
|
|
propPath = normalize(path);
|
|
|
|
|
} else {
|
|
|
|
|
propPath = /** @type{string} */(path);
|
2016-07-07 10:50:34 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
if (this._setPendingPropertyOrPath(propPath, value, true, true)) {
|
|
|
|
|
this._invalidateProperties();
|
2016-09-01 15:24:28 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2016-09-01 15:24:28 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
};
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2016-02-19 10:23:22 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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}
|
2016-08-31 19:09:55 -07:00
|
|
|
});
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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 + "'");
|
2016-10-27 09:51:41 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
createMethodEffect(this, sig, TYPES.OBSERVE, runMethodEffect, null, dynamicFn);
|
|
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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: {
|
2018-08-16 14:29:39 -07:00
|
|
|
eventName: camelToDashCase(property) + '-changed',
|
2018-04-13 16:40:26 -07:00
|
|
|
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,
|
2016-07-11 14:39:54 -07:00
|
|
|
info: {
|
2018-04-13 16:40:26 -07:00
|
|
|
attrName: attr
|
2016-07-11 14:39:54 -07:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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 + "'");
|
2016-08-05 11:14:38 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
createMethodEffect(this, sig, TYPES.COMPUTE, runComputedEffect, property, dynamicFn);
|
|
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
|
2018-08-21 10:53:42 -07:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
// -- static class methods ------------
|
2016-08-31 19:09:55 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2017-05-11 14:08:36 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2017-05-11 14:08:36 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2017-05-11 14:08:36 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2017-05-11 14:08:36 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2017-05-11 14:08:36 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2017-05-11 14:08:36 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2016-08-31 19:09:55 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2017-05-11 14:08:36 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
// -- 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);
|
2017-04-06 02:33:03 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
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;
|
2016-10-27 09:51:41 -07:00
|
|
|
}
|
2017-04-06 02:33:03 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
return this.__templateInfo = templateInfo;
|
|
|
|
|
}
|
2017-04-06 02:33:03 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2017-04-06 02:33:03 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
2017-04-13 15:56:48 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
|
|
|
|
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;
|
2017-04-06 02:33:03 -07:00
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
return noted;
|
|
|
|
|
}
|
2017-04-06 02:33:03 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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';
|
2017-04-06 02:33:03 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
// Initialize attribute bindings with any literal parts
|
|
|
|
|
let literal = literalFromParts(parts);
|
|
|
|
|
if (literal && kind == 'attribute') {
|
2018-11-09 18:03:55 -08:00
|
|
|
// 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);
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
node.setAttribute(name, literal);
|
2017-04-06 02:33:03 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
// 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, '');
|
2017-05-11 14:08:36 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
// 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);
|
2017-04-06 02:33:03 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
addBinding(this, templateInfo, nodeInfo, kind, name, parts, literal);
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
return super._parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, value);
|
2016-02-19 18:38:04 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
2017-03-06 15:04:34 -08:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2017-03-06 15:04:34 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
let dynamicFns = templateInfo.dynamicFns;
|
|
|
|
|
if (dynamicFns && dynamicFns[methodName] || signature.static) {
|
|
|
|
|
dependencies.push(methodName);
|
|
|
|
|
signature.dynamicFn = true;
|
2017-03-06 15:04:34 -08:00
|
|
|
}
|
|
|
|
|
} else {
|
2018-04-13 16:40:26 -07:00
|
|
|
// Property or path
|
|
|
|
|
dependencies.push(source);
|
2017-03-06 15:04:34 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
parts.push({
|
|
|
|
|
source, mode, negate, customEvent, signature, dependencies,
|
|
|
|
|
event: notifyEvent
|
|
|
|
|
});
|
|
|
|
|
lastIndex = bindingRegex.lastIndex;
|
2017-03-06 15:04:34 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
// Add a final literal part
|
|
|
|
|
if (lastIndex && lastIndex < text.length) {
|
|
|
|
|
let literal = text.substring(lastIndex);
|
|
|
|
|
if (literal) {
|
2017-04-06 02:33:03 -07:00
|
|
|
parts.push({
|
2018-04-13 16:40:26 -07:00
|
|
|
literal: literal
|
2017-04-06 02:33:03 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
if (parts.length) {
|
|
|
|
|
return parts;
|
|
|
|
|
} else {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-04-06 02:33:03 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* 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) {
|
2018-08-21 10:53:42 -07:00
|
|
|
value = get(inst, part.source);
|
2018-04-13 16:40:26 -07:00
|
|
|
} else {
|
2018-08-21 10:53:42 -07:00
|
|
|
if (hasPaths && isPath(path)) {
|
|
|
|
|
value = get(inst, path);
|
2017-04-06 02:33:03 -07:00
|
|
|
} else {
|
2018-04-13 16:40:26 -07:00
|
|
|
value = inst.__data[path];
|
2017-04-06 02:33:03 -07:00
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
if (part.negate) {
|
|
|
|
|
value = !value;
|
|
|
|
|
}
|
|
|
|
|
return value;
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
2016-07-07 12:59:31 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2017-06-20 17:30:57 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
// 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
|
|
|
|
|
*/
|
2018-06-27 14:52:45 -07:00
|
|
|
class HostStack {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.stack = [];
|
|
|
|
|
}
|
2016-02-19 10:23:22 -08:00
|
|
|
|
2017-05-12 13:06:50 -07:00
|
|
|
/**
|
2018-04-13 16:40:26 -07:00
|
|
|
* @param {*} inst Instance to add to hostStack
|
|
|
|
|
* @return {void}
|
2017-05-12 13:06:50 -07:00
|
|
|
*/
|
2018-04-13 16:40:26 -07:00
|
|
|
registerHost(inst) {
|
|
|
|
|
if (this.stack.length) {
|
|
|
|
|
let host = this.stack[this.stack.length-1];
|
|
|
|
|
host._enqueueClient(inst);
|
|
|
|
|
}
|
2018-06-27 14:52:45 -07:00
|
|
|
}
|
2017-05-12 13:06:50 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* @param {*} inst Instance to begin hosting
|
|
|
|
|
* @return {void}
|
|
|
|
|
*/
|
|
|
|
|
beginHosting(inst) {
|
|
|
|
|
this.stack.push(inst);
|
2018-06-27 14:52:45 -07:00
|
|
|
}
|
2017-05-12 13:06:50 -07:00
|
|
|
|
2018-04-13 16:40:26 -07:00
|
|
|
/**
|
|
|
|
|
* @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();
|
2017-05-12 13:06:50 -07:00
|
|
|
}
|
2018-04-13 16:40:26 -07:00
|
|
|
}
|
2018-06-27 14:52:45 -07:00
|
|
|
}
|
|
|
|
|
const hostStack = new HostStack();
|