Documentation

Documentation versions (currently viewing)

Creating Components

How to create reactive web components with Lit.

To build views in Hilla, you use web components, a native technology that allows you to create custom HTML elements. Web components are almost like a framework built into the browser, and it’s faster and smaller than many component authoring libraries or frameworks, while still sharing their advantages.

The Lit library is a convenient way of authoring web components, and it’s recommended over the low-level native browser APIs.

Lit provides a base class named LitElement, and a set of tools to create reactive web components. Under the hood, it uses lit-html, a tiny library to render and re-render HTML templates in an efficient and declarative way.

Minimal Lit Component

To create a minimal working component, you must do two things: extend the LitElement class and implement a render() method.

import { html, LitElement, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('my-component') // (1)
class MyComponent extends LitElement {
  render(): TemplateResult {
    // (2)
    return html`<h1>My component</h1>`;
  }
}

export default MyComponent;
  1. A web component class must be added to the browser’s custom element registry and assigned a unique element name. The @customElement decorator is a convenient way of doing this with LitElement. It receives an HTML element name that should contain at least one dash - (as required by the HTML specification). For example, my-view, some-fancy-button or vaadin-checkbox.

  2. The render() method defines the component’s HTML template. It should return a TemplateResult instance produced by the html tagged template literal. The string tagged with the html`` tag is displayed in the browser as the component’s content.

Data Binding

A web component produces a view based on the data it receives from outside or preserves within. That data is bound to the component template declaratively, and each time the data is changed, the template is updated. This section describes approaches to receiving and storing the data, and to binding the data to the template.

Fields, Methods, and Accessors

Since a web component is a simple JavaScript class, you can define any number of fields, methods, and accessors you need and call them as you are used to doing. TypeScript, as a superset of JavaScript, also allows you to define private and protected methods.

You don’t need to follow any specific requirements when naming class members, but you may want to follow the convention that any non-public class members should have an underscore _ at the beginning of their names.

Properties

Components can have properties that receive data from the outside (from HTML or an HTML templating system) or preserve the internal state. Changing these properties triggers an asynchronous re-rendering of the component to fill the template with the new data.

Lit provides two decorators that can transform a regular class field into a component data property.

The @property decorator

Makes a field a data property that’s allowed to receive data from the outside. This means that you can send the data to the component in the following ways:

  • Using the html tag from Lit:

    html`<my-component .myProperty=${this.myValue}></my-component>`;
  • As an attribute or property of an instance of the component:

    document.querySelector('my-component').setAttribute('myProperty', this.myValue);
    document.querySelector('my-component').myProperty = this.myValue;

The @state Decorator

Makes a field a data property that contains an internal state of the component. These properties must not be used from outside. For example, you can use it if you need to trigger re-rendering on some events fired by children.

Read more about data properties in the Lit documentation.

Template Bindings

The html function is designed to bind JavaScript values to HTML elements in an obvious and easy way. Lit provides four ways to do this.

Binding to an Attribute

Attribute bindings accept only simple types as their value, including String, Number and Boolean. The value is converted to a string (Boolean values are corrected directly to their string representations) and also reflected in the HTML code. To bind to an attribute, you can use the standard HTML attribute syntax.

html`<my-component attribute="${this.myValue}"></my-component>`;

Binding to a Boolean Attribute

The only difference between binding to an regular attribute and a boolean attribute is that instead of the attribute value being set directly, the attribute is added (with an empty value) or removed depending on the value you set for the attribute (true or false accordingly). To bind a boolean attribute, add a question mark ? before the attribute name. One common use case for this is to conditionally hide certain elements.

html`<my-component ?hidden="${this.isHidden}"></my-component>`;

Binding to a Property

Property bindings accept any types as their value, including arrays and objects. The property value isn’t reflected in the HTML code, unless the property is configured to "reflect to attribute" by the component developer. It’s recommended to use this approach over binding to an attribute, because it’s more performant (no conversion from a string value is needed). To bind to a property, insert a dot . before the property name.

html`<my-component .property=${this.myValue}></my-component>`;

Binding an Event Listener

Event listener bindings accept only functions as their value. When the element fires an event, the function bound for that event name is called with the event object as an argument. To bind an event listener, add an "at" sign, @, before the event name. For example, to listen to click events on a component:

html`<my-component @click=${this.onClick}></my-component>`;
Note
Correct this reference in event listeners
You don’t need to bind this to the event listener method. It refers to the component class in LitElement listeners automatically.

Example

import { html, LitElement, TemplateResult } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';

@customElement('data-binding-view')
class DataBindingView extends LitElement {
  @property() message = '';
  @property() name = '';

  @state() active = false;

  render(): TemplateResult {
    return html`
      <example-template-header ?active=${this.active} name=${this.name} .message=${this.message}>
        <button @click=${this.onClick}>Toggle</button>
      </example-template-header>
    `;
  }

  private onClick() {
    this.active = !this.active;
  }
}

export default DataBindingView;

Learn more about writing templates from the Lit documentation.

Lifecycle

Each component has lifecycle methods that you can override to execute code at a specific time. LitElement adds its own methods to handle the asynchronous render() calls to the standard web component lifecycle.

constructor()

Specificity: JavaScript class

The constructor is invoked when a class is instantiated. For a component, it happens when document.createElement('my-view') is called.

In most cases, you don’t need a constructor(), because LitElement handles everything important. However, if you do create one, try to avoid performing any heavy work; the element might be discarded after invoking the constructor.

Note
The element needs to be connected to run the render() callback.
When the element is created, no render happens.
connectedCallback()

Specificity: Web Components

This callback is invoked each time the element is connected to the DOM.

In most cases, the connectedCallback() is used to set up component-level event listeners, etc.

Note
Use the firstUpdated() callback to execute the code after the first render
Since render() is asynchronous, it’s not over when the connectedCallback() finishes.
Note
Remember to call the super.connectedCallback().
You must call super.connectedCallback() in your callback method. Otherwise, you may lose the default LitElement lifecycle.
disconnectedCallback()

Specificity: Web Components

This callback is invoked each time the element is disconnected from the DOM.

The default role of the disconnectedCallback() is to remove or close everything that’s set up during the connectedCallback().

Note
No DOM access
This callback is invoked after the element is disconnected from the DOM.
Note
Remember to call the super.disconnectedCallback().
You must call super.disconnectedCallback() in your callback method. Otherwise, you may lose the default LitElement lifecycle.
firstUpdated()

Specificity: Lit

This callback is invoked right after the first render is over and before the first updated() callback. It receives an object that contains all changed properties.

updated()

Specificity: Lit

This callback is invoked after each render; for the first render, it’s invoked right after the firstUpdated() callback. Just like firstUpdated(), it receives an object that contains all properties changed after the previous render.

import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('my-button')
class MyButton extends LitElement {
  constructor() {
    super();
    this.onClick = this.onClick.bind(this);
  }

  connectedCallback() {
    this.addEventListener('click', this.onClick);
  }

  disconnectedCallback() {
    this.removeEventListener('click', this.onClick);
  }

  render() {
    return html`<button>Some button</button>`;
  }

  private onClick() {
    console.log('I am clicked');
  }
}

export default MyButton;

Read more about the Lit-specific lifecycle methods in the Lit documentation.

Styling

For styling, LitElement provides a static property styles. You can use either a style sheet imported from a .css file or define an inline style sheet using the css tagged function.

The styles property also accepts an array of style sheets.

import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import myComponentStyles from './my-component.css?inline';

@customElement('my-component')
class MyComponent extends LitElement {
  static styles = [
    myComponentStyles,
    css`
      h1 {
        color: red;
      }
    `,
  ];

  render() {
    return html`<h1>My Component</h1>`;
  }
}

export default MyComponent;

Shadow & Light DOM

You can use two main approaches for web components. You can find more information in the Style Scopes article in the Vaadin documentation.

Shadow DOM

A web component can create a shadow tree within. It’s a separate document fragment, almost inaccessible from the regular DOM, existing as a part of the component and displayed in the browser as a regular element tree. With this, you can create elements with styles independent of the global CSS and HTML content not added to children nodes.

Shadow DOM is enabled by default when using the LitElement class.

import { html, LitElement, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('my-component')
class MyComponent extends LitElement {
  render(): TemplateResult {
    return html`<h1>My Component</h1>`;
  }
}

export default MyComponent;

Light DOM

If you don’t want to use shadow DOM, you can still use light DOM, another part of the DOM controlled by the component’s class. It lacks some of the advantages of shadow DOM, such as scoped CSS or slotted content, but it’s less complex, making development partly easier and performance better.

To enable light DOM for a LitElement component, add a createRenderRoot() method to your component which returns this.

import { html, LitElement, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('my-component')
class MyComponent extends LitElement {
  render(): TemplateResult {
    return html`<h1>My Component</h1>`;
  }

  protected createRenderRoot() {
    return this;
  }
}

export default MyComponent;

Best Practices

Shadow DOM is recommended only for "leaf" components (checkboxes, text fields, combo boxes, etc.) and light DOM for other components such as views or their parts. Deeply nested shadow trees can negatively impact performance.