-
Notifications
You must be signed in to change notification settings - Fork 38
Property Change Events
Does the component raise property change events when — and only when — properties change in response to internal component activity?
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.
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.
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'));
}
}
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.