Building a Native Web Component with LitElement 🔥

Web Components

“Web Components” is a series of specifications for extending the default set of HTML elements. These custom elements can be used by any HTML page or web application. Web components are supported by all major browsers, and custom elements work quite well with the most popular front-end frameworks.

Creating Custom Elements

Custom elements can be created simply by extending the built-in HTMLElement class. Then, the new element can be registered in the browser’s Custom Element Registry with a new tag, using the customElements API, such as in this example:

class AwesomeButton extends HTMLElement {
// Component definition goes here.
}
customElements.define("awesome-button", AwesomeButton);

This is the vanilla way of defining custom elements, however, it’s more common to use a library that gives extra features and makes component development easier. Some of these libraries are Stencil, Slim.js and Google’s LitElement, which is built by the Polymer team.

LitElement is a super lightweight base class for web components, using native web and Javascript standards, such as the Custom Elements API, the Shadow DOM, and JS template literals. Similar to React, the UI structure can be expressed declaratively, as a function of the component state, however, instead of JSX, LitElement uses lit-html to define and render HTML templates.

In this tutorial, we’ll build a simple keyword tag as a native web component, using LitElement. We will use NPM for dependency management, and Typescript as our language.

Component Requirements

First, let’s define our component specification. We would like to build a simple keyword tag, that can be later used by a keyword editor. The component should display a single keyword, and a small, clickable “×” icon to remove itself from the set. It should look something like this:

The element needs a keyword string as an input, and it emits a removeClicked event along with the keyword, when the user clicks on the remove icon. This will be our simple component interface. Let’s get started! 🔥

Setting up the Development Environment

For this tutorial, let’s just use StackBlitz. It’s an online IDE for web applications, powered by Visual Studio Code. It automagically takes care of:

  • installing our NPM dependencies,
  • compiling our project,
  • importing our code into the HTML page, and
  • serving the website and hot-reloading.

Welcome to 2019, you don’t even need to leave the browser. 🎉

Click here to get started with a new Typescript project.

Adding Dependencies and our index.html

To build a web component using LitElement, first, we need to add it to our dependencies. To make sure that our component will work in unsupported browsers too, we should also add webcomponentsjs, which contains all the necessary polyfills. As of writing this article, the latest versions are 2.1.0 and 2.2.7, respectively. Change the name of the package, and add the following dependencies to our package.json file:

  • lit-element”: “2.1.0”,
  • @webcomponents/webcomponentsjs”: “2.2.7”.
/// package.json{
"name": "my-web-components",
"version": "0.0.1",
"private": true,
"dependencies": {
"lit-element": "2.1.0",
"@webcomponents/webcomponentsjs": "2.2.7"
}
}

Let’s create our index.html, and import the polyfills using script tags, to support old browsers. Also, let’s add some heading to make sure our page preview updates properly.

<!doctype html>
<html>
<head>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<title>Web Components Example</title>
</head>
<body>
<h1>LitElement Keyword Tag 🔥</h1>
</body>
</html>

Note: The index.ts file in our project will automatically be compiled and imported for us by StackBlitz, otherwise we would need to take care of it ourselves. Of course, in production, components should be lazy-loaded as they’re rendered. ☝🏼

Building the Component

Let’s add a new keyword-tag.ts file to our project. We need to import the following dependencies:

  • LitElement — the base class of our custom element,
  • customElement — a class decorator that takes care of registering our component in the browser’s Custom Element Registry.

Then, let’s add, and export our component class, which extends LitElement. Let’s use the customElement class decorator to register our component as “keyword-tag”. Please note that custom element names must include a hyphen, by definition.

So far, our keyword-tag.ts should look like this:

/// keyword-tag.tsimport { LitElement, customElement } from "lit-element";@customElement('keyword-tag')
export class KeywordTag extends LitElement {
}

Note: Decorators are still experimental features of Typescript. If you’re using a local IDE, you’ll need to enable the experimentalDecorators compiler option in your tsconfig.json file.

Component Properties

Properties are the inputs of components, now we have only one: the keyword string we would like to display. Let’s add this as a class property, along with the property decorator. The decorator is needed to identify keyword as part of the component state. As such, it will be observed, and the element’s relevant parts will be re-rendered, when there’s a new value. We also specify the property type in the decorator’s parameter — see here why.

/// part of keyword-tag.ts@customElement('keyword-tag')
export class KeywordTag extends LitElement {
@property({ type: String })
keyword = "";
}

Component Template

Now, let’s add a render function to our component. Optimally, the render function is a pure function of the component state. The function returns a lit-html template result. Here’s a cheat sheet for the template literal syntax. 😉

/// part of keyword-tag.tsrender() {
return html`
<div class="tag">
<span class="word">
${this.keyword}
</span>
<span class="remove-icon">
×
</span>
</div>
`;
}

There’s a couple of things to mention here.

Note that we’re using a standard, tagged template literal. Similar to JSX, we can add expressions — for example, component properties, conditions, and loops. Here’s a great article that compares the rendering performance of Preact and lit-html.

Also, everything returned by the render function will be rendered into the component’s shadow DOM, under the host element <keyword-tag>. Therefore, all CSS class names will be scoped to the component’s shadow root, so we don’t need to worry about name collision and style leaks. Read more about the Shadow DOM here.

Component Styles

There are 3 ways to style components: in a static styles property of the component class (this is recommended), inside a <style> element in the component’s HTML template, or in a dedicated stylesheet, referenced by the component’s template by a <link> tag. All 3 methods have their advantages, you can find more information about them here.

Now, we will define the component styles in a static styles property, which is the recommended way. This is a static function that returns a lit-html CSS result. After importing css from “lit-element”, let’s add the static styles function to our component class:

/// part of keyword-tag.tsstatic get styles() {
return css`
:host {
display: inline-flex;
cursor: default;
}
.tag {
display: flex;
flex-direction: row;
align-items: center;
padding: 5px 10px;
border-radius: 20px;
background-color: #2196F3; /* fallback */
background: linear-gradient(115deg, #2196F3 0%, #8FCDFF 100%);
/* LitElement colors :) */
background-attachment: fixed;
/* This will give a cool effect. :) */
font-size: 0.8rem;
color: #FFFFFF;
}
.word {
font-weight: bold;
text-transform: lowercase;
}
.remove-icon {
padding: 3px;
cursor: pointer;
margin-left: 5px;
}
`;
}

The :host selector will select our component’s shadow root, the <keyword-tag> element. Styles in this rule are “visible”, and can be easily overridden from outside.

Note: The styles defined here can be changed by the component’s users in multiple ways. For example, inheritable CSS properties (such as font-size, by default) pierce the shadow DOM boundary. The common practice is to let the users configure the component’s internal styles is to use custom CSS properties (a.k.a. CSS variables). The values of these properties also go through the shadow DOM boundary.

Read more here about shadow DOM styling, both as a component developer and consumer.

Component Events

The last thing to do is to dispatch a custom event when the remove icon is clicked. For this, we need to listen to the remove icon’s click event. The most convenient way is to use the lit-html “@event” notation declaratively, in the element’s template. Let’s add an event listener to the remove icon’s click event:

/// part of render function in keyword-tag.tsrender () {
return html`
...
<span
class="remove-icon"
@click="${this.handleRemoveIconClick}"
>
...
`;
}

Let’s implement the event handler. When the remove icon is clicked, our component needs to dispatch a custom event with the keyword in it, so the parent knows which keyword to remove from the list.

/// part of keyword-tag.tshandleRemoveIconClick() {
const event = new CustomEvent('remove-clicked', {
detail: {
keyword: this.keyword
}
});
this.dispatchEvent(event);
}

Now we can listen to our component’s custom remove-clicked event. Read more about handling events fired by LitElement based components here.

Using the Component

We’re done with implementing our simple component. 😎 The new custom element can be used by other web components or any HTML page. Let’s import the component file in index.ts:

/// index.tsimport "./keyword-tag";

And finally, use it in index.html:

/// part of index.html<body>
<h1>LitElement Keyword Tag 🔥</h1>
<keyword-tag keyword="litelement"></keyword-tag>
</body>

The page should display our web component. 😊

Next Steps

As homework, you can go ahead and quickly implement a simple <keyword-set> LitElement component that takes an array of keywords, and renders it as a list of <keyword-tag> elements. Listen to the tags’ remove-clicked events to remove them from the list. See a working example here.

Let me know how it goes! 🔥 🤓

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store