mirror of
https://github.com/Polymer/polymer.git
synced 2025-02-25 18:55:30 -06:00
2247 lines
80 KiB
HTML
2247 lines
80 KiB
HTML
<!--
|
|
@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
|
|
-->
|
|
|
|
|
|
<link rel="import" href="../utils/boot.html">
|
|
<link rel="import" href="../utils/mixin.html">
|
|
<link rel="import" href="../utils/path.html">
|
|
<!-- for notify, reflect -->
|
|
<link rel="import" href="../utils/case-map.html">
|
|
<link rel="import" href="property-accessors.html">
|
|
<!-- for annotated effects -->
|
|
<link rel="import" href="template-stamp.html">
|
|
|
|
|
|
<script>
|
|
(function() {
|
|
|
|
'use strict';
|
|
|
|
const CaseMap = Polymer.CaseMap;
|
|
const mixin = Polymer.mixin;
|
|
|
|
// For tracking nested clients that will require flushing
|
|
let hostStack = {
|
|
|
|
stack: [],
|
|
|
|
isEmpty() {
|
|
return !this.stack.length;
|
|
},
|
|
|
|
registerHost(inst) {
|
|
if (this.stack.length) {
|
|
let host = this.stack[this.stack.length-1];
|
|
host._enqueueClient(inst);
|
|
}
|
|
},
|
|
|
|
beginHosting(inst) {
|
|
this.stack.push(inst);
|
|
},
|
|
|
|
endHosting(inst) {
|
|
let stackLen = this.stack.length;
|
|
if (stackLen && this.stack[stackLen-1] == inst) {
|
|
this.stack.pop();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Monotonically increasing unique ID used for de-duping effects triggered
|
|
// from multiple properties in the same turn
|
|
let dedupeId = 0;
|
|
|
|
// Property effect types; effects are stored on the prototype using these keys
|
|
const TYPES = {
|
|
ANY: '__propertyEffects',
|
|
COMPUTE: '__computeEffects',
|
|
REFLECT: '__reflectEffects',
|
|
NOTIFY: '__notifyEffects',
|
|
PROPAGATE: '__propagateEffects',
|
|
OBSERVE: '__observeEffects',
|
|
READ_ONLY: '__readOnly'
|
|
}
|
|
|
|
/**
|
|
* Ensures that the model has an own-property map of effects for the given type.
|
|
* The model may be a prototype or an instance.
|
|
*
|
|
* Property effects are stored as arrays of effects by property in a map,
|
|
* by named type on the model. e.g.
|
|
*
|
|
* __computeEffects: {
|
|
* foo: [ ... ],
|
|
* bar: [ ... ]
|
|
* }
|
|
*
|
|
* If the model does not yet have an effect map for the type, one is created
|
|
* and returned. If it does, but it is not an own property (i.e. the
|
|
* prototype had effects), the the map is deeply cloned and the copy is
|
|
* set on the model and returned, ready for new effects to be added.
|
|
*
|
|
* @param {Object} model Prototype or instance
|
|
* @param {string} type Property effect type
|
|
* @return {Object} The own-property map of effects for the given type
|
|
* @private
|
|
*/
|
|
function ensureOwnEffectMap(model, type) {
|
|
let effects = model[type];
|
|
if (!effects) {
|
|
effects = model[type] = {};
|
|
} else if (!model.hasOwnProperty(type)) {
|
|
effects = model[type] = Object.create(model[type]);
|
|
for (let p in effects) {
|
|
let protoFx = effects[p];
|
|
let instFx = effects[p] = Array(protoFx.length);
|
|
for (let i=0; i<protoFx.length; i++) {
|
|
instFx[i] = protoFx[i];
|
|
}
|
|
}
|
|
}
|
|
return effects;
|
|
}
|
|
|
|
// -- effects ----------------------------------------------
|
|
|
|
/**
|
|
* Runs all effects of a given type for the given set of property changes
|
|
* on an instance.
|
|
*
|
|
* @param {Object} inst The instance with effects to run
|
|
* @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
|
|
* @private
|
|
*/
|
|
function runEffects(inst, effects, props, oldProps, hasPaths) {
|
|
if (effects) {
|
|
let ran;
|
|
let id = dedupeId++;
|
|
for (let prop in props) {
|
|
if (runEffectsForProperty(inst, effects, id, prop, props, oldProps, hasPaths)) {
|
|
ran = true;
|
|
}
|
|
}
|
|
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, dedupeId, prop, props, oldProps, hasPaths) {
|
|
let ran;
|
|
let rootProperty = hasPaths ? Polymer.Path.root(prop) : prop;
|
|
let fxs = effects[rootProperty];
|
|
if (fxs) {
|
|
for (let i=0, l=fxs.length, fx; (i<l) && (fx=fxs[i]); i++) {
|
|
if ((!fx.info || fx.info.lastRun !== dedupeId) &&
|
|
(!hasPaths || pathMatchesTrigger(prop, fx.trigger))) {
|
|
fx.fn(inst, prop, props, oldProps, fx.info, hasPaths);
|
|
if (fx.info) {
|
|
fx.info.lastRun = dedupeId;
|
|
}
|
|
ran = true;
|
|
}
|
|
}
|
|
}
|
|
return ran;
|
|
}
|
|
|
|
/**
|
|
* Determines whether a property/path that has changed matches the trigger
|
|
* criteria for an effect. A trigger is a descriptor with the following
|
|
* structure, which matches the descriptors returned from `parseArg`.
|
|
* e.g. for `foo.bar.*`:
|
|
* ```
|
|
* trigger: {
|
|
* name: 'a.b',
|
|
* structured: true,
|
|
* wildcard: true
|
|
* }
|
|
* ```
|
|
* If no trigger is given, the path is deemed to match.
|
|
*
|
|
* @param {string} path Path or property that changed
|
|
* @param {Object} trigger Descriptor
|
|
* @return {boolean} Whether the path matched the trigger
|
|
*/
|
|
function pathMatchesTrigger(path, trigger) {
|
|
if (trigger) {
|
|
let triggerPath = trigger.name;
|
|
return (triggerPath == path) ||
|
|
(trigger.structured && Polymer.Path.isAncestor(triggerPath, path)) ||
|
|
(trigger.wildcard && Polymer.Path.isDescendant(triggerPath, path));
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the "observer" effect.
|
|
*
|
|
* Calls the method with `info.methodName` on the instance, passing the
|
|
* new and old values.
|
|
*
|
|
* @param {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 runObserverEffect(inst, property, props, oldProps, info) {
|
|
let fn = inst[info.methodName];
|
|
let changedProp = info.property;
|
|
if (fn) {
|
|
fn.call(inst, inst.__data[changedProp], oldProps[changedProp]);
|
|
} else {
|
|
console.warn('observer method `' + info.methodName + '` not defined');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs "notify" effects for a set of changed properties.
|
|
*
|
|
* This method differs from the generic `runEffects` method in that it
|
|
* will dispatch path notification events in the case that the property
|
|
* changed was a path and the root property for that path didn't have a
|
|
* "notify" effect. This is to maintain 1.0 behavior that did not require
|
|
* `notify: true` to ensure object sub-property notifications were
|
|
* sent.
|
|
*
|
|
* @param {Element} 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, notifyProps, props, oldProps, hasPaths) {
|
|
// Notify
|
|
let fxs = inst.__notifyEffects;
|
|
let notified;
|
|
let id = dedupeId++;
|
|
// Try normal notify effects; if none, fall back to try path notification
|
|
for (let prop in notifyProps) {
|
|
if (notifyProps[prop]) {
|
|
if (fxs && runEffectsForProperty(inst, fxs, id, prop, props, oldProps, hasPaths)) {
|
|
notified = true;
|
|
} else if (hasPaths && notifyPath(inst, prop, props)) {
|
|
notified = true;
|
|
}
|
|
}
|
|
}
|
|
// Flush host if we actually notified and host was batching
|
|
let host;
|
|
if (notified && (host = inst.__dataHost) && host._flushProperties) {
|
|
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
|
|
* @param {*} value
|
|
* @private
|
|
*/
|
|
function notifyPath(inst, path, props) {
|
|
let rootProperty = Polymer.Path.root(path);
|
|
if (rootProperty !== path) {
|
|
let eventName = Polymer.CaseMap.camelToDashCase(rootProperty) + '-changed';
|
|
dispatchNotifyEvent(inst, eventName, props[path], path);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 | null | undefined} 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 }));
|
|
}
|
|
|
|
/**
|
|
* Implements the "notify" effect.
|
|
*
|
|
* Dispatches a non-bubbling event named `info.eventName` on the instance
|
|
* with a detail object containing the new `value`.
|
|
*
|
|
* @param {Element} 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 runNotifyEffect(inst, property, props, oldProps, info, hasPaths) {
|
|
let rootProperty = hasPaths ? Polymer.Path.root(property) : property;
|
|
let path = rootProperty != property ? property : null;
|
|
let value = path ? Polymer.Path.get(inst, path) : inst.__data[property];
|
|
if (path && value === undefined) {
|
|
value = props[property]; // specifically for .splices
|
|
}
|
|
dispatchNotifyEvent(inst, info.eventName, value, path);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* `__notifyListeners` metadata.
|
|
*
|
|
* Sets the value of the notified property to the host property or path. If
|
|
* the event contained path information, translate that path to the host
|
|
* scope's name for that path first.
|
|
*
|
|
* @param {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.__readOnly || !inst.__readOnly[path]) {
|
|
inst._setPendingPropertyOrPath(path, value, true, Boolean(detail.path));
|
|
}
|
|
} else {
|
|
inst.set(path, value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function runReflectEffect(inst, property, props, oldProps, info) {
|
|
let value = inst.__data[property];
|
|
if (Polymer.sanitizeDOMValue) {
|
|
value = Polymer.sanitizeDOMValue(value, info.attrName, 'attribute', inst);
|
|
}
|
|
inst._propertyToAttribute(property, info.attrName, value);
|
|
}
|
|
|
|
/**
|
|
* Runs "computed" effects for a set of changed properties.
|
|
*
|
|
* This method differs from the generic `runEffects` method in that it
|
|
* continues to run computed effects based on the output of each pass until
|
|
* there are no more newly computed properties. This ensures that all
|
|
* properties that will be computed by the initial set of changes are
|
|
* computed before other effects (binding propagation, observers, and notify)
|
|
* run.
|
|
*
|
|
* @param {Element} 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
|
|
* @private
|
|
*/
|
|
function runComputedEffects(inst, changedProps, oldProps, hasPaths) {
|
|
let computeEffects = inst.__computeEffects;
|
|
if (computeEffects) {
|
|
let inputProps = changedProps;
|
|
while (runEffects(inst, computeEffects, inputProps, oldProps, hasPaths)) {
|
|
mixin(oldProps, inst.__dataOld);
|
|
mixin(changedProps, inst.__dataPending);
|
|
inputProps = inst.__dataPending;
|
|
inst.__dataPending = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the "computed property" effect by running the method with the
|
|
* values of the arguments specified in the `info` object and setting the
|
|
* return value to the computed property specified.
|
|
*
|
|
* @param {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 runComputedEffect(inst, property, props, oldProps, info) {
|
|
var result = runMethodEffect(inst, property, props, oldProps, info);
|
|
var computedProp = info.methodInfo;
|
|
if (inst.__propertyEffects && inst.__propertyEffects[computedProp]) {
|
|
inst._setPendingProperty(computedProp, result, true);
|
|
} else {
|
|
inst[computedProp] = result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computes path changes based on path links set up using the `linkPaths`
|
|
* API.
|
|
*
|
|
* @param {Element} inst The instance whose props are changing
|
|
* @param {Object} changedProps Bag of changed properties
|
|
* @private
|
|
*/
|
|
function computeLinkedPaths(inst, changedProps, hasPaths) {
|
|
let links;
|
|
if (hasPaths && (links = inst.__dataLinkedPaths)) {
|
|
const cache = inst.__dataTemp;
|
|
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);
|
|
cache[link] = changedProps[link] = changedProps[path];
|
|
let notifyProps = inst.__dataToNotify || (inst.__dataToNotify = {});
|
|
notifyProps[link] = true;
|
|
} else if (Polymer.Path.isDescendant(b, path)) {
|
|
link = Polymer.Path.translate(b, a, path);
|
|
cache[link] = changedProps[link] = changedProps[path];
|
|
let notifyProps = inst.__dataToNotify || (inst.__dataToNotify = {});
|
|
notifyProps[link] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- bindings ----------------------------------------------
|
|
|
|
/**
|
|
* Adds "binding" property effects for the template annotation
|
|
* ("note" for short) and node index specified. These may either be normal
|
|
* "binding" effects (property/path bindings) or "method binding"
|
|
* 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 `_templateNodes` list of annotated nodes that the
|
|
* note applies to
|
|
* @param {Object=} dynamicFns Map indicating whether method names should
|
|
* be included as a dependency to the effect.
|
|
* @private
|
|
*/
|
|
function addBindingEffect(model, note, index, dynamicFns) {
|
|
for (let i=0; i<note.parts.length; i++) {
|
|
let part = note.parts[i];
|
|
if (part.signature) {
|
|
addMethodBindingEffect(model, note, part, index, dynamicFns);
|
|
} 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 {
|
|
model._addPropertyEffect(Polymer.Path.root(part.value), TYPES.PROPAGATE, {
|
|
fn: runBindingEffect,
|
|
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
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the "binding" (property/path binding) effect.
|
|
*
|
|
* @param {Element} inst The instance the effect will be run on
|
|
* @param {string} path Name of property
|
|
* @param {*} value Current value of property
|
|
* @param {*} old Previous value of property
|
|
* @param {Object} info Effect metadata
|
|
* @private
|
|
*/
|
|
function runBindingEffect(inst, path, props, oldProps, info, hasPaths) {
|
|
let value;
|
|
let node = inst._templateNodes[info.index];
|
|
// Subpath notification: transform path and set to client
|
|
// e.g.: foo="{{obj.sub}}", path: 'obj.sub.prop', set 'foo.prop'=obj.sub.prop
|
|
if (hasPaths && (path.length > info.value.length) &&
|
|
(info.kind == 'property') && !info.isCompound &&
|
|
node.__propertyEffects && node.__propertyEffects[info.name]) {
|
|
let value = props[path];
|
|
path = Polymer.Path.translate(info.value, info.name, path);
|
|
if (node._setPendingPropertyOrPath(path, value, false, true)) {
|
|
inst._enqueueClient(node);
|
|
}
|
|
} 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) {
|
|
value = Polymer.Path.get(inst, info.value);
|
|
} else {
|
|
if (hasPaths && Polymer.Path.isPath(path)) {
|
|
value = Polymer.Path.get(inst, path);
|
|
} else {
|
|
value = inst.__data[path];
|
|
}
|
|
}
|
|
// Propagate value to child
|
|
applyBindingValue(inst, info, value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the value for an "binding" (binding) effect to a node,
|
|
* either as a property or attribute.
|
|
*
|
|
* @param {Object} inst The instance owning the binding effect
|
|
* @param {Object} info Effect metadata
|
|
* @param {*} value Value to set
|
|
* @private
|
|
*/
|
|
function applyBindingValue(inst, info, value) {
|
|
let node = inst._templateNodes[info.index];
|
|
value = computeBindingValue(node, value, info);
|
|
if (Polymer.sanitizeDOMValue) {
|
|
value = Polymer.sanitizeDOMValue(value, info.name, info.kind, node);
|
|
}
|
|
if (info.kind == 'attribute') {
|
|
// Attribute binding
|
|
inst._valueToNodeAttribute(node, value, info.name);
|
|
} else {
|
|
// Property binding
|
|
let prop = info.name;
|
|
if (node.__propertyEffects && node.__propertyEffects[prop]) {
|
|
if (!node.__readOnly || !node.__readOnly[prop]) {
|
|
if (node._setPendingProperty(prop, value)) {
|
|
inst._enqueueClient(node);
|
|
}
|
|
}
|
|
} else {
|
|
inst._setUnmanagedPropertyToNode(node, prop, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transforms an "binding" effect value based on compound & negation
|
|
* effect metadata, as well as handling for special-case properties
|
|
*
|
|
* @param {Node} node Node the value will be set to
|
|
* @param {*} value Value to set
|
|
* @param {Object} info Effect metadata
|
|
* @return {*} Transformed value to set
|
|
* @private
|
|
*/
|
|
function computeBindingValue(node, value, info) {
|
|
if (info.negate) {
|
|
value = !value;
|
|
}
|
|
if (info.isCompound) {
|
|
let storage = node.__dataCompoundStorage[info.name];
|
|
storage[info.compoundIndex] = value;
|
|
value = storage.join('');
|
|
}
|
|
if (info.kind !== 'attribute') {
|
|
// Some browsers serialize `undefined` to `"undefined"`
|
|
if (info.name === 'textContent' ||
|
|
(node.localName == 'input' && info.name == 'value')) {
|
|
value = value == undefined ? '' : value;
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Adds "binding method" property effects for the template binding
|
|
* ("note" for short), part metadata, and node index specified.
|
|
*
|
|
* @param {Object} model Prototype or instance
|
|
* @param {Object} note Binding note returned from Annotator
|
|
* @param {Object} part The compound part metadata
|
|
* @param {number} index Index into `_templateNodes` list of annotated nodes that the
|
|
* note applies to
|
|
* @param {Object=} dynamicFns Map indicating whether method names should
|
|
* be included as a dependency to the effect.
|
|
* @private
|
|
*/
|
|
function addMethodBindingEffect(model, note, part, index, dynamicFns) {
|
|
createMethodEffect(model, part.signature, TYPES.PROPAGATE,
|
|
runMethodBindingEffect, {
|
|
index: index,
|
|
isCompound: note.isCompound,
|
|
compoundIndex: part.compoundIndex,
|
|
kind: note.kind,
|
|
name: note.name,
|
|
negate: part.negate,
|
|
part: part
|
|
}, dynamicFns
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Implements the "binding method" (inline computed function) effect.
|
|
*
|
|
* 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
|
|
*/
|
|
function runMethodBindingEffect(inst, property, props, oldProps, info) {
|
|
let val = runMethodEffect(inst, property, props, oldProps, info);
|
|
applyBindingValue(inst, info.methodInfo, val);
|
|
}
|
|
|
|
/**
|
|
* Post-processes template bindings (notes for short) provided by the
|
|
* Bindings library for use by the effects system:
|
|
* - 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
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Recurse into nested templates & bind host props
|
|
if (note.templateContent) {
|
|
processAnnotations(note.templateContent._notes);
|
|
let hostProps = note.templateContent._hostProps =
|
|
discoverTemplateHostProps(note.templateContent._notes);
|
|
let bindings = [];
|
|
for (let prop in hostProps) {
|
|
bindings.push({
|
|
index: note.index,
|
|
kind: 'property',
|
|
name: '_host_' + prop,
|
|
parts: [{
|
|
mode: '{',
|
|
value: prop
|
|
}]
|
|
});
|
|
}
|
|
note.bindings = note.bindings.concat(bindings);
|
|
}
|
|
}
|
|
notes._processed = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function discoverTemplateHostProps(notes) {
|
|
let hostProps = {};
|
|
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) {
|
|
hostProps[rootProperty] = true;
|
|
}
|
|
}
|
|
hostProps[p.signature.methodName] = true;
|
|
} else {
|
|
if (p.rootProperty) {
|
|
hostProps[p.rootProperty] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Merge child _hostProps into this _hostProps
|
|
if (n.templateContent) {
|
|
let templateHostProps = n.templateContent._hostProps;
|
|
Polymer.Base.mixin(hostProps, templateHostProps);
|
|
}
|
|
}
|
|
return hostProps;
|
|
}
|
|
|
|
/**
|
|
* 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 === '{';
|
|
}
|
|
|
|
/**
|
|
* Sets up a prototypical `__notifyListeners` 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 `_templateNodes` 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) {
|
|
let eventName = event ||
|
|
(CaseMap.camelToDashCase(property) + '-changed');
|
|
model.__notifyListeners = model.__notifyListeners || [];
|
|
model.__notifyListeners.push({
|
|
index: index,
|
|
property: property,
|
|
path: path,
|
|
event: eventName,
|
|
negate: negate
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adds all 2-way binding notification listeners to a host based on
|
|
* `__notifyListeners` metadata recorded by prior calls to`addAnnotatedListener`
|
|
*
|
|
* @param {Object} inst Host element instance
|
|
* @private
|
|
*/
|
|
function setupNotifyListeners(inst) {
|
|
let b$ = inst.__notifyListeners;
|
|
for (let i=0, l=b$.length, info; (i<l) && (info=b$[i]); i++) {
|
|
let node = inst._templateNodes[info.index];
|
|
addNotifyListener(node, inst, info);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* On the `inst` element that was previously bound, uses `inst._templateNotes`
|
|
* to setup compound binding storage structures onto the bound
|
|
* nodes (`inst._templateNodes`).
|
|
* (`inst._, and 2-way binding event listeners are also added.)
|
|
*
|
|
* @param {Object} inst Instance that bas been previously bound
|
|
* @private
|
|
*/
|
|
function setupBindings(inst) {
|
|
let notes = inst._templateNotes;
|
|
if (notes.length) {
|
|
for (let i=0; i < notes.length; i++) {
|
|
let note = notes[i];
|
|
let node = inst._templateNodes[i];
|
|
node.__dataHost = inst;
|
|
if (note.bindings) {
|
|
setupCompoundBinding(note, node);
|
|
}
|
|
}
|
|
}
|
|
if (inst.__notifyListeners) {
|
|
setupNotifyListeners(inst);
|
|
}
|
|
}
|
|
|
|
// -- for method-based effects (complexObserver & computed) --------------
|
|
|
|
/**
|
|
* Adds property effects for each argument in the method signature (and
|
|
* optionally, for the method name if `dynamic` is true) that calls the
|
|
* provided effect function.
|
|
*
|
|
* @param {Element | Object} model Prototype or instance
|
|
* @param {Object} sig Method signature metadata
|
|
* @param {string} type
|
|
* @param {Function} effectFn Function to run when arguments change
|
|
* @param {*=} methodInfo
|
|
* @param {Object=} dynamicFns Map indicating whether method names should
|
|
* be included as a dependency to the effect.
|
|
* @private
|
|
*/
|
|
function createMethodEffect(model, sig, type, effectFn, methodInfo, dynamicFns) {
|
|
let dynamicFn = sig.static || dynamicFns && dynamicFns[sig.methodName];
|
|
let info = {
|
|
methodName: sig.methodName,
|
|
args: sig.args,
|
|
methodInfo,
|
|
dynamicFn
|
|
};
|
|
for (let i=0, arg; (i<sig.args.length) && (arg=sig.args[i]); i++) {
|
|
if (!arg.literal) {
|
|
model._addPropertyEffect(arg.rootProperty, type, {
|
|
fn: effectFn, info: info, trigger: arg
|
|
});
|
|
}
|
|
}
|
|
if (dynamicFn) {
|
|
model._addPropertyEffect(sig.methodName, type, {
|
|
fn: effectFn, info: info
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls a method with arguments marshaled from properties on the instance
|
|
* based on the method signature contained in the effect metadata.
|
|
*
|
|
* Multi-property observers, computed properties, and inline computing
|
|
* functions call this function to invoke the method, then use the return
|
|
* value accordingly.
|
|
*
|
|
* @param {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 runMethodEffect(inst, property, props, oldProps, info) {
|
|
// Instances can optionally have a _methodHost which allows redirecting where
|
|
// to find methods. Currently used by `templatize`.
|
|
let context = inst._methodHost || inst;
|
|
let fn = context[info.methodName];
|
|
if (fn) {
|
|
let args = marshalArgs(inst.__data, info.args, property, props);
|
|
return fn.apply(context, args);
|
|
} else if (!info.dynamicFn) {
|
|
console.warn('method `' + info.methodName + '` not defined');
|
|
}
|
|
}
|
|
|
|
const emptyArray = [];
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Parses an array of arguments and sets the `args` property of the supplied
|
|
* signature metadata object. Sets the `static` property to false if any
|
|
* argument is a non-literal.
|
|
*
|
|
* @param {Array<string>} argList Array of argument names
|
|
* @param {Object} sig Method signature metadata object
|
|
* @return {Object} The updated signature metadata object
|
|
* @private
|
|
*/
|
|
function parseArgs(argList, sig) {
|
|
sig.args = argList.map(function(rawArg) {
|
|
let arg = parseArg(rawArg);
|
|
if (!arg.literal) {
|
|
sig.static = false;
|
|
}
|
|
return arg;
|
|
}, this);
|
|
return sig;
|
|
}
|
|
|
|
/**
|
|
* Parses an individual argument, and returns an argument metadata object
|
|
* with the following fields:
|
|
*
|
|
* {
|
|
* value: 'prop', // property/path or literal value
|
|
* literal: false, // whether argument is a literal
|
|
* structured: false, // whether the property is a path
|
|
* rootProperty: 'prop', // the root property of the path
|
|
* wildcard: false // whether the argument was a wildcard '.*' path
|
|
* }
|
|
*
|
|
* @param {string} rawArg The string value of the argument
|
|
* @return {Object} Argument metadata object
|
|
* @private
|
|
*/
|
|
function parseArg(rawArg) {
|
|
// clean up whitespace
|
|
let arg = rawArg.trim()
|
|
// replace comma entity with comma
|
|
.replace(/,/g, ',')
|
|
// repair extra escape sequences; note only commas strictly need
|
|
// escaping, but we allow any other char to be escaped since its
|
|
// likely users will do this
|
|
.replace(/\\(.)/g, '\$1')
|
|
;
|
|
// basic argument descriptor
|
|
let a = {
|
|
name: arg
|
|
};
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* 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, props) {
|
|
let values = [];
|
|
for (let i=0, l=args.length; i<l; i++) {
|
|
let arg = args[i];
|
|
let name = arg.name;
|
|
let v;
|
|
if (arg.literal) {
|
|
v = arg.value;
|
|
} else {
|
|
if (arg.structured) {
|
|
v = Polymer.Path.get(data, name);
|
|
// when data is not stored e.g. `splices`
|
|
if (v === undefined) {
|
|
v = props[name];
|
|
}
|
|
} else {
|
|
v = data[name];
|
|
}
|
|
}
|
|
if (arg.wildcard) {
|
|
// Only send the actual path changed info if the change that
|
|
// caused the observer to run matched the wildcard
|
|
let baseChanged = (name.indexOf(path + '.') === 0);
|
|
let matches = (path.indexOf(name) === 0 && !baseChanged);
|
|
values[i] = {
|
|
path: matches ? path : name,
|
|
value: matches ? props[path] : v,
|
|
base: v
|
|
};
|
|
} else {
|
|
values[i] = v;
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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') {
|
|
node[name] = binding.literal;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// data api
|
|
|
|
/**
|
|
* Sends array splice notifications (`.splices` and `.length`)
|
|
*
|
|
* Note: this implementation only accepts normalized paths
|
|
*
|
|
* @param {Element} 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
|
|
*/
|
|
function notifySplices(inst, array, path, splices) {
|
|
let splicesPath = path + '.splices';
|
|
inst.notifyPath(splicesPath, { indexSplices: splices });
|
|
inst.notifyPath(path + '.length', array.length);
|
|
// Null here to allow potentially large splice records to be GC'ed.
|
|
inst.__data[splicesPath] = {indexSplices: null};
|
|
}
|
|
|
|
/**
|
|
* Creates a splice record and sends an array splice notification for
|
|
* the described mutation
|
|
*
|
|
* Note: this implementation only accepts normalized paths
|
|
*
|
|
* @param {Element} 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) {
|
|
notifySplices(inst, array, path, [{
|
|
index: index,
|
|
addedCount: addedCount,
|
|
removed: removed,
|
|
object: array,
|
|
type: 'splice'
|
|
}]);
|
|
}
|
|
|
|
/**
|
|
* Returns an upper-cased version of the string.
|
|
*
|
|
* @param {string} name String to uppercase
|
|
* @return {string} Uppercased string
|
|
* @private
|
|
*/
|
|
function upper(name) {
|
|
return name[0].toUpperCase() + name.substring(1);
|
|
}
|
|
|
|
/**
|
|
* Sets the provided properties into pending data on the instance.
|
|
*
|
|
* @param {HTMLElement} inst Instance to apply data to
|
|
* @param {object} props Bag of instance properties to set
|
|
* @private
|
|
*/
|
|
function initalizeInstanceProperties(inst, props) {
|
|
inst.__dataOld = inst.__dataOld || {};
|
|
inst.__dataPending = inst.__dataPending || {};
|
|
let readOnly = inst.__readOnly;
|
|
for (let prop in props) {
|
|
if (!readOnly || !readOnly[prop]) {
|
|
inst.__data[prop] = inst.__dataPending[prop] = props[prop];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Element class mixin that provides meta-programming for Polymer's template
|
|
* binding and data observation (collectively, "property effects") system.
|
|
*
|
|
* This mixin uses provides the following key methods for adding property effects
|
|
* to this element:
|
|
* - `_createPropertyObserver`
|
|
* - `_createMethodObserver`
|
|
* - `_createNotifyingProperty`
|
|
* - `_createReadOnlyProperty`
|
|
* - `_createReflectedProperty`
|
|
* - `_createComputedProperty`
|
|
* - `_bindTemplate`
|
|
*
|
|
* Each method creates one or more property accessors, along with metadata
|
|
* used by this mixin's implementation of `_propertiesChanged` to perform
|
|
* the property effects. These methods may be called on element instances,
|
|
* but are designed to be called on element prototypes such that the work to
|
|
* set up accessors and effect metadata are done once per element class.
|
|
*
|
|
* @polymerMixin
|
|
* @mixes Polymer.TemplateStamp
|
|
* @mixes Polymer.PropertyAccessors
|
|
* @memberof Polymer
|
|
* @summary Element class mixin that provides meta-programming for Polymer's
|
|
* template binding and data observation system.
|
|
*/
|
|
Polymer.PropertyEffects = Polymer.dedupingMixin(function(superClass) {
|
|
|
|
const propertyEffectsBase = Polymer.TemplateStamp(Polymer.PropertyAccessors(superClass));
|
|
|
|
/**
|
|
* @polymerMixinClass
|
|
* @unrestricted
|
|
*/
|
|
class PropertyEffects extends propertyEffectsBase {
|
|
|
|
get PROPERTY_EFFECT_TYPES() {
|
|
return TYPES;
|
|
}
|
|
|
|
/**
|
|
* Overrides `Polymer.PropertyAccessors` implementation to initialize
|
|
* additional property-effect related properties.
|
|
*
|
|
* @override
|
|
*/
|
|
_initializeProperties() {
|
|
super._initializeProperties();
|
|
hostStack.registerHost(this);
|
|
this.__dataInitialized = false;
|
|
this.__dataPendingClients = null;
|
|
this.__dataToNotify = null;
|
|
this.__dataLinkedPaths = null;
|
|
this.__dataHasPaths = false;
|
|
// May be set on instance prior to upgrade
|
|
this.__dataCompoundStorage = this.__dataCompoundStorage || null;
|
|
this.__dataHost = this.__dataHost || null;
|
|
this.__dataTemp = {};
|
|
// Capture instance properties; these will be set into accessors
|
|
// during first flush. Don't set them here, since we want
|
|
// these to overwrite defaults/constructor assignments
|
|
for (let p in this.__propertyEffects) {
|
|
if (this.hasOwnProperty(p)) {
|
|
this.__dataInstanceProps = this.__dataInstanceProps || {};
|
|
this.__dataInstanceProps[p] = this[p];
|
|
delete this[p];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides `Polymer.PropertyAccessors` implementation to provide a
|
|
* more efficient implementation of initializing properties from
|
|
* the prototype on the instance.
|
|
*
|
|
* @override
|
|
*/
|
|
_initializeProtoProperties(props) {
|
|
this.__data = Object.create(props);
|
|
this.__dataPending = Object.create(props);
|
|
this.__dataOld = {};
|
|
}
|
|
|
|
// Prototype setup ----------------------------------------
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
_addPropertyEffect(property, type, effect) {
|
|
let effects = ensureOwnEffectMap(this, TYPES.ANY)[property];
|
|
if (!effects) {
|
|
effects = this.__propertyEffects[property] = [];
|
|
this._createPropertyAccessor(property,
|
|
type == TYPES.READ_ONLY);
|
|
}
|
|
// effects are accumulated into arrays per property based on type
|
|
if (effect) {
|
|
effects.push(effect);
|
|
}
|
|
effects = ensureOwnEffectMap(this, type)[property];
|
|
if (!effects) {
|
|
effects = this[type][property] = [];
|
|
}
|
|
effects.push(effect);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the current prototype/instance has a property effect
|
|
* of a certain type.
|
|
*
|
|
* @param {string} property Property name
|
|
* @param {string=} type Effect type, from this.PROPERTY_EFFECT_TYPES
|
|
* @return {boolean} True if the prototype/instance has an effect of this type
|
|
* @protected
|
|
*/
|
|
_hasPropertyEffect(property, type) {
|
|
let effects = this[type || TYPES.ANY];
|
|
return Boolean(effects && effects[property]);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the current prototype/instance has a "read only"
|
|
* accessor for the given property.
|
|
*
|
|
* @param {string} property Property name
|
|
* @return {boolean} True if the prototype/instance has an effect of this type
|
|
* @protected
|
|
*/
|
|
_hasReadOnlyEffect(property) {
|
|
return this._hasPropertyEffect(property, TYPES.READ_ONLY);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the current prototype/instance has a "notify"
|
|
* property effect for the given property.
|
|
*
|
|
* @param {string} property Property name
|
|
* @return {boolean} True if the prototype/instance has an effect of this type
|
|
* @protected
|
|
*/
|
|
_hasNotifyEffect(property) {
|
|
return this._hasPropertyEffect(property, TYPES.NOTIFY);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the current prototype/instance has a "reflect to attribute"
|
|
* property effect for the given property.
|
|
*
|
|
* @param {string} property Property name
|
|
* @return {boolean} True if the prototype/instance has an effect of this type
|
|
* @protected
|
|
*/
|
|
_hasReflectEffect(property) {
|
|
return this._hasPropertyEffect(property, TYPES.REFLECT);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the current prototype/instance has a "computed"
|
|
* property effect for the given property.
|
|
*
|
|
* @param {string} property Property name
|
|
* @return {boolean} True if the prototype/instance has an effect of this type
|
|
* @protected
|
|
*/
|
|
_hasComputedEffect(property) {
|
|
return this._hasPropertyEffect(property, TYPES.COMPUTE);
|
|
}
|
|
|
|
// Runtime ----------------------------------------
|
|
|
|
/**
|
|
* Sets a pending property or path. If the root property of the path in
|
|
* question had no accessor, the path is set, otherwise it is enqueued
|
|
* via `_setPendingProperty`.
|
|
*
|
|
* This function isolates relatively expensive functionality necessary
|
|
* for the public API (`set`, `setProperties`, `notifyPath`, and property
|
|
* change listeners via {{...}} bindings), such that it is only done
|
|
* when paths enter the system, and not at every propagation step. It
|
|
* also sets a `__dataHasPaths` flag on the instance which is used to
|
|
* fast-path slower path-matching code in the property effects host paths.
|
|
*
|
|
* `path` can be a path string or array of path parts as accepted by the
|
|
* public API.
|
|
*
|
|
* @param {string | !Array<number|string>} path Path to set
|
|
* @param {*} value Value to set
|
|
* @param {boolean=} isPathNotification If the path being set is a path
|
|
* notification of an already changed value, as opposed to a request
|
|
* to set and notify the change. In the latter `false` case, a dirty
|
|
* check is performed and then the value is set to the path before
|
|
* enqueuing the pending property change.
|
|
* @return {boolean} Returns true if the property/path was enqueued in
|
|
* the pending changes bag.
|
|
* @protected
|
|
*/
|
|
_setPendingPropertyOrPath(path, value, shouldNotify, isPathNotification) {
|
|
let rootProperty = Polymer.Path.root(Array.isArray(path) ? path[0] : path);
|
|
let hasEffect = this.__propertyEffects && this.__propertyEffects[rootProperty];
|
|
let isPath = (rootProperty !== path);
|
|
if (hasEffect) {
|
|
if (isPath) {
|
|
if (!isPathNotification) {
|
|
// Dirty check changes being set to a path against the actual object,
|
|
// since this is the entry point for paths into the system; from here
|
|
// the only dirty checks are against the `__dataTemp` cache to prevent
|
|
// duplicate work in the same turn only. Note, if this was a notification
|
|
// of a change already set to a path (isPathNotification: true),
|
|
// we always let the change through and skip the `set` since it was
|
|
// already dirty checked at the point of entry and the underlying
|
|
// object has already been updated
|
|
let old = Polymer.Path.get(this, path);
|
|
path = /** @type {string} */ Polymer.Path.set(this, path, value);
|
|
// Use property-accessor's simpler dirty check
|
|
if (!path || !super._shouldPropertyChange(path, value, old)) {
|
|
return false;
|
|
}
|
|
}
|
|
this.__dataHasPaths = true;
|
|
}
|
|
return this._setPendingProperty(path, value, shouldNotify);
|
|
} else {
|
|
if (isPath) {
|
|
Polymer.Path.set(this, path, value);
|
|
} else {
|
|
this[path] = value;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Applies a value to a non-Polymer element/node's property.
|
|
*
|
|
* The implementation makes a best-effort at binding interop:
|
|
* Some native element properties have side-effects when
|
|
* re-setting the same value (e.g. setting <input>.value resets the
|
|
* cursor position), so we do a dirty-check before setting the value.
|
|
* However, for better interop with non-Polymer custom elements that
|
|
* accept objects, we explicitly re-set object changes coming from the
|
|
* Polymer world (which may include deep object changes without the
|
|
* top reference changing), erring on the side of providing more
|
|
* information.
|
|
*
|
|
* Users may override this method to provide alternate approaches.
|
|
*
|
|
* @param {Node} node The node to set a property on
|
|
* @param {string} prop The property to set
|
|
* @param {*} value The value to set
|
|
* @protected
|
|
*/
|
|
_setUnmanagedPropertyToNode(node, prop, value) {
|
|
// It is a judgment call that resetting primitives is
|
|
// "bad" and resettings objects is also "good"; alternatively we could
|
|
// implement a whitelist of tag & property values that should never
|
|
// be reset (e.g. <input>.value && <select>.value)
|
|
if (value !== node[prop] || typeof value == 'object') {
|
|
node[prop] = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides the `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': {...})
|
|
* Stored in `__dataTemp` and `__data`, dirty checked against
|
|
* `__dataTemp` by default implementation of `_shouldPropertyChange`
|
|
* 3. Primitive value set to simple property (e.g. 'prop': 42)
|
|
* Stored in `__data`, dirty checked against `__data`
|
|
*
|
|
* The dirty-check is important to prevent cycles due to two-way
|
|
* notification, but paths and objects are only dirty checked against any
|
|
* previous value set during this turn via a "temporary cache" that is
|
|
* cleared when the last `_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, shouldNotify) {
|
|
let isPath = this.__dataHasPaths && Polymer.Path.isPath(property);
|
|
let prevProps = isPath ? this.__dataTemp : this.__data;
|
|
if (this._shouldPropertyChange(property, value, prevProps[property])) {
|
|
if (!this.__dataPending) {
|
|
this.__dataPending = {};
|
|
this.__dataOld = {};
|
|
}
|
|
// Ensure old is captured from the last turn
|
|
if (!(property in this.__dataOld)) {
|
|
this.__dataOld[property] = this.__data[property];
|
|
}
|
|
// Paths and objects are stored in temporary cache (cleared at end of
|
|
// turn), which is used for dirty-checking
|
|
if (isPath || typeof value == 'object') {
|
|
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;
|
|
// Track properties that should notify separately
|
|
if (isPath || (this.__notifyEffects && this.__notifyEffects[property])) {
|
|
this.__dataToNotify = this.__dataToNotify || {};
|
|
this.__dataToNotify[property] = shouldNotify;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides base implementation to ensure all accessors set `shouldNotify`
|
|
* to true, for per-property notification tracking.
|
|
*
|
|
* @override
|
|
*/
|
|
_setProperty(property, value) {
|
|
if (this._setPendingProperty(property, value, true)) {
|
|
this._invalidateProperties();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides default `PropertyAccessors` implementation to pull the value
|
|
* 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).
|
|
*
|
|
* Override this to provide more strict dirty checking, i.e. immutable
|
|
* (`value === old`) or based on type.
|
|
*
|
|
* @override
|
|
* @param {string} property
|
|
* @param {*} value
|
|
* @param {*} old
|
|
* @return {boolean}
|
|
*/
|
|
_shouldPropertyChange(property, value, old) {
|
|
// Pull `old` for Objects from temp cache, but treat `null` as a primitive
|
|
if (typeof value == 'object' && value !== null) {
|
|
old = this.__dataTemp[property];
|
|
}
|
|
return super._shouldPropertyChange(property, value, old);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @override
|
|
*/
|
|
_invalidateProperties() {
|
|
if (this.__dataInitialized) {
|
|
this._flushProperties();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enqueues the given client on a list of pending clients, whose
|
|
* pending property changes can later be flushed via a call to
|
|
* `_flushClients`.
|
|
*
|
|
* @param {Object} client PropertyEffects client to enqueue
|
|
* @protected
|
|
*/
|
|
_enqueueClient(client) {
|
|
this.__dataPendingClients = this.__dataPendingClients || [];
|
|
if (client !== this) {
|
|
this.__dataPendingClients.push(client);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
for (let i=0; i < clients.length; i++) {
|
|
let client = clients[i];
|
|
if (!client.__dataInitialized || client.__dataPending) {
|
|
client._flushProperties();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a bag of property changes to this instance, and
|
|
* synchronously processes all effects of the properties as a batch.
|
|
*
|
|
* Property names must be simple properties, not paths. Batched
|
|
* path propagation is not supported.
|
|
*
|
|
* @param {Object} props Bag of one or more key-value pairs whose key is
|
|
* a property and value is the new value to set for that property.
|
|
* @public
|
|
*/
|
|
setProperties(props) {
|
|
for (let path in props) {
|
|
if (!this.__readOnly || !this.__readOnly[path]) {
|
|
//TODO(kschaaf): explicitly disallow paths in setProperty?
|
|
// wildcard observers currently only pass the first changed path
|
|
// in the `info` object, and you could do some odd things batching
|
|
// paths, e.g. {'foo.bar': {...}, 'foo': null}
|
|
this._setPendingPropertyOrPath(path, props[path], true);
|
|
}
|
|
}
|
|
this._invalidateProperties();
|
|
}
|
|
|
|
/**
|
|
* Overrides 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.
|
|
*
|
|
* @override
|
|
*/
|
|
_flushProperties() {
|
|
if (!this.__dataInitialized) {
|
|
this.ready()
|
|
} else if (this.__dataPending || this.__dataPendingClients) {
|
|
super._flushProperties();
|
|
if (!this.__dataCounter) {
|
|
// Clear temporary cache at end of turn
|
|
this.__dataTemp = {};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* "binding effects". `super.ready()` must be called to ensure the
|
|
* data system becomes enabled.
|
|
*
|
|
* @public
|
|
*/
|
|
ready() {
|
|
// Update instance properties that shadowed proto accessors; these take
|
|
// priority over any defaults set in `properties` or constructor
|
|
let instanceProps = this.__dataInstanceProps;
|
|
if (instanceProps) {
|
|
initalizeInstanceProperties(this, instanceProps);
|
|
}
|
|
// Enable acceessors
|
|
this.__dataInitialized = true;
|
|
// Run normal flush
|
|
this._flushProperties();
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
_stampTemplate(template) {
|
|
hostStack.beginHosting(this);
|
|
let dom = super._stampTemplate(template);
|
|
hostStack.endHosting(this);
|
|
setupBindings(this);
|
|
return dom;
|
|
}
|
|
|
|
/**
|
|
* Implements `PropertyAccessors`'s properties changed callback.
|
|
*
|
|
* Runs each class of effects for the batch of changed properties in
|
|
* a specific order (compute, propagate, reflect, observe, notify).
|
|
*
|
|
* @override
|
|
*/
|
|
_propertiesChanged(currentProps, changedProps, oldProps) {
|
|
// ----------------------------
|
|
// let c = Object.getOwnPropertyNames(changedProps || {});
|
|
// window.debug && console.group(this.localName + '#' + this.id + ': ' + c);
|
|
// if (window.debug) { debugger; }
|
|
// ----------------------------
|
|
let hasPaths = this.__dataHasPaths;
|
|
this.__dataHasPaths = false;
|
|
// Compute properties
|
|
runComputedEffects(this, changedProps, oldProps, hasPaths);
|
|
// Compute linked paths
|
|
computeLinkedPaths(this, changedProps, hasPaths);
|
|
// Clear notify properties prior to possible reentry (propagate, observe),
|
|
// but after computing effects have a chance to add to them
|
|
let notifyProps = this.__dataToNotify;
|
|
this.__dataToNotify = null;
|
|
// Propagate properties to clients
|
|
runEffects(this, this.__propagateEffects, changedProps, oldProps, hasPaths);
|
|
// Flush clients
|
|
this._flushClients();
|
|
// Reflect properties
|
|
runEffects(this, this.__reflectEffects, changedProps, oldProps, hasPaths);
|
|
// Observe properties
|
|
runEffects(this, this.__observeEffects, changedProps, oldProps, hasPaths);
|
|
// Notify properties to host
|
|
if (notifyProps) {
|
|
runNotifyEffects(this, notifyProps, changedProps, oldProps, hasPaths);
|
|
}
|
|
// ----------------------------
|
|
// window.debug && console.groupEnd(this.localName + '#' + this.id + ': ' + c);
|
|
// ----------------------------
|
|
}
|
|
|
|
/**
|
|
* Aliases one data path as another, such that path notifications from one
|
|
* are routed to the other.
|
|
*
|
|
* @method linkPaths
|
|
* @param {string | !Array<string|number>} to Target path to link.
|
|
* @param {string | !Array<string|number>} from Source path to link.
|
|
* @public
|
|
*/
|
|
linkPaths(to, from) {
|
|
to = Polymer.Path.normalize(to);
|
|
from = Polymer.Path.normalize(from);
|
|
this.__dataLinkedPaths = this.__dataLinkedPaths || {};
|
|
this.__dataLinkedPaths[to] = from;
|
|
}
|
|
|
|
/**
|
|
* Removes a data path alias previously established with `_linkPaths`.
|
|
*
|
|
* Note, the path to unlink should be the target (`to`) used when
|
|
* linking the paths.
|
|
*
|
|
* @method unlinkPaths
|
|
* @param {string | !Array<string|number>} path Target path to unlink.
|
|
* @public
|
|
*/
|
|
unlinkPaths(path) {
|
|
path = Polymer.Path.normalize(path);
|
|
if (this.__dataLinkedPaths) {
|
|
delete this.__dataLinkedPaths[path];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify that an array has changed.
|
|
*
|
|
* Example:
|
|
*
|
|
* this.items = [ {name: 'Jim'}, {name: 'Todd'}, {name: 'Bill'} ];
|
|
* ...
|
|
* this.items.splice(1, 1, {name: 'Sam'});
|
|
* this.items.push({name: 'Bob'});
|
|
* this.notifySplices('items', [
|
|
* { index: 1, removed: [{name: 'Todd'}], addedCount: 1, 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).
|
|
* @public
|
|
*/
|
|
notifySplices(path, splices) {
|
|
let info = {};
|
|
let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
|
|
notifySplices(this, array, info.path, splices);
|
|
}
|
|
|
|
/**
|
|
* Convenience method for reading a value from a path.
|
|
*
|
|
* Note, if any part in the path is undefined, this method returns
|
|
* `undefined` (this method does not throw when dereferencing undefined
|
|
* paths).
|
|
*
|
|
* @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.
|
|
* @public
|
|
*/
|
|
get(path, root) {
|
|
return Polymer.Path.get(root || this, path);
|
|
}
|
|
|
|
/**
|
|
* Convenience method for setting a value to a path and notifying any
|
|
* elements bound to the same path.
|
|
*
|
|
* Note, if any part in the path except for the last is undefined,
|
|
* this method does nothing (this method does not throw when
|
|
* dereferencing undefined paths).
|
|
*
|
|
* @method set
|
|
* @param {(string|!Array<(string|number)>)} path Path to the value
|
|
* to write. The path may be specified as a string (e.g. `'foo.bar.baz'`)
|
|
* or an array of path parts (e.g. `['foo.bar', 'baz']`). Note that
|
|
* bracketed expressions are not supported; string-based path parts
|
|
* *must* be separated by dots. Note that when dereferencing array
|
|
* indices, the index may be used as a dotted part directly
|
|
* (e.g. `'users.12.name'` or `['users', 12, 'name']`).
|
|
* @param {*} value Value to set at the specified path.
|
|
* @param {Object=} root Root object from which the path is evaluated.
|
|
* When specified, no notification will occur.
|
|
* @public
|
|
*/
|
|
set(path, value, root) {
|
|
if (root) {
|
|
Polymer.Path.set(root, path, value);
|
|
} else {
|
|
if (!this.__readOnly || !this.__readOnly[/** @type {string} */(path)]) {
|
|
if (this._setPendingPropertyOrPath(path, value, true)) {
|
|
this._invalidateProperties();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds items onto the end of the array at the path specified.
|
|
*
|
|
* The arguments after `path` and return value match that of
|
|
* `Array.prototype.push`.
|
|
*
|
|
* This method notifies other paths to the same array that a
|
|
* splice occurred to the array.
|
|
*
|
|
* @method push
|
|
* @param {string} path Path to array.
|
|
* @param {...*} items Items to push onto array
|
|
* @return {number} New length of the array.
|
|
* @public
|
|
*/
|
|
push(path, ...items) {
|
|
let info = {};
|
|
let array = /** @type {Array}*/(Polymer.Path.get(this, path, info));
|
|
let len = array.length;
|
|
let ret = array.push(...items);
|
|
if (items.length) {
|
|
notifySplice(this, array, info.path, len, items.length, []);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Removes an item from the end of array at the path specified.
|
|
*
|
|
* The arguments after `path` and return value match that of
|
|
* `Array.prototype.pop`.
|
|
*
|
|
* This method notifies other paths to the same array that a
|
|
* splice occurred to the array.
|
|
*
|
|
* @method pop
|
|
* @param {string} path Path to array.
|
|
* @return {*} Item that was removed.
|
|
* @public
|
|
*/
|
|
pop(path) {
|
|
let info = {};
|
|
let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
|
|
let hadLength = Boolean(array.length);
|
|
let ret = array.pop();
|
|
if (hadLength) {
|
|
notifySplice(this, array, info.path, array.length, 0, [ret]);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Starting from the start index specified, removes 0 or more items
|
|
* from the array and inserts 0 or more new items in their place.
|
|
*
|
|
* The arguments after `path` and return value match that of
|
|
* `Array.prototype.splice`.
|
|
*
|
|
* This method notifies other paths to the same array that a
|
|
* splice occurred to the array.
|
|
*
|
|
* @method splice
|
|
* @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 {...*} items Items to insert into array.
|
|
* @return {Array} Array of removed items.
|
|
* @public
|
|
*/
|
|
splice(path, start, deleteCount, ...items) {
|
|
let info = {};
|
|
let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
|
|
// 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;
|
|
}
|
|
let ret = array.splice(start, deleteCount, ...items);
|
|
if (items.length || ret.length) {
|
|
notifySplice(this, array, info.path, start, items.length, ret);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Removes an item from the beginning of array at the path specified.
|
|
*
|
|
* The arguments after `path` and return value match that of
|
|
* `Array.prototype.pop`.
|
|
*
|
|
* This method notifies other paths to the same array that a
|
|
* splice occurred to the array.
|
|
*
|
|
* @method shift
|
|
* @param {string} path Path to array.
|
|
* @return {*} Item that was removed.
|
|
* @public
|
|
*/
|
|
shift(path) {
|
|
let info = {};
|
|
let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
|
|
let hadLength = Boolean(array.length);
|
|
let ret = array.shift();
|
|
if (hadLength) {
|
|
notifySplice(this, array, info.path, 0, 0, [ret]);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Adds items onto the beginning of the array at the path specified.
|
|
*
|
|
* The arguments after `path` and return value match that of
|
|
* `Array.prototype.push`.
|
|
*
|
|
* This method notifies other paths to the same array that a
|
|
* splice occurred to the array.
|
|
*
|
|
* @method unshift
|
|
* @param {string} path Path to array.
|
|
* @param {...*} items Items to insert info array
|
|
* @return {number} New length of the array.
|
|
* @public
|
|
*/
|
|
unshift(path, ...items) {
|
|
let info = {};
|
|
let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
|
|
let ret = array.unshift(...items);
|
|
if (items.length) {
|
|
notifySplice(this, array, info.path, 0, items.length, []);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Notify that a path has changed.
|
|
*
|
|
* Example:
|
|
*
|
|
* this.item.user.name = 'Bob';
|
|
* this.notifyPath('item.user.name');
|
|
*
|
|
* @param {string} path Path that should be notified.
|
|
* @param {*=} value Value at the path (optional).
|
|
* @public
|
|
*/
|
|
notifyPath(path, value) {
|
|
/** @type {string} */
|
|
let propPath;
|
|
if (arguments.length == 1) {
|
|
// Get value if not supplied
|
|
let info = {};
|
|
value = Polymer.Path.get(this, path, info);
|
|
propPath = info.path;
|
|
} else if (Array.isArray(path)) {
|
|
// Normalize path if needed
|
|
propPath = Polymer.Path.normalize(path);
|
|
} else {
|
|
propPath = /** @type{string} */(path);
|
|
}
|
|
if (this._setPendingPropertyOrPath(propPath, value, true, true)) {
|
|
this._invalidateProperties();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
this._addPropertyEffect(property, TYPES.READ_ONLY);
|
|
if (protectedSetter) {
|
|
this['_set' + upper(property)] = function(value) {
|
|
this._setProperty(property, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a single-property observer for the given property.
|
|
*
|
|
* @param {string} property Property name
|
|
* @param {string} methodName Name of observer method to call
|
|
* @param {boolean=} dynamicFn Whether the method name should be included as
|
|
* a dependency to the effect.
|
|
* @protected
|
|
*/
|
|
_createPropertyObserver(property, methodName, dynamicFn) {
|
|
let info = { property, methodName };
|
|
this._addPropertyEffect(property, TYPES.OBSERVE, {
|
|
fn: runObserverEffect, info, trigger: {name: property}
|
|
});
|
|
if (dynamicFn) {
|
|
this._addPropertyEffect(methodName, TYPES.OBSERVE, {
|
|
fn: runObserverEffect, info, trigger: {name: methodName}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a multi-property "method observer" based on the provided
|
|
* expression, which should be a string in the form of a normal Javascript
|
|
* function signature: `'methodName(arg1, [..., argn])'`. Each argument
|
|
* should correspond to a property or path in the context of this
|
|
* prototype (or instance), or may be a literal string or number.
|
|
*
|
|
* @param {string} expression Method expression
|
|
* @param {Object=} dynamicFns Map indicating whether method names should
|
|
* be included as a dependency to the effect.
|
|
* @protected
|
|
*/
|
|
_createMethodObserver(expression, dynamicFns) {
|
|
let sig = parseMethod(expression);
|
|
if (!sig) {
|
|
throw new Error("Malformed observer expression '" + expression + "'");
|
|
}
|
|
createMethodEffect(this, sig, TYPES.OBSERVE, runMethodEffect, null, dynamicFns);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
_createNotifyingProperty(property) {
|
|
this._addPropertyEffect(property, TYPES.NOTIFY, {
|
|
fn: runNotifyEffect,
|
|
info: {
|
|
eventName: CaseMap.camelToDashCase(property) + '-changed',
|
|
property: property
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
_createReflectedProperty(property) {
|
|
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 {
|
|
this._addPropertyEffect(property, TYPES.REFLECT, {
|
|
fn: runReflectEffect,
|
|
info: {
|
|
attrName: attr
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a computed property whose value is set to the result of the
|
|
* method described by the given `expression` each time one or more
|
|
* arguments to the method changes. The expression should be a string
|
|
* in the form of a normal Javascript function signature:
|
|
* `'methodName(arg1, [..., argn])'`
|
|
*
|
|
* @param {string} property Name of computed property to set
|
|
* @param {string} expression Method expression
|
|
* @param {Object=} dynamicFns Map indicating whether method names should
|
|
* be included as a dependency to the effect.
|
|
* @protected
|
|
*/
|
|
_createComputedProperty(property, expression, dynamicFns) {
|
|
let sig = parseMethod(expression);
|
|
if (!sig) {
|
|
throw new Error("Malformed computed expression '" + expression + "'");
|
|
}
|
|
createMethodEffect(this, sig, TYPES.COMPUTE, runComputedEffect, property, dynamicFns);
|
|
}
|
|
|
|
// -- binding ----------------------------------------------
|
|
|
|
/**
|
|
* Creates "binding" property effects for all binding bindings
|
|
* in the provided template that forward host properties into DOM stamped
|
|
* from the template via `_stampTemplate`.
|
|
*
|
|
* @param {HTMLTemplateElement} template Template containing binding
|
|
* bindings
|
|
* @param {Object=} dynamicFns Map indicating whether method names should
|
|
* be included as a dependency to the effect.
|
|
* @protected
|
|
*/
|
|
_bindTemplate(template, dynamicFns) {
|
|
// Clear any existing propagation effects inherited from superClass
|
|
this.__propagateEffects = {};
|
|
this.__notifyListeners = [];
|
|
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);
|
|
}
|
|
addBindingEffect(this, binding, i, dynamicFns);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return PropertyEffects;
|
|
});
|
|
|
|
})();
|
|
</script>
|