LitElement Basics

For creating views on the frontend side, Vaadin Fusion uses web components, a native technology that allows creating custom HTML elements. It is almost like a framework built into the browser, and usually faster and smaller than other frameworks, while still sharing their advantages.

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

LitElement provides a base class, 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 LitElement Component

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

Template

The render() method is responsible for rendering and re-rendering a component template. It should return a TemplateResult instance produced by the html tag. The string tagged with the html tag is displayed in the browser as the component’s content.

Learn more about tags and tagged template literals from MDN Web Docs.

Custom Element Definition

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 that with LitElement. It receives an HTML element name that should contain at least one dash -. For example, my-view, some-fancy-button or vaadin-checkbox.

Example

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

@customElement('minimal-view')
class MinimalView extends LitElement {
  render(): TemplateResult {
    return html`<h1>My View</h1>`;
  }
}

export default MinimalView;

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 changes as well. This section describes approaches to receive and store the data, and to bind the data to the template.

Fields, Methods, and Accessors

Since the 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. Typescript as the JavaScript superset also allows defining private and protected methods.

No specific requirements for naming exist, but you may want to follow a 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 HTML templating system) or preserve the internal state. Changing those properties triggers an asynchronous re-render of the component to fill the template with the new data.

LitElement provides two decorators that can transform a regular class field to a component data property.

@property

Makes a field a data property that is allowed to receive data from the outside. It means that you can send the data to the component via one of the following options:

  • Using the html tag from LitElement: html`<my-view .myProperty="${"myValue"}"></my-view>`

  • As an attribute to an instance of the component: document.querySelector('my-view').setAttribute('myProperty', 'myValue').

  • As a property to an instance of the component: document.querySelector('my-view').myProperty = 'myValue'.

@state

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

Read more about data properties in the LitElement documentation.

Template Binding

The html function is designed to assign JavaScript values to HTML elements in an obvious and easy way. LitElement considers four ways to do that:

Assign to an attribute

Only values of simple types can be assigned to an attribute: String, Number or Boolean. The value is converted to the string (Boolean values are corrected directly to their string representations) and also reflected in the HTML code. For that approach, you can use the regular HTML attribute syntax.

Assign to a property

Allows any value type. Value is not reflected in the HTML code. It is recommended to use that approach over the assigning to an attribute because it is simpler and more performant. For that approach, you must add a dot . before the property name: .property.

Assign a boolean attribute

Allows only boolean values. The only difference from the attribute assigning is that the attribute is empty and instead of the direct assigning is removed and added again depending on the value. For that approach, you must add a question mark ? before the property name.

Listen to an event by the name

Allows only functions. When the element fires an event, the function assigned to the name is called with the event as an argument. For that approach, you must add an at sign @ before the event name: @event.

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.

Learn about how to write templates from the LitElement documentation.

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;

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.

List of lifecycle callbacks
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, 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 for setting up component-level event listeners, etc.

Note
Use firstUpdated callback to execute the code after the first render
Since render is asynchronous it does not over when the connectedCallback finishes.
Note
Remember to call the super.connectedCallback()
You must call super.connectedCallback() in your connectedCallback 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 is 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 disconnectedCallback method. Otherwise, you may lose the default LitElement lifecycle.
firstUpdated

Specificity: LitElement

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: LitElement

This callback is invoked after each render; for the first render, it is 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 LitElement-specific lifecycle methods in the LitElement 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 styles from './my-view.css';

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

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

export default MyView;

Shadow and Light DOM

You may use two main approaches for web components. You can find additional information in the Style Scopes article.

Shadow DOM

A web component can create a shadow tree within. It is 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 it, 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 in LitElement.

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

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

export default MyView;

Light DOM

If you don’t want to use the Shadow DOM you may still use the Light DOM, a regular part of the DOM controlled by the component’s class. It lacks Shadow DOM advantages like scoped CSS or non-children HTML but it is lighter and simpler for both development and performance.

To enable Light DOM for the LitElement component, add a createRenderRoot() { return this; } method to your component.

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

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

  protected createRenderRoot(): Element | ShadowRoot {
    return this;
  }
}

export default MyView;

Best Practices

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

Using in Routes

Read more about how to use components together with Vaadin Router in the Vaadin Router documentation.