lamplightdev

What's the difference between Web Component attributes and properties?

Thu Apr 30 2020

A common confusion with Web Components is the difference between attributes and properties and the relation between them.

Attributes

In common with the built in elements, attributes are strings that are set declaratively on the tag itself:

<my-wc myattribute="Ada"></my-wc>

or set imperatively using setAttribute:

const myWC = document.querySelector('my-wc');
myWC.setAttribute('myattribute', 'Ada');

Properties

Properties on the other hand are values of any type that can be set imperatively directly on the element instance:

const myWC = document.querySelector('my-wc');
myWC.myattribute = 'Lovelace';
// the value could be any type - string, number, boolean, object, function etc.

Relationship between attributes and properties

By default there is none - they can co-exist with the same name and changing one has no effect on the other:

console.log(myWC.getAttribute('myattribute')); // 'Ada'
console.log(myWC.myattribute)); // 'Lovelace'

Of course that can be confusing in contrast with most of the built in elements where there is a relationship between attributes and properties with the same name. There are generally two scenarios:

  1. The declared attribute initialises a property with the same name, but subsequent changes to the attribute have no effect on the property, and likewise changes to the property are not reflected in an updated attribute value. This is how the value attribute/property pairing works on the standard input element.

  2. The attribute and property are always kept in sync - any changes to the property are reflected in the attribute and vice-versa. An example of this is the id attribute/property pairing of all elements.

So how do we reproduce these two scenarios on our new element which has no attribute/property relationship by default? To do this we need to manually set up the relationship ourselves.

Using the attribute as a property initialiser

This is the simplest case - all we need to do is check the value of the attribute when the element is added to the DOM - the property will then take the value of the attribute currently defined (either in the markup, or set using setAttribute on a programmatically defined instance):

class MyWC extends HTMLElement {
constructor() {
super();

this.myattribute = 'Kevin'; // our default property value
}

connectedCallback() {
const attributeValue = this.getAttribute('myattribute');
// Note non-existant attributes will return null
if (attributeValue !== null) {
this.myattribute = attributeValue;
}
}
}

Keeping the attribute and property in sync

This scenario takes more set up - we need to monitor the attribute and property for changes so we can mirror the values to each other:

class MyWC extends HTMLElement {
// ensure `attributeChangedCallback` is called when our attribute changes:
static get observedAttributes() {
return ['myattribute'];
}

constructor() {
super();

// We need to store our property value in a new object - without this
// we can't use the getter/setters we need below

this._props = {
myattribute: 'Kevin',
};
}

connectedCallback() {
const attributeValue = this.getAttribute('myattribute');
// Note non-existant attributes will return null
if (attributeValue !== null) {
this._props.myattribute = attributeValue;
}
}

get myattribute() {
// return our property value
return this._props.myattribute;
}

set myattribute(value) {
// set our property value
this._props.myattribute = value;

// update our attribute of the same name
this.setAttribute('myattribute', value);
}

attributeChangedCallback(name, oldValue, newValue) {
if (name === 'myattribute') {
// when our attribute changes update our property value
// we can't set the property using the setter (i.e. this.myattribute = value)
// as this would cause an infinite loop
this._props.myattribute = newValue;
}
}
}

Which scenario should I choose?

Initialising properties from attributes is the simplest choice, and the one I would recommend unless you have a reason not to. However there are times when it's useful to keep the attribute value in sync with the property value - a common case is being able to use CSS attribute selectors to match elements depending on their current state - in which case the second method is required.


Did you enjoy this post?

I'd love to know - send me a message on twitter @lamplightdev or sign up for my occasional newsletter on Web Components and Progressive Enhancement - your email will never be shared with anyone else.