Skip to content
This repository has been archived by the owner on Jul 26, 2022. It is now read-only.

Property Change Events

Jan Miksovsky edited this page Jan 5, 2017 · 6 revisions

Checklist » API

✓ Property Change Events

Does the component raise property change events when — and only when — properties change in response to internal component activity?

Background

Your component may have properties that can be changed in response to activity that only your component can easily know about. For example:

  • Your component wires up its own event listener for a UI event (e.g., click), and the event listener causes a component property to change.
  • Your component sets a timeout that causes a component property to change.
  • Your component initiates an asynchronous action (e.g., fetch, XMLHttpRequest) that, when completed, will cause a component property to change.

These situations all share the trait that the component knows about them, but the component host does not. In such situations, your component should raise property change events to let the component's host know whenever any affected properties have changed. The standard convention is that an internally-generated change to property called fooBar should raise an event called foo-bar-changed. That is, map the camel case property name to kebab case and add -changed.

However, your component should not raise a property change events in response to external actions:

  • The component's host directly sets a property (or sets an attribute that maps to a property) on the component.
  • The component's host invokes a method on the component that in turn synchronously updates a component property.

In these situations, the component's host can already know that a property is changing, so raising property change events is unnecessary and could negatively affect performance.

Example

Consider a Counter component which maintains a count property and offers a button that increments that count. Clicks on the button count as internal changes, so when the count property changes as a result of a click, the component should raise a count-changed event. However, if the component's host directly sets the count property or count attribute, that should not raise the count-changed event. The host initiated the change, so it doesn't need to be told about it.

const counter = new Counter();
counter.count = 0; // External change to property; shouldn't raise change event.
counter.setAttribute('count', 0); // Ditto

This is standard DOM behavior. For comparison, a standard input element will raise the change event if the user directly changes the input value, but does not raise the change event if the value is programmatically set by the element's host.

Pattern

A natural place to raise property change events is in a property setter, as this colocates related code and ensures the event will be raised consistently. However, a property setter on its own has no way of knowing whether the setter was invoked in response to internal or external activity.

A simple pattern for handling this is to have a component maintain an internal flag that indicates whether current component activity is being performed in response to internal activity, and should therefore raise property change events. In this pattern, the component sets the internal flag at the beginning of any UI event handler, timeout, Promise, or other form of asynchronous callback. At the end of the handler, the flag is cleared.

const raiseChangeEvents = Symbol('raiseChangeEvents');

class Counter extends HTMLElement {
  constructor() {
    this.addEventListener('click', event => {
      this[raiseChangeEvents] = true;
      // Now we can change properties, or call methods that change properties.
      this.count++;
      this[raiseChangeEvents] = false;
    });
  }
}

Elsewhere, the component's property setters can inspect this flag to detect whether it's doing work in response to internal activity, and should therefore raise a property change event.

const countSymbol = Symbol('count');

get count() {
  return this[countSymbol];
}
set count(value) {
  const changed = value !== this[countSymbol];
  this[countSymbol] = value;
  if (changed && this[raiseChangeEvents]) {
    // Property actually changed, and did so in response to internal activity.
    this.dispatchEvent(new CustomEvent('count-changed'));
  }
}

Live demo

Note that property setter above only raises the event if the property is changing as a result of internal activity and the property has actually changed. Locating such logic in the property setter ensures it will be executed consistently, even if that setter is being invoked by handlers for multiple events (e.g., both keyboard and touch/mouse events).

This flag pattern works because event handlers, promises, and timeouts are not reentrant.

Clone this wiki locally