Files
polymer/src/properties/property-effects.html

2124 lines
76 KiB
HTML
Raw Normal View History

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;
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
/**
* 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
* @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
*/
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
}
}
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;
// TODO(kschaaf) ideally a system exists to parse path information once
// and send structured information through the system for better perf
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)) {
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);
if (fx.info) {
fx.info.lastRun = id;
}
ran = true;
}
}
}
return ran;
2016-08-31 19:09:55 -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');
}
}
/**
* 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
// 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`.
for (let prop in props) {
if (notifyEffects && runEffectsForProperty(inst, notifyEffects, id,
prop, props[prop], oldProps && oldProps[prop])) {
notified = true;
} else if (notifyPath(inst, prop, props[prop])) {
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
*/
function notifyPath(inst, path, value) {
let rootProperty = Polymer.Path.root(path);
if (rootProperty !== path) {
let eventName = Polymer.CaseMap.camelToDashCase(rootProperty) + '-changed';
dispatchNotifyEvent(inst, eventName, value, 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=} 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
*/
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
*/
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;
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;
const cache = inst.__dataTemp;
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);
cache[link] = changedProps[link] = computedProps[link] = changedProps[path];
} else if (Polymer.Path.isDescendant(b, path)) {
link = Polymer.Path.translate(b, a, path);
cache[link] = changedProps[link] = computedProps[link] = changedProps[path];
}
}
}
}
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
/**
* 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
* "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
*/
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) {
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, {
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
/**
* 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
*/
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);
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
applyBindingValue(inst, info, value);
2016-09-01 12:44:48 -07:00
}
}
/**
* Called by "binding effect" to set a property to a node. Note,
* 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
*/
function setPropertyToNodeFromBinding(inst, node, prop, value) {
if (!node._hasReadOnlyEffect(prop)) {
if (node._setPendingProperty(prop, value)) {
inst._enqueueClient(node);
}
}
}
2016-10-27 09:51:41 -07: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.
*
* @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
*/
function applyBindingValue(inst, info, value) {
2016-10-27 09:51:41 -07:00
let node = inst.__dataNodes[info.index];
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)) {
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)
} 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
/**
* 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
*/
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
/**
* 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
* @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
*/
function addMethodBindingEffect(model, note, part, index) {
2016-10-27 09:51:41 -07:00
createMethodEffect(model, part.signature, TYPES.PROPAGATE,
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
/**
* 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
*/
function runMethodBindingEffect(inst, property, value, old, info) {
2016-09-01 12:44:48 -07:00
let val = runMethodEffect(inst, property, value, old, info);
applyBindingValue(inst, info.methodInfo, val);
2016-09-01 12:44:48 -07:00
}
2016-10-27 09:51:41 -07: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
* 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, '&comma;').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(/&comma;/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
*/
function notifySplices(inst, array, path, splices) {
let splicesPath = path + '.splices';
2016-12-08 12:27:56 -08:00
inst._setProperty(splicesPath, { indexSplices: splices });
inst._setProperty(path + '.length', array.length);
// Null here to allow potentially large splice records to be GC'ed.
inst.__data[splicesPath] = {indexSplices: null};
}
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) {
notifySplices(inst, array, path, [{
index: index,
2016-10-27 09:51:41 -07:00
addedCount: addedCount,
removed: removed,
object: array,
type: 'splice'
}]);
2016-08-31 19:09:55 -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
* @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-09-01 15:24:28 -07:00
return class PropertyEffects extends Polymer.TemplateStamp(
Polymer.Attributes(Polymer.PropertyAccessors(superClass))) {
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();
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
* @param {boolean} fromPath If the value being set was from a path; in
* 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
* 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
*/
_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,
// 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
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
if (!super._shouldPropertyChange(path, value, old)) {
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
/**
* 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) {
let isPath = 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;
return true;
}
}
/**
* 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).
*
* 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
}
/**
* 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
* 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();
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
* "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.
*
* 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) {
// ----------------------------
// 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) {
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) {
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);
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
* 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
* (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.
* @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
*/
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);
let len = array.length;
2016-08-31 19:09:55 -07:00
let ret = array.push(...items);
if (items.length) {
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) {
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);
// 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) {
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) {
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) {
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
}
/**
* 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-01 15:24:28 -07:00
notifyPath(path, value) {
2016-09-14 18:52:47 -07:00
if (arguments.length == 1) {
// 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-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
}
createMethodEffect(this, sig, TYPES.COMPUTE, runComputedEffect, property);
2016-08-31 19:09:55 -07:00
}
// -- binding ----------------------------------------------
2016-08-31 19:09:55 -07:00
2016-10-27 09:51:41 -07: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
* 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);
}
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-08-31 19:09:55 -07:00
});
2016-02-19 10:23:22 -08:00
})();
</script>