Add computed to propertyConfig, rename bind->observers. Remove published BC.

This commit is contained in:
Kevin Schaaf
2015-03-02 15:41:40 -08:00
parent 7005e758a5
commit bee4ee4285
20 changed files with 259 additions and 278 deletions
+82 -91
View File
@@ -22,8 +22,8 @@ Bare-minum Custom Element sugaring
| [Bespoke constructor support](#bespoke-constructor) | constructor: function() { … }
| [Basic lifecycle callbacks](#basic-callbacks) | created, attached, detached, attributeChanged
| [Native HTML element extension](#type-extension) | extends: ‘…’
| [Publish API](#published-api) | published: { … }
| [Attribute deserialization to property](#attribute-deserialization) | published: { \<property>: \<Type> }
| [Configure properties](#property-config) | propertyConfig: { … }
| [Attribute deserialization to property](#attribute-deserialization) | propertyConfig: { \<property>: \<Type> }
| [Module registry](#module-registry) | modularize, using
| [Prototype Mixins](#prototype-mixins) | mixins: [ … ]
@@ -46,15 +46,15 @@ Declarative data binding, events, and property nofication
| [Local node marshalling](#node-marshalling) | this.$.\<id>
| [Event listener setup](#event-listeners)| listeners: { \<node>.\<event>: function, ... }
| [Annotated event listener setup](#annotated-listeners) | \<element on-[event]=”function”>
| [Property change callbacks](#change-callbacks) | bind: { \<property>: function }
| [Property change callbacks](#change-callbacks) | propertyConfig: \<prop>: { observer: function }
| [Declarative property binding](#property-binding) | \<element prop=”{{property\|path}}”>
| [Property change notification](#property-notification) | published: { \<prop>: { notify: true } }
| [Property change notification](#property-notification) | propertyConfig: { \<prop>: { notify: true } }
| [Binding to structured data](#path-binding) | \<element prop=”{{obj.sub.path}}”>
| [Path change notification](#set-path) | setPathValue(\<path>, \<value>)
| [Declarative attribute binding](#attribute-binding) | \<element attr$=”{{property\|path}}”>
| [Reflecting properties to attributes](#attribute-reflection) | published: \<prop>: { reflect: true } }
| [Reflecting properties to attributes](#attribute-reflection) | propertyConfig: \<prop>: { reflect: true } }
| [Computed properties](#computed-properties) | computed: { \<property>: function(\<property>) }
| [Read-only properties](#read-only) | published: { \<prop>: { readOnly: true } }
| [Read-only properties](#read-only) | propertyConfig: { \<prop>: { readOnly: true } }
| [Utility functions](#utility-functions) | toggleClass, toggleAttribute, fire, async, …
| [Attribute-based layout](#layout-html) | layout.html (layout horizontal flex ...)
@@ -211,12 +211,12 @@ MyElement = Polymer({
See the [section on configuring elements](#configuring-elements) for a more in-depth description of the practical uses of each callback.
<a name="published-api"></a>
## Published API
<a name="property-config"></a>
## Configuring properties
Placing an object-valued `published` property on your prototype allows you to define metadata regarding your Custom Element's API, which can then be accessed by an API for use by other Polymer features.
Placing an object-valued `propertyConfig` property on your prototype allows you to define metadata regarding your Custom Element's properties, which can then be accessed via an API for use by other Polymer features.
By itself, the `published` feature **doesn't do anything**. It only provides API for asking questions about these special properties (see featues below for details).
By itself, the `propertyConfig` feature **doesn't do anything**. It only provides API for asking questions about these special properties (see featues below for details).
Example:
@@ -225,7 +225,7 @@ Polymer({
is: 'x-custom',
published: {
propertyConfig: {
user: String,
isHappy: Boolean,
count: {
@@ -247,7 +247,7 @@ Remember that the fields assigned to `count`, such as `readOnly` and `notify` do
<a name="attribute-deserialization"></a>
## Attribute deserialization
If an attribute matches a property listed in the `published` object, the attribute value will be assigned to a property of the same name on the element instance. Attribute values (always strings) will be automatically converted to the published type when assigned to the property. If no other `published` options are specified for a property, the type (specified using the type constructor, e.g. `Object`, `String`, etc.) can be set directly as the value of the property in the published object; otherwise it should be provided as the value to the `type` key in the `published` configuration object.
If an attribute matches a property listed in the `propertyConfig` object, the attribute value will be assigned to a property of the same name on the element instance. Attribute values (always strings) will be automatically converted to the propertyConfig type when assigned to the property. If no other `propertyConfig` options are specified for a property, the type (specified using the type constructor, e.g. `Object`, `String`, etc.) can be set directly as the value of the property in the propertyConfig object; otherwise it should be provided as the value to the `type` key in the `propertyConfig` configuration object.
The type system includes support for Object values expressed as JSON, or Date objects expressed as any Date-parsable string representation. Boolean properties set based on the existence of the attribute: if the attribute exists at all, its value is true, regardless of its string-value (and the value is only false if the attribute does not exist).
@@ -260,7 +260,7 @@ Example:
is: 'x-custom',
published: {
propertyConfig: {
user: String,
manager: {
type: Boolean,
@@ -286,8 +286,6 @@ This user is a manager.
-->
```
**Warning:** Currently only lower-case published properties are supported. Camel-case property support will be added in this sprint.
<a name="module-registry"></a>
## Module registry
@@ -358,7 +356,7 @@ using(['FunSupport', ...], function(funSupport, ...) {
Polymer will "mixin" objects specified in a `mixin` array into the prototype. This can be useful for adding common code between multiple elements.
The current mixin feature in 0.8 is basic; it simply loops over properties in the provided object and adds property descriptors for those on the prototype (such that `set`/`get` accessors are copied in addition to properties and functions). Note that there is currently no support for publishing properties or hooking lifecycle callbacks directly via mixins. The general pattern is for the mixin to supply functions to be called by the target element as part of its usage contract (and should be documented as such). These limitations will likely be revisited in the future.
The current mixin feature in 0.8 is basic; it simply loops over properties in the provided object and adds property descriptors for those on the prototype (such that `set`/`get` accessors are copied in addition to properties and functions). Note that there is currently no support for configuring properties or hooking lifecycle callbacks directly via mixins. The general pattern is for the mixin to supply functions to be called by the target element as part of its usage contract (and should be documented as such). These limitations will likely be revisited in the future.
The [module registry](#module-registry) should generally be used for registering mixins. Mixins registered with the Polymer module registry may be referred to by String name without needing to expliitly request the module via `using`. Otherwise, values in the `mixin` array should be an Object reference (generally retrieved via `using`).
@@ -667,9 +665,9 @@ Example:
```
<a name="change-callbacks"></a>
## Property change callbacks
## Property change callbacks (observers)
Custom element properties may be observed for changes by specifying an object-valued `bind` property that maps element properties to chagne handler names. When the property changes, the change handler will be called with the new and old values.
Custom element properties may be observed for changes by specifying `observer` property in the `propertyConfig` 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:
@@ -678,13 +676,14 @@ Polymer({
is: 'x-custom',
published: {
disabled: Boolean
},
bind: {
disabled: 'disabledChanged',
highlight: 'highlightChanged'
propertyConfig: {
disabled: {
type: Boolean,
observer: 'disabledChanged'
},
highlight: {
observer: 'highlightChanged'
}
},
disabledChanged: function(newValue, oldValue) {
@@ -702,11 +701,9 @@ Polymer({
});
```
Note as in the example above, change handlers can be bound to properties that are not necessarily published.
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).
Observing changes to object sub-properties is also supported via the `bind` object, by specifying a full (e.g. `user.manager.name`) or partial path (`user.*`).
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.
Example:
@@ -715,11 +712,37 @@ Polymer({
is: 'x-custom',
published: {
propertyConfig: {
preload: Boolean,
src: String,
size: String
},
observers: {
'preload src size': 'updateImage'
},
updateImage: function(preload, src, size) {
// ... do work using dependent values
}
});
```
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.
Example:
```js
Polymer({
is: 'x-custom',
propertyConfig: {
user: Object
},
bind: {
observers: {
'user.manager.*': 'userManagerChanged'
},
@@ -768,7 +791,7 @@ To bind to textContent, the binding annotation must currently span the entire co
is: 'user-view',
published: {
propertyConfig: {
first: String,
last: String
}
@@ -796,7 +819,7 @@ To bind to properties, the binding annotation should be provided as the value to
is: 'main-view',
published: {
propertyConfig: {
user: Object
}
@@ -823,15 +846,15 @@ Note that currently binding to `style` is a special case which results in the va
Polymer supports cooperative two-way binding between elements, allowing elements that "produce" data or changes to data to propagate those changes upwards to hosts when desired.
When a Polymer elements changes a property that was "published" as part of its public API with the `notify` flag set to true, it automatically fires a non-bubbling DOM event to indicate those changes to interested hosts. These events follow a naming convention of `<property>-changed`, and contain a `value` property in the `event.detail` object indicating the new value.
When a Polymer elements changes a property that was configured in `propertyConfig` with the `notify` flag set to true, it automatically fires a non-bubbling DOM event to indicate those changes to interested hosts. These events follow a naming convention of `<property>-changed`, and contain a `value` property in the `event.detail` object indicating the new value.
As such, one could attach an `on-<property>-changed` listener to an element to be notified of changes to such properties, set the `event.detail.value` to a property on itself, and take necessary actions based on the new value. However, given this is a common pattern, bindings using "curly-braces" (e.g. `{{property}}`) will automatically perform this upwards binding automatically without the user needing to perform those tasks. This can be defeated by using "square-brace" syntax (e.g. `[[property]]`), which results in only one-way (downward) data-binding.
To summarize, two-way data-binding is achieved when both the host and the child agree to participate, satisfying these three conditions:
1. The host must use curly-brace `{{property}}` syntax. Square-brace `[[property]]` syntax results in one-way downward binding, regardless of the notify state of the child's property.
2. The child property being bound to must be published with the `notify` flag set to true (or otherwise send a `<propety>-changed` custom event). If the property being bound is not published or if the `notify` flag is not set, only one-way (downward) binding will occur.
3. The child property being bound to must not published with the `readOnly` flag set to true. If the child property is `notify: true` and `readOnly:true`, and the host binding uses curly-brace syntax, the binding will effectively be one-way (upward).
2. The child property being bound to must be configured with the `notify` flag set to true (or otherwise send a `<propety>-changed` custom event). If the property being bound does not have the `notify` flag set, only one-way (downward) binding will occur.
3. The child property being bound to must not be configured with the `readOnly` flag set to true. If the child property is `notify: true` and `readOnly:true`, and the host binding uses curly-brace syntax, the binding will effectively be one-way (upward).
Example 1: Two-way binding
@@ -840,7 +863,7 @@ Example 1: Two-way binding
<script>
Polymer({
is: 'custom-element',
published: {
propertyConfig: {
prop: {
type: String,
notify: true
@@ -863,7 +886,7 @@ Example 2: One-way binding (downward)
<script>
Polymer({
is: 'custom-element',
published: {
propertyConfig: {
prop: {
type: String,
notify: true
@@ -886,7 +909,7 @@ Example 3: One-way binding (downward)
<script>
Polymer({
is: 'custom-element',
published: {
propertyConfig: {
prop: String // no `notify:true`!
}
});
@@ -906,7 +929,7 @@ Example 4: One-way binding (upward)
<script>
Polymer({
is: 'custom-element',
published: {
propertyConfig: {
prop: {
type: String,
notify: true,
@@ -930,7 +953,7 @@ Example 5: Error / non-sensical state
<script>
Polymer({
is: 'custom-element',
published: {
propertyConfig: {
prop: {
type: String,
notify: true,
@@ -969,7 +992,7 @@ Note that path bindings are distinct from property bindings in a subtle way: whe
<a name="set-path"></a>
### Path change notification
Two-way data-binding and observation of paths in Polymer is achieved using a similar strategy to the one described above for [2-way property binding](#property-notification): When a sub-property of a published `Object` changes, an element fires a non-bubbling `<property>-path-changed` DOM event with a `detail.path` value indicating the path on the object that changed. Elements that have registered interest in that object (either via binding or change handler) may then take side effects based on knowledge of the path having changed. Finally, those elements will forward the notification on to any children they have bound the object to, and if the element published the root object for the path that changed on its API, it will also fire a new `<propety>-path-changed` event appropriately. Through this method, a notification will reach any part of the tree that has registered interest in that path so that side effects occur.
Two-way data-binding and observation of paths in Polymer is achieved using a similar strategy to the one described above for [2-way property binding](#property-notification): When a sub-property of a property configured with `type: Object` changes, an element fires a non-bubbling `<property>-path-changed` DOM event with a `detail.path` value indicating the path on the object that changed. Elements that have registered interest in that object (either via binding or change handler) may then take side effects based on knowledge of the path having changed. Finally, those elements will forward the notification on to any children they have bound the object to, and if the element configured the root object as `type: Object` for the path that changed on its API, it will also fire a new `<propety>-path-changed` event appropriately. Through this method, a notification will reach any part of the tree that has registered interest in that path so that side effects occur.
This system "just works" to the extent that changes to object sub-properties occur as a result of being bound to a notifying custom element property that changed. However, often imperative code needs to "poke" at an object's sub-properties directly. As we avoid more sophisticated observation mechanisms such as Object.observe or dirty-checking in order to achieve the best startup and runtime performance cross-platform for the most common use cases, changing an object's sub-properties directly requires cooperation from the user.
@@ -1048,13 +1071,13 @@ Again, as values must be serialized to strings when binding to attributes, it is
<a name="attribute-reflection"></a>
## Reflecting properties to attributes
In specific cases, it may be useful to keep an HTML attribute value in sync with a property value. This may be achieved by setting `reflect: true` on a property in the published configuration object. This will cause any change to the property to be serialized out to an attribute of the same name.
In specific cases, it may be useful to keep an HTML attribute value in sync with a property value. This may be achieved by setting `reflect: true` on a property in the `propertyConfig` configuration object. This will cause any change to the property to be serialized out to an attribute of the same name.
```html
<script>
Polymer({
published: {
propertyConfig: {
response: {
type: Object,
reflect: true
@@ -1075,7 +1098,7 @@ Values will be serialized according to type: Arrays/Objects will be `JSON.string
<a name="computed-properties"></a>
## Computed properties
Polymer supports virtual properties whose values are calculated from other properties. Computed properties can be defined by providing an object-valued `computed` property on the prototype that maps property names to computing functions. 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 `propertyConfig` 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.
```html
<dom-module id="x-custom">
@@ -1089,10 +1112,19 @@ Polymer supports virtual properties whose values are calculated from other prope
is: 'x-custom',
computed: {
// when `first` or `last` changes `computeFullName` is called once
// (asynchronously) and the value it returns is stored as `fullName`
fullName: 'computeFullName(first, last)',
propertyConfig: {
first: String,
last: String,
fullName: {
type: String,
// when `first` or `last` changes `computeFullName` is called once
// (asynchronously) and the value it returns is stored as `fullName`
computed: 'computeFullName(first, last)'
}
},
computeFullName: function(first, last) {
@@ -1110,13 +1142,13 @@ Note: Only direct properties of the element (as opposed to sub-properties of an
<a name="read-only"></a>
## Read-only properties
When a property only "produces" data and never consumes data, this can be made explicit to avoid accidental changes from the host by setting the `readOnly` flag to `true` in the published property definition. In order for the element to actually change the value of the property, it must use a private generated setter of the convention `_set<Property>(value)`.
When a property only "produces" data and never consumes data, this can be made explicit to avoid accidental changes from the host by setting the `readOnly` flag to `true` in the `propertyConfig` property definition. In order for the element to actually change the value of the property, it must use a private generated setter of the convention `_set<Property>(value)`.
```html
<script>
Polymer({
published: {
propertyConfig: {
response: {
type: Object,
readOnly: true,
@@ -1275,47 +1307,6 @@ Current limitations that are on the backlog for evaluation/improvement are liste
* May bind entire inline style from one property to `style` _property_:
`<div style="{{styles}}">`
* Otherwise, assign `this.style.props` from change handlers
* Support for compound property binding
* See below
## Compound observation
Polymer 0.8 does not currently support observer functions called once for changes to a set of dependent properties, outside of computed properties. If the side-effects of the binding are expensive, then you may want to ensure side-effects only occur once for any number of changes to them during a turn by manually introducing asynchronicity.
The `debounce` API on the Polymer Base prototype can be used to achieve this. The `debounce` API takes a signal name (String), callback, and optional wait time, and only calls the callback once for any number `debounce` calls with the same `signalName` started within the wait period.
Example:
```html
<dom-module id="x-parent">
<template>
<x-child disabled="{{shouldDisable}}"></my-child>
</template>
</dom-module>
<script>
Polymer({
is: 'x-parent',
bind: {
isManager: 'computeShouldDisableDebounced',
mode: 'computeShouldDisableDebounced',
},
computeShouldDisableDebounced: function() {
this.debounce('computeShouldDisable', this.computeShouldDisable);
},
// Better: called once for multiple changes
computeShouldDisable: function() {
this.shouldDisable = this.isManager || (this.mode == 2);
}
});
</script>
```
Note, the `computed` property feature uses `debounce` under the hood to achieve a similar effect.
## Structured data and path notification
+6 -6
View File
@@ -65,18 +65,18 @@ You can always fallback to using the low-level methods if you wish (iow, you cou
By default, the default Polymer distribution include several features. Although `Polymer.Base` itself is tiny, if you examine `Polymer.Base` you will probably see several methods that have been plugged-in to that prototype by feature definitions. The next few sections will explain these features and why we include them in the default set. Keep in mind that it's entirely possible to construct custom feature sets, or even use a trivial, featureless form of `Polymer()`.
### Feature: _published_
### Feature: _property-config_
The first feature implements support for the `published` property. By placing a object-valued `published` property on your prototype, let's you define various aspects of your custom-elements public API.
The first feature implements support for the `propertyConfig` property. By placing a object-valued `propertyConfig` property on your prototype, let's you define various aspects of your custom-elements public API.
By itself, the `published` feature **doesn't do anything**. It only provides API for asking questions about these special properties (see [link to docs] for details).
By itself, the `propertyConfig` feature **doesn't do anything**. It only provides API for asking questions about these special properties (see [link to docs] for details).
```js
Polymer({
is: 'x-custom',
published: {
propertyConfig: {
user: String,
isHappy: Boolean,
count: {
@@ -128,7 +128,7 @@ Many custom elements want to support configuration using HTML attributes. Custom
Although it's relatively simple, having to write this code becomes annoying when working with multiple attributes or non-String types. It's also not very DRY.
Instead, Polymer's `attributes` feature handles this work for you (using the `published` feature data). If an attribute is set that matches a property listed in the `published` object, the value is captured into the matching property. Strings are automatically converted to the published type.
Instead, Polymer's `attributes` feature handles this work for you (using the `propertyConfig` feature data). If an attribute is set that matches a property listed in the `propertyConfig` object, the value is captured into the matching property. Strings are automatically converted to the specified type.
The type system includes support for Object values expressed as JSON, or Date objects expressed as any Date-parsable string representation. Boolean properties are mapped to Boolean attributes, in other words, if the attribute exists at all, its value is true, regardless of its string-value (and the value is only false if the attribute does not exist).
@@ -141,7 +141,7 @@ Here is the equivalent of the above code, taking advantage of the `attributes` f
is: 'x-custom',
published: {
propertyConfig: {
user: String
},
+1 -1
View File
@@ -36,7 +36,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
<x-doc-viewer flex sources='[
"src/features/standard/bind.html",
"src/features/standard/notify-path.html",
"src/features/micro/published.html",
"src/features/micro/property-config.html",
"src/features/standard/annotations.html",
"../x-elements/x-doc-viewer/x-doc-viewer.html"
]'></x-doc-viewer>
+1 -1
View File
@@ -33,7 +33,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
is: 'x-custom',
published: {
propertyConfig: {
user: String
},
+1 -1
View File
@@ -12,7 +12,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
<link rel="import" href="src/features/micro/mixins.html">
<link rel="import" href="src/features/micro/extends.html">
<link rel="import" href="src/features/micro/constructor.html">
<link rel="import" href="src/features/micro/published.html">
<link rel="import" href="src/features/micro/property-config.html">
<link rel="import" href="src/features/micro/attributes.html">
<script>
+7 -6
View File
@@ -19,14 +19,15 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
*
* Support for mapping attributes to properties.
*
* Properties that are `published` are mapped to attributes.
* Properties that are configured in `propertyConfig` with a type are mapped
* to attributes.
*
* A value set in a published attribute is deserialized into the specified
* A value set in an attribute is deserialized into the specified
* data-type and stored into the matching property.
*
* Example:
*
* published: {
* propertyConfig: {
* // values set to index attribute are converted to Number and propagated
* // to index property
* index: Number,
@@ -77,8 +78,8 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
},
_takeAttributesToModel: function(model) {
for (var name in (this.propertyConfig ||this.published)) {
var type = this.getPublishedPropertyType(name);
for (var name in this.propertyConfig) {
var type = this.getPropertyType(name);
if (type === Boolean || this.hasAttribute(name)) {
this.setAttributeToProperty(model, name, type);
}
@@ -86,7 +87,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
},
setAttributeToProperty: function(model, name, type) {
type = type || this.getPublishedPropertyType(name);
type = type || this.getPropertyType(name);
if (type) {
model[name] = this.deserialize(name, this.getAttribute(name), type);
}
@@ -16,16 +16,16 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
/**
* Define property metadata.
*
* published: {
* propertyConfig: {
* <property>: <Type || Object>,
* ...
* }
*
* Example:
*
* published: {
* propertyConfig: {
* // `foo` property can be assigned via attribute, will be deserialized to
* // the specified data-type. All `published` properties have this behavior.
* // the specified data-type. All `propertyConfig` properties have this behavior.
* foo: String,
*
* // `bar` property has additional behavior specifiers.
@@ -39,7 +39,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
* }
* }
*
* By itself the published feature doesn't do anything but provide property
* By itself the propertyConfig feature doesn't do anything but provide property
* information. Other features use this information to control behavior.
*
* The `type` information is used by the `attributes` feature to convert
@@ -54,45 +54,44 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
* `readOnly` properties have a getter, but no setter. To set a read-only
* property, use the private setter method `_set_<property>(value)`.
*
* @class base feature: published
* @class base feature: propertyConfig
*/
Base.addFeature({
published: {
propertyConfig: {
},
nob: Object.create(null),
getPublishInfo: function(property) {
return this._getPublishInfo(property,
this.propertyConfig || this.published);
getPropertyInfo: function(property) {
return this._getPropertyInfo(property, this.propertyConfig);
},
_getPublishInfo: function(property, published) {
var p = published[property];
_getPropertyInfo: function(property, propertyConfig) {
var p = propertyConfig[property];
if (typeof(p) === 'function') {
p = published[property] = {
p = propertyConfig[property] = {
type: p
};
}
return p || Base.nob;
},
getPublishedPropertyType: function(property) {
return this.getPublishInfo(property).type;
getPropertyType: function(property) {
return this.getPropertyInfo(property).type;
},
isReadOnlyProperty: function(property) {
return this.getPublishInfo(property).readOnly;
return this.getPropertyInfo(property).readOnly;
},
isNotifyProperty: function(property) {
return this.getPublishInfo(property).notify;
return this.getPropertyInfo(property).notify;
},
isReflectedProperty: function(property) {
return this.getPublishInfo(property).reflect;
return this.getPropertyInfo(property).reflect;
}
});
+53 -94
View File
@@ -16,7 +16,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
/**
* Support for the declarative property sugaring via mustache `{{ }}`
* annotations in templates, and via the `bind` and `computed` objects on
* annotations in templates, and via the `propertyConfig` objects on
* prototypes.
*
* Example:
@@ -25,57 +25,26 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
* <span hidden="{{hideSpan}}">{{name}}</span> is on the hook.
* </template>
*
* In this template, the attribute `hidden` is bound to the `hideSpan`
* property of the element, and the textContent of the span is bound to the
* `name` property of the element.
*
* The `bind` object syntax is as follows:
* The `propertyConfig` object syntax is as follows:
*
* Polymer({
*
* bind: {
* // if `method` is the name of a method on the current object, the
* // method is invoked with the property changes. The method is provided
* // arguments as follows: `method(value, oldValue)`
* property: 'method'
*
* // Multiple side effects can be declared using an array.
* property2: [
* 'property2Changed',
* 'renderStuff',
* 'fixupThings'
* ]
* propertyConfig: {
* myProp: {
* observer: 'myPropChanged',
* computed: 'computemyProp(input1, input2)'
* }
* }
*
* ...
*
* });
*
* The `computed` object supports virtual properties whose values are
* calculated from other properties. Only one dependency is supported
* at this time.
*
* Polymer({
*
* computed: {
* // when `user` changes `computeFullName` is called and the
* // value it returns is stored as `fullName`
* fullName: 'computeFullName(user)',
* },
*
* computeFullName: function(user) {
* return user.firstName + ' ' + user.lastName;
* }
*
* ...
*
* });
*
* The `bind` feature also provides an API for registering effects against
* properties.
*
* Property effects can be created imperatively, by template-annotations
* (e.g. mustache notation), or by declaration in the `bind` object.
* (e.g. mustache notation), or by declaration in the `propertyConfig` object.
*
* The effect data is consumed by the `bind` subsystem (`/src/bind/*`),
* which compiles the effects into efficient JavaScript that is triggered,
@@ -90,16 +59,8 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
Base.addFeature({
addPropertyEffect: function(property, kind, effect) {
// TODO(kschaaf): Branch here to set up more runtime-efficient
// data structure than _propertyEffects for path-only effects;
// Consider breaking all path effects to a general addPathEffect
// API for more consistency
var model = property.split('.').shift();
if (kind == 'method' && (property != model)) {
this.addPathBindMethod(property, effect);
}
// TODO(kschaaf): path observers won't get the right `new` argument...care?
Bind.addPropertyEffect(this, model, kind, effect);
var model = property.split('.').shift();
Bind.addPropertyEffect(this, model, kind, effect);
},
// prototyping
@@ -108,14 +69,8 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
_prepEffects: function() {
Bind.prepareModel(this);
//
this._addDynamicEffects(this.propertyConfig);
//
this._addBindEffects(this.bind);
this._addComputedEffects(this.computed);
//
this._addPublishedEffects(this.propertyConfig || this.published);
//
this._addObserverEffects(this.observers);
this._addAnnotationEffects(this._annotes);
Bind.createBindings(this);
},
@@ -124,53 +79,57 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
if (dynamic) {
for (var n in dynamic) {
var effect = dynamic[n];
if (effect.observe) {
this._addBindEffect(n, effect.observe);
} else {
// items: { type: Object, 'items.*': 'itemsPathChanged', observe: 'itemsChanged' }
var wild = n + '.*';
if (effect[wild]) {
this._addBindEffect(wild, effect[wild]);
}
if (effect.observer) {
this._addObserverEffect(n, effect.observer);
}
if (effect.computed) {
Bind.addComputedPropertyEffect(this, n, effect.computed);
}
if (this.isNotifyProperty(n)) {
this.addPropertyEffect(n, 'notify');
}
if (this.isReflectedProperty(n)) {
this.addPropertyEffect(n, 'reflect');
}
}
}
},
_addBindEffects: function(effects) {
for (var n in effects) {
var effect = effects[n];
if (typeof effect === 'object') {
// multiplexed definition
for (var nn in effect) {
this._addBindEffect(n, effect[nn]);
}
} else {
// single definition
this._addBindEffect(n, effect);
}
_addObserverEffects: function(observers) {
for (var n in observers) {
this._addObserverEffect(n, observers[n]);
}
},
_addBindEffect: function(property, effect) {
this.addPropertyEffect(property, 'method', effect);
},
_addComputedEffects: function(computed) {
if (computed) {
for (var n in computed) {
Bind.addComputedPropertyEffect(this, n, computed[n]);
_addObserverEffect: function(property, observer) {
var props = property.split(' ');
var methodString;
if (props.length == 1) {
// Single property synchronous observer (supports paths)
var model = property.split('.').shift();
if (model != property) {
// TODO(kschaaf): path observers won't get the right `new` argument...care?
this.addPathObserver(property, observer);
}
}
},
_addPublishedEffects: function(published) {
for (var n in published) {
if (this.isNotifyProperty(n)) {
this.addPropertyEffect(n, 'notify');
}
if (this.isReflectedProperty(n)) {
this.addPropertyEffect(n, 'reflect');
methodString = 'this.' + observer + '(this._data.' + model + ', old);';
this.addPropertyEffect(model, 'observer', {
method: observer,
property: model,
methodString: methodString
});
} else {
// Multiple-property debounced observer
var methodArgs = 'this._data.' + props.join(', this._data.');
methodString = 'this.debounce(\'_' + observer + '\', function() {\n' +
'\t\tthis.' + observer + '(' + methodArgs + ');\n' +
'\t});';
var effect = {
method: observer,
properties: props,
methodString: methodString
};
for (var i=0; i<props.length; i++) {
this.addPropertyEffect(props[i], 'observer', effect);
}
}
},
+2 -2
View File
@@ -26,7 +26,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
*
* is: 'x-date',
*
* published: {
* propertyConfig: {
* date: {
* type: Object,
* notify: true
@@ -127,7 +127,7 @@ using(['Base', 'Annotations'], function(Base, Annotations) {
return prop[last];
},
addPathBindMethod: function(path, method) {
addPathObserver: function(path, method) {
var fx$ = this._pathEffects || (this._pathEffects = []);
var match = path.indexOf('.*') == (path.length-2);
if (match) {
+10 -4
View File
@@ -20,8 +20,14 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
var methodString = 'this.debounce(\'_' + method + '\', function() {\n' +
'\t\tthis.' + name + ' = this.' + method + '(' + methodArgs + ');\n' +
'\t});';
var effect = {
property: name,
args: args,
methodName: method,
methodString: methodString
};
for (var i=0; i<args.length; i++) {
this.addPropertyEffect(model, args[i], 'compute', methodString);
this.addPropertyEffect(model, args[i], 'compute', effect);
}
};
@@ -69,7 +75,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
Bind.addBuilders({
method: function(model, source, effect) {
observer: function(model, source, effect) {
// TODO(sjmiles): validation system requires a blessed
// validator effect which needs to be processed first.
/*
@@ -85,7 +91,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
}
*/
//
return 'this.' + effect + '(this._data.' + source + ', old);'
return effect.methodString;
},
// basic modus operandi
@@ -102,7 +108,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
},
compute: function(model, source, effect) {
return effect;
return effect.methodString;
},
reflect: function(model, source) {
+2 -2
View File
@@ -162,7 +162,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
var node = inst._nodes[info.index];
node.addEventListener(info.property + '-changed', inst._notifyListener.bind(inst, info.changedFn));
// Path listeners:
var type = node.getPublishedPropertyType && node.getPublishedPropertyType(info.property);
var type = node.getPropertyType && node.getPropertyType(info.property);
if (type == Object || type == Array) {
node.addEventListener(info.property + '-path-changed', inst._notifyListener.bind(inst, function(e) {
// re-jigger path
@@ -215,7 +215,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
'annotation': 1,
'reflect': 2,
'notify': 3,
'method': 4
'observer': 4
};
return Bind;
+4 -4
View File
@@ -15,10 +15,10 @@
Bind.prepareModel(model);
// 'method' effects are called if foo changes value as fx(foo, old)
// 'observer' effects are called if foo changes value as fx(foo, old)
Bind.addPropertyEffect(model, 'foo', 'method', 'fooChange');
Bind.addPropertyEffect(model, 'foo', 'method', 'fooWork');
Bind.addPropertyEffect(model, 'foo', 'observer', 'fooChange');
Bind.addPropertyEffect(model, 'foo', 'observer', 'fooWork');
model.fooChange = function(foo) {
out.innerHTML += '<b>fooChange</b>: effect of changing foo to ' + foo + '\n';
@@ -30,7 +30,7 @@
console.log('fooWork: effect of changing foo to %d', foo);
};
// 'method' effect sets the value of bar to the result of computeBar when
// 'compute' effect sets the value of bar to the result of computeBar when
// foo changes value
/*
+3 -4
View File
@@ -15,7 +15,7 @@ using('Collection', function(Collection) {
Polymer({
is: 'x-array-selector',
published: {
propertyConfig: {
items: Array,
selected: {
type: Object,
@@ -25,9 +25,8 @@ using('Collection', function(Collection) {
multi: Boolean
},
bind: {
multi: 'update',
items: 'update'
observers: {
'multi items': 'update',
},
update: function() {
+17 -9
View File
@@ -32,11 +32,22 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
is: 'x-repeat',
extends: 'template',
published: {
items: Array,
sort: Function,
filter: Function,
observe: String,
propertyConfig: {
items: {
type: Array
},
sort: {
type: Function,
observer: '_sortChanged'
},
filter: {
type: Function,
observer: '_filterChanged'
},
observe: {
type: String,
observer: '_observeChanged'
},
delay: Number
},
@@ -44,10 +55,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
'Templatizer'
],
bind: {
'sort': '_sortChanged',
'filter': '_filterChanged',
'observe': '_observeChanged',
observers: {
'items.*': '_itemsChanged'
},
+1 -1
View File
@@ -256,7 +256,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
click: 'clickAction'
},
bind: {
observers: {
// property: target (set property is pushed to $.<target>[.textContent])
exclaim: 'exclaim',
state: 'state'
+1 -1
View File
@@ -199,7 +199,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
hostAttributes: 'block',
published: {
propertyConfig: {
attribute: String
},
+23 -19
View File
@@ -4,18 +4,29 @@
<script>
Polymer({
is: 'x-basic',
published: {
propertyConfig: {
value: {
observer: 'valueChanged'
},
computedvalue: {
computed: 'computeValue(value)',
observer: 'computedvalueChanged'
},
notifyingvalue: {
type: Number,
notify: true
notify: true,
computed: 'notifyingvalueChanged'
},
computednotifyingvalue: {
type: Number,
notify: true
notify: true,
computed: 'computeNotifyingValue(notifyingvalue)'
},
computedFromMultipleValues: {
type: Number,
notify: true
notify: true,
computed: 'computeFromMultipleValues(sum1, sum2, divide)',
observer: 'computedFromMultipleValuesChanged'
},
camelNotifyingValue: {
type: Number,
@@ -24,20 +35,12 @@
readonlyvalue: {
type: Number,
readOnly: true,
notify: true
notify: true,
observer: 'readonlyvalueChanged'
}
},
computed: {
computedvalue: 'computeValue(value)',
computednotifyingvalue: 'computeNotifyingValue(notifyingvalue)',
computedFromMultipleValues: 'computeFromMultipleValues(sum1, sum2, divide)'
},
bind: {
value: 'valueChanged',
computedvalue: 'computedvalueChanged',
notifyingvalue: 'notifyingvalueChanged',
readonlyvalue: 'readonlyvalueChanged',
computedFromMultipleValues: 'computedFromMultipleValuesChanged'
observers: {
'dep1 dep2 dep3': 'multipleDepChangeHandler'
},
valueChanged: function() {},
computeValue: function(val) {
@@ -52,7 +55,8 @@
computeFromMultipleValues: function(sum1, sum2, divide) {
return (sum1 + sum2) / divide;
},
computedFromMultipleValuesChanged: function() {}
computedFromMultipleValuesChanged: function() {},
multipleDepChangeHandler: function() {}
});
</script>
@@ -75,7 +79,7 @@
<script>
Polymer({
is: 'x-compose',
bind: {
observers: {
boundvalue: 'boundvalueChanged',
boundnotifyingvalue: 'boundnotifyingvalueChanged',
boundcomputedvalue: 'boundcomputedvalueChanged',
@@ -94,7 +98,7 @@
<script>
Polymer({
is: 'x-reflect',
published: {
propertyConfig: {
reflectedobject: {
type: Object,
reflect: true
+20 -3
View File
@@ -108,9 +108,9 @@ suite('single-element binding effects', function() {
});
test('computed property with multiple dependencies', function(done) {
var called = false;
var called = 0;
el.computedFromMultipleValuesChanged = function() {
called = true;
called++;
};
var notified = false;
el.addEventListener('computed-from-multiple-values-changed', function(e) {
@@ -122,7 +122,7 @@ suite('single-element binding effects', function() {
setTimeout(function() {
assert.equal(el.computedFromMultipleValues, 15, 'Computed value wrong');
assert.equal(notified, true, 'Notification event not sent');
assert.equal(called, true, 'Change handler not called');
assert.equal(called, 1, 'Change handler not called');
done();
});
});
@@ -166,6 +166,23 @@ suite('single-element binding effects', function() {
assert.equal(notified, true, 'Notification event not sent');
});
test('multiple dependency change handler called once', function(done) {
var called = 0;
el.multipleDepChangeHandler = function(dep1, dep2, dep3) {
called++;
assert.equal(dep1, el.dep1, 'dependency 1 argument wrong');
assert.equal(dep2, el.dep2, 'dependency 2 argument wrong');
assert.equal(dep3, el.dep3, 'dependency 3 argument wrong');
};
el.dep1 = true;
el.dep2 = {};
el.dep3 = 42;
setTimeout(function() {
assert.equal(called, 1, 'change handler not called once');
done();
});
});
});
</script>
+3 -6
View File
@@ -1,17 +1,14 @@
<script>
var configureMixin = {
published: {
propertyConfig: {
content: {
type: String,
notify: true
notify: true,
observer: 'contentChanged'
}
},
bind: {
content: 'contentChanged'
},
changeHandlerCount: 0,
contentChanged: function() {
+6 -6
View File
@@ -1,7 +1,7 @@
<script>
Polymer({
is: 'x-basic',
published: {
propertyConfig: {
notifyingValue: {
type: Number,
notify: true
@@ -18,13 +18,13 @@
<script>
Polymer({
is: 'x-compose',
published: {
propertyConfig: {
obj: {
type: Object,
notify: true
}
},
bind: {
observers: {
'obj.*': 'objSubpathChanged',
'obj.value': 'objValueChanged',
},
@@ -39,13 +39,13 @@
<script>
Polymer({
is: 'x-forward',
published: {
propertyConfig: {
obj: {
type: Object,
notify: true
}
},
bind: {
observers: {
'obj.*': 'objSubpathChanged',
'obj.value': 'objValueChanged',
},
@@ -62,7 +62,7 @@
<script>
Polymer({
is: 'x-stuff',
bind: {
observers: {
'nested.*': 'nestedSubpathChanged',
'nested.obj': 'nestedObjChanged',
'nested.obj.*': 'objSubpathChanged',