Building Web Components with lit-html
In our previous post A introduction to Web Components we learned the basics of the APIs that when used together can create a compelling and reusable way to make UI components. We learned Web Components communicate primarily via properties and events. Web Components can also use the Shadow DOM API to create template and CSS encapsulation. In this post we are going to look into a project ran by Polymer called lit-html.
Lit-html is a small, lightweight templating library. Lit-html uses JavaScript template strings to create expressive, dynamic templates. Alongside of the lit-html library, Polymer has also released a lightweight base Class called lit-element. Using lit-element, we can easily create Web Components with higher level APIs built on the Web Component primitives we discussed in
introduction to Web Components.
lit-element
In this post, we are going to create a counter component using lit-html and lit-element. Here is a sample of what our counter looks like.
Let's start with the minimal code needed to create our Web Component using the base class lit-element.
import { LitElement, html } from 'lit-element';
class XCounter extends LitElement {
render() {
return html`
Hello from lit-element
`;
}
}
customElements.define('x-counter', XCounter);
To create our Web Component, we import the LitElement
class from the lit-element package lit-element
. Extending the LitElement
class will allow us to use the built in functionality to make it easy to construct our component. With lit-element
we implement a method render()
. The render method will be executed whenever a property on our component changes.
The render()
method expects a template to be returned. To create a lit-template we use the html
function to create a tagged template. The html
function allows us to use JavaScript template strings to attach additional behavior that lit-html provides. In this example so far we only render a static value. Next, we need to create our template.
Lit-html templates and event listeners
We need to add the template for our counter component. Lit-html also automatically creates our template with a Shadow DOM instance, so we get CSS encapsulation. On our counter component, we also need to create some event handlers for the click events on our buttons. Let's go ahead and take a look at the code.
import { LitElement, html } from 'lit-element';
class XCounter extends LitElement {
constructor() {
super();
this.value = 0;
}
render() {
return html`
<style>
button,
p {
display: inline-block;
}
</style>
<button @click="${() => this.value--}" aria-label="decrement">-</button>
<p>${this.value}</p>
<button @click="${() => this.value++}" aria-label="increment">+</button>
`;
}
}
customElements.define('x-counter', XCounter);
To create an event listener in lit-html templates, we can declaratively bind to elements. For example to bind to a click event we use the @
symbol with click
. In our the template we are going to add one click event for each button.
<button @click="${() => this.value--}" aria-label="decrement">-</button>
When binding to an event, you can run any JavaScript expression so you can call a method in the class or in our case just this.value--
. Now if we try to run our code, nothing happens. We still need to add some additional information for our value
property.
Properties, Attributes and Decorators
We need to give lit-element some additional metadata to tell it what properties should trigger a re-render on the component. To do this, we will add a static list of properties for lit-html to track.
import { LitElement, html, property } from 'lit-element';
class XCounter extends LitElement {
static get properties() {
return {
value: { type: Number }
};
}
// Alternative syntax, if using TypeScript or Babel experimental decorators and field assignments are available
// @property({type: Number})
// value = 0;
constructor() {
super();
this.value = 0;
}
render() {
return html`
<style>
button,
p {
display: inline-block;
}
</style>
<button @click="${() => this.value--}" aria-label="decrement">-</button>
<p>${this.value}</p>
<button @click="${() => this.value++}" aria-label="increment">+</button>
`;
}
}
customElements.define('x-counter', XCounter);
The static property list allows lit-html to track which properties to trigger re-renders as well as what properties to expose to consumers of our components.
The alternative syntax @property
, is a decorator provided by lit-element. The @property
decorator with the field initialization syntax is a shorthand over using the static list syntax. If you are using lit-html with TypeScript you can use this shorter syntax. If using Babel you will have to enable some experimental settings.
const counter = document.querySelector('x-counter');
counter.value = 10; // will trigger the re-render
The second behavior of the static list describes how the corresponding attribute value should be parsed as. For example if we set the value via the HTML,
<x-counter value="5"></x-counter>
With HTML attributes the attribute value is always treated as a string. In our static property list we are explicitly telling the lit-element when it reads the string '5' from the HTML it should parse it as a number type instead.
Custom Events
Just like events with vanilla custom elements, lit-element uses the same event mechanism.
import { LitElement, html, property } from 'lit-element';
class XCounter extends LitElement {
static get properties() {
return {
value: { type: Number }
};
}
constructor() {
super();
this.value = 0;
}
render() {
return html`
<style>
button,
p {
display: inline-block;
}
</style>
<button @click="${() => this.decrement()}" aria-label="decrement">
-
</button>
<p>${this.value}</p>
<button @click="${() => this.increment()}" aria-label="increment">
+
</button>
`;
}
decrement() {
this.value--;
this._valueChanged();
}
increment() {
this.value++;
this._valueChanged();
}
_valueChanged() {
// Fire a custom event for others to listen to
this.dispatchEvent(new CustomEvent('valueChange', { detail: this.value }));
}
}
customElements.define('x-counter', XCounter);
In our final version of our counter component, we refactored to have a increment()
and decrement()
method. When the click events call these methods, they trigger the _valueChanged()
method to be called which triggers our standard custom event for Custom Elements.
const counter = document.querySelector('x-counter');
counter.value = 10;
counter.addEventListener('valueChange', e => console.log(e));
Now we can listen to our custom valueChange
event just like any other DOM event.
Binding to other Web Components with lit-html
We have covered how to build a Web Component with lit-html and lit-element. Next we are going to cover how to use lit-html to declaratively bind and use other Web Components.
In our example, we are going to create a top-level app component that will declaratively use our x-counter
component.
import { LitElement, html, property } from 'lit-element';
import 'counter.js';
class XApp extends LitElement {
render() {
return html`
<x-counter></x-counter>
`;
}
log(e) {
console.log(e);
}
}
customElements.define('x-app', XApp);
Now in our app template, we need to listen to the valueChange
event. To do this, we bind just as we would with a click event. @valueChange=${(e) => this.log(e)}
import { LitElement, html, property } from 'lit-element';
import 'counter.js';
class XApp extends LitElement {
render() {
return html`
<x-counter @valueChange=${e => this.log(e)}> </x-counter>
`;
}
log(e) {
console.log(e);
}
}
customElements.define('x-app', XApp);
If we want to bind/set to properties, we use a slightly different syntax. We can bind to properties by prefixing with a .value="${this.customValue}
.
import { LitElement, html, property } from 'lit-element';
import 'counter.js';
class XApp extends LitElement {
constructor() {
super();
this.customValue = 5;
}
render() {
return html`
<x-counter
@valueChange=${e => this.log(e)}
.value="${this.customValue}>
</x-counter>
`;
}
log(e) {
console.log(e);
}
}
customElements.define('x-app', XApp);
Using lit-html's declarative syntax, it is easy to build lightweight Web Components as well as use other existing Web Components in our applications. The full working demo can be found in the link below.
Want to learn more about Web Components in depth? Check out my early release book, Web Component Essentials!