Polymer lets you observe changes to an element's properties and take various actions based on data changes. These actions, or property effects, include:
-
Observers. Callbacks invoked when data changes.
-
Computed properties. Virtual properties computed based on other properties, and recomputed when the input data changes.
-
Data bindings. Annotations that update the properties, attributes, or text content of a DOM node when data changes.
Each Polymer element manages its own data model and local DOM elements. The model for an element is the element's properties. Data bindings link an element's model with the elements in its local DOM.
Consider a very simple element:
<dom-module id="name-card">
<template>
<div>[[name.first]] [[name.last]]</div>
</template>
<script>Polymer({ is: 'name-card' });</script>
</dom-module>
- The
<name-card>
element has aname
property that refers to a JavaScript object. - The
<name-card>
element hosts a local DOM tree, that contains a single<div>
element. - Data bindings in the template link the JavaScript object to the
<div>
element.
The data system is based on paths, not objects, where a path represents a property or subproperty
relative to an element. For example, the <name-card>
element has data bindings for the paths
"name.first"
and "name.last"
. If a <name-card>
has the following object for its name
property:
{
first: 'Lizzy',
last: 'Bennet'
}
The path "name
" refers to the element's name
property (an object).The paths "name.first
" and
"name.last"
refer to properties of that object.
- The
name
property refers to the JavaScript object. - The path "name" refers to the object itself. The path "name.first" refers to its subproperty,
first
(the string,Lizzy
).
Polymer doesn't automatically detect all data changes. Property effects occur when there is an observable change to a property or subproperty.
Why use paths? Paths and observable changes might seem a little strange at first. But this results in a very high-performance data binding system. When an observable change occurs, Polymer can make a change to just those points in the DOM that are affected by that change.
In summary:
- While a single JavaScript object or array can be referenced by multiple elements, a path is always relative to an element.
- An observable change to a path can produce property effects, like updating bindings or calling observers.
- Data bindings establish connections between paths on different elements.
Observable changes
An observable change is a data change that Polymer can associate with a path. Certain changes are automatically observable:
- Setting a direct property of the element.
this.owner = 'Jane';
If a property has any associated property effects (such as observers, computed properties, or data bindings), Polymer creates a setter for the property, which automatically invokes the property effects.
-
Setting a subproperty of the element using a two-way data binding.
<local-dom-child name="{{hostProperty.subProperty}}"></local-dom-child>
Changes made by the data binding system are automatically propagated. In this example, if
<local-dom-child>
makes a change to itsname
property, the change propagates up to the host, as a change to the path"hostProperty.subProperty"
.
Unobservable changes
Changes that imperatively mutate an object or array are not observable. This includes:
-
Setting a subproperty of an object:
// unobservable subproperty change this.address.street = 'Elm Street';
Changing the subproperty
address.street
here doesn't invoke the setter onaddress
, so it isn't automatically observable. -
Mutating an array:
// unobservable change using native Array.push this.users.push({ name: 'Maturin});
Mutating the array doesn't invoke the setter on
users
, so it isn't automatically observable.
In both cases, you need to use Polymer methods to ensure that the changes are observable.
Mutating objects and arrays observably
Polymer provides methods for making observable changes to subproperties and arrays:
// mutate an object observably
this.set('address.street', 'Half Moon Street');
// mutate an array observably
this.push('users', { name: 'Maturin'});
In some cases, you can't use the Polymer methods to mutate objects and arrays (for example, if
you're using a third-party library). In this case, you can use the notifyPath
and notifySplices
methods to notify the element about changes that have already taken place.
// Notify Polymer that the value has changed
this.notifyPath('address.street');
When you call notifyPath
or notifySplices
, the element applies the appropriate property effects,
as if the changes had just taken place.
When calling set
or notifyPath
, you need to use the exact path that changed. For example,
calling this.notifyPath('address')
doesn't pick up a change to address.street
if the address
object itself remains unchanged. This is because Polymer performs dirty checking and doesn't produce
any property effects if the value at the specified path hasn't changed.
In most cases, if one or more properties of an object have changed, or one or more items in an array have changed, you can force Polymer to skip the dirty check by setting the property to an empty object or array, then back to the original object or array.
var address = this.address;
this.address = {};
this.address = address;
Related tasks:
- Set object subproperties.
- Mutate an array.
- Notify Polymer of subproperty changes.
- Notify Polymer of array mutations.
Data paths
In the data system, a path is a string that identifies a property or subproperty relative to a scope. In most cases, the scope is a host element. For example, consider the following relationships:
- The
<user-profile>
element has a propertyprimaryAddress
that refers to a JavaScript object. - The
<address-card>
element has a propertyaddress
that refers to the same object.
Importantly, Polymer doesn't automatically know that these properties refer to the same object.
If <address-card>
makes a change to the object, no property effects are invoked on <user-profile>
.
For data changes to flow from one element to another, the elements must be connected with a data binding.
Linking paths with data bindings
Data bindings can create links between paths on different elements. In fact, data binding is the
only way to link paths on different elements. For example, consider the <user-profile>
example
in the previous section. If <address-card>
is in the local DOM of the <user-profile>
element,
the two paths can be connected with a data binding:
<dom-module id="user-profile">
<template>
…
<address-card
address="{{primaryAddress}}"></address-card>
</template>
…
</dom-module>
Now the paths are connected by data binding.
- The
<user-profile>
element has a propertyprimaryAddress
that refers to a JavaScript object. - The
<address-card>
element has a propertyaddress
that refers to the same object. - The data binding connects the path
"primaryAddress"
on<user-profile>
to the path"address"
on<address-card>
If <address-card>
makes an observable change to the object, property effects are invoked on
<user-profile>
as well.
Data binding scope
Paths are relative to the current data binding scope.
The topmost scope for any element is the element's properties. Certain data binding helper elements (like template repeaters) introduce new, nested scopes.
For observers and computed properties, the scope is always the element's properties.
Special paths
A path is a series of path segments. In most cases, each path segment is a property name.
There are a few special types of path segments.
- The wildcard character,
*
, can be used as the last segment in a path (likefoo.*
). This wildcard path represents all changes to a given path and its subproperties, including array mutations. - The string
splices
can be used as the last segment in a path (likefoo.splices
) to represent all array mutations to a given array. - Array item paths (like
foo.11
) represent an item in an array.
Wildcard paths
When used as the last segment in a path, the wildcard character (*
) represents any change to the
previous property or its subproperties. For example, if users
is an array, users.*
indicates an
interest in any changes to the users
array or its subproperties.
Wildcard paths are only used in observers, computed properties and computed bindings. You can't use a wildcard path in a data binding.
Array mutation paths
When used as the last segment in a path, splices
represents any array mutations to the
identified array (additions or deletions). For example, if users
is an array, the path
users.splices
identifies any additions to or deletions from the array.
A .splices
path can be used in an observer, computed property or computed binding to identify
interest in array mutations. Observing a .splices
path gives you a subset of the changes
registered by a wildcard path (for example, you won't see changes to subproperties of objects
inside the array). In most cases, it's more useful to use a wildcard observer for arrays.
Paths to array items
Polymer supports two ways of identifying an array item in a path: by index or by an opaque, immutable key.
-
An index, for example,
"myArray.1"
indicates the array item at position 1. -
An opaque key, prefixed with the pound sign (
#
), for example,"myArray.#1"
indicates the array item with the key "1".
Why use keys? Polymer uses keys internally to provide a stable path to a specific array item, regardless of its current position in the array. This allows an element that manages a list, for example, to create a stable association between an array item and the DOM that it generates. Keys are generated by Polymer and last as long as the item is in the array.
When you notify Polymer of a change to an array path, you can use either the index or the key.
When Polymer generates a notification for an array item, it identifies the item by key, because it requires extra work to retrieve the index. Polymer provides methods to retrieve the original array item given the item's key.
In most cases, you don't need to deal with array keys directly. Polymer provides helper elements, such as the template repeater element that abstract away many of the complexities of working with arrays.
Related tasks:
Two paths referencing the same object
Sometimes an element has two paths that point to the same object.
For example, an element has two properties, users
(an array) and selectedUser
(an object). When
a user is selected, selectedUser
refers to one of the objects in the array.
- The
users
property references the array itself. - The
selectedUser
property references an item in the array.
In this example, the element has several paths that refer to the second item in the array:
"selectedUser"
"users.1"
(where 1 is the item's index in theusers
array)"users.#5"
(where 5 is the item's immutable key)
By default, Polymer has no way to associate the array paths (like users.1
) with selectedUser
.
For this exact use case, Polymer provides a data binding helper element, <array-selector>
, that
maintains path linkages between an array and a selected item from that array. (<array-selector>
also works when selecting multiple items from an array.)
For other use cases, there's an imperative method,
linkPaths
to associate two paths. When two paths
are linked, an observable change to one path is observable on the other
path, as well.
Related task:
Data flow
Polymer implements the mediator pattern, where a host element manages data flow between itself and its local DOM nodes.
When two elements are connected with a data binding, data changes can flow downward, from host to target, upward, from target to host, or both ways.
When two elements in the local DOM are bound to the same property data appears to flow from one element to the other, but this flow is mediated by the host. A change made by one element propagates up to the host, then the host propagates the change down to the second element.
Data flow is synchronous
Data flow is synchronous. When your code makes an observable change, all of the data flow and property effects from that change occur before the next line of your JavaScript is executed, unless an element explicitly defers action (for example, by calling an asynchronous method).
How data flow is controlled
The type of data flow supported by an individual binding depends on:
- The type of binding annotation used.
- The configuration of the target property.
The two types of data binding annotations are:
-
Automatic, which allows upward (target to host) and downwards (host to target) data flow. Automatic bindings use double curly brackets (
{{ }}
):<my-input value="{{name}}"></my-input>
-
One-way, which only allows downwards data flow. Upward data flow is disabled. One-way bindings use double square brackets (
[[ ]]
).<name-tag name="[[name]]"></name-tag>
The following configuration flags affect data flow to and from target properties:
notify
. A notifying property supports upward data flow. By default, properties are non-notifying, and don't support upward data flow.readOnly
. A read-only property prevents downward data flow. By default, properties are read/write, and support downward data flow.
properties: {
// default prop, read/write, non-notifying.
basicProp: {
},
// read/write, notifying
notifyingProp: {
notify: true
},
// read-only, notifying
fancyProp: {
readOnly: true,
notify: true
}
}
This table shows what kind of data flow is supported by automatic bindings based on the configuration of the target property:
Configuration | Result |
---|---|
|
One-way, downward |
|
No data flow |
|
Two-way |
|
One-way, upward |
By contrast, one-way bindings only allow one-way, downward data flow, so the notify
flag doesn't
affect the outcome:
Configuration | Result |
---|---|
|
One-way, downward |
|
No data flow |
Property configuration only affects the property itself, not subproperties. In particular, binding a property that's an object or array creates shared data between the host and target element. There's no way to prevent either element from mutating a shared object or array. For more information, see Data flow for objects and arrays
Data flow examples
The following examples show the various data flow scenarios described above.
<script>
Polymer({
is: 'custom-element',
properties: {
someProp: {
type: String,
notify: true
}
}
});
</script>
...
<!-- changes to "value" propagate downward to "someProp" on child -->
<!-- changes to "someProp" propagate upward to "value" on host -->
<custom-element some-prop="{{value}}"></custom-element>
<script>
Polymer({
is: 'custom-element',
properties: {
someProp: {
type: String,
notify: true
}
}
});
</script>
...
<!-- changes to "value" propagate downward to "someProp" on child -->
<!-- changes to "someProp" are ignored by host due to square-bracket syntax -->
<custom-element some-prop="[[value]]"></custom-element>
<script>
Polymer({
is: 'custom-element',
properties: {
someProp: String // no notify:true!
}
});
</script>
...
<!-- changes to "value" propagate downward to "someProp" on child -->
<!-- changes to "someProp" are not notified to host due to notify:falsey -->
<custom-element some-prop="{{value}}"></custom-element>
<script>
Polymer({
is: 'custom-element',
properties: {
someProp: {
type: String,
notify: true,
readOnly: true
}
}
});
</script>
...
<!-- changes to "value" are ignored by child due to readOnly:true -->
<!-- changes to "someProp" propagate upward to "value" on host -->
<custom-element some-prop="{{value}}"></custom-element>
<script>
Polymer({
is: 'custom-element',
properties: {
someProp: {
type: String,
notify: true,
readOnly: true
}
}
});
</script>
...
<!-- changes to "value" are ignored by child due to readOnly:true -->
<!-- changes to "someProp" are ignored by host due to square-bracket syntax -->
<!-- binding serves no purpose -->
<custom-element some-prop="[[value]]"></custom-element>
Upward and downward data flow
Since the host element manages data flow, it can directly interact with the target element. The host propagates data downward by setting the target element’s properties or invoking its methods.
- When a property changes on the host element, it sets the corresponding property on the target element, triggering the associated property effects.
Polymer elements use events to propagate data upward. The target element fires a non-bubbling event when an observable change occurs. (Change events are described in more detail in Change notification events.)
For two-way bindings, the host element listens for these change events and propagates the changes—for example, setting a property and invoking any related property effects. The property effects may include:
- Updating data bindings to propagate changes to sibling elements.
- Generating another change event to propagate the change upward.
- A property change on the target element causes a property change event to fire.
- The host element receives the event and sets the corresponding property, invoking the related property effects.
For one-way binding annotations, the host doesn't create a change listener, so upward data changes aren't propagated.
Data flow for objects and arrays
For object and array properties, data flow is a little more complicated. An object or array can be referenced by multiple elements, and there's no way to prevent one element from mutating a shared array or changing a subproperty of an object.
As a result, Polymer treats the contents of arrays and objects as always being available for two- way binding. That is:
- Data updates always flow downwards, even if the target property is marked read-only.
- Change events for upward data flow are always fired, even if the target property is not marked as notifying.
Since one-way binding annotations don't create an event listener, they prevent these change notifications from being propagated to the host element.
Change notification events
An element fires a change notification event when one of the following observable changes occurs:
-
A change to a notifying property.
-
A subproperty change.
-
An array mutation.
The event's type property indicates which property changed: it follows a naming convention of
property-changed
, where property
is the property
name, in dash case (so changing this.firstName
fires first-name-changed
).
You can manually attach a property-changed
listener to an element to
notify external elements, frameworks, or libraries of property changes.
The contents of the event vary depending on the change.
-
For a property change, the new value of the property is included in the
detail.value
field. -
For a subproperty change, the path to the subproperty is included in the
detail.path
field, and the new value is included in thedetail.value
field. -
For an array mutation, the
detail.path
field is an array mutation path, such as "myArray.splices", and thedetail.value
field is a change record, like the one described in the documentation on array observation.When you mutate an array, Polymer also generates a change event for the array's
length
property (for example,detail.path
is "myArray.length
" anddetail.value
is the new length of the array).
Custom change notification events
Native elements like <input>
don't provide the change notification events that Polymer uses for
upward data flow. To support two-way data binding of native input elements, Polymer lets you
associate a custom change notification event with a data binding. For example, when binding to a
text input, you could specify the input
or change
event:
<input value="{{firstName::change}}">
In this example, the firstName
property is bound to the input's value
property. Whenever the
input element fires its change
event, Polymer updates the firstName
property to match the input
value, and invokes any associated property effects. The contents of the event aren't important.
This technique is especially useful for native input elements, but can be used to provide two-way binding for any non-Polymer component that exposes a property and fires an event when the property changes.
Related tasks:
Element initialization
When an element initializes its local DOM, it configures the properties of its local DOM children and initializes data bindings.
The host’s values take priority during initialization. For example, when a host property is bound to a target property, if both host and target elements specify default values, the parent's default value is used.
Property effects
Property effects are actions triggered by observable changes to a given property (or path). Property effects include:
- Recomputing computed properties.
- Updating data bindings.
- Reflecting a property value to an attribute on the host element.
- Firing change notification events.
- Invoking observers.
Data bindings
A data binding establishes a connection between data on the host
element and a property or attribute of a target node
in the host's local DOM. You create data
bindings by adding annotations to an element's local DOM template.
Annotations are attribute values set on a target element that include the data binding delimiters
{{ }}
or [[ ]]
.
Two-way property binding:
target-property="{{hostProperty}}"
One-way property binding:
target-property="[[hostProperty]]"
Attribute binding:
target-attribute$="[[hostProperty]]"
You can also use a data binding annotation in the body of an element, which is equivalent to binding
to the element's textContent
property.
<div>{{hostProperty}}</div>
The text inside the delimiters can be one of the following:
- A property or subproperty path (
users
,address.street
). - A computed binding (
_computeName(firstName, lastName, locale)
) - Any of the above, preceded by the negation operator (
!
).
For more information, see Data binding.