Understanding attribute property reflection in Lit components
For some reason, I found it very hard to understand attribute reflection when I first started working with Lit. In this article, I want to go over Lit reactive properties step by step to understand attribute reflection. Note: all code examples are going to be in TypeScript.
To understand the attribute reflection it is important to understand the reactive properties of Lit, so let’s examine them.
In Lit we can create reactive properties by using the @property
decorator, let’s create property called name
:
export class SimpleComponent extends LitElement {
@property() name: string = "John";
override render() {
return html`Hello from: ${this.name}`;
}
}
It gets rendered like this (I omitted shadow DOM parts for simplicity):
<simple-component> Hello from: John </simple-component>
There are three ways in which reactive properties can be changed
- Direct value setting from inside
- Direct value setting from outside
- Value setting via DOM element attribute change
Attribute reflection applies in the first two cases when we do direct value setting from inside or outside.
Value setting via DOM element attribute change
Lit allows us to change the value of the reactive property by setting an attribute on the custom elements DOM node like this when attribute name
is added to our custom element in DOM.
<simple-componen name="Jay">
Hello from: Jay
</simple-component>
Also, we can observe the workings of Lit template reactivity, and we can see how subsequent changes to the attribute on DOM element changes rendered output of html
function:
override render() {
return html`Hello from: ${this.name}`;
}
Direct value setting from outside
The second way how reactive property value can be changed is by reassigning its value on the DOM elment:
document.querySelector("simple-component").name = "Nick";
document.querySelector("simple-component").name;
// Output is "Nick"
After the value change the element would be represented in DOM like this:
<simple-component> Hello from: Nick </simple-component>
Notice: how we also can access reactive property value from outside. I call it setting and getting from outside because we are accessing and setting properties via the DOM node of the custom element.
Direct value setting from inside
The third way how reactive property values can be changed is by reassigning their values to the instance of the Lit element class, inside the component code.
export class SimpleComponent extends LitElement {
@property() name: string = "John";
override connected() {
super.connected();
this.name = "Bob";
// After component initialization we are
// changing our reactive name property to "Bob"
}
override render() {
return html`Hello from: ${this.name}`;
}
}
This is how the component would look like in DOM now:
<simple-component> Hello from: Bob </simple-component>
Reactive property value is reflected on the DOM element attribute
Let’s look at the rendered component HTML output again side by side after setting reactive property value in all three ways.
<!-- Value setting via DOM element attribute change -->
<simple-componen name="Jay">
Hello from: Jay
</simple-component>
<!-- Direct value setting from outside -->
<simple-component>
Hello from: Nick
</simple-component>
<!-- Direct value setting from inside -->
<simple-component>
Hello from: Bob
</simple-component>
What do we notice here? It is that in only one case “Value setting via DOM element attribute change” the custom element has an attribute that matches the value of the reactive property inside the component.
override render() {
return html`Hello from: ${this.name}`;
}
@property({reflect: true})
Lit gives us a way how to achieve matching of DOM element attributes to the reactive property values inside the component for two other cases as well when we are doing reactive property value change via “Direct value setting from inside” or “Direct value setting from outside”
All we have to do is pass a flag {reflect: true}
in our @property
decorator
export class SimpleComponent extends LitElement {
@property({ reflect: true }) name: string = "John";
override render() {
return html`Hello from: ${this.name}`;
}
}
Let’s look at the DOM element html output side by side again after all three value-setting cases now with attribute reflection enabled @property({reflect: true})
.
<!-- Value setting via DOM element attribute change -->
<simple-componen name="Jay">
Hello from: Jay
</simple-component>
<!-- Direct value setting from outside -->
<simple-component name="Nick">
Hello from: Nick
</simple-component>
<!-- Direct value setting from inside -->
<simple-component name="Bob">
Hello from: Bob
</simple-component>
What we see now is that in all cases the value of reactive properties is reflected on the DOM element attributes.
How it works behind the scenes is that Lit has watchers on reactive properties, so it knows when their values are changed from inside or outside, so when it detects a change in the property value and if it was created with the flag {reflect: true}
it will call setAttriube
on DOM element and do equivalent to this:
document.querySelecto("simple-component").setAttribute("name", "Jay");
My biggest confusion for understanding the attribute reflection was due to the case of “Value setting via DOM element attribute change” because in this case it seems like there already is attribute reflection but from outside-in where a change to DOM element attribute changes reactive value property inside the component, wheres the flag {reflect: true}
is doing attribute reflection from inside-out where the change to reactive property value inside the component is causing change to DOM element attribute.