The Polymer library is in maintenance mode. For new development, we recommend Lit.

You're viewing an older version of the Polymer library documentation. Please see Polymer 3.0 for the latest.

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.

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 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.

Define a simple observer by adding an observer key to the property's declaration. The

Example:

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 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

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 default value defined in properties (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 the properties object receive both old and new values.

Example

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.

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, or onNameChange(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:

Example:

<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>

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. Each indexSplices record contains the following properties:

    • index. Position where the splice started.
    • removed. Array of removed items.
    • addedCount. Number of new items inserted at index.
    • 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.

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"});
  },
});

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. Each keySplices 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.

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.

Example:

<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>

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);
  }
}

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.

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 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.

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.