Observers are methods invoked when observable changes occur to the element's data. There are two basic types of observers:
- Simple observers observe a single property.
- Complex observers can observe one or more properties or paths.
You use different syntax for declaring these two types of observers, but in most cases they function the same way.
Computed properties are virtual properties based on one or more pieces of the element's data. A computed property is generated by a computing function—essentially, a complex observer that returns a value.
Unless otherwise specified, notes about observers apply to simple observers, complex observers, and computed properties.
Observers are synchronous
Like all property effects, observers are synchronous. If the observer is likely to be invoked
frequently, consider deferring time-consuming work, like inserting or removing DOM. For example, you
can use the async
method to defer work, or use the
debounce
method to ensure that a task is only run
once during a given time period.
However, if you handle a data change asynchronously, note that the change data may be out of date by the time you handle it.
Simple observers
Simple observers are declared in the properties
object, and always observe a single property. You
shouldn't assume any particular initialization order for properties: if your observer depends on
multiple properties being initialized, use a complex observer instead.
Simple observers are fired the first time the property becomes defined (!= undefined
), and on
every change thereafter, even if the property becomes undefined.
Simple observers only fire when the property itself changes. They don't fire on subproperty changes, or array mutation. If you need these changes, use a complex observer with a wildcard path, as described in Observe all changes related to a path.
The observer method receives the new and old values of the property as arguments.
Observe a property
Define a simple observer by adding an observer
key to the property's declaration. The
Polymer({
is: 'x-custom',
properties: {
disabled: {
type: Boolean,
observer: '_disabledChanged'
},
highlight: {
observer: '_highlightChanged'
}
},
_disabledChanged: function(newValue, oldValue) {
this.toggleClass('disabled', newValue);
this.highlight = true;
},
_highlightChanged: function() {
this.classList.add('highlight');
this.async(function() {
this.classList.remove('highlight');
}, 300);
}
});
Warning: A single property observer shouldn't rely on any other properties, sub-properties, or paths because the observer can be called while these dependencies are undefined. See Always include dependencies as observer arguments for details.
Complex observers
Complex observers are declared in the observers
array, and can monitor one or more paths. These
paths are called the observer's dependencies.
observers: [
'userListChanged(users.*, filter)'
]
Each dependency represents:
-
A specific property (for example,
firstName
). -
A specific subproperty (for example,
address.street
). -
Mutations on a specific array (for example,
users.splices
). -
All subproperty changes and array mutations below a given path (for example,
users.*
).
The observer method is called with one argument for each dependency. The argument type varies depending on the path being observed.
-
For simple property or subproperty dependencies, the argument is the new value of the property or subproperty.
-
For array mutation or wildcard paths, the argument is a change record describing the change.
Handling of undefined values depends on the number of properties being observed:
-
The initial call to a complex observer is deferred until all of the dependencies are defined (that is, they don't have the value
undefined
). -
For a single property observer, the rules are identical to a simple observer: the observer is called each time an observable change is made to one of the dependencies, even if the new value for the path is
undefined
.
- A multi-property observer is called each time an observable change is
made to one of the dependencies, unless the new value for one of the paths is
undefined
.
Complex observers should only depend on their declared dependencies.
Related task:
- Observe multiple properties or paths
- Observe array changes
- Observe all changes to a path
Observe changes to multiple properties
To observe changes to a set of properties, use the observers
array.
These observers differ from single-property observers in a few ways:
- Observers are not invoked until all dependent properties are defined (
!== undefined
). So each dependent property should have a defaultvalue
defined inproperties
(or otherwise be initialized to a non-undefined
value) to ensure the observer is called. - Observers do not receive
old
values as arguments, only new values. Only single-property observers defined in theproperties
object receive bothold
andnew
values.
Polymer({
is: 'x-custom',
properties: {
preload: Boolean,
src: String,
size: String
},
observers: [
'updateImage(preload, src, size)'
],
updateImage: function(preload, src, size) {
// ... do work using dependent values
}
});
In addition to properties, observers can also observe paths to sub-properties, paths with wildcards, or array changes.
Observe sub-property changes
To observe changes in object sub-properties:
- Define an
observers
array. - Add an item to the
observers
array. The item must be a method name followed by a comma-separated list of one or more paths. For example,onNameChange(dog.name)
for one path, oronNameChange(dog.name, cat.name)
for multiple paths. Each path is a sub-property that you want to observe. - Define the method in your element prototype. When the method is called, the argument to the method is the new value of the sub-property.
In order for Polymer to properly detect the sub-property change, the sub-property must be updated in one of the following two ways:
- Via a property binding.
- By calling
set
.
<dom-module id="x-sub-property-observer">
<template>
<!-- Sub-property is updated via property binding. -->
<input value="{{user.name::input}}">
</template>
<script>
Polymer({
is: 'x-sub-property-observer',
properties: {
user: {
type: Object,
value: function() {
return {};
}
}
},
// Each item of observers array is a method name followed by
// a comma-separated list of one or more paths.
observers: [
'userNameChanged(user.name)'
],
// Each method referenced in observers must be defined in
// element prototype. The argument to the method is the new value
// of the sub-property.
userNameChanged: function(name) {
console.log('new name: ' + name);
},
});
</script>
</dom-module>
Observe array mutations
Use an array mutation observer to call an observer function whenever an array item is added or deleted using Polymer's array mutation methods. Whenever the array is mutated, the observer receives a change record representing the mutation as a set of array splices.
In many cases, you'll want to observe both array mutations and changes to sub-properties of array items, in which case you should use a wildcard path, as described in Observe all changes related to a path.
Avoid native JavaScript array mutation methods. Use Polymer's array mutation methods wherever possible to ensure that elements with registered interest in the array mutations are properly notified. If you can't avoid the native methods, you need to notify Polymer about array changes as described in Using native array mutation methods.
To create a splice observer, specify a path to an array followed by .splices
in your observers
array.
observers: [
'usersAddedOrRemoved(users.splices)'
]
Your observer method should accept a single argument. When your observer method is called, it receives a change record of the mutations that occurred on the array. Each change record provides the following property:
-
indexSplices
. The set of changes that occurred to the array, in terms of array indexes. EachindexSplices
record contains the following properties:index
. Position where the splice started.removed
. Array ofremoved
items.addedCount
. Number of new items inserted atindex
.object
: A reference to the array in question.type
: The string literal 'splice'.
Change record may be undefined. The change record may be undefined the first time the observer is invoked, so your code should guard against this, as shown in the example.
Polymer({
is: 'x-custom',
properties: {
users: {
type: Array,
value: function() {
return [];
}
}
},
observers: [
'usersAddedOrRemoved(users.splices)'
],
usersAddedOrRemoved: function(changeRecord) {
if (changeRecord) {
changeRecord.indexSplices.forEach(function(s) {
s.removed.forEach(function(user) {
console.log(user.name + ' was removed');
});
for (var i=0; i<s.addedCount; i++) {
var index = s.index + i;
var newUser = s.object[index];
console.log('User ' + newUser.name + ' added at index ' + index);
}
}, this);
}
},
ready: function() {
this.push('users', {name: "Jack Aubrey"});
},
});
Track key splices
In some situtations, you may need to know about the immutable opaque keys that Polymer uses to track array items. This is an advanced use case, only required if you're implementing an element like the template repeater.
You can register interest in key additions and deletions by retrieving the
array's Collection
object:
Polymer.Collection.get(array);
If you've registered interest, the change record includes an additional property:
-
keySplices
. The set of changes that occurred to the array in terms of array keys. EachkeySplices
record contains the following properties:added
. Array of added keys.removed
. Array of removed keys.
Template repeaters and key splices. The template repeater (dom-repeat
)
element uses keys internally, so if an array is used by a dom-repeat
,
observers for that array receive the keySplices
property.
Observe all changes related to a path
To call an observer when any (deep) sub-property of an
object or array changes, specify a path with a wildcard (*
).
When you specify a path with a wildcard, the argument passed to your observer is a change record object with the following properties:
path
. Path to the property that changed. Use this to determine whether a property changed, a sub-property changed, or an array was mutated.value
. New value of the path that changed.base
. The object matching the non-wildcard portion of the path.
For array mutations, path
is the path to the array that changed,
followed by .splices
. And the change record includes the indexSplices
and
keySplices
properties described in
Observe array mutations.
<dom-module id="x-deep-observer">
<template>
<input value="{{user.name.first::input}}"
placeholder="First Name">
<input value="{{user.name.last::input}}"
placeholder="Last Name">
</template>
<script>
Polymer({
is: 'x-deep-observer',
properties: {
user: {
type: Object,
value: function() {
return {'name':{}};
}
}
},
observers: [
'userNameChanged(user.name.*)'
],
userNameChanged: function(changeRecord) {
console.log('path: ' + changeRecord.path);
console.log('value: ' + changeRecord.value);
},
});
</script>
</dom-module>
Deep sub-property changes on array items
When a sub-property of an array is modified, changeRecord.path
references
the "key" of the array item that was modified, not the array index. For
example:
console.log(changeRecord.path); // users.#0.name
#0
signifies the key of this example array item. All keys are prefixed
with a number sign (#
) by convention to distinguish them from array indexes.
Keys provide stable references to array items, regardless of any splices
(additions or removals) on the array.
Use the get
method to retrieve an item by path.
var item = this.get('users.#0');
If you need a reference to the index of an array item, you
can retrieve it using indexOf
:
var item = this.get('users.#0');
var index = this.users.indexOf(item);
The following example shows one way to use path manipulation and
get
to retrieve an array item and its index from inside an observer:
// Log user name changes by index
usersChanged(cr) {
// handle paths like 'users.#4.name'
var nameSubpath = cr.path.indexOf('.name');
if (nameSubpath) {
var itempath = cr.path.substring(0, nameSubpath);
var item = this.get(itempath);
var index = cr.base.indexOf(item);
console.log('Item ' + index + ' changed, new name is: ' + item.name);
}
}
Always include dependencies as observer arguments
Observers shouldn't rely on any properties, sub-properties, or paths other than those listed as arguments to the observer. This is because the observer can be called while the other dependencies are still undefined. For example:
properties: {
firstName: {
type: String,
observer: 'nameChanged'
},
lastName: {
type: String
}
},
// WARNING: ANTI-PATTERN! DO NOT USE
nameChanged: function(newFirstName, oldFirstName) {
// this.lastName could be undefined!
console.log('new name:', newFirstName, this.lastName);
}
Note that Polymer doesn't guarantee that properties are initialized in any particular order.
In general, if your observer relies on multiple dependencies, use a multi-property observer and list every dependency as an argument to the observer. This ensures that all dependencies are defined before the observer is called.
properties: {
firstName: {
type: String
},
lastName: {
type: String
}
},
observers: [
'nameChanged(firstName, lastName)'
],
nameChanged: function(firstName, lastName) {
console.log('new name:', firstName, lastName);
}
If you must use a single property and must rely on other properties (for example, if you need access to the old value of the observed property, which you won't be able to access with a multi-property observer), take the following precautions:
- Check that all dependecies are defined
(for example,
if this.lastName !== undefined
) before using them in your observer. - Set default values on the dependencies.
Keep in mind, however, that the observer is only called when one of the
dependencies listed in its arguments changes. For example, if an observer
relies on this.firstName
but does not list it as an argument, the observer
is not called when this.firstName
changes.
Avoid infinite loops when modifying the observed value in an observer
If you change an observed value, be very careful to avoid an infinite loop.
The following code will lead to an infinite loop:
properties: {
firstName: {
type: String,
observer: 'nameChanged'
}
},
// WARNING: ANTI-PATTERN! DO NOT USE
nameChanged: function(newFirstName) {
this.firstName = 'Sr. ' + newFirstName;
// will jump recursively to nameChanged
}
Two examples of techniques that avoid infinite loops are:
-
Input validation (if it's not capitalized, capitalize it)—because it's conditional, it doesn't loop infinitely.
-
Explicitly suppressing the observer:
properties: { firstName: { type: String, observer: 'nameChanged' } }, nameChanged: function(newFirstName) { if (! this.updatingName) { this.updatingName = true; this.firstName = 'Sr. ' + newFirstName; this.updatingName = false; } }
Computed properties
Computed properties are virtual properties computed on the basis of one or more paths. The computing function for a computed property follows the same rules as a complex observer, except that it returns a value, which is used as the value of the computed property.
As with complex observers, the handling of undefined
values depends on the number of properties
being observed. See the description of Complex observers for details.
Define a computed property
Polymer supports virtual properties whose values are calculated from other properties.
To define a computed property, add it to the properties
object with a
computed
key mapping to a computing function:
fullName: {
type: String,
computed: 'computeFullName(first, last)'
}
The function is provided as a string with dependent properties as arguments in parenthesis. The function will be called once for any observable change to the dependent properties.
As with complex observers, the handling of undefined
values depends on the number of properties
being observed.
The computing function is not invoked until all dependent properties are defined
(!== undefined
). So each dependent properties should have a
default value
defined in properties
(or otherwise be initialized to a
non-undefined
value) to ensure the property is computed.
Note: The definition of a computing function looks like the definition of a multi-property observer, and the two act almost identically. The only difference is that the computed property function returns a value that's exposed as a virtual property.
<dom-module id="x-custom">
<template>
My name is <span>{{fullName}}</span>
</template>
<script>
Polymer({
is: 'x-custom',
properties: {
first: String,
last: String,
fullName: {
type: String,
// when `first` or `last` changes `computeFullName` is called once
// and the value it returns is stored as `fullName`
computed: 'computeFullName(first, last)'
}
},
computeFullName: function(first, last) {
return first + ' ' + last;
}
});
</script>
</dom-module>
Arguments to computing functions may be simple properties on the element, as
well as any of the arguments types supported by observers
, including paths,
paths with wildcards, and paths to array splices.
The arguments received by the computing function match those described in the sections referenced above.
Note: If you only need a computed property for a data binding, you can use a computed binding instead. See Computed bindings.