Problem
Building a Lit Element component, and defining their properties for internal consumption is very well explained. However, in this article we are interested more with how to define the interface between our lit element and its consumer. In other words, we want to examine how to set lit element attributes and properties from outside of the component, and use these in the component. We also want to understand some of the intricacies and quirks associated with this process.
So how do we expose our Lit Element properties?
It’s actually pretty straightforward: Simply define the usual static properties, with the static properties = { ... }
syntax, like so:
export class MyWebComponent extends LitElement {
static properties = {
myString: {type: String},
myObject: {type: Object},
myArray: {type: Array},
};
// ...
}
myString
, myObject
and myArray
will be exposed for use outside the component immediately as attributes.
Intricacies / Quirks
However, there are some things we have to be aware of when building and using these components.
Differentiating between Attributes and Properties
The biggest thing we haven’t addressed so far is how to differentiate between what gets exposed as attributes and what gets exposed as properties.
One potentially confusing aspect of the static properties = { ... }
signature is that it suggests to us that we are defining JavaScript Object properties. This is unfortunately not the case. Rather, all such “properties” are viewed from the outside as attributes, unless designated with the option attribute: false
.
Take the following example:
export class MyWebComponent extends LitElement {
static properties = {
myString: {type: String},
myObject: {type: Object, attribute: false},
myArray: {type: Array, attribute: false},
};
// ...
}
In the above, myString
will remain an attribute, while myObject
and myArray
will be designated as Object properties, with their respective property getters and setters. Our example’s choice to define Objects and Arrays as properties is also in line with recommendations to pass rich data into Web Components using properties rather than attributes.
Objects and Arrays as Attributes need to be passed in as stringified JSON
If we choose to designate a Lit Element property as type: Object
or type: Array
, but keep it as an attribute, we have to be careful to pass in only stringified JSON to these attributes, like so:
<my-web-element objectattribute='{"u":{"a":"b"}}'></my-web-element>
This is because HTML attributes are always strings. This is not as relevant to vanilla HTML, but could be useful when trying to work with some frameworks like React, which do not play nice with Web Component attributes.
Quirk: Attribute naming conventions
Unlike Polymer 3, snake-case to camelcase conversion is not supported in Lit. Instead, a dashless syntax is preferred, and HTML attributes are lowercased by Lit Element, before being matched to similarly lowercased Lit Element property attributes.
So for example, in
<my-element propNAME="xyz"></my-element>
propNAME
will map to pROPname
in the following element:
export class MyWebComponent extends LitElement {
static properties = {
pROPname: {type: String},
}
// ...
}
In general, lowercased strings are preferred for HTML attributes. As an example, to set the myString
attribute from our very first MyWebComponent
component above, we would do the following:
<my-web-component mystring='I am a string'></my-web-component>
Quirk: multiple attributes with identical spelling, but different capitalisations
Because of the way HTML attributes and Lit Element property attributes are matched, multiple attributes with different capitalisations can end up with identical spellings. To resolve such conflicts, the conditions are
- If there are multiple HTML attributes that are identical after lowercasing, then the first attribute will be used, and the rest discarded.
- If there are multiple property attributes that are identical after lowercasing, then the last property attribute will be used, and the rest will retain their original value (usually
undefined
, unless the property attribute has been set some other way internally).
So for example, in
<my-element propname="abc" pROPNAME="xyz"></my-element>
propname
will be used. This will be mapped to pROPNAME
in the following element:
export class MyWebComponent extends LitElement {
static properties = {
propname: {type: String},
pROPNAME: {type: String},
}
render() {
return html`
<p>propname: ${this.propname}</p>
<p>pROPNAME: ${this.pROPNAME}</p>
`
}
}
This will render something along these lines:
propname:
pROPNAME: abc
The same is not true for properties, since these are handled by established JavaScript conventions, making propname
and pROPNAME
different keys.
Quirk: Setting properties without reactivity
When we set static properties = { ... }
we are actually defining our reactive properties.
Not defining a property in this method, however, doesn’t mean that we cannot set it from outside our component. It just means that such a property will not trigger an update, and hence will not cause a re-render.
What’s interesting though is that on a subsequent update of another reactive property, unreactive properties' new values will still be used in the templates.
export class MyWebComponent extends LitElement {
static properties = {
reactiveString: {type: String},
// unreactiveString: {type: String}, // not defined, so not reactive
}
render() {
return html`
<p>reactiveString: ${this.reactiveString}</p>
<p>unreactiveString: ${this.unreactiveString}</p>
`
}
}
If we render <my-web-component></my-web-component>
, then select it and set a property called unreactiveString
on the component:
const el = document.getElementsByTagName('my-web-component')[0]
el.unreactiveString = "I want to be rendered please"
Nothing would show, like so:
reactiveString:
unreactiveString:
But if we then proceeded to update the reactive property:
el.reactiveString = "I am updated next"
This happens:
reactiveString: I am updated next
unreactiveString: I want to be rendered please
That’s because when the render is triggered by the setting of reactiveString
, the template, which relies on unreactiveString
, pulls in unreactiveString
’s updated value as well.
Bonus: firing Lit Element custom events for external use
Going by Web Component conventions, we always pass attributes and properties down, and events up. Events are a crucial way of interacting with the external environment, so let’s show how they can be dispatched from our component.
To do so, we just have to use the this.dispatchEvent
method to dispatch a Custom Event in our element.
export class MyWebComponent extends LitElement {
add() {
this.dispatchEvent(new CustomEvent('add'))
}
render() {
return html`
<button @click=${this.add}>Add</button>
`
}
}
Notice how even though our button
element is within the Shadow DOM due to Lit Element magic, we don’t have to deal with the composed: true
flag usually needed when dealing with Custom Events in the Shadow DOM. That’s because the @event
declarative syntax automatically binds event listeners to the Lit Element component itself.
Demo as Proof
To validate the above, let’s put them all together into a demo project. With this demo we will see how attributes and properties are passed through, including demonstrating our intricacies and quirks in action.
Setup our demo project
There are two ways to setup our demo project, the hard way and the easy way.
Hard way: scaffold a new project with the Open Web Components Generator
Scaffold the project:
npm init @open-wc
✔ What would you like to do today? › Scaffold a new project
✔ What would you like to scaffold? › Web Component
✔ What would you like to add? ›
✔ Would you like to use typescript? › No
✔ What is the tag name of your web component? … my-web-component
Replace the code in the following files:
// my-web-component/src/MyWebComponent.js
import { html, css, LitElement } from 'lit';
export class MyWebComponent extends LitElement {
static properties = {
stringAttribute: {type: String},
numberAttribute: {type: Number},
booleanAttribute: {type: Boolean},
objectProperty: {type: Object, attribute: false},
arrayProperty: {type: Array, attribute: false},
jsonStringifiedObjectAttribute: {type: Object},
jsonStringifiedArrayAttribute: {type: Array},
lowercaseattributecheck: {type: String},
lowercaseattributeCHECK: {type: String},
reactiveCount: {type: Number, attribute: false},
};
constructor() {
super();
}
updateNonReactiveCount() {
this.dispatchEvent(new CustomEvent('update-non-reactive-count', {
detail: {
nonReactiveCount: ++this.nonReactiveCount,
}
}))
}
updateReactiveCount() {
this.dispatchEvent(new CustomEvent('update-reactive-count', {
detail: {
reactiveCount: ++this.reactiveCount,
}
}))
}
render() {
return html`
<p>Welcome to My Web Componet. Let's display some attributes and properties.</p>
<h3>Attributes and Properties</h3>
<ul>
<li>stringAttribute: ${this.stringAttribute}, which is of type ${typeof this.stringAttribute}</li>
<li>numberAttribute: ${this.numberAttribute}, which is of type ${typeof this.numberAttribute}</li>
<li>booleanAttribute: ${this.booleanAttribute}, which is of type ${typeof this.booleanAttribute}</li>
<li>objectProperty: ${JSON.stringify(this.objectProperty)}
<li>arrayProperty: ${JSON.stringify(this.arrayProperty)}
</ul>
<h3>JSON Stringified attributes</h3>
<ul>
<li>jsonStringifiedObjectAttribute: ${JSON.stringify(this.jsonStringifiedObjectAttribute)}</li>
<li>jsonStringifiedArrayAttribute: ${JSON.stringify(this.jsonStringifiedArrayAttribute)}</li>
</ul>
<h3>Attribute naming conventions + identical lowercased attributes</h3>
<ul>
<li>lowercaseattributecheck: ${this.lowercaseattributecheck}</li>
<li>lowercaseattributeCHECK: ${this.lowercaseattributeCHECK}</li>
</ul>
</h3>
<h3>Setting properties without reactivity + sending events</h3>
<ul>
<li>nonReactiveCount: ${this.nonReactiveCount}</li>
<li>reactiveCount: ${this.reactiveCount}</li>
</ul>
<button @click=${this.updateNonReactiveCount}>Update Non Reactive Count</button>
<button @click=${this.updateReactiveCount}>Update Reactive Count</button>
</h3>
`;
}
}
<!-- my-web-component/demo/index.html -->
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<style>
body {
background: #fafafa;
}
</style>
</head>
<body>
<div id="demo"></div>
<script type="module">
import { html, render } from 'lit';
import '../my-web-component.js';
render(
html`
<my-web-component
id="my-web-component-id"
stringattribute="i-am-a-kebab-cased-string"
numberattribute="123"
booleanattribute="booleanattribute"
jsonStringifiedObjectAttribute='{"o": "O"}'
jsonStringifiedArrayAttribute='["a", "b", 3]'
lowercaseattributecheck="i-am-selected"
lowercaseattributeCHECK="i-am-ignored"
></my-web-component>
`,
// note that objectasprop does not work because we set `attribute: false`
document.querySelector('#demo')
);
const el = document.getElementById('my-web-component-id')
el.objectProperty = {'a': 'b'}
el.arrayProperty = [1, 2, 3]
el.addEventListener('update-non-reactive-count', (e) => {
console.log(`updated count: ${JSON.stringify(e.detail)}`)
})
el.addEventListener('update-reactive-count', (e) => {
console.log(`updated count: ${JSON.stringify(e.detail)}`)
})
el.nonReactiveCount = 10
el.reactiveCount = 10
</script>
</body>
</html>
Run the demo, and play around.
cd my-web-component
npm start
Easy way: git clone this repository
git clone git@github.com:joeltok/wc-lit-element-demo-attr-props.git
cd wc-lit-element-demo-attr-props
npm install
npm start
Run the demo, and play around.
Conclusion
Through this tutorial we have seen how we can define the Lit Element component interface, to enable interaction with an external environment, along with some intricacies and quirks.