Web Components with Lit
Lit is a small Javascript library that makes writing web components easier than using the core web standards. This chapter describes some of the main parts of Lit; it is not a comprehensive tutorial.
Using Lit
Lit is a third-party Javascript library, so the first problem we have to solve is how
to include it in our web application. The standard way is to use the npm
package
manager to install it in your project and use a bundler like webkit
to combine the
Lit code with your own for delivery to the browser. In this text, we will avoid that
complexity for now and take a more direct approach of loading a version of the Lit
code hosted on the web.
According to the Lit documentation we can import Lit into our code with the following:
import {LitElement, html, css} from 'https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js';
Lit is distributed as a Javascript module and this line imports three functions from that module via a URL that refers to a hosted version of the code. Loading Lit in this way means that we don't need to use a bundler like Webpack and the code we write can run directly in the browser.
Simple Example
To mirror the example in the Web Components chapter here is a simple example of a Lit element:
import {LitElement, html, css} from 'https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js';
class DemoComponent extends LitElement {
static styles = css`
:host {
display: block;
background-color: red;
width: 200px;
height: 200px;
}`
render () {
return html`
<h2>DEMO</h2>
<p class="demo">I am a demonstration of custom elements!</p>`;
}
};
customElements.define('demo-component', DemoComponent);
The first difference is that our new class extends LitElement
rather than HTMLElement
.
In the plain custom element example, the HTML content of the element was generated in
the connectedCallback
method of the class. Here, the work is done in a special render
method which returns the required HTML content.
The other subtle difference is that this function uses a tagged template string html\
xxx`to create the HTML. [Tagged templates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates)
are a Javascript construct that allows you to
pass the template string through a function (in this case
html`) to generate
the result. In this
case, the result of the html string is a parsed HTML fragment - a set of
elements ready to be inserted into the page. This may seem a minor feature but it makes
custom elements made with Lit a lot more efficient than the style introduced in
the previous chapter.
The style information is also a little different. It is added by setting up a static
variable called styles
and the value is a full CSS stylesheet. In this example,
I have
used the selector :host
which refers to the custom element itself.
A hidden difference with the Lit element is that it will use the Shadow DOM to insert the element content without any additional effort on our part. Since we're using the shadow DOM, the stylesheet will only apply to the contents of this element and won't 'leak' to the rest of the page.
Attributes
Lit handles attributes for the new custom element in a very convenient way. To define
an attribute, I declare a static variable properties
in the class containing a
definition of the attributes that are allowed.
class BookInfo extends LitElement {
static properties = {
title: {},
author: {},
image: {}
}
render() {
return html`
<h2>${this.title}</h2>
<p>By ${this.author}</p>
<img src="${this.image}" alt="book cover">
`;
}
}
customElements.define('book-info', BookInfo);
This can be used for example as:
<book-info title="The Grapes of Wrath"
author="John Steinbeck"
image="https://example.com/image.jpg">
</book-info>
The attribute values are automatically copied into the class properties and can be referenced in the render function.
State Variables
The class properties are actually a lot more powerful than the previous example shows.
Any change to the value of a property will trigger a re-render of the element - the
render()
method will be called and the element will be updated in the page.
It is possible to define properties that are not reflected into attributes for the element. These define internal reactive state and provide a way to have information stored internally to the element that can affect the display.
To illustrate these ideas, we'll develop a counter component that shows a numerical value and increments it every time it is clicked. Here's the basic class:
class CounterBlock extends LitElement {
static properties = {
start: {type: Number},
_count: {state: true}
}
static styles = css`
:host {
display: block;
width: 100px;
height: 100px;
background-color: red;
}
`
render() {
return html`<p>Counter: ${this._count}</p>`
}
}
customElements.define('counter-block', CounterBlock);
This element has one external attribute (start
) and one internal property (_count
).
The attribute start
is declared as being numbers which instructs Lit to try to
parse a number from the attribute value. The _count
property is declared as
a state property which means it will not be reflected as an attribute of the
element - it is an internal reactive state variable.
The render()
method outputs content based on the value of _count
. The
first thing we need to do is to initialise _count
to the value of start
if it was provided or zero if not. We can do this in the connectedCallback
method which is run just before the element is inserted into the DOM.
connectedCallback() {
super.connectedCallback();
this._count = this.start || 0;
}
Note that we must call the connectedCallback method of the super class (LitElement
)
at the start of this method. If you miss that out, you won't see an error but your
element won't work.
You might think to do this in the constructor for the class, however, when the constructor
is called, the attributes have not yet been parsed so we can't get the value
of start
.
The next step is to add an event listener for a click event on the element and increment the counter when it is called. The listener itself is a method on the class. We can then add an event listener in the connectedCallback:
_increment(e) {
this._count ++;
}
connectedCallback() {
super.connectedCallback();
this._count = this.start || 0;
this.addEventListener('click', this._increment);
}
Since the event listener modifies the state variable _count
, a render of the element
will be triggered and we'll see the updated count shown the page. No additional
code is needed to trigger a redraw as it would be for a regular custom element.
This shows how we can maintain internal state that is reflected in the HTML that
is generated. The same is also true of external attributes. If some other code
was to modify the value of the start
attribute for this element, a render would
be triggered (in this case it would not change since start
is not used in the HTML).
Event Handlers on Child Elements
Adding a listener to the custom element is one way to capture user input but often we want to have more kinds of interaction with the child elements. Lit provides a shorthand way to add an event listener for part of the HTML that is generated.
If we wanted to add a button to the counter element that incremented by 10 we could do it this way:
_plus10(e) {
this._count += 10;
e.stopPropagation();
}
render() {
return html`
<p>Counter: ${this._count}</p>
<button @click=${this._plus10}>+10</button>
`;
}
In the render method, the @click
attribute is used to assign the event handler to
the click event on the button element. The same shorthand can be used for any other
event (@keyup
, @mouseenter
, etc).
The _plus10
method just adds 10 to the counter but note the use of e.StopPropagation()
to prevent the event listener on the parent from being called as well.