2016-02-19 10:23:22 -08:00
|
|
|
<!--
|
|
|
|
|
@license
|
|
|
|
|
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
|
|
|
|
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
|
|
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
|
|
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
|
|
|
Code distributed by Google as part of the polymer project is also
|
|
|
|
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
|
|
|
-->
|
|
|
|
|
|
2016-02-22 17:56:30 -08:00
|
|
|
|
|
|
|
|
<link rel="import" href="../utils/boot.html">
|
2016-02-19 10:23:22 -08:00
|
|
|
<link rel="import" href="../utils/utils.html">
|
2016-08-09 20:43:30 -07:00
|
|
|
<link rel="import" href="../utils/path.html">
|
2016-02-19 10:23:22 -08:00
|
|
|
<link rel="import" href="property-accessors.html">
|
2016-09-01 15:24:28 -07:00
|
|
|
<link rel="import" href="../attributes/attributes.html">
|
2016-02-19 18:38:04 -08:00
|
|
|
<!-- for notify, reflect -->
|
|
|
|
|
<link rel="import" href="../utils/case-map.html">
|
|
|
|
|
<!-- for annotated effects -->
|
2016-09-01 15:24:28 -07:00
|
|
|
<link rel="import" href="../template/template-stamp.html">
|
2016-02-19 18:38:04 -08:00
|
|
|
|
|
|
|
|
|
2016-02-19 10:23:22 -08:00
|
|
|
<script>
|
|
|
|
|
(function() {
|
|
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
2016-08-31 19:09:55 -07:00
|
|
|
const CaseMap = Polymer.CaseMap;
|
2016-12-09 11:44:22 -08:00
|
|
|
const mixin = Polymer.Utils.mixin;
|
|
|
|
|
|
|
|
|
|
// Monotonically increasing unique ID used for de-duping effects triggered
|
|
|
|
|
// from multiple properties in the same turn
|
|
|
|
|
let effectUid = 0;
|
2016-08-31 19:09:55 -07:00
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
// Property effect types; effects are stored on the prototype using these keys
|
2016-08-31 19:09:55 -07:00
|
|
|
const TYPES = {
|
2016-09-01 15:24:28 -07:00
|
|
|
ANY: '__propertyEffects',
|
2016-08-31 19:09:55 -07:00
|
|
|
COMPUTE: '__computeEffects',
|
|
|
|
|
REFLECT: '__reflectEffects',
|
|
|
|
|
NOTIFY: '__notifyEffects',
|
|
|
|
|
PROPAGATE: '__propagateEffects',
|
|
|
|
|
OBSERVE: '__observeEffects',
|
|
|
|
|
READ_ONLY: '__readOnly'
|
|
|
|
|
}
|
2016-02-19 10:23:22 -08:00
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* 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) {
|
2016-08-31 19:09:55 -07:00
|
|
|
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) {
|
|
|
|
|
// TODO(kschaaf): replace with fast array copy #!%&$!
|
|
|
|
|
effects[p] = effects[p].slice();
|
2016-07-07 10:50:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
2016-08-31 19:09:55 -07:00
|
|
|
return effects;
|
|
|
|
|
}
|
2016-07-07 10:50:34 -07:00
|
|
|
|
2016-09-01 12:44:48 -07:00
|
|
|
// -- effects ----------------------------------------------
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
2016-12-09 11:44:22 -08:00
|
|
|
* Runs all effects of a given type for the given set of property changes
|
|
|
|
|
* on an instance.
|
2016-10-27 09:51:41 -07:00
|
|
|
*
|
|
|
|
|
* @param {Object} inst The instance with effects to run
|
2016-12-09 11:44:22 -08:00
|
|
|
* @param {string} type Type of effect to run
|
|
|
|
|
* @param {Object} props Bag of current property changes
|
|
|
|
|
* @param {Object} oldProps Bag of previous values for changed properties
|
2016-10-27 09:51:41 -07:00
|
|
|
* @private
|
|
|
|
|
*/
|
2016-12-09 11:44:22 -08:00
|
|
|
function runEffects(inst, type, props, oldProps) {
|
|
|
|
|
let ran;
|
|
|
|
|
let effects = inst[type];
|
|
|
|
|
if (effects) {
|
|
|
|
|
let id = effectUid++;
|
|
|
|
|
for (let prop in props) {
|
|
|
|
|
if (runEffectsForProperty(inst, effects, id, prop, props[prop],
|
|
|
|
|
oldProps && oldProps[prop])) {
|
|
|
|
|
ran = true;
|
|
|
|
|
}
|
2016-07-07 10:50:34 -07:00
|
|
|
}
|
2016-08-16 23:45:41 -07:00
|
|
|
}
|
2016-12-09 11:44:22 -08:00
|
|
|
return ran;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Runs a list of effects for a given property.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} inst The instance with effects to run
|
|
|
|
|
* @param {Array} effects Array of effects
|
|
|
|
|
* @param {number} id Effect run id used for de-duping effects
|
|
|
|
|
* @param {string} prop Name of changed property
|
|
|
|
|
* @param {*} value Value of changed property
|
|
|
|
|
* @param {*} old Previous value of changed property
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function runEffectsForProperty(inst, effects, id, prop, value, old) {
|
|
|
|
|
let ran;
|
2016-12-19 11:13:56 -08:00
|
|
|
// TODO(kschaaf) ideally a system exists to parse path information once
|
|
|
|
|
// and send structured information through the system for better perf
|
2016-12-09 11:44:22 -08:00
|
|
|
let rootProperty = Polymer.Path.root(prop);
|
|
|
|
|
let fxs = effects[rootProperty];
|
|
|
|
|
if (fxs) {
|
|
|
|
|
let fromAbove = inst.__dataFromAbove;
|
|
|
|
|
for (let i=0, l=fxs.length, fx; (i<l) && (fx=fxs[i]); i++) {
|
|
|
|
|
if (Polymer.Path.matches(fx.path, prop) &&
|
|
|
|
|
(!fx.info || fx.info.lastRun !== id)) {
|
2016-12-19 12:51:35 -08:00
|
|
|
if (rootProperty !== prop) {
|
|
|
|
|
// Pull the latest path value to pass to effect
|
|
|
|
|
let v = Polymer.Path.get(inst, prop);
|
|
|
|
|
// Fall back to the original changed value; this is mostly to thread
|
|
|
|
|
// array.splices through without it actually being on the array
|
|
|
|
|
if (v === undefined) {
|
|
|
|
|
v = value;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Pull the latest property value to pass to effect
|
|
|
|
|
value = inst[prop];
|
|
|
|
|
}
|
|
|
|
|
fx.fn(inst, prop, value, old, fx.info, fromAbove);
|
2016-12-09 11:44:22 -08:00
|
|
|
if (fx.info) {
|
|
|
|
|
fx.info.lastRun = id;
|
|
|
|
|
}
|
|
|
|
|
ran = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ran;
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
2016-08-16 23:45:41 -07:00
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Implements the "observer" effect.
|
|
|
|
|
*
|
|
|
|
|
* Calls the method with `info.methodName` on the instance, passing the
|
|
|
|
|
* new and old values.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} inst The instance the effect will be run on
|
|
|
|
|
* @param {string} property Name of property
|
|
|
|
|
* @param {*} value Current value of property
|
|
|
|
|
* @param {*} old Previous value of property
|
|
|
|
|
* @param {Object} info Effect metadata
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
function runObserverEffect(inst, property, value, old, info) {
|
2016-10-27 09:51:41 -07:00
|
|
|
let fn = inst[info.methodName];
|
2016-09-01 12:44:48 -07:00
|
|
|
if (fn) {
|
|
|
|
|
fn.call(inst, value, old);
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('observer method `' + info.methodName + '` not defined');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-09 11:44:22 -08: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 {Object} inst The instance with effects to run
|
|
|
|
|
* @param {Object} props Bag of current property changes
|
|
|
|
|
* @param {Object} oldProps Bag of previous values for changed properties
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function runNotifyEffects(inst, props, oldProps) {
|
|
|
|
|
// Notify
|
|
|
|
|
let notified;
|
|
|
|
|
let notifyEffects = inst[TYPES.NOTIFY];
|
|
|
|
|
let id = effectUid++;
|
|
|
|
|
// Try normal notify effects; if none, fall back to try path notification
|
2016-12-19 11:13:56 -08:00
|
|
|
// TODO(kschaaf) This is a hot path which we could avoid if (1) no
|
|
|
|
|
// "notify" effects AND (2) if we knew there were no paths to notify. This
|
|
|
|
|
// 2nd piece of info is not currently available but could be added perhaps
|
|
|
|
|
// in `_setProperty`.
|
2016-12-09 11:44:22 -08:00
|
|
|
for (let prop in props) {
|
|
|
|
|
if (notifyEffects && runEffectsForProperty(inst, notifyEffects, id,
|
|
|
|
|
prop, props[prop], oldProps && oldProps[prop])) {
|
|
|
|
|
notified = true;
|
2016-12-19 10:21:53 -08:00
|
|
|
} else if (notifyPath(inst, prop, props[prop])) {
|
2016-12-09 11:44:22 -08:00
|
|
|
notified = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Flush host if we actually notified and host was batching
|
|
|
|
|
let host;
|
|
|
|
|
if (notified && (host = inst.__dataHost) && host.setProperties) {
|
|
|
|
|
host._flushProperties();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Dispatches {property}-changed events with path information in the detail
|
|
|
|
|
* object to indicate a sub-path of the property was changed.
|
|
|
|
|
*
|
|
|
|
|
* @param {Element} inst The element from which to fire the event
|
|
|
|
|
* @param {string} path The path that was changed
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-12-19 10:21:53 -08:00
|
|
|
function notifyPath(inst, path, value) {
|
2016-12-09 11:44:22 -08:00
|
|
|
let rootProperty = Polymer.Path.root(path);
|
|
|
|
|
if (rootProperty !== path) {
|
|
|
|
|
let eventName = Polymer.CaseMap.camelToDashCase(rootProperty) + '-changed';
|
2016-12-19 12:51:35 -08:00
|
|
|
dispatchNotifyEvent(inst, eventName, value, path);
|
2016-12-09 11:44:22 -08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Dispatches {property}-changed events to indicate a property (or path)
|
|
|
|
|
* changed.
|
|
|
|
|
*
|
|
|
|
|
* @param {Element} 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=} path If a sub-path of this property changed, the path
|
|
|
|
|
* that changed (optional).
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function dispatchNotifyEvent(inst, eventName, value, path) {
|
|
|
|
|
let detail = {
|
|
|
|
|
value: value,
|
|
|
|
|
queueProperty: true
|
|
|
|
|
};
|
|
|
|
|
if (path) {
|
|
|
|
|
detail.path = path;
|
|
|
|
|
}
|
|
|
|
|
inst.dispatchEvent(new CustomEvent(eventName, { detail }));
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Implements the "notify" effect.
|
|
|
|
|
*
|
|
|
|
|
* Dispatches a non-bubbling event named `info.eventName` on the instance
|
|
|
|
|
* with a detail object containing the new `value`.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} inst The instance the effect will be run on
|
|
|
|
|
* @param {string} property Name of property
|
|
|
|
|
* @param {*} value Current value of property
|
|
|
|
|
* @param {*} old Previous value of property
|
|
|
|
|
* @param {Object} info Effect metadata
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-12-09 11:44:22 -08:00
|
|
|
function runNotifyEffect(inst, property, value, old, info) {
|
|
|
|
|
let rootProperty = Polymer.Path.root(property);
|
|
|
|
|
let path = rootProperty != property ? property : null;
|
|
|
|
|
dispatchNotifyEvent(inst, info.eventName, value, path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Adds a 2-way binding notification event listener to the node specified
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} node Child element to add listener to
|
|
|
|
|
* @param {Object} inst Host element instance to handle notification event
|
|
|
|
|
* @param {Object} info Listener metadata stored via addAnnotatedListener
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function addNotifyListener(node, inst, info) {
|
|
|
|
|
node.addEventListener(info.event, function(e) {
|
|
|
|
|
handleNotification(e, inst, info.property, info.path, info.negate);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handler function for 2-way notification events. Receives context
|
|
|
|
|
* information captured in the `addNotifyListener` closure from the
|
|
|
|
|
* `_bindListeners` 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 {Event} e Notification event (e.g. '<property>-changed')
|
|
|
|
|
* @param {Object} inst Host element instance handling the notification event
|
|
|
|
|
* @param {string} property Child element property that was bound
|
|
|
|
|
* @param {string} path Host property/path that was bound
|
|
|
|
|
* @param {boolean} negate Whether the binding was negated
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function handleNotification(e, inst, property, path, negate) {
|
|
|
|
|
let value;
|
|
|
|
|
let targetPath = e.detail && e.detail.path;
|
|
|
|
|
if (targetPath) {
|
|
|
|
|
path = Polymer.Path.translate(property, path, targetPath);
|
|
|
|
|
value = e.detail && e.detail.value;
|
|
|
|
|
} else {
|
|
|
|
|
value = e.target[property];
|
|
|
|
|
}
|
|
|
|
|
value = negate ? !value : value;
|
|
|
|
|
setPropertyFromNotification(inst, path, value, e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Called by 2-way binding notification event listeners to set a property
|
|
|
|
|
* or path to the host based on a notification from a bound child.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} path Path on this instance to set
|
|
|
|
|
* @param {*} value Value to set to given path
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
|
|
|
|
function setPropertyFromNotification(inst, path, value, event) {
|
|
|
|
|
let detail = event.detail;
|
|
|
|
|
if (detail && detail.queueProperty) {
|
|
|
|
|
if (!inst._hasReadOnlyEffect(path)) {
|
|
|
|
|
if ((path = inst._setPathOrUnmanagedProperty(path, value, Boolean(detail.path)))) {
|
|
|
|
|
inst._setPendingProperty(path, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
inst.set(path, value);
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Implements the "reflect" effect.
|
|
|
|
|
*
|
|
|
|
|
* Sets the attribute named `info.attrName` to the given property value.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} inst The instance the effect will be run on
|
|
|
|
|
* @param {string} property Name of property
|
|
|
|
|
* @param {*} value Current value of property
|
|
|
|
|
* @param {*} old Previous value of property
|
|
|
|
|
* @param {Object} info Effect metadata
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
function runReflectEffect(inst, property, value, old, info) {
|
2016-12-19 11:47:30 -08:00
|
|
|
if (Polymer.sanitizeDOMValue) {
|
|
|
|
|
value = Polymer.sanitizeDOMValue(value, info.attrName, 'attribute', inst);
|
|
|
|
|
}
|
|
|
|
|
inst._propertyToAttribute(property, info.attrName, value);
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Implements the "method observer" effect by running the method with the
|
|
|
|
|
* values of the arguments specified in the `info` object.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} inst The instance the effect will be run on
|
|
|
|
|
* @param {string} property Name of property
|
|
|
|
|
* @param {*} value Current value of property
|
|
|
|
|
* @param {*} old Previous value of property
|
|
|
|
|
* @param {Object} info Effect metadata
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function runMethodObserverEffect(inst, property, value, old, info) {
|
2016-09-01 12:44:48 -07:00
|
|
|
runMethodEffect(inst, property, value, old, info);
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-14 10:40:31 -08:00
|
|
|
/**
|
|
|
|
|
* 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 {Object} 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
|
|
|
|
|
* @return {Object} Bag of newly computed properties from "computed" effects
|
|
|
|
|
*/
|
2016-12-09 11:44:22 -08:00
|
|
|
function runComputedEffects(inst, changedProps, oldProps) {
|
|
|
|
|
if (inst[TYPES.COMPUTE]) {
|
|
|
|
|
let inputProps = changedProps;
|
|
|
|
|
let computedProps;
|
|
|
|
|
while (runEffects(inst, TYPES.COMPUTE, inputProps)) {
|
|
|
|
|
mixin(oldProps, inst.__dataOld);
|
|
|
|
|
mixin(changedProps, inst.__dataPending);
|
|
|
|
|
computedProps = mixin(computedProps || {}, inst.__dataPending);
|
|
|
|
|
inputProps = inst.__dataPending;
|
|
|
|
|
inst.__dataPending = null;
|
|
|
|
|
}
|
|
|
|
|
return computedProps;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -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 {Object} inst The instance the effect will be run on
|
|
|
|
|
* @param {string} property Name of property
|
|
|
|
|
* @param {*} value Current value of property
|
|
|
|
|
* @param {*} old Previous value of property
|
|
|
|
|
* @param {Object} info Effect metadata
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
function runComputedEffect(inst, property, value, old, info) {
|
|
|
|
|
var result = runMethodEffect(inst, property, value, old, info);
|
|
|
|
|
var computedProp = info.methodInfo;
|
2016-12-09 11:44:22 -08:00
|
|
|
if (inst._hasPropertyEffect(computedProp)) {
|
|
|
|
|
inst._setPendingProperty(computedProp, result);
|
|
|
|
|
} else {
|
|
|
|
|
inst[computedProp] = result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Computes path changes based on path links set up using the `linkPaths`
|
|
|
|
|
* API.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} inst The instance whose props are changing
|
|
|
|
|
* @param {Object} changedProps Bag of changed properties
|
|
|
|
|
* @param {Object} computedProps Bag of properties newly computed this turn
|
|
|
|
|
* via "computed" effects; any linked paths generated via this method
|
|
|
|
|
* will be added both to the set of `changedProps` as well as to the
|
|
|
|
|
* set of `computedProps`; this is because the `fromAbove: true` case will
|
|
|
|
|
* notify only from the `computedProps` bag.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function computeLinkedPaths(inst, changedProps, computedProps) {
|
|
|
|
|
const links = inst.__dataLinkedPaths;
|
2016-12-12 11:08:40 -08:00
|
|
|
const cache = inst.__dataTemp;
|
2016-12-09 11:44:22 -08:00
|
|
|
if (links) {
|
|
|
|
|
computedProps = computedProps || {};
|
|
|
|
|
let link;
|
|
|
|
|
for (let a in links) {
|
|
|
|
|
let b = links[a];
|
|
|
|
|
for (let path in changedProps) {
|
|
|
|
|
if (Polymer.Path.isDescendant(a, path)) {
|
|
|
|
|
link = Polymer.Path.translate(a, b, path);
|
2016-12-12 11:08:40 -08:00
|
|
|
cache[link] = changedProps[link] = computedProps[link] = changedProps[path];
|
2016-12-09 11:44:22 -08:00
|
|
|
} else if (Polymer.Path.isDescendant(b, path)) {
|
|
|
|
|
link = Polymer.Path.translate(b, a, path);
|
2016-12-12 11:08:40 -08:00
|
|
|
cache[link] = changedProps[link] = computedProps[link] = changedProps[path];
|
2016-12-09 11:44:22 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return computedProps;
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
|
2016-12-19 11:47:30 -08:00
|
|
|
// -- bindings ----------------------------------------------
|
2016-09-01 12:44:48 -07:00
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
2016-12-19 12:09:16 -08:00
|
|
|
* Adds "binding" property effects for the template annotation
|
2016-10-27 09:51:41 -07:00
|
|
|
* ("note" for short) and node index specified. These may either be normal
|
2016-12-19 12:09:16 -08:00
|
|
|
* "binding" effects (property/path bindings) or "method binding"
|
2016-10-27 09:51:41 -07:00
|
|
|
* effects, aka inline computing functions, depending on the type of binding
|
|
|
|
|
* detailed in the note.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} model Prototype or instance
|
|
|
|
|
* @param {Object} note Annotation note returned from Annotator
|
|
|
|
|
* @param {number} index Index into `__dataNodes` list of annotated nodes that the
|
|
|
|
|
* note applies to
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-12-19 12:09:16 -08:00
|
|
|
function addBindingEffect(model, note, index) {
|
2016-09-01 12:44:48 -07:00
|
|
|
for (let i=0; i<note.parts.length; i++) {
|
|
|
|
|
let part = note.parts[i];
|
|
|
|
|
if (part.signature) {
|
2016-12-19 12:09:16 -08:00
|
|
|
addMethodBindingEffect(model, note, part, index);
|
2016-09-01 12:44:48 -07:00
|
|
|
} else if (!part.literal) {
|
|
|
|
|
if (note.kind === 'attribute' && note.name[0] === '-') {
|
|
|
|
|
console.warn('Cannot set attribute ' + note.name +
|
|
|
|
|
' because "-" is not a valid attribute starting character');
|
|
|
|
|
} else {
|
2016-10-27 09:51:41 -07:00
|
|
|
model._addPropertyEffect(part.value, TYPES.PROPAGATE, {
|
2016-12-19 12:09:16 -08:00
|
|
|
fn: runBindingEffect,
|
2016-09-01 12:44:48 -07:00
|
|
|
info: {
|
|
|
|
|
kind: note.kind,
|
|
|
|
|
index: index,
|
|
|
|
|
name: note.name,
|
|
|
|
|
propertyName: note.propertyName,
|
|
|
|
|
value: part.value,
|
|
|
|
|
isCompound: note.isCompound,
|
|
|
|
|
compoundIndex: part.compoundIndex,
|
|
|
|
|
event: part.event,
|
|
|
|
|
customEvent: part.customEvent,
|
|
|
|
|
negate: part.negate
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
2016-12-19 12:09:16 -08:00
|
|
|
* Implements the "binding" (property/path binding) effect.
|
2016-10-27 09:51:41 -07:00
|
|
|
*
|
|
|
|
|
* @param {Object} inst The instance the effect will be run on
|
|
|
|
|
* @param {string} property Name of property
|
|
|
|
|
* @param {*} value Current value of property
|
|
|
|
|
* @param {*} old Previous value of property
|
|
|
|
|
* @param {Object} info Effect metadata
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-12-19 12:09:16 -08:00
|
|
|
function runBindingEffect(inst, path, value, old, info) {
|
2016-10-27 09:51:41 -07:00
|
|
|
let node = inst.__dataNodes[info.index];
|
2016-09-01 12:44:48 -07:00
|
|
|
// Subpath notification: transform path and set to client
|
|
|
|
|
// e.g.: foo="{{obj.sub}}", path: 'obj.sub.prop', set 'foo.prop'=obj.sub.prop
|
|
|
|
|
if ((path.length > info.value.length) &&
|
|
|
|
|
(info.kind == 'property') && !info.isCompound &&
|
|
|
|
|
node._hasPropertyEffect && node._hasPropertyEffect(info.name)) {
|
|
|
|
|
path = Polymer.Path.translate(info.value, info.name, path);
|
2016-12-19 12:09:16 -08:00
|
|
|
setPropertyToNodeFromBinding(inst, node, path, value);
|
2016-09-01 12:44:48 -07:00
|
|
|
} else {
|
|
|
|
|
// Root or deeper path was set; extract bound path value
|
|
|
|
|
// e.g.: foo="{{obj.sub}}", path: 'obj', set 'foo'=obj.sub
|
|
|
|
|
// or: foo="{{obj.sub}}", path: 'obj.sub.prop', set 'foo'=obj.sub
|
|
|
|
|
if (path != info.value) {
|
2016-09-01 19:18:47 -07:00
|
|
|
value = Polymer.Path.get(inst, info.value);
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
// Propagate value to child
|
2016-12-19 12:09:16 -08:00
|
|
|
applyBindingValue(inst, info, value);
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-09 11:44:22 -08:00
|
|
|
/**
|
2016-12-19 12:09:16 -08:00
|
|
|
* Called by "binding effect" to set a property to a node. Note,
|
2016-12-09 11:44:22 -08:00
|
|
|
* the caller must ensure that the target node has a property effect for
|
|
|
|
|
* the property in question, otherwise this method will error.
|
|
|
|
|
*
|
|
|
|
|
* @param {Node} node Node to set property on
|
|
|
|
|
* @param {string} prop Property (or path) name to set
|
|
|
|
|
* @param {*} value Value to set
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2016-12-19 12:09:16 -08:00
|
|
|
function setPropertyToNodeFromBinding(inst, node, prop, value) {
|
2016-12-09 11:44:22 -08:00
|
|
|
if (!node._hasReadOnlyEffect(prop)) {
|
|
|
|
|
if (node._setPendingProperty(prop, value)) {
|
|
|
|
|
inst._enqueueClient(node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
2016-12-19 12:09:16 -08:00
|
|
|
* Sets the value for an "binding" (binding) effect to a node,
|
2016-10-27 09:51:41 -07:00
|
|
|
* either as a property or attribute.
|
|
|
|
|
*
|
2016-12-19 12:09:16 -08:00
|
|
|
* @param {Object} inst The instance owning the binding effect
|
2016-10-27 09:51:41 -07:00
|
|
|
* @param {Object} info Effect metadata
|
|
|
|
|
* @param {*} value Value to set
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-12-19 12:09:16 -08:00
|
|
|
function applyBindingValue(inst, info, value) {
|
2016-10-27 09:51:41 -07:00
|
|
|
let node = inst.__dataNodes[info.index];
|
2016-12-19 12:09:16 -08:00
|
|
|
value = computeBindingValue(node, value, info);
|
2016-12-19 11:47:30 -08:00
|
|
|
if (Polymer.sanitizeDOMValue) {
|
|
|
|
|
value = Polymer.sanitizeDOMValue(value, info.name, info.kind, node);
|
|
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
if (info.kind == 'attribute') {
|
2016-10-27 09:51:41 -07:00
|
|
|
// Attribute binding
|
|
|
|
|
inst._valueToNodeAttribute(node, value, info.name);
|
2016-09-01 12:44:48 -07:00
|
|
|
} else {
|
2016-10-27 09:51:41 -07:00
|
|
|
// Property binding
|
|
|
|
|
let prop = info.name;
|
|
|
|
|
if (node._hasPropertyEffect && node._hasPropertyEffect(prop)) {
|
2016-12-19 12:09:16 -08:00
|
|
|
setPropertyToNodeFromBinding(inst, node, prop, value);
|
2016-12-08 12:27:56 -08:00
|
|
|
// The `else` clause is for interop: binding to a non-Polymer element's
|
|
|
|
|
// property. 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
|
2016-12-19 11:54:14 -08:00
|
|
|
// information. 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)
|
2016-12-19 17:58:33 -08:00
|
|
|
} else if (value !== node[prop] || typeof value == 'object') {
|
2016-10-27 09:51:41 -07:00
|
|
|
node[prop] = value;
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
2016-12-19 12:09:16 -08:00
|
|
|
* Transforms an "binding" effect value based on compound & negation
|
2016-10-27 09:51:41 -07:00
|
|
|
* 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 {Object} info Effect metadata
|
|
|
|
|
* @return {*} Transformed value to set
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-12-19 12:09:16 -08:00
|
|
|
function computeBindingValue(node, value, info) {
|
2016-09-01 12:44:48 -07:00
|
|
|
if (info.negate) {
|
|
|
|
|
value = !value;
|
|
|
|
|
}
|
|
|
|
|
if (info.isCompound) {
|
2016-10-27 09:51:41 -07:00
|
|
|
let storage = node.__dataCompoundStorage[info.name];
|
2016-09-01 12:44:48 -07:00
|
|
|
storage[info.compoundIndex] = value;
|
|
|
|
|
value = storage.join('');
|
|
|
|
|
}
|
|
|
|
|
if (info.kind !== 'attribute') {
|
|
|
|
|
// Some browsers serialize `undefined` to `"undefined"`
|
2016-10-27 09:51:41 -07:00
|
|
|
if (info.name === 'textContent' ||
|
|
|
|
|
(node.localName == 'input' && info.name == 'value')) {
|
2016-09-01 12:44:48 -07:00
|
|
|
value = value == undefined ? '' : value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
2016-12-19 12:09:16 -08:00
|
|
|
* Adds "binding method" property effects for the template binding
|
2016-10-27 09:51:41 -07:00
|
|
|
* ("note" for short), part metadata, and node index specified.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} model Prototype or instance
|
2016-12-19 12:09:16 -08:00
|
|
|
* @param {Object} note Binding note returned from Annotator
|
2016-10-27 09:51:41 -07:00
|
|
|
* @param {number} part The compound part metadata
|
|
|
|
|
* @param {number} index Index into `__dataNodes` list of annotated nodes that the
|
|
|
|
|
* note applies to
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-12-19 12:09:16 -08:00
|
|
|
function addMethodBindingEffect(model, note, part, index) {
|
2016-10-27 09:51:41 -07:00
|
|
|
createMethodEffect(model, part.signature, TYPES.PROPAGATE,
|
2016-12-19 12:09:16 -08:00
|
|
|
runMethodBindingEffect, {
|
2016-09-01 12:44:48 -07:00
|
|
|
index: index,
|
|
|
|
|
isCompound: note.isCompound,
|
|
|
|
|
compoundIndex: part.compoundIndex,
|
|
|
|
|
kind: note.kind,
|
|
|
|
|
name: note.name,
|
|
|
|
|
negate: part.negate,
|
|
|
|
|
part: part
|
|
|
|
|
}, true
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
2016-12-19 12:09:16 -08:00
|
|
|
* Implements the "binding method" (inline computed function) effect.
|
2016-10-27 09:51:41 -07:00
|
|
|
*
|
|
|
|
|
* Runs the method with the values of the arguments specified in the `info`
|
|
|
|
|
* object and setting the return value to the node property/attribute.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} inst The instance the effect will be run on
|
|
|
|
|
* @param {string} property Name of property
|
|
|
|
|
* @param {*} value Current value of property
|
|
|
|
|
* @param {*} old Previous value of property
|
|
|
|
|
* @param {Object} info Effect metadata
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-12-19 12:09:16 -08:00
|
|
|
function runMethodBindingEffect(inst, property, value, old, info) {
|
2016-09-01 12:44:48 -07:00
|
|
|
let val = runMethodEffect(inst, property, value, old, info);
|
2016-12-19 12:09:16 -08:00
|
|
|
applyBindingValue(inst, info.methodInfo, val);
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
2016-12-19 12:09:16 -08:00
|
|
|
* Post-processes template bindings (notes for short) provided by the
|
|
|
|
|
* Bindings library for use by the effects system:
|
2016-10-27 09:51:41 -07:00
|
|
|
* - Parses bindings for methods into method `signature` objects
|
|
|
|
|
* - Memoizes the root property for path bindings
|
|
|
|
|
* - Recurses into nested templates and processes those templates and
|
|
|
|
|
* extracts any host properties, which are set to the template's
|
|
|
|
|
* `_content._hostProps`
|
|
|
|
|
* - Adds bindings from the host to <template> elements for any nested
|
|
|
|
|
* template's lexically bound "host properties"; template handling
|
|
|
|
|
* elements can then add accessors to the template for these properties
|
|
|
|
|
* to forward host properties into template instances accordingly.
|
|
|
|
|
*
|
|
|
|
|
* @param {Array<Object>} notes List of notes to process; the notes are
|
|
|
|
|
* modified in place.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
function processAnnotations(notes) {
|
|
|
|
|
if (!notes._processed) {
|
|
|
|
|
for (let i=0; i<notes.length; i++) {
|
|
|
|
|
let note = notes[i];
|
|
|
|
|
// Parse bindings for methods & path roots (models)
|
|
|
|
|
for (let j=0; j<note.bindings.length; j++) {
|
|
|
|
|
let b = note.bindings[j];
|
|
|
|
|
for (let k=0; k<b.parts.length; k++) {
|
|
|
|
|
let p = b.parts[k];
|
|
|
|
|
if (!p.literal) {
|
|
|
|
|
p.signature = parseMethod(p.value);
|
|
|
|
|
if (!p.signature) {
|
|
|
|
|
p.rootProperty = Polymer.Path.root(p.value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-27 09:51:41 -07:00
|
|
|
// Recurse into nested templates & bind host props
|
2016-09-01 12:44:48 -07:00
|
|
|
if (note.templateContent) {
|
|
|
|
|
processAnnotations(note.templateContent._notes);
|
2016-10-27 09:51:41 -07:00
|
|
|
let hostProps = note.templateContent._hostProps =
|
2016-09-01 12:44:48 -07:00
|
|
|
discoverTemplateHostProps(note.templateContent._notes);
|
|
|
|
|
let bindings = [];
|
2016-10-27 09:51:41 -07:00
|
|
|
for (let prop in hostProps) {
|
2016-09-01 12:44:48 -07:00
|
|
|
bindings.push({
|
|
|
|
|
index: note.index,
|
|
|
|
|
kind: 'property',
|
|
|
|
|
name: '_host_' + prop,
|
|
|
|
|
parts: [{
|
|
|
|
|
mode: '{',
|
|
|
|
|
rootProperty: prop,
|
|
|
|
|
value: prop
|
|
|
|
|
}]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
note.bindings = note.bindings.concat(bindings);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
notes._processed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Finds all property usage in templates (property/path bindings and function
|
|
|
|
|
* arguments) and returns the path roots as keys in a map. Each outer template
|
|
|
|
|
* merges inner _hostProps to propagate inner host property needs to outer
|
|
|
|
|
* templates.
|
|
|
|
|
*
|
|
|
|
|
* @param {Array<Object>} notes List of notes to process for a given template
|
|
|
|
|
* @return {Object<string,boolean>} Map of host properties that the template
|
|
|
|
|
* (or any nested templates) uses
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
function discoverTemplateHostProps(notes) {
|
2016-10-27 09:51:41 -07:00
|
|
|
let hostProps = {};
|
2016-09-01 12:44:48 -07:00
|
|
|
for (let i=0, n; (i<notes.length) && (n=notes[i]); i++) {
|
|
|
|
|
// Find all bindings to parent.* and spread them into _parentPropChain
|
|
|
|
|
for (let j=0, b$=n.bindings, b; (j<b$.length) && (b=b$[j]); j++) {
|
|
|
|
|
for (let k=0, p$=b.parts, p; (k<p$.length) && (p=p$[k]); k++) {
|
|
|
|
|
if (p.signature) {
|
|
|
|
|
let args = p.signature.args;
|
|
|
|
|
for (let kk=0; kk<args.length; kk++) {
|
|
|
|
|
let rootProperty = args[kk].rootProperty;
|
|
|
|
|
if (rootProperty) {
|
2016-10-27 09:51:41 -07:00
|
|
|
hostProps[rootProperty] = true;
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
2016-10-27 09:51:41 -07:00
|
|
|
hostProps[p.signature.methodName] = true;
|
2016-09-01 12:44:48 -07:00
|
|
|
} else {
|
|
|
|
|
if (p.rootProperty) {
|
2016-10-27 09:51:41 -07:00
|
|
|
hostProps[p.rootProperty] = true;
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Merge child _hostProps into this _hostProps
|
|
|
|
|
if (n.templateContent) {
|
2016-10-27 09:51:41 -07:00
|
|
|
let templateHostProps = n.templateContent._hostProps;
|
|
|
|
|
Polymer.Base.mixin(hostProps, templateHostProps);
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
2016-10-27 09:51:41 -07:00
|
|
|
return hostProps;
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* 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 {Object} binding Binding metadata
|
|
|
|
|
* @return {boolean} True if 2-way listener should be added
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function shouldAddListener(binding) {
|
|
|
|
|
return binding.name &&
|
|
|
|
|
binding.kind != 'attribute' &&
|
|
|
|
|
binding.kind != 'text' &&
|
|
|
|
|
!binding.isCompound &&
|
|
|
|
|
binding.parts[0].mode === '{';
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Sets up a prototypical `_bindListeners` metadata array to be used at
|
|
|
|
|
* instance time to add event listeners for 2-way bindings.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} model Prototype (instances not currently supported)
|
|
|
|
|
* @param {number} index Index into `__dataNodes` list of annotated nodes that the
|
|
|
|
|
* event should be added to
|
|
|
|
|
* @param {string} property Property of target node to listen for changes
|
|
|
|
|
* @param {string} path Host path that the change should be propagated to
|
|
|
|
|
* @param {string=} event A custom event name to listen for (e.g. via the
|
|
|
|
|
* `{{prop::eventName}}` syntax)
|
|
|
|
|
* @param {boolean=} negate Whether the notified value should be negated before
|
|
|
|
|
* setting to host path
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function addAnnotatedListener(model, index, property, path, event, negate) {
|
|
|
|
|
if (!model._bindListeners) {
|
|
|
|
|
model._bindListeners = [];
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
let eventName = event ||
|
|
|
|
|
(CaseMap.camelToDashCase(property) + '-changed');
|
2016-10-27 09:51:41 -07:00
|
|
|
model._bindListeners.push({
|
2016-09-01 12:44:48 -07:00
|
|
|
index: index,
|
|
|
|
|
property: property,
|
|
|
|
|
path: path,
|
|
|
|
|
event: eventName,
|
|
|
|
|
negate: negate
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Adds all 2-way binding notification listeners to a host based on
|
|
|
|
|
* `_bindListeners` metadata recorded by prior calls to`addAnnotatedListener`
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} inst Host element instance
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
function setupBindListeners(inst) {
|
|
|
|
|
let b$ = inst._bindListeners;
|
|
|
|
|
for (let i=0, l=b$.length, info; (i<l) && (info=b$[i]); i++) {
|
2016-10-27 09:51:41 -07:00
|
|
|
let node = inst.__dataNodes[info.index];
|
2016-09-01 12:44:48 -07:00
|
|
|
addNotifyListener(node, inst, info);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Finds all bound nodes in the given `dom` fragment that were recorded in the
|
|
|
|
|
* provided Annotator `notes` array and stores them in `__dataNodes` for this
|
|
|
|
|
* instance. The index of nodes in `__dataNodes` corresponds to the index
|
2016-12-19 12:09:16 -08:00
|
|
|
* of a note in the `notes` array, and binding effect metadata uses this
|
2016-10-27 09:51:41 -07:00
|
|
|
* index to identify bound nodes when propagating data.
|
|
|
|
|
*
|
|
|
|
|
* Compound binding storage structures are also initialized onto the bound
|
|
|
|
|
* nodes, and 2-way binding event listeners are also added.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} inst Instance that bas been previously bound
|
|
|
|
|
* @param {DocumentFragment} dom Document fragment containing stamped nodes
|
|
|
|
|
* @param {Array<Object>} notes Array of annotation notes provided by
|
|
|
|
|
* Polymer.Annotator
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function setupBindings(inst, dom, notes) {
|
|
|
|
|
if (notes.length) {
|
|
|
|
|
let nodes = new Array(notes.length);
|
|
|
|
|
for (let i=0; i < notes.length; i++) {
|
|
|
|
|
let note = notes[i];
|
|
|
|
|
let node = nodes[i] = inst._findTemplateAnnotatedNode(dom, note);
|
|
|
|
|
node.__dataHost = inst;
|
|
|
|
|
if (note.bindings) {
|
|
|
|
|
setupCompoundBinding(note, node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
inst.__dataNodes = nodes;
|
|
|
|
|
}
|
|
|
|
|
if (inst._bindListeners) {
|
|
|
|
|
setupBindListeners(inst);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-01 12:44:48 -07:00
|
|
|
// -- for method-based effects (complexObserver & computed) --------------
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* 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 {Object} inst Prototype or instance
|
|
|
|
|
* @param {Object} sig Method signature metadata
|
|
|
|
|
* @param {Function} effectFn Function to run when arguments change
|
|
|
|
|
* @param {boolean=} dynamic Whether the method name should be included as
|
|
|
|
|
* a dependency to the effect.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function createMethodEffect(model, sig, type, effectFn, methodInfo, dynamic) {
|
2016-09-01 12:44:48 -07:00
|
|
|
let info = {
|
|
|
|
|
methodName: sig.methodName,
|
|
|
|
|
args: sig.args,
|
|
|
|
|
methodInfo: methodInfo,
|
|
|
|
|
dynamicFn: dynamic
|
|
|
|
|
};
|
|
|
|
|
// TODO(sorvell): why still here?
|
|
|
|
|
if (sig.static) {
|
2016-10-27 09:51:41 -07:00
|
|
|
model._addPropertyEffect('__static__', type, {
|
2016-09-01 12:44:48 -07:00
|
|
|
fn: effectFn, info: info
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
for (let i=0, arg; (i<sig.args.length) && (arg=sig.args[i]); i++) {
|
|
|
|
|
if (!arg.literal) {
|
2016-10-27 09:51:41 -07:00
|
|
|
model._addPropertyEffect(arg.name, type, {
|
2016-09-01 12:44:48 -07:00
|
|
|
fn: effectFn, info: info
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (dynamic) {
|
2016-10-27 09:51:41 -07:00
|
|
|
model._addPropertyEffect(sig.methodName, type, {
|
2016-09-01 12:44:48 -07:00
|
|
|
fn: effectFn, info: info
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* 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 {Object} inst The instance the effect will be run on
|
|
|
|
|
* @param {string} property Name of property
|
|
|
|
|
* @param {*} value Current value of property
|
|
|
|
|
* @param {*} old Previous value of property
|
|
|
|
|
* @param {Object} info Effect metadata
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
function runMethodEffect(inst, property, value, old, info) {
|
|
|
|
|
// TODO(kschaaf): ideally rootDataHost would be a detail of Templatizer only
|
|
|
|
|
let context = inst._rootDataHost || inst;
|
|
|
|
|
let fn = context[info.methodName];
|
|
|
|
|
if (fn) {
|
2016-10-27 09:51:41 -07:00
|
|
|
let args = marshalArgs(inst.__data, info.args, property, value);
|
2016-09-01 12:44:48 -07:00
|
|
|
return fn.apply(context, args);
|
|
|
|
|
} else if (!info.dynamicFn) {
|
|
|
|
|
console.warn('method `' + info.methodName + '` not defined');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const emptyArray = [];
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* 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 {?Object} The method metadata object if a method expression was
|
|
|
|
|
* found, otherwise `undefined`
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
function parseMethod(expression) {
|
|
|
|
|
// tries to match valid javascript property names
|
|
|
|
|
let m = expression.match(/([^\s]+?)\(([\s\S]*)\)/);
|
|
|
|
|
if (m) {
|
|
|
|
|
let sig = { methodName: m[1], static: true };
|
|
|
|
|
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 {
|
|
|
|
|
sig.args = emptyArray;
|
|
|
|
|
return sig;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* 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 {Object} sig Method signature metadata object
|
|
|
|
|
* @return {Object} The updated signature metadata object
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
function parseArgs(argList, sig) {
|
|
|
|
|
sig.args = argList.map(function(rawArg) {
|
|
|
|
|
let arg = parseArg(rawArg);
|
|
|
|
|
if (!arg.literal) {
|
|
|
|
|
sig.static = false;
|
|
|
|
|
}
|
|
|
|
|
return arg;
|
|
|
|
|
}, this);
|
|
|
|
|
return sig;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* 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 {Object} Argument metadata object
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
// detect literal value (must be String or Number)
|
|
|
|
|
let fc = arg[0];
|
|
|
|
|
if (fc === '-') {
|
|
|
|
|
fc = arg[1];
|
|
|
|
|
}
|
|
|
|
|
if (fc >= '0' && fc <= '9') {
|
|
|
|
|
fc = '#';
|
|
|
|
|
}
|
|
|
|
|
switch(fc) {
|
|
|
|
|
case "'":
|
|
|
|
|
case '"':
|
|
|
|
|
a.value = arg.slice(1, -1);
|
|
|
|
|
a.literal = true;
|
|
|
|
|
break;
|
|
|
|
|
case '#':
|
|
|
|
|
a.value = Number(arg);
|
|
|
|
|
a.literal = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// if not literal, look for structured path
|
|
|
|
|
if (!a.literal) {
|
|
|
|
|
a.rootProperty = Polymer.Path.root(arg);
|
|
|
|
|
// detect structured path (has dots)
|
|
|
|
|
a.structured = Polymer.Path.isDeep(arg);
|
|
|
|
|
if (a.structured) {
|
|
|
|
|
a.wildcard = (arg.slice(-2) == '.*');
|
|
|
|
|
if (a.wildcard) {
|
|
|
|
|
a.name = arg.slice(0, -2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return a;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -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 {Object} data Instance data storage object to read properties from
|
|
|
|
|
* @param {Array<Object>} args Array of argument metadata
|
|
|
|
|
* @return {Array<*>} Array of argument values
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function marshalArgs(data, args, path, value) {
|
2016-09-01 12:44:48 -07:00
|
|
|
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 (path == name) {
|
|
|
|
|
v = value;
|
|
|
|
|
} else {
|
|
|
|
|
// TODO(kschaaf): confirm design of this
|
|
|
|
|
v = data[name];
|
|
|
|
|
if (v === undefined && arg.structured) {
|
|
|
|
|
v = Polymer.Path.get(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 ? value : v,
|
|
|
|
|
base: v
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
values[i] = v;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return values;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -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 {Object} note Annotation metadata
|
|
|
|
|
* @param {Node} node Bound node to initialize
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
function setupCompoundBinding(note, node) {
|
|
|
|
|
let bindings = note.bindings;
|
|
|
|
|
for (let i=0; i<bindings.length; i++) {
|
|
|
|
|
let binding = bindings[i];
|
|
|
|
|
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 name = binding.name;
|
|
|
|
|
storage[name] = literals;
|
|
|
|
|
// Configure properties with their literal parts
|
|
|
|
|
if (binding.literal && binding.kind == 'property') {
|
|
|
|
|
// TODO(kschaaf) config integration
|
|
|
|
|
// if (node._configValue) {
|
|
|
|
|
// node._configValue(name, binding.literal);
|
|
|
|
|
// } else {
|
|
|
|
|
node[name] = binding.literal;
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// data api
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Sends array splice notifications (`.splices` and `.length`)
|
|
|
|
|
*
|
|
|
|
|
* Note: this implementation only accepts normalized paths
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} 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
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2016-09-19 15:19:22 -07:00
|
|
|
function notifySplices(inst, array, path, splices) {
|
|
|
|
|
let splicesPath = path + '.splices';
|
2016-12-08 12:27:56 -08:00
|
|
|
inst._setProperty(splicesPath, { indexSplices: splices });
|
2016-09-19 15:19:22 -07:00
|
|
|
inst._setProperty(path + '.length', array.length);
|
|
|
|
|
// Null here to allow potentially large splice records to be GC'ed.
|
2016-09-22 19:12:21 -07:00
|
|
|
inst.__data[splicesPath] = {indexSplices: null};
|
2016-09-19 15:19:22 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Creates a splice record and sends an array splice notification for
|
|
|
|
|
* the described mutation
|
|
|
|
|
*
|
|
|
|
|
* Note: this implementation only accepts normalized paths
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} 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
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function notifySplice(inst, array, path, index, addedCount, removed) {
|
2016-09-19 15:19:22 -07:00
|
|
|
notifySplices(inst, array, path, [{
|
|
|
|
|
index: index,
|
2016-10-27 09:51:41 -07:00
|
|
|
addedCount: addedCount,
|
2016-09-19 15:19:22 -07:00
|
|
|
removed: removed,
|
|
|
|
|
object: array,
|
|
|
|
|
type: 'splice'
|
|
|
|
|
}]);
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
2016-08-16 23:45:41 -07:00
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Returns an upper-cased version of the string.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} name String to uppercase
|
|
|
|
|
* @return {string} Uppercased string
|
2016-12-05 16:25:11 -08:00
|
|
|
* @private
|
2016-10-27 09:51:41 -07:00
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
function upper(name) {
|
|
|
|
|
return name[0].toUpperCase() + name.substring(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Polymer.PropertyEffects = Polymer.Utils.dedupingMixin(function(superClass) {
|
2016-08-16 23:45:41 -07:00
|
|
|
|
2016-09-01 15:24:28 -07:00
|
|
|
return class PropertyEffects extends Polymer.TemplateStamp(
|
|
|
|
|
Polymer.Attributes(Polymer.PropertyAccessors(superClass))) {
|
2016-08-16 23:45:41 -07:00
|
|
|
|
2016-09-01 15:24:28 -07:00
|
|
|
get PROPERTY_EFFECT_TYPES() {
|
2016-08-31 19:09:55 -07:00
|
|
|
return TYPES;
|
2016-08-17 16:29:27 -07:00
|
|
|
}
|
|
|
|
|
|
2016-08-31 19:09:55 -07:00
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this._asyncEffects = false;
|
2016-09-02 16:05:16 -07:00
|
|
|
this.__dataInitialized = false;
|
2016-08-31 19:09:55 -07:00
|
|
|
this.__dataPendingClients = null;
|
|
|
|
|
this.__dataFromAbove = false;
|
|
|
|
|
this.__dataLinkedPaths = null;
|
2016-10-27 09:51:41 -07:00
|
|
|
this.__dataNodes = null;
|
2016-09-02 16:05:16 -07:00
|
|
|
// May be set on instance prior to upgrade
|
|
|
|
|
this.__dataCompoundStorage = this.__dataCompoundStorage || null;
|
|
|
|
|
this.__dataHost = this.__dataHost || null;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Adds to default initialization in `PropertyAccessors` by initializing
|
|
|
|
|
* local property & pending data storage with any accessor values saved
|
|
|
|
|
* in `__dataProto`. If instance properties had been set before the
|
|
|
|
|
* element upgraded and gained accessors on its prototype, these values
|
|
|
|
|
* are set into the prototype's accessors after being deleted from the
|
|
|
|
|
* instance.
|
|
|
|
|
*
|
|
|
|
|
* @override
|
|
|
|
|
*/
|
2016-09-02 16:05:16 -07:00
|
|
|
_initializeProperties() {
|
|
|
|
|
super._initializeProperties();
|
2016-12-19 11:13:56 -08:00
|
|
|
this.__dataTemp = {};
|
2016-08-31 19:09:55 -07:00
|
|
|
// initialize data with prototype values saved when creating accessors
|
|
|
|
|
if (this.__dataProto) {
|
|
|
|
|
this.__data = Object.create(this.__dataProto);
|
|
|
|
|
this.__dataPending = Object.create(this.__dataProto);
|
|
|
|
|
this.__dataOld = {};
|
2016-09-02 16:05:16 -07:00
|
|
|
} else {
|
|
|
|
|
this.__dataPending = null;
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
|
|
|
|
// update instance properties
|
|
|
|
|
for (let p in this.__propertyEffects) {
|
|
|
|
|
if (this.hasOwnProperty(p)) {
|
|
|
|
|
let value = this[p];
|
|
|
|
|
delete this[p];
|
|
|
|
|
this[p] = value;
|
|
|
|
|
}
|
2016-08-19 12:04:38 -07:00
|
|
|
}
|
2016-08-17 16:29:27 -07:00
|
|
|
}
|
|
|
|
|
|
2016-08-31 19:09:55 -07:00
|
|
|
// Prototype setup ----------------------------------------
|
|
|
|
|
|
2016-10-27 09:51:41 -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
|
|
|
|
|
* // path: '...' // Will be set by this method based on path arg
|
|
|
|
|
* }
|
|
|
|
|
*
|
|
|
|
|
* Effect functions are called with the following signature:
|
|
|
|
|
*
|
|
|
|
|
* effectFunction(inst, property, currentValue, oldValue, info)
|
|
|
|
|
*
|
|
|
|
|
* This method may be called either on the prototype of a class
|
|
|
|
|
* using the PropertyEffects mixin (for best performance), or on
|
|
|
|
|
* an instance to add dynamic effects. When called on an instance or
|
|
|
|
|
* subclass of a class that has already had property effects added to
|
|
|
|
|
* its prototype, the property effect lists will be cloned and added as
|
|
|
|
|
* own properties of the caller.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} path Property (or path) that should trigger the effect
|
|
|
|
|
* @param {string} type Effect type, from this.PROPERTY_EFFECT_TYPES
|
|
|
|
|
* @param {Object} effect Effect metadata object
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2016-08-31 19:09:55 -07:00
|
|
|
_addPropertyEffect(path, type, effect) {
|
|
|
|
|
let property = Polymer.Path.root(path);
|
2016-10-27 09:51:41 -07:00
|
|
|
let effects = ensureOwnEffectMap(this, TYPES.ANY)[property];
|
2016-08-31 19:09:55 -07:00
|
|
|
if (!effects) {
|
|
|
|
|
effects = this.__propertyEffects[property] = [];
|
2016-10-27 09:51:41 -07:00
|
|
|
this._createPropertyAccessor(property,
|
2016-08-31 19:09:55 -07:00
|
|
|
type == TYPES.READ_ONLY);
|
2016-08-17 16:29:27 -07:00
|
|
|
}
|
2016-08-31 19:09:55 -07:00
|
|
|
// effects are accumulated into arrays per property based on type
|
|
|
|
|
if (effect) {
|
|
|
|
|
effect.path = path;
|
|
|
|
|
effects.push(effect);
|
|
|
|
|
}
|
2016-10-27 09:51:41 -07:00
|
|
|
effects = ensureOwnEffectMap(this, type)[property];
|
2016-08-31 19:09:55 -07:00
|
|
|
if (!effects) {
|
|
|
|
|
effects = this[type][property] = [];
|
|
|
|
|
}
|
|
|
|
|
effects.push(effect);
|
2016-08-17 16:29:27 -07:00
|
|
|
}
|
2016-08-09 20:43:30 -07:00
|
|
|
|
2016-10-27 09:51:41 -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
|
|
|
|
|
*/
|
2016-08-31 19:09:55 -07:00
|
|
|
_hasPropertyEffect(property, type) {
|
|
|
|
|
let effects = this[type || TYPES.ANY];
|
|
|
|
|
return Boolean(effects && effects[property]);
|
2016-08-17 16:29:27 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -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
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
_hasReadOnlyEffect(property) {
|
|
|
|
|
return this._hasPropertyEffect(property, TYPES.READ_ONLY);
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -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
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
_hasNotifyEffect(property) {
|
|
|
|
|
return this._hasPropertyEffect(property, TYPES.NOTIFY);
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -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
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
_hasReflectEffect(property) {
|
|
|
|
|
return this._hasPropertyEffect(property, TYPES.REFLECT);
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -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
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
_hasComputedEffect(property) {
|
|
|
|
|
return this._hasPropertyEffect(property, TYPES.COMPUTE);
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-31 19:09:55 -07:00
|
|
|
// Runtime ----------------------------------------
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Sets an unmanaged property (property without accessor) or leaf property
|
|
|
|
|
* of a path to the given value. If the path in question was a simple
|
|
|
|
|
* property with an accessor, no action is taken.
|
|
|
|
|
*
|
|
|
|
|
* This function isolates relatively expensive functionality necessary
|
|
|
|
|
* for the public API, such that it is only done when paths enter the
|
|
|
|
|
* system, and not in every step of the hot path.
|
|
|
|
|
*
|
|
|
|
|
* If `path` is an unmanaged property (property without an accessor)
|
|
|
|
|
* or a path, sets the value at that path.
|
|
|
|
|
*
|
|
|
|
|
* `path` can be a path string or array of path parts as accepted by the
|
|
|
|
|
* public API.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} path Path to set
|
|
|
|
|
* @param {*} value Value to set
|
2016-12-05 16:25:11 -08:00
|
|
|
* @param {boolean} fromPath If the value being set was from a path; in
|
2016-12-09 11:44:22 -08:00
|
|
|
* this case the value was shared, so no dirty check is performed.
|
2016-12-19 16:57:03 -08:00
|
|
|
* @return {?string} If the root of the path is a managed property,
|
|
|
|
|
* returns a normalized string path suitable for setting into the system
|
2016-12-19 17:58:33 -08:00
|
|
|
* via `_setProperty`/`_setPendingProperty`. A null path is returned if
|
2016-12-19 16:57:03 -08:00
|
|
|
* a path was being set and fails a dirty check, unless `fromPath`
|
|
|
|
|
* was true (in which case no dirty check is performed since this is a
|
|
|
|
|
* notification of change to a shared path).
|
2016-10-27 09:51:41 -07:00
|
|
|
* @protected
|
|
|
|
|
*/
|
2016-12-05 16:25:11 -08:00
|
|
|
_setPathOrUnmanagedProperty(path, value, fromPath) {
|
2016-08-31 19:09:55 -07:00
|
|
|
let rootProperty = Polymer.Path.root(Array.isArray(path) ? path[0] : path);
|
2016-09-14 18:52:47 -07:00
|
|
|
let hasEffect = this._hasPropertyEffect(rootProperty);
|
2016-08-31 19:09:55 -07:00
|
|
|
let isPath = (rootProperty !== path);
|
2016-12-19 11:54:14 -08:00
|
|
|
if (!hasEffect) {
|
|
|
|
|
Polymer.Path.set(this, path, value);
|
|
|
|
|
} else if (isPath && !fromPath) {
|
2016-12-19 16:57:03 -08:00
|
|
|
// Dirty check changes being set to a path against the actual object,
|
2016-12-19 17:58:33 -08:00
|
|
|
// 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 (fromPath: true),
|
2016-12-19 18:23:11 -08:00
|
|
|
// 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
|
2016-12-05 16:25:11 -08:00
|
|
|
let old = Polymer.Path.get(this, path);
|
2016-08-31 19:09:55 -07:00
|
|
|
path = Polymer.Path.set(this, path, value);
|
2016-12-19 18:23:11 -08:00
|
|
|
// Use property-accessor's simpler dirty check
|
2016-12-19 17:58:33 -08:00
|
|
|
if (!super._shouldPropertyChange(path, value, old)) {
|
2016-12-05 16:25:11 -08:00
|
|
|
return null;
|
|
|
|
|
}
|
2016-12-19 11:54:14 -08:00
|
|
|
}
|
2016-09-14 18:52:47 -07:00
|
|
|
if (hasEffect) {
|
2016-08-31 19:09:55 -07:00
|
|
|
return path;
|
2016-08-09 20:43:30 -07:00
|
|
|
}
|
|
|
|
|
}
|
2016-07-07 10:50:34 -07:00
|
|
|
|
2016-12-19 11:13:56 -08:00
|
|
|
/**
|
|
|
|
|
* Overrides the `PropertyAccessors` 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': {...})
|
2016-12-19 17:58:33 -08:00
|
|
|
* Stored in `__dataTemp` and `__data`, dirty checked against
|
|
|
|
|
* `__dataTemp` by default implementation of `_shouldPropertyChange`
|
2016-12-19 11:13:56 -08:00
|
|
|
* 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 `_propertiesChaged` 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
|
|
|
|
|
*/
|
|
|
|
|
_setPendingProperty(property, value) {
|
|
|
|
|
let isPath = Polymer.Path.isPath(property);
|
2016-12-19 17:58:33 -08:00
|
|
|
let prevProps = isPath ? this.__dataTemp : this.__data;
|
|
|
|
|
if (this._shouldPropertyChange(property, value, prevProps[property])) {
|
2016-12-19 11:13:56 -08:00
|
|
|
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 and objects are stored in temporary cache (cleared at end of
|
|
|
|
|
// turn), which is used for dirty-checking
|
2016-12-19 17:58:33 -08:00
|
|
|
if (isPath || typeof value == 'object') {
|
2016-12-19 11:13:56 -08:00
|
|
|
this.__dataTemp[property] = value;
|
|
|
|
|
}
|
|
|
|
|
// Properties (but not sub-paths) get stored __data cache, used to
|
|
|
|
|
// return accessor values from getters
|
|
|
|
|
if (!isPath) {
|
|
|
|
|
this.__data[property] = value;
|
|
|
|
|
}
|
|
|
|
|
// All changes go into pending property bag, passed to _propertiesChanged
|
|
|
|
|
this.__dataPending[property] = value;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-19 17:58:33 -08:00
|
|
|
/**
|
|
|
|
|
* Overrides default PropertyAccessors implementation to pull the value
|
2016-12-19 18:26:46 -08:00
|
|
|
* to dirty check against from the `__dataTemp` cache (rather than the
|
|
|
|
|
* normal `__data` cache) for Objects. Since the temp cache is cleared
|
|
|
|
|
* at the end of a turn, this implementation allows side-effects of deep
|
|
|
|
|
* object changes to be processed by re-setting the same object (using
|
|
|
|
|
* the temp cache as a backstop to prevent cycles due to 2-way
|
|
|
|
|
* notification).
|
2016-12-19 17:58:33 -08:00
|
|
|
*
|
|
|
|
|
* Override this to provide more strict dirty checking, i.e. immutable
|
|
|
|
|
* (`value === old`) or based on type.
|
|
|
|
|
*
|
|
|
|
|
* @override
|
|
|
|
|
*/
|
|
|
|
|
_shouldPropertyChange(property, value, old) {
|
|
|
|
|
if (typeof value == 'object') {
|
|
|
|
|
old = this.__dataTemp[property];
|
|
|
|
|
}
|
|
|
|
|
return super._shouldPropertyChange(property, value, old);
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Overrides PropertyAccessor's default async queuing of
|
|
|
|
|
* `_propertiesChanged`: if `__dataInitialized` is false (has not yet been
|
|
|
|
|
* manually flushed), the function no-ops; otherwise flushes
|
|
|
|
|
* `_propertiesChanged` synchronously.
|
|
|
|
|
*
|
|
|
|
|
* Subclasses may set `this._asyncEffects = true` to cause
|
|
|
|
|
* `_propertiesChanged` to be flushed asynchronously.
|
|
|
|
|
*
|
|
|
|
|
* @override
|
|
|
|
|
*/
|
2016-08-31 19:09:55 -07:00
|
|
|
_invalidateProperties() {
|
|
|
|
|
if (this.__dataInitialized) {
|
|
|
|
|
if (this._asyncEffects) {
|
|
|
|
|
super._invalidateProperties();
|
|
|
|
|
} else {
|
|
|
|
|
this._flushProperties();
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-07-07 10:50:34 -07:00
|
|
|
}
|
|
|
|
|
|
2016-12-09 11:44:22 -08: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
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
|
|
|
|
_enqueueClient(client) {
|
|
|
|
|
this.__dataPendingClients = this.__dataPendingClients || new Map();
|
|
|
|
|
if (client !== this) {
|
|
|
|
|
this.__dataPendingClients.set(client, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Flushes any clients previously enqueued via `_enqueueClient`, causing
|
|
|
|
|
* their `_flushProperties` method to run.
|
|
|
|
|
*
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
|
|
|
|
_flushClients() {
|
|
|
|
|
// Flush all clients
|
|
|
|
|
let clients = this.__dataPendingClients;
|
|
|
|
|
if (clients) {
|
|
|
|
|
this.__dataPendingClients = null;
|
|
|
|
|
clients.forEach((v, client) => {
|
|
|
|
|
// TODO(kschaaf): more explicit check?
|
|
|
|
|
if (client._flushProperties) {
|
|
|
|
|
client._flushProperties(true);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sets a bag of property (or path) changes to this instance, and
|
|
|
|
|
* synchronously processes all effects of the properties as a batch.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} props Bag of one or more key-value pairs whose key is
|
|
|
|
|
* a property (or path, such as `'object.foo'`) and value is the new
|
|
|
|
|
* value to set for that property.
|
|
|
|
|
*/
|
|
|
|
|
setProperties(props) {
|
|
|
|
|
for (let path in props) {
|
|
|
|
|
if (!this._hasReadOnlyEffect(path)) {
|
|
|
|
|
let value = props[path];
|
|
|
|
|
if ((path = this._setPathOrUnmanagedProperty(path, value))) {
|
|
|
|
|
this._setPendingProperty(path, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this._invalidateProperties();
|
|
|
|
|
}
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Overrides PropertyAccessor's default async queuing of
|
|
|
|
|
* `_propertiesChanged`, to instead synchronously flush
|
|
|
|
|
* `_propertiesChanged` unless the `this._asyncEffects` property is true.
|
|
|
|
|
*
|
|
|
|
|
* If this is the first time properties are being flushed, the `ready`
|
|
|
|
|
* callback will be called.
|
|
|
|
|
*
|
|
|
|
|
* Also adds an optional `fromAbove` argument to indicate when properties
|
|
|
|
|
* are being flushed by a host during data propagation. This information
|
|
|
|
|
* is used to avoid sending upwards notification events in response to
|
|
|
|
|
* downward data flow. This is a performance optimization, but also
|
|
|
|
|
* critical to avoid infinite looping when an object is notified, since
|
2016-12-19 17:58:33 -08:00
|
|
|
* the default implementation of `_shouldPropertyChange` always returns
|
2016-10-27 09:51:41 -07:00
|
|
|
* true for Objects, and without would result in a notify-propagate-notify
|
|
|
|
|
* loop.
|
|
|
|
|
*
|
|
|
|
|
* @param {boolean=} fromAbove When true, sets `this.__dataFromAbove` to
|
|
|
|
|
* `true` for the duration of the call to `_propertiesChanged`.
|
|
|
|
|
* @override
|
|
|
|
|
*/
|
2016-08-31 19:09:55 -07:00
|
|
|
_flushProperties(fromAbove) {
|
|
|
|
|
if (!this.__dataInitialized) {
|
2016-09-01 15:24:28 -07:00
|
|
|
this.ready();
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
|
|
|
|
if (this.__dataPending || this.__dataPendingClients) {
|
|
|
|
|
this.__dataFromAbove = fromAbove;
|
|
|
|
|
super._flushProperties();
|
2016-12-19 11:13:56 -08:00
|
|
|
if (!this.__dataCounter) {
|
|
|
|
|
// Clear temporary cache at end of turn
|
|
|
|
|
this.__dataTemp = {};
|
|
|
|
|
}
|
2016-08-31 19:09:55 -07:00
|
|
|
this.__dataFromAbove = false;
|
2016-07-07 10:50:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Polymer-specific lifecycle callback called the first time properties
|
|
|
|
|
* are being flushed. Prior to `ready`, all property sets through
|
|
|
|
|
* accessors are queued and their effects are flushed after this method
|
|
|
|
|
* returns.
|
|
|
|
|
*
|
|
|
|
|
* Users may override this function to implement behavior that is
|
|
|
|
|
* dependent on the element having its properties initialized, e.g.
|
|
|
|
|
* from defaults (initialized from `constructor`, `_initializeProperties`),
|
|
|
|
|
* `attributeChangedCallback`, or binding values propagated from host
|
2016-12-19 12:09:16 -08:00
|
|
|
* "binding effects". `super.ready()` must be called to ensure the
|
2016-10-27 09:51:41 -07:00
|
|
|
* data system becomes enabled.
|
|
|
|
|
*
|
|
|
|
|
* @public
|
|
|
|
|
*/
|
2016-09-08 11:47:49 -07:00
|
|
|
ready() {
|
|
|
|
|
this.__dataInitialized = true;
|
|
|
|
|
}
|
2016-07-07 10:50:34 -07:00
|
|
|
|
2016-10-27 09:51:41 -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.
|
|
|
|
|
*
|
|
|
|
|
* Note that for host data to be bound into the stamped DOM, the template
|
|
|
|
|
* must have been previously bound to the prototype via a call to
|
|
|
|
|
* `_bindTemplate`, which performs one-time template binding work.
|
|
|
|
|
*
|
|
|
|
|
* Note that this method currently only supports being called once per
|
|
|
|
|
* instance.
|
|
|
|
|
*
|
|
|
|
|
* @param {HTMLTemplateElement} template Template to stamp
|
|
|
|
|
* @return {DocumentFragment} Cloned template content
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2016-08-31 19:09:55 -07:00
|
|
|
_stampTemplate(template) {
|
|
|
|
|
let dom = super._stampTemplate(template);
|
|
|
|
|
let notes = (template._content || template.content)._notes;
|
2016-10-27 09:51:41 -07:00
|
|
|
setupBindings(this, dom, notes);
|
2016-08-31 19:09:55 -07:00
|
|
|
return dom;
|
2016-08-17 01:10:38 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Implements `PropertyAccessors`'s properties changed callback.
|
|
|
|
|
*
|
2016-12-09 11:44:22 -08:00
|
|
|
* Runs each class of effects for the batch of changed properties in
|
|
|
|
|
* a specific order (compute, propagate, reflect, observe, notify).
|
2016-10-27 09:51:41 -07:00
|
|
|
*
|
|
|
|
|
* @override
|
|
|
|
|
*/
|
2016-08-31 19:09:55 -07:00
|
|
|
_propertiesChanged(currentProps, changedProps, oldProps) {
|
2016-12-09 11:44:22 -08:00
|
|
|
// ----------------------------
|
|
|
|
|
// let c = Object.getOwnPropertyNames(changedProps || {});
|
|
|
|
|
// window.debug && console.group(this.localName + '#' + this.id + ': ' + c);
|
|
|
|
|
// if (window.debug) { debugger; }
|
|
|
|
|
// ----------------------------
|
|
|
|
|
let fromAbove = this.__dataFromAbove;
|
|
|
|
|
// Compute properties
|
|
|
|
|
let computedProps = runComputedEffects(this, changedProps, oldProps);
|
|
|
|
|
// Compute linked paths
|
|
|
|
|
computedProps = computeLinkedPaths(this, changedProps, computedProps);
|
|
|
|
|
// Propagate properties to clients
|
|
|
|
|
runEffects(this, TYPES.PROPAGATE, changedProps);
|
|
|
|
|
// Flush clients
|
|
|
|
|
this._flushClients();
|
|
|
|
|
// Reflect properties
|
|
|
|
|
runEffects(this, TYPES.REFLECT, changedProps, oldProps);
|
|
|
|
|
// Observe properties
|
|
|
|
|
runEffects(this, TYPES.OBSERVE, changedProps, oldProps);
|
|
|
|
|
// Notify properties to host
|
|
|
|
|
runNotifyEffects(this, fromAbove ? computedProps : changedProps, oldProps);
|
|
|
|
|
// ----------------------------
|
|
|
|
|
// window.debug && console.groupEnd(this.localName + '#' + this.id + ': ' + c);
|
|
|
|
|
// ----------------------------
|
2016-08-17 01:10:38 -07:00
|
|
|
}
|
2016-08-15 19:17:29 -07:00
|
|
|
|
2016-08-31 19:09:55 -07:00
|
|
|
/**
|
|
|
|
|
* Aliases one data path as another, such that path notifications from one
|
|
|
|
|
* are routed to the other.
|
|
|
|
|
*
|
2016-09-01 15:24:28 -07:00
|
|
|
* @method linkPaths
|
2016-08-31 19:09:55 -07:00
|
|
|
* @param {string} to Target path to link.
|
|
|
|
|
* @param {string} from Source path to link.
|
2016-10-27 09:51:41 -07:00
|
|
|
* @public
|
2016-08-31 19:09:55 -07:00
|
|
|
*/
|
2016-09-01 15:24:28 -07:00
|
|
|
linkPaths(to, from) {
|
2016-09-22 19:12:21 -07:00
|
|
|
to = Polymer.Path.normalize(to);
|
|
|
|
|
from = Polymer.Path.normalize(from);
|
2016-08-31 19:09:55 -07:00
|
|
|
this.__dataLinkedPaths = this.__dataLinkedPaths || {};
|
2016-12-20 16:21:35 -08:00
|
|
|
this.__dataLinkedPaths[to] = from;
|
2016-08-15 19:17:29 -07:00
|
|
|
}
|
|
|
|
|
|
2016-08-31 19:09:55 -07:00
|
|
|
/**
|
|
|
|
|
* Removes a data path alias previously established with `_linkPaths`.
|
|
|
|
|
*
|
|
|
|
|
* Note, the path to unlink should be the target (`to`) used when
|
|
|
|
|
* linking the paths.
|
|
|
|
|
*
|
2016-09-01 15:24:28 -07:00
|
|
|
* @method unlinkPaths
|
2016-08-31 19:09:55 -07:00
|
|
|
* @param {string} path Target path to unlink.
|
2016-10-27 09:51:41 -07:00
|
|
|
* @public
|
2016-08-31 19:09:55 -07:00
|
|
|
*/
|
2016-09-01 15:24:28 -07:00
|
|
|
unlinkPaths(path) {
|
2016-09-22 19:12:21 -07:00
|
|
|
path = Polymer.Path.normalize(path);
|
2016-08-31 19:09:55 -07:00
|
|
|
if (this.__dataLinkedPaths) {
|
|
|
|
|
delete this.__dataLinkedPaths[path];
|
|
|
|
|
}
|
2016-08-15 19:17:29 -07:00
|
|
|
}
|
|
|
|
|
|
2016-08-31 19:09:55 -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'});
|
2016-09-01 15:24:28 -07:00
|
|
|
* this.notifySplices('items', [
|
2016-08-31 19:09:55 -07:00
|
|
|
* { index: 1, removed: [{name: 'Todd'}], addedCount: 1, obect: 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).
|
2016-10-27 09:51:41 -07:00
|
|
|
* @public
|
2016-08-31 19:09:55 -07:00
|
|
|
*/
|
2016-09-01 15:24:28 -07:00
|
|
|
notifySplices(path, splices) {
|
2016-08-31 19:09:55 -07:00
|
|
|
let info = {};
|
|
|
|
|
let array = Polymer.Path.get(this, path, info);
|
2016-09-19 15:19:22 -07:00
|
|
|
notifySplices(this, array, info.path, splices);
|
2016-08-15 19:17:29 -07:00
|
|
|
}
|
2016-08-31 19:09:55 -07:00
|
|
|
|
2016-09-01 12:44:48 -07:00
|
|
|
/**
|
2016-10-27 09:51:41 -07:00
|
|
|
* Convenience method for reading a value from a path.
|
2016-09-01 12:44:48 -07:00
|
|
|
*
|
|
|
|
|
* Note, if any part in the path is undefined, this method returns
|
|
|
|
|
* `undefined` (this method does not throw when dereferencing undefined
|
|
|
|
|
* paths).
|
|
|
|
|
*
|
|
|
|
|
* @method get
|
|
|
|
|
* @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.
|
2016-10-27 09:51:41 -07:00
|
|
|
* @public
|
2016-09-01 12:44:48 -07:00
|
|
|
*/
|
|
|
|
|
get(path, root) {
|
|
|
|
|
return Polymer.Path.get(root || this, path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-10-27 09:51:41 -07:00
|
|
|
* Convenience method for setting a value to a path and notifying any
|
2016-09-01 12:44:48 -07:00
|
|
|
* 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).
|
|
|
|
|
*
|
|
|
|
|
* @method set
|
|
|
|
|
* @param {(string|Array<(string|number)>)} path Path to the value
|
2016-09-22 19:12:21 -07:00
|
|
|
* to write. The path may be specified as a string (e.g. `'foo.bar.baz'`)
|
2016-09-01 12:44:48 -07:00
|
|
|
* 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
|
2016-09-22 19:12:21 -07:00
|
|
|
* (e.g. `'users.12.name'` or `['users', 12, 'name']`).
|
2016-09-01 12:44:48 -07:00
|
|
|
* @param {*} value Value to set at the specified path.
|
2016-09-22 18:05:26 -07:00
|
|
|
* @param {Object=} root Root object from which the path is evaluated.
|
|
|
|
|
* When specified, no notification will occur.
|
2016-10-27 09:51:41 -07:00
|
|
|
* @public
|
2016-09-01 12:44:48 -07:00
|
|
|
*/
|
2016-09-22 18:05:26 -07:00
|
|
|
set(path, value, root) {
|
|
|
|
|
if (root) {
|
|
|
|
|
Polymer.Path.set(root, path, value);
|
|
|
|
|
} else {
|
|
|
|
|
if (!this._hasReadOnlyEffect(path)) {
|
|
|
|
|
if ((path = this._setPathOrUnmanagedProperty(path, value))) {
|
|
|
|
|
this._setProperty(path, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-01 12:44:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-31 19:09:55 -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.
|
|
|
|
|
*
|
2016-09-01 12:44:48 -07:00
|
|
|
* @method push
|
2016-08-31 19:09:55 -07:00
|
|
|
* @param {String} path Path to array.
|
|
|
|
|
* @param {...any} var_args Items to push onto array
|
|
|
|
|
* @return {number} New length of the array.
|
2016-10-27 09:51:41 -07:00
|
|
|
* @public
|
2016-08-31 19:09:55 -07:00
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
push(path, ...items) {
|
2016-08-31 19:09:55 -07:00
|
|
|
let info = {};
|
|
|
|
|
let array = Polymer.Path.get(this, path, info);
|
2016-09-19 15:19:22 -07:00
|
|
|
let len = array.length;
|
2016-08-31 19:09:55 -07:00
|
|
|
let ret = array.push(...items);
|
|
|
|
|
if (items.length) {
|
2016-09-19 15:19:22 -07:00
|
|
|
notifySplice(this, array, info.path, len, items.length, []);
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
|
|
|
|
return ret;
|
2016-08-15 19:17:29 -07:00
|
|
|
}
|
|
|
|
|
|
2016-08-31 19:09:55 -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.
|
|
|
|
|
*
|
2016-09-01 12:44:48 -07:00
|
|
|
* @method pop
|
2016-08-31 19:09:55 -07:00
|
|
|
* @param {String} path Path to array.
|
|
|
|
|
* @return {any} Item that was removed.
|
2016-10-27 09:51:41 -07:00
|
|
|
* @public
|
2016-08-31 19:09:55 -07:00
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
pop(path) {
|
2016-08-31 19:09:55 -07:00
|
|
|
let info = {};
|
|
|
|
|
let array = Polymer.Path.get(this, path, info);
|
|
|
|
|
let hadLength = Boolean(array.length);
|
|
|
|
|
let ret = array.pop();
|
|
|
|
|
if (hadLength) {
|
2016-09-19 15:19:22 -07:00
|
|
|
notifySplice(this, array, info.path, array.length, 0, [ret]);
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
|
|
|
|
return ret;
|
2016-08-15 19:17:29 -07:00
|
|
|
}
|
|
|
|
|
|
2016-08-31 19:09:55 -07:00
|
|
|
/**
|
|
|
|
|
* Starting from the start index specified, removes 0 or more items
|
2016-10-27 09:51:41 -07:00
|
|
|
* from the array and inserts 0 or more new items in their place.
|
2016-08-31 19:09:55 -07:00
|
|
|
*
|
|
|
|
|
* 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.
|
|
|
|
|
*
|
2016-09-01 12:44:48 -07:00
|
|
|
* @method splice
|
2016-08-31 19:09:55 -07:00
|
|
|
* @param {String} path Path to array.
|
|
|
|
|
* @param {number} start Index from which to start removing/inserting.
|
|
|
|
|
* @param {number} deleteCount Number of items to remove.
|
|
|
|
|
* @param {...any} var_args Items to insert into array.
|
|
|
|
|
* @return {Array} Array of removed items.
|
2016-10-27 09:51:41 -07:00
|
|
|
* @public
|
2016-08-31 19:09:55 -07:00
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
splice(path, start, deleteCount, ...items) {
|
2016-08-31 19:09:55 -07:00
|
|
|
let info = {};
|
|
|
|
|
let array = Polymer.Path.get(this, path, info);
|
2016-09-19 15:19:22 -07:00
|
|
|
// Normalize fancy native splice handling of crazy start values
|
|
|
|
|
if (start < 0) {
|
|
|
|
|
start = array.length - Math.floor(-start);
|
|
|
|
|
} else {
|
|
|
|
|
start = Math.floor(start);
|
|
|
|
|
}
|
|
|
|
|
if (!start) {
|
|
|
|
|
start = 0;
|
|
|
|
|
}
|
2016-08-31 19:09:55 -07:00
|
|
|
let ret = array.splice(start, deleteCount, ...items);
|
|
|
|
|
if (items.length || ret.length) {
|
2016-09-19 15:19:22 -07:00
|
|
|
notifySplice(this, array, info.path, start, items.length, ret);
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
|
|
|
|
return ret;
|
2016-08-15 19:17:29 -07:00
|
|
|
}
|
|
|
|
|
|
2016-08-31 19:09:55 -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.
|
|
|
|
|
*
|
2016-09-01 12:44:48 -07:00
|
|
|
* @method shift
|
2016-08-31 19:09:55 -07:00
|
|
|
* @param {String} path Path to array.
|
|
|
|
|
* @return {any} Item that was removed.
|
2016-10-27 09:51:41 -07:00
|
|
|
* @public
|
2016-08-31 19:09:55 -07:00
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
shift(path) {
|
2016-08-31 19:09:55 -07:00
|
|
|
let info = {};
|
|
|
|
|
let array = Polymer.Path.get(this, path, info);
|
|
|
|
|
let hadLength = Boolean(array.length);
|
|
|
|
|
let ret = array.shift();
|
|
|
|
|
if (hadLength) {
|
2016-09-19 15:19:22 -07:00
|
|
|
notifySplice(this, array, info.path, 0, 0, [ret]);
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
|
2016-08-31 19:09:55 -07:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*
|
2016-09-01 12:44:48 -07:00
|
|
|
* @method unshift
|
2016-08-31 19:09:55 -07:00
|
|
|
* @param {String} path Path to array.
|
|
|
|
|
* @param {...any} var_args Items to insert info array
|
|
|
|
|
* @return {number} New length of the array.
|
2016-10-27 09:51:41 -07:00
|
|
|
* @public
|
2016-08-31 19:09:55 -07:00
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
unshift(path, ...items) {
|
2016-08-31 19:09:55 -07:00
|
|
|
let info = {};
|
|
|
|
|
let array = Polymer.Path.get(this, path, info);
|
|
|
|
|
let ret = array.unshift(...items);
|
|
|
|
|
if (items.length) {
|
2016-09-19 15:19:22 -07:00
|
|
|
notifySplice(this, array, info.path, 0, items.length, []);
|
2016-07-07 10:50:34 -07:00
|
|
|
}
|
2016-08-31 19:09:55 -07:00
|
|
|
return ret;
|
2016-07-07 10:50:34 -07:00
|
|
|
}
|
|
|
|
|
|
2016-09-22 19:12:21 -07:00
|
|
|
/**
|
|
|
|
|
* 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).
|
2016-10-27 09:51:41 -07:00
|
|
|
* @public
|
2016-09-22 19:12:21 -07:00
|
|
|
*/
|
2016-09-01 15:24:28 -07:00
|
|
|
notifyPath(path, value) {
|
2016-09-14 18:52:47 -07:00
|
|
|
if (arguments.length == 1) {
|
2016-09-22 19:12:21 -07:00
|
|
|
// Get value if not supplied
|
|
|
|
|
let info = {};
|
|
|
|
|
value = Polymer.Path.get(this, path, info);
|
|
|
|
|
path = info.path;
|
|
|
|
|
} else if (Array.isArray(path)) {
|
|
|
|
|
// Normalize path if needed
|
|
|
|
|
path = Polymer.Path.normalize(path);
|
2016-09-14 18:52:47 -07:00
|
|
|
}
|
2016-09-01 15:24:28 -07:00
|
|
|
this._setProperty(path, value);
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-27 09:51:41 -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`.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
|
|
|
|
_createReadOnlyProperty(property, protectedSetter) {
|
2016-09-01 15:24:28 -07:00
|
|
|
this._addPropertyEffect(property, TYPES.READ_ONLY);
|
2016-10-27 09:51:41 -07:00
|
|
|
if (protectedSetter) {
|
2016-09-01 12:44:48 -07:00
|
|
|
this['_set' + upper(property)] = function(value) {
|
|
|
|
|
this._setProperty(property, value);
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
2016-02-19 10:23:22 -08:00
|
|
|
}
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
2016-02-19 10:23:22 -08:00
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
|
|
|
|
* Creates a single-property observer for the given property.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} property Property name
|
|
|
|
|
* @param {string} methodName Name of observer method to call
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
|
|
|
|
_createObservedProperty(property, methodName) {
|
2016-09-01 15:24:28 -07:00
|
|
|
this._addPropertyEffect(property, TYPES.OBSERVE, {
|
2016-09-01 12:44:48 -07:00
|
|
|
fn: runObserverEffect,
|
2016-08-31 19:09:55 -07:00
|
|
|
info: {
|
2016-10-27 09:51:41 -07:00
|
|
|
methodName: methodName
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
|
2016-10-27 09:51:41 -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
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
|
|
|
|
_createMethodObserver(expression) {
|
|
|
|
|
let sig = parseMethod(expression);
|
|
|
|
|
if (!sig) {
|
|
|
|
|
throw new Error("Malformed observer expression '" + expression + "'");
|
|
|
|
|
}
|
|
|
|
|
createMethodEffect(this, sig, TYPES.OBSERVE, runMethodObserverEffect);
|
|
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
|
2016-10-27 09:51:41 -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
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
_createNotifyingProperty(property) {
|
|
|
|
|
this._addPropertyEffect(property, TYPES.NOTIFY, {
|
|
|
|
|
fn: runNotifyEffect,
|
2016-07-11 14:39:54 -07:00
|
|
|
info: {
|
2016-08-31 19:09:55 -07:00
|
|
|
eventName: CaseMap.camelToDashCase(property) + '-changed',
|
|
|
|
|
property: property
|
2016-07-11 14:39:54 -07:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
|
2016-10-27 09:51:41 -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
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
_createReflectedProperty(property) {
|
2016-08-31 19:09:55 -07:00
|
|
|
let attr = CaseMap.camelToDashCase(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 thisead.');
|
|
|
|
|
} else {
|
2016-09-01 12:44:48 -07:00
|
|
|
this._addPropertyEffect(property, TYPES.REFLECT, {
|
|
|
|
|
fn: runReflectEffect,
|
2016-08-31 19:09:55 -07:00
|
|
|
info: {
|
|
|
|
|
attrName: attr
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2016-08-05 11:14:38 -07:00
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
|
2016-10-27 09:51:41 -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
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
_createComputedProperty(property, expression) {
|
|
|
|
|
let sig = parseMethod(expression);
|
2016-08-31 19:09:55 -07:00
|
|
|
if (!sig) {
|
|
|
|
|
throw new Error("Malformed computed expression '" + expression + "'");
|
2016-02-19 18:38:04 -08:00
|
|
|
}
|
2016-12-09 11:44:22 -08:00
|
|
|
createMethodEffect(this, sig, TYPES.COMPUTE, runComputedEffect, property);
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
|
|
|
|
|
2016-12-19 12:09:16 -08:00
|
|
|
// -- binding ----------------------------------------------
|
2016-08-31 19:09:55 -07:00
|
|
|
|
2016-10-27 09:51:41 -07:00
|
|
|
/**
|
2016-12-19 12:09:16 -08:00
|
|
|
* Creates "binding" property effects for all binding bindings
|
2016-10-27 09:51:41 -07:00
|
|
|
* in the provided template that forward host properties into DOM stamped
|
|
|
|
|
* from the template via `_stampTemplate`.
|
|
|
|
|
*
|
|
|
|
|
* @param {HTMLTemplateElement} template Template containing binding
|
2016-12-19 12:09:16 -08:00
|
|
|
* bindings
|
2016-10-27 09:51:41 -07:00
|
|
|
* @protected
|
|
|
|
|
*/
|
2016-09-01 12:44:48 -07:00
|
|
|
_bindTemplate(template) {
|
2016-10-27 09:51:41 -07:00
|
|
|
// Clear any existing propagation effects inherited from superClass
|
|
|
|
|
this[TYPES.PROPAGATE] = {};
|
|
|
|
|
let notes = this._parseTemplateAnnotations(template);
|
|
|
|
|
processAnnotations(notes);
|
|
|
|
|
for (let i=0, note; (i<notes.length) && (note=notes[i]); i++) {
|
|
|
|
|
// where to find the node in the concretized list
|
|
|
|
|
let b$ = note.bindings;
|
|
|
|
|
for (let j=0, binding; (j<b$.length) && (binding=b$[j]); j++) {
|
|
|
|
|
if (shouldAddListener(binding)) {
|
|
|
|
|
addAnnotatedListener(this, i, binding.name,
|
|
|
|
|
binding.parts[0].value,
|
|
|
|
|
binding.parts[0].event,
|
|
|
|
|
binding.parts[0].negate);
|
|
|
|
|
}
|
2016-12-19 12:09:16 -08:00
|
|
|
addBindingEffect(this, binding, i);
|
2016-10-27 09:51:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
2016-02-19 18:38:04 -08:00
|
|
|
}
|
|
|
|
|
|
2016-08-31 19:09:55 -07:00
|
|
|
}
|
2016-07-07 12:59:31 -07:00
|
|
|
|
2016-08-31 19:09:55 -07:00
|
|
|
});
|
2016-02-19 10:23:22 -08:00
|
|
|
|
|
|
|
|
})();
|
|
|
|
|
</script>
|