Web components, a.k.a. custom elements, are HTML elements that participate in HTML markup. As such they can have attributes:
<my-hello value="world"></my-hello>
But, they are also JavaScript objects, and as such they can have object properties.
let myHello = document.querySelector('my-hello'); myHello.value = 'foo';
And here's the tricky part about that: these are not the same thing! In fact, custom element attributes and properties by default have zero relationship between them, even when they share a name. Here's a live proof of this fact:
Now, to be fair, we can get at the attribute value just fine from JavaScript:
But what if we would also like it to have a value
property?
What should the relationship between attribute and property be like?
- Does updating the attribute always update the property?
- Does updating the property always update the attribute?
- When updates can go either way, does the property read and update the value of the attribute, or do both attribute and property wrap around a private field on the custom element's class?
- When updates can go either way, how to avoid loops where the property updates the attribute, which updates the property, which...
- When is it fine to have just an attribute without a property, or a property without an attribute?
In framework-based code, we typically don't get a say in these things. Frameworks generally like to pretend that attributes and properties are the same thing, and they automatically create code to make sure this is the case. In vanilla custom elements however, not only do we get to decide these things, we must decide them.
Going native
Seasoned developers will intuitively grasp what the sensible relationship between attributes and properties should be. This is because built-in HTML elements all implement similar kinds of relationships between their attributes and their properties. To explore that in depth, I recommend reading Making Web Component properties behave closer to the platform. Without fully restating that article, here's a quick recap:
- Properties can exist independent of an attribute, but an attribute will typically have a related property.
- If changing the attribute updates the property, then updating the property will update the attribute.
- Properties reflect either an internal value of an element, or the value of the corresponding attribute.
- Assigning a value of an invalid type will coerce the value to the right type, instead of rejecting the change.
- Change events are only dispatched for changes by user input, not from programmatic changes to attribute or property.
An easy way to get much of this behavior is to make a property wrap around an attribute:
Notice how updating the property will update the attribute in the HTML representation, and how the property's assigned value is coerced into the attribute's string type. Attributes are always strings.
Into the weeds
Up to this point, things are looking straightforward. But this is web development, things are never as straightforward as they seem. For instance, what boolean attribute value should make a corresponding boolean property become true? The surprising but standard behavior on built-in elements is that any attribute value will be interpreted as true, and only the absence of the attribute will be interpreted as false.
Time for another iteration of our element:
Which leaves us with the last bit of tricky trivia: it's possible for the custom element's class to be instantiated and attached to the element after the property is assigned. In that case the property's setter is never called, and the attribute is not updated.
This can be avoided by reassigning any previously set properties when the element is connected:
In conclusion
If that seems like a lot of work to do a very simple thing, that is because it is. The good news is: we don't have to always do this work.
When we're using web components as framework components in a codebase that we control, we don't have to follow any of these unwritten rules and can keep the web component code as simple as we like. However, when using web components as custom elements to be used in HTML markup then we do well to follow these best practices to avoid surprises, especially when making web components that may be used by others. YMMV.
In the next article, I'll be looking into custom elements that accept input, and how that adds twists to the plot.