loading...

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

lamplightdev profile image Chris Haynes Originally published at lamplightdev.com on ・3 min read

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

Keep in mind the above implementations are for string based properties - you'll need a few extra steps for boolean or numeric properties. Non-primitive types (functions, objects, etc.) are not generally reflected to or initialised from attributes.

There is also a convention to camelCase (e.g. myAttribute) property names and kebab-case attribute names (e.g. my-attribute). This conversion would also require some further steps not detailed above.

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.

Posted on by:

lamplightdev profile

Chris Haynes

@lamplightdev

Interested in web performance and web components

Discussion

markdown guide