Skip to main content Accessibility Feedback

How to detect when attributes change on a Web Component

Yesterday, we looked at the Web Component lifecycle. While Web Components don’t have data reactivity, one of the lifecycle methods built into the Web Component API can be used to detect when an attribute on your custom element changes.

Today, we’re going to learn how to do that. Let’s dig in!

The attributeChangedCallback() method

The attributeChangedCallback() method is part of the Web Component lifecycle, and runs whenever an attribute on the Web Component is added, removed, or changes in value.

It accepts three arguments: the name of the attribute that’s been changed, its oldValue, and its newValue.

/**
 * Runs when the value of an attribute is changed on the component
 * @param  {String} name     The attribute name
 * @param  {String} oldValue The old attribute value
 * @param  {String} newValue The new attribute value
 */
attributeChangedCallback (name, oldValue, newValue) {
	console.log('attribute changed', name, oldValue, newValue, this);
}

For performance reasons, the attributeChangedCallback() method only watches and reacts to attributes you tell it to.

To do that, you create a static observedAttributes property, with an array of attributes to watch as its value.

You can use any attributes you’d like, including non-standard ones. Here, we’ll tell our Web Component to watch for changes to the [text] and [pause] attributes.

// Define the attributes to observe
static observedAttributes = ['text', 'pause'];

/**
 * Runs when the value of an attribute is changed on the component
 * @param  {String} name     The attribute name
 * @param  {String} oldValue The old attribute value
 * @param  {String} newValue The new attribute value
 */
attributeChangedCallback (name, oldValue, newValue) {
	console.log('attribute changed', name, oldValue, newValue, this);
}

Now, we can do something like this…

let count = document.querySelector('wc-count');

// logs "attribute changed" "text" null "You clicked it: {{count}}"
count.setAttribute('text', 'You clicked it: {{count}}');

But if we modify an attribute that’s not on our observedAttributes list, nothing happens.

// Nothing happens
count.setAttribute('id', 'count-1234');

Here’s a demo.

Reacting to attribute changes on your Web Component

Now that we’re detecting our attribute changes, we can actually react to them.

For example, when the [text] attribute is added or modified on the <wc-count> element, we might update the this.text property to match it, and re-render the text in this.button with the updated text.

/**
 * Runs when the value of an attribute is changed on the component
 * @param  {String} name     The attribute name
 * @param  {String} oldValue The old attribute value
 * @param  {String} newValue The new attribute value
 */
attributeChangedCallback (name, oldValue, newValue) {

	// If the [text] attribute, update this.text and render button text
	if (name === 'text') {
		this.text = newValue;
		this.button.textContent = this.text.replace('{{count}}', this.count);
	}

}

And maybe when the [pause] attribute is added, we stop counting clicks and [disable] the button.

/**
 * Runs when the value of an attribute is changed on the component
 * @param  {String} name     The attribute name
 * @param  {String} oldValue The old attribute value
 * @param  {String} newValue The new attribute value
 */
attributeChangedCallback (name, oldValue, newValue) {

	// If the [text] attribute, update this.text and render button text
	if (name === 'text') {
		this.text = newValue;
		this.button.textContent = this.text.replace('{{count}}', this.count);
	}

	// If the [pause] attribute, stop counting
	if (name === 'pause') {
		this.button.removeEventListener('click', this);
		this.button.setAttribute('disabled', '');
	}

}

When the [pause] attribute is removed, we probably want to start counting again.

To do that, we’ll check the value of the newValue parameter. If it’s null, the attribute was removed. Otherwise, it’s been added.

/**
 * Runs when the value of an attribute is changed on the component
 * @param  {String} name     The attribute name
 * @param  {String} oldValue The old attribute value
 * @param  {String} newValue The new attribute value
 */
attributeChangedCallback (name, oldValue, newValue) {

	// If the [text] attribute, update this.text and render button text
	if (name === 'text') {
		this.text = newValue;
		this.button.textContent = this.text.replace('{{count}}', this.count);
	}

	// If the [pause] attribute, stop counting
	if (name === 'pause') {
		if (newValue === null) {
			this.button.addEventListener('click', this);
			this.button.removeAttribute('disabled');
		} else {
			this.button.removeEventListener('click', this);
			this.button.setAttribute('disabled', '');
		}
	}

}

Here’s a demo you can play with.

Organizing your code

If you’re observing multiple attributes, the attributeChangedCallback() can get pretty unruly.

To make it easier to work with, I like to abstract the code to handle those changes into handler methods. I prefix them with handlechange*, where * is the attribute name.

/**
 * Handle [text] attribute changes
 * @param  {String} oldValue The old attribute value
 * @param  {String} newValue The new attribute value
 */
handlechangetext (oldValue, newValue) {
	this.text = newValue;
	this.button.textContent = this.text.replace('{{count}}', this.count);
}

/**
 * Handle [pause] attribute changes
 * @param  {String} oldValue The old attribute value
 * @param  {String} newValue The new attribute value
 */
handlechangepause (oldValue, newValue) {
	if (newValue === null) {
		this.button.addEventListener('click', this);
		this.button.removeAttribute('disabled');
	} else {
		this.button.removeEventListener('click', this);
		this.button.setAttribute('disabled', '');
	}
}

Then, in the attributeChangedCallback() method, I can automatically run the correct method on this by using bracket notation and a template literal. I pass in the oldValue and newValue.

/**
 * Runs when the value of an attribute is changed on the component
 * @param  {String} name     The attribute name
 * @param  {String} oldValue The old attribute value
 * @param  {String} newValue The new attribute value
 */
attributeChangedCallback (name, oldValue, newValue) {
	this[`handlechange${name}`](oldValue, newValue);
}

Here’s one last demo for you.