Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.8 path effects #1422

Merged
merged 4 commits into from
Apr 18, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 108 additions & 17 deletions PRIMER.md
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,8 @@ Example:
<a name="change-callbacks"></a>
## Property change callbacks (observers)

### Single property observation

Custom element properties may be observed for changes by specifying `observer` property in the `properties` for the property that gives the name of a funciton to call. When the property changes, the change handler will be called with the new and old values as arguments.

Example:
Expand Down Expand Up @@ -774,9 +776,15 @@ Polymer({
});
```

Property change observation is achieved in Polymer by installing setters on the custom element prototype for properties with registered interest (as opposed to observation via Object.observe or dirty checking, for example).
Note that property change observation is achieved in Polymer by installing setters on the custom element prototype for properties with registered interest (as opposed to observation via Object.observe or dirty checking, for example).

### Multipe property observation

Observing changes to multiple properties is supported via the `observers` array on the prototype, using a string containing a method signature that includes any dependent arguments. Once all properties are defined (`!== undefined`), the observer method will be called once for each change to a dependent property. The current values of the dependent properties will be passed as arguments to the observer method in the order defined in the `observers` method signature.

*Note, computing functions will only be called once all dependent properties are defined (`!=undefined`). If one or more of the properties are optional, they would need default `value`'s defined in `properties` to ensure the observer is called.*

Observing changes to multiple properties is supported via the `observers` object, by specifying a string-separated list of dependent properties that should result in a change function being called. These observers differ from single-property observers in that the change handler is called asynchronously.
*Note that any observers defined in the `observers` array will not receive `old` values as arguments, only new values. Only single-property observers defined in the `properties` object received both `old` and `new` values.*

Example:

Expand All @@ -791,9 +799,9 @@ Polymer({
size: String
},

observers: {
'preload src size': 'updateImage'
},
observers: [
'updateImage(preload, src, size)'
],

updateImage: function(preload, src, size) {
// ... do work using dependent values
Expand All @@ -802,7 +810,9 @@ Polymer({
});
```

Additionally, observing changes to object sub-properties is also supported via the same `observers` object, by specifying a full (e.g. `user.manager.name`) or partial path (`user.*`) and function name to call. In this case, the third argument will indicate the path that changed. Note that currently the second argument (old value) will not be valid.
### Path observation

Observing changes to object sub-properties is also supported via the same `observers` array, by specifying a path (e.g. `user.manager.name`).

Example:

Expand All @@ -815,25 +825,51 @@ Polymer({
user: Object
},

observers: {
'user.manager.*': 'userManagerChanged'
observers: [
'userManagerChanged(user.manager)'
],

userManagerChanged: function(user) {
console.log('new manager name is ' + user.name);
}

});
```

*Note that observing changes to paths (object sub-properties) is dependent on one of two requirements: either the value at the path in question changed via a Polymer [property binding](#property-binding) to another element, or the value was changed using the [`setPathValue`](#set-path) API, which provides the required notification to elements with registered interest.*

### Deep path observation

Additionally, wildcard matching of path changes is also supported via the `observers` array, which allows notification when any (deep) sub-property of an object changes. Note that the argument passed for a path with a wildcard is a change record object containing the `path` that changed, the new `value` of the path that changed, and the `base` value of the wildcard expression.

Example:

```js
Polymer({

is: 'x-custom',

properties: {
user: Object
},

userManagerChanged: function(newValue, oldValue, path) {
if (path) {
// sub-property of user.manager changed
console.log('manager ' + path.split('.').pop() + ' changed to ' + newValue);
} else {
observers: [
'userManagerChanged(user.manager.*)'
],

userManagerChanged: function(changeRecord) {
if (changeRecord.path == 'user.manager') {
// user.manager object itself changed
console.log('new manager name is ' + newValue.name);
} else {
// sub-property of user.manager changed
console.log(changeRecord.path + ' changed to ' + changeRecord.value);
}
}

});
```

Note that observing changes to paths (object sub-properties) is dependent on one of two requirements: either the value at the path in question changed via a Polymer [property binding](#property-binding) to another element, or the value was changed using the [`setPathValue`](#set-path) API, which provides the required notification to elements with registered interest.

<a name="property-binding"></a>
## Annotated property binding

Expand Down Expand Up @@ -1245,7 +1281,9 @@ Values will be serialized according to type; by default Arrays/Objects will be `
<a name="computed-properties"></a>
## Computed properties

Polymer supports virtual properties whose values are calculated from other properties. Computed properties can be defined in the `properties` object by providing a `computed` key mapping to a computing function. The name of the function to compute the value is provided as a string with dependent properties as arguments in parenthesis. The function will be called once (asynchronously) for any change to the dependent properties.
Polymer supports virtual properties whose values are calculated from other properties. Computed properties can be defined in the `properties` object by providing a `computed` key mapping to a computing function. The name of the function to compute the value is provided as a string with dependent properties as arguments in parenthesis. Once all properties are defined (`!== undefined`), the computing function will be called to update the computed property once for each change to a dependent property.

*Note, computing functions will only be called once all dependent properties are defined (`!=undefined`). If one or more of the properties are optional, they would need default `value`'s defined in `properties` to ensure the property is computed.*

```html
<dom-module id="x-custom">
Expand Down Expand Up @@ -1289,7 +1327,11 @@ Note: Only direct properties of the element (as opposed to sub-properties of an
<a name="annotated-computed"></a>
## Annotated computed properties

Anonymous computed properties may also be placed directly in template binding annotations. This is useful when the property need not be a part of the element's API or otherwise used by logic in the element, and is only used for downward data propagation. Note: this is the only form of functions allowed in template bindings.
Anonymous computed properties may also be placed directly in template binding annotations. This is useful when the property need not be a part of the element's API or otherwise used by logic in the element, and is only used for downward data propagation.

*Note: this is the only form of functions allowed in template bindings, and they must specify one or more dependent properties as arguments, otherwise the function will not be called.*

*Note, computing functions will only be called once all dependent properties are defined (`!=undefined`). If one or more of the properties are optional, they would need default `value`'s defined in `properties` to ensure the property is computed.*

Example:

Expand Down Expand Up @@ -1459,6 +1501,8 @@ Then the `observe` property should be configured as follows:
filter="isEngineer" observe="type manager.type">
```

Note, to reach the outer parent scope, bindings in an `x-repeat` template may be prefixed with `parent.<property>`.

<a name="x-array-selector"></a>
## Array selector (x-array-selector)
EXPERIMENTAL - API MAY CHANGE
Expand Down Expand Up @@ -1507,6 +1551,53 @@ Keeping structured data in sync requires that Polymer understand the path associ
</dom-module>
```

<a name="x-if"></a>
## Conditional template
EXPERIMENTAL - API MAY CHANGE

Elements can be conditionally stamped based on a boolean property by wrapping them in a custom `HTMLTemplateElement` type extension called `x-if`. The `x-if` template stamps itself into the DOM only when its `if` property becomes truthy.

If the `if` property becomes falsy again, by default all stamped elements will be hidden (but will remain in DOM) for faster performance should the `if` property become truthy again. This behavior may be defeated by setting the `restamp` property, which results in slower `if` switching behavior as the elements are destroyed and re-stamped each time.

Note, to reach the outer parent scope, all bindings in an `x-if` template must be prefixed with `parent.<property>`, as shown below.

Example:

**Note, this is a simple example for illustrative purposes only. Read below for guidance on recommended usage of conditional templates.**

```html
<dom-module id="user-page">

<template>

All users will see this:
<div>{{user.name}}</div>

<template is="x-if" if="{{user.isAdmin}}">
Only admins will see this.
<div>{{parent.user.secretAdminStuff}}</div>
</template>

</template>

<script>
Polymer({
is: 'user-page',
properties: {
user: Object
}
});
</script>

</dom-module>
```

Note, since it is generally much faster to hide/show elements rather than create/destroy them, conditional templates are only useful to save initial creation cost when the elements being stamped are relatively heavyweight and the conditional may rarely (or never) be true in given useages. Otherwise, liberal use of conditional templates can actually *add* significant runtime performance overhead.

Consider an app with 4 screens, plus an optional admin screen. If most users will use all 4 screens during normal use of the app, it is generally better to incur the cost of stamping those elements once at startup (where some app initialization time is expected) and simply hide/show the screens as the user navigates through the app, rather than re-create and destroy all the elements of each screen as the user navigates. Using a conditional template here may be a poor choice, since although it may save time at startup by stamping only the first screen, that saved time gets shifted to runtime latency for each user interaction, since the time to show the second screen will be *slower* as it must create the second screen from scratch rather than simply showing that screen. Hiding/showing elements is as simple as attribute-binding to the `hidden` attribute (e.g. `<div hidden$="{{!shouldShow}}">`), and does not require conditional templating at all.

However, using a conditional template may be appropriate in the case of an admin screen that should only be shown to admin users of an app. Since most users would not be admins, there may be performance benefits to not burdening most of the users with the cost of stamping the elements for the admin page, especially if it is relatively heavyweight.

<a name="x-autobind"></a>
## Auto-binding template
EXPERIMENTAL - API MAY CHANGE
Expand Down
6 changes: 3 additions & 3 deletions src/lib/annotations/annotations.html
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
},

// 1. Parse annotations from the template and memoize them on
// content._annotes (recurses into nested templates)
// content._notes (recurses into nested templates)
// 2. Parse template bindings for parent.* properties and memoize them on
// content._parentProps
// 3. Create bindings in current scope's annotation list to template for
Expand All @@ -167,7 +167,7 @@
// TODO(sjmiles): simply altering the .content reference didn't
// work (there was some confusion, might need verification)
var content = document.createDocumentFragment();
content._annotes = this.parseAnnotations(node);
content._notes = this.parseAnnotations(node);
content.appendChild(node.content);
// Special-case treatment of 'parent.*' props for nested templates
// Automatically bind `prop` on host to `_parent_prop` on template
Expand Down Expand Up @@ -206,7 +206,7 @@
// template) are stored in content._parentProps.
_discoverTemplateParentProps: function(content) {
var chain = content._parentPropChain = [];
content._annotes.forEach(function(n) {
content._notes.forEach(function(n) {
// Find all bindings to parent.* and spread them into _parentPropChain
n.bindings.forEach(function(b) {
var m;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/bind/accessors.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@
'reflect': 3,
'notify': 4,
'observer': 5,
'function': 6
'complexObserver': 6,
'function': 7
};
return function(a, b) {
return EFFECT_ORDER[a.kind] - EFFECT_ORDER[b.kind];
Expand Down
89 changes: 53 additions & 36 deletions src/lib/bind/effects.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@

Polymer.Base.extend(Polymer.Bind, {

_shouldAddListener: function(info) {
return info.name &&
info.mode === '{' &&
!info.negate &&
info.kind != 'attribute'
_shouldAddListener: function(effect) {
return effect.name &&
effect.mode === '{' &&
!effect.negate &&
effect.kind != 'attribute'
;
},

annotationEffect: function(source, value, info) {
if (source != info.value) {
value = this.getPathValue(info.value);
this._data[info.value] = value;
annotationEffect: function(source, value, effect) {
if (source != effect.value) {
value = this.getPathValue(effect.value);
this._data[effect.value] = value;
}
var calc = info.negate ? !value : value;
return this._applyEffectValue(calc, info);
var calc = effect.negate ? !value : value;
return this._applyEffectValue(calc, effect);
},

reflectEffect: function(source) {
Expand All @@ -37,50 +37,67 @@
this._notifyChange(source);
},

// Raw effect for extension; info.function is an actual function
functionEffect: function(source, value, info, old) {
info.function.call(this, source, value, info, old);
// Raw effect for extension; effect.function is an actual function
functionEffect: function(source, value, effect, old) {
effect.function.call(this, source, value, effect, old);
},

observerEffect: function(source, value, info, old) {
//console.log(value, info);
if (info.property) {
this[info.method](value, old);
} else {
var args = Polymer.Bind._marshalArgs(this._data, info.properties);
if (args) {
this[info.method].apply(this, args);
}
observerEffect: function(source, value, effect, old) {
this[effect.method](value, old);
},

complexObserverEffect: function(source, value, effect) {
var args = Polymer.Bind._marshalArgs(this._data, effect, source, value);
if (args) {
this[effect.method].apply(this, args);
}
},

computeEffect: function(source, value, info) {
var args = Polymer.Bind._marshalArgs(this._data, info.args);
computeEffect: function(source, value, effect) {
var args = Polymer.Bind._marshalArgs(this._data, effect, source, value);
if (args) {
this[info.property] = this[info.methodName].apply(this, args);
this[effect.property] = this[effect.method].apply(this, args);
}
},

annotatedComputationEffect: function(source, value, info) {
var args = Polymer.Bind._marshalArgs(this._data, info.args);
annotatedComputationEffect: function(source, value, effect) {
var args = Polymer.Bind._marshalArgs(this._data, effect, source, value);
if (args) {
var computedHost = this._rootDataHost || this;
var computedvalue =
computedHost[info.methodName].apply(computedHost, args);
this._applyEffectValue(computedvalue, info);
computedHost[effect.method].apply(computedHost, args);
this._applyEffectValue(computedvalue, effect);
}
},

_marshalArgs: function(model, properties) {
var a=[];
for (var i=0, l=properties.length, v; i<l; i++) {
v = model[properties[i]];
// path & value are used to fill in wildcard descriptor when effect is
// being called as a result of a path notification
_marshalArgs: function(model, effect, path, value) {
var values = [];
var args = effect.args;
for (var i=0, l=args.length; i<l; i++) {
var arg = args[i];
var name = arg.name;
var v = arg.structured ?
Polymer.Base.getPathValue(name, model) : model[name];
if (v === undefined) {
return;
}
a[i] = v;
if (arg.wildcard) {
// Only send the actual path changed info if the change that
// caused the observer to run matched the wildcard
var baseChanged = (name.indexOf(path + '.') === 0);
var matches = (effect.arg.name.indexOf(name) === 0 && !baseChanged);
values[i] = {
path: matches ? path : name,
value: matches ? value : v,
base: v
};
} else {
values[i] = v;
}
}
return a;
return values;
}

});
Expand Down
Loading