Web Components

Building Web Components with lit-html

Cory Rylan

- 5 minutes

Updated

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!

View Demo Code   
 

No spam. Short occasional updates on Web Development articles, videos, and new courses in your inbox.

Related Posts

React JS

Trigger Input Updates with React Controlled Inputs

Learn how to update inputs via the native DOM APIs while using React Controlled Component APIs.

Read Article
Web Components

State of Web Components in 2020

Learn a brief overview on Web Components and the latest tech available to build and distribute components across the Web.

Read Article
Web Components

Understanding Slot Updates with Web Components

The Shadow DOM Slot API allows us to project content into our Web Components and observe updates to the content. This post, we will take a look at some of the unexpected slot behaviors.

Read Article