Skip to content

About Lit 2.0

Kevin Schaaf edited this page Apr 2, 2021 · 7 revisions

What is Lit 2.0?

Lit 2.0 is a new major version of the LitElement and lit-html libraries for writing fast, lightweight, close-to-the-platform web components. Lit combines the lit-html templating library and LitElement custom element base class into one easy-to-use library, with a single name, even more modern core, and unified documentation.

Lit's mission is to provide component authors with exactly what they need to make durable, interoperable components that work anywhere HTML does, including within any framework.

Lit 2.0's journey started with issues brainstorming what we could change in major versions of lit-html and lit-element:

Lit 2.0 includes new and updated versions of several libraries:

  • lit 2.0: The new primary home of Lit which includes everything from lit-html and lit-element
  • lit-html 2.0: Improvements in perf and code size. SSR-ready. New features and new dev mode build.
  • lit-element 3.0: Improvements in perf and code size. SSR-ready. New features and new dev mode build.
  • @lit/reactive-element: The new home and name of LitElement's underlying base class (which used to be UpdatingElement). We've broken this library out so that it's easier for other rendering libraries to use the core reactive properties and reactive lifecycle of LitElement. Also includes the new Reactive Controller interface.
  • @lit/localize: A library and command-line tool for localizing web applications built using Lit.
  • @lit/localize-tools: Localization tooling for use with @lit/localize.
  • @lit/virtualizer: List virtualization

It also includes a new "labs" org where we we're publishing new features or libraries we're incubating while collecting feedback:

  • @lit-labs/ssr: A server package for rendering Lit templates and components on the server.
  • @lit-labs/ssr-cient: A set of client-side support modules for rendering Lit components and templates on the server using @lit-labs/ssr.
  • @lit-labs/react: A React component wrapper for web components.
  • @lit-labs/task: A controller for Lit that renders asynchronous tasks.
  • @lit-labs/scoped-registry-mixin: A mixin for LitElement that integrates with the speculative Scoped CustomElementRegistry polyfill.

Backwards compatibility

Even though this is a new major version and contains some breaking changes, we have tried to make the breaking changes as small as possible, and to add new features to the current versions to enable forward-compatibility and ease migration.

For most users the new versions will be drop-in replacements. The main lit-html and LitElement APIs have not changed. The most significant break change is in the lit-html directive definition API, which we changed to make easier to use and more SSR-friendly. We've added most of this API to lit-html 1.4.0.

Motivation

lit-html and LitElement have been stable and their usage has been growing healthily since their initial release 3 years ago. There were no major problems with the current versions that needed fixing, but we saw some opportunities to make improvements with breaking changes. A lot of these were discovered when developing SSR for Lit.

  • lit-html allowed for an extreme amount of customization - you could completely change the syntax and create dynamic parts anywhere. This customization was never well supported in tools though (linters, etc) and wouldn't be supported in SSR. We simplified the codebase and improved perf by removing it
  • Directives were hard to write and make compatible with SSR.
  • Browsers have fixed bugs. In particular working around one Safari template literal bug had a perf impact for all browsers.
  • Modern browsers had to pay for IE support. We wanted to be able to optimize better for modern browsers.

New Features

lit

A new package, named simply lit, is now the preferred entrypoint to both lit-html and LitElement. Most users should only need to install the lit package to start writing components or templates with Lit.

All packages: development builds

All the Lit npm packages now publish production and development builds. The production builds are highly minified, while the development builds are not and include additional error detection and warnings.

The production build is the package default, and development builds are published using npm-standard conditional exports. Most tools like bundlers and dev servers can now select an export condition, like 'development', to use across all packages in a project.

lit-html

Element expressions

Element expressions are bound to elements, similar to an attribute expression, but they don't require an attribute name. Currently, there's no built-in support for passing values to an element expression, so only directives can appear in them.

html`<div ${animate()}></div>`

Class-based directive API

The new class-based directive API makes it easier to write stateful directives that are compatible with SSR. lit-html now takes care of creating a directive instance and associating it with a template part. Storing state is as simple as using a class field.

The new directive API has an SSR-compatible render() callback which returns lit-html values like template results, and a client-side only update() callback that can manipulate DOM directly.

Async Directives

A long-requested feature of directives has been some way to do work when the directive or DOM it's attached to is removed from the document. AsyncDirective is a new base class that enables developers to write disconnected() and reconnected() callbacks.

This is great for use cases like directives that subscribe to external resources like RxJS observables.

Refs

With element expressions we also have a nice place to put a new ref() directive. ref() is a way to set up a reference to an element that's fulfilled once the element is actually rendered.

Refs can be forwarded to other components, or stored locally and used later in places like event handlers.

  private divRef = createRef();
  render() { return html`<div ${ref(this.divRef)}></div>`; }
  onClick() { console.log(divRef.value); }

Static Expressions

Static expressions allow developers to parameterize the static parts of templates. Usually bindings can only appear in a few position: attribute values, children/text, and elements, and the template has to be well-formed with balanced tags.

Static expressions are evaluated before normal template processing, so they can contain any HTML. The requirements for normal dynamic expressions apply after the static expressions have been evaluated. In effect static expressions let developers create new templates at runtime. You can bind to element tag names, attribute names, insert header and footer fragments that are individually unbalanced, but balanced together, and more.

Compiled template support

We've added a new template result type that is intended to be used by compilers and other template systems. This result must be prepared outside of lit-html - like at build time - and lit-html skips template preparation. We plan to publish tools to allow pre-compiling Lit templates to take advantage of this in the future.

Multiple expressions in unquoted attributes

lit-html 2.0 has a more accurate HTML syntax scanner and lifts the restriction that multiple attribute bindings in a single attribute value be quoted. So you can do:

html`<div foo=${a}${b}></div>`

LitElement

Reactive Controllers

Reactive controllers are a new way for helper objects to hook into a component's reactive update lifecycle.

ReactiveElement

Shadow root & styles support

LitElement's code for creating a shadow root, static styles support, the css template tag have been moved into the ReactiveElement base class. This means that LitElement is now entirely focused on using lit-html to render and all other functionality has been moved into ReactiveElement.

If you're building a component with purely imperative rendering or using another template system like Preact, you can extend from ReactiveElement instead of LitElement.

Shadow root rendering can still be opted-out by overriding createRenderRoot() to return this.

static shadowRootOptions

If you need to customize the options passed to attachShadow() you can now specify them in static shadowRootOptions rather than overriding createRenderRoot().

static finalizeStyles()

Classes can override static finalizeStyles() in order to integrate user styles with styles from another source such as a theming system.

Labs

In order to try new ideas and get feedback on them before they are production-ready, we have created a new npm organization called @lit-labs to host experimental packages.

@lit-labs/ssr and @lit-labs/ssr-client

As mentioned above, a large motivator for the changes in Lit 2.0 were driven by a desire to support server-side rendering. The @lit-labs/ssr package includes a node implementation for server-rendering of Lit components and templates. It takes advantage of Lit's declarative tagged string-based templating to avoid needing a full DOM shim on the server, and allows incremental streaming of HTML and efficient hydration and reuse of server-rendered DOM on the client. It takes advantage of declarative shadow root, a new feature that is shipping in Chrome 90, with a polyfill for other browsers.

Although the SSR package will initially remain in labs at Lit 2.0's launch, we've developed and tested the SSR support alongside the new libraries, ensuring that components written with Lit 2.0 today can take advantage of SSR support in the future.

The SSR package still needs some fine tuning, refinement of the end-user server APIs, and a full documentation suite before it's ready for general consumption, but motivated users can kick the tires on it today and provide feedback.

@lit-labs/react

@lit-labs/react is a package of utilities to help use web components in React.

createComponent()

createComponent() creates a React component definition from a web component class. It allows JSX to set properties (not just attributes) on a web component, and sets up event listener properties, since React has no way of adding event listeners generally via JSX.

useController()

useController() wraps a reactive controller as a React hook, allowing you to write cross-framework, reusable units of state and logic with reactive controllers.

@lit-labs/task

The Task reactive controller lets you easily define an asynchronous task as a function, and triggers host element updates as the task completes or fails. It includes a template helper to render different templates depending on the task state.

@lit-labs/scoped-registry-host

The ScopedRegistryHost mixin gives LitElement support for the scoped custom element registry proposal, which allows web components to have their own isolated registry of custom element definitions.

Upgrade Guide

Lit 2.0 is designed to work with most code written for LitElement 2.x and lit-html 1.x. There are a small number of changes required to migrate your code to Lit 2.0. The high-level changes required include:

  1. Updating npm packages and import paths.
  2. Loading polyfill-support script when loading the webcomponents polyfills.
  3. Updating any custom directive implementations to use new class-based API and associated helpers
  4. Updating code to renamed APIs.
  5. Adapting to minor breaking changes, mostly in uncommon cases.

The following sections will go through each of these changes in detail.

Update packages and import paths

Use the lit package

Lit 2.0 ships with a one-stop-shop lit package, which consolidates lit-html and lit-element into an easy-to-use package. Use the following commands to upgrade:

npm uninstall lit-element lit-html
npm install lit

And re-write your module imports appropriately:

From:

import {LitElement, html} from `lit-element`;

To:

import {LitElement, html} from `lit`;

Although the lit-element@^3 and lit-html@^2 packages should be largely backward-compatible, we recommend updating to the lit package as the other packages are moving towards eventual deprecation.

Update decorator imports

The previous version of lit-element exported all TypeScript decorators from the main module. In Lit 2.0, these have been moved to a separate module, to enable smaller bundle sizes when the decorators are unused.

From:

import {property, customElement} from `lit-element`;

To:

import {property, customElement} from `lit/decorators.js`;

Update directive imports

Built-in lit-html directives are also now exported from the lit package.

From:

import {repeat} from `lit-html/directives/repeat.js`;

To:

import {repeat} from `lit/decorators/repeat.js`;

Load polyfill-support when using webcomponents polyfills

Lit 2.0 still supports the same browsers down to IE11. However, given the broad adoption of Web Components APIs in modern browsers, we have taken the opportunity to move all of the code required for interfacing with the webcomponents polyfills out of the core libraries and into an opt-in support file, so that the tax for supporting older browsers is only paid when required.

In general, any time you use the webcomponents polyfills, you should also load the lit/polyfill-support.js support file once on the page, similar to a polyfill. For example:

<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js">
<script src="node_modules/lit/platform-support.js">

If using @web/test-runner or @web/dev-server with the legacyPlugin for development, adding the following configuration to your web-test-runner.config.js or web-dev-server.config.js file will configure it to automatically inject the support file when needed:

export default {
  ...
  plugins: [
    legacyPlugin({
      polyfills: {
        webcomponents: true,
        custom: [
          {
            name: 'lit-polyfill-support',
            path: 'node_modules/lit/polyfill-support.js',
            test: "!('attachShadow' in Element.prototype)",
            module: false,
          },
        ],
      },
    }),
  ],
};

Update to renamed APIs

The following advanced API's have been renamed in Lit 2.0. It should be safe to simply rename these across your codebase if used:

Previous name New name Notes
UpdatingElement ReactiveElement The base class underpinning LitElement. Naming now aligns with terminology we use to describe its reactive lifecycle.
@internalProperty @state Decorator for LitElement / ReactiveElement used to denote private state that trigger updates, as opposed to public properties on the element settable by the user which use the @property decorator.
static getStyles() static finalizeStyles(styles) Method on LitElement and ReactiveElement class used for overriding style processing. Note it now also takes an argument reflecting the static styles for the class.
_getUpdateComplete() getUpdateComplete() Method on LitElement and ReactiveElement class used for overriding the updateComplete promise
NodePart ChildPart Typically only used in directive code; see below.

Update custom directive implementations

While the API for using directives should be 100% backward-compatible with 1.x, there is a breaking change to how custom directives are authored. The API change improves ergonomics around making stateful directives while providing a clear pattern for SSR-compatible directives: only render will be called on the server, while update will not be.

Overview of directive API changes

Concept Previous API New API
Code idiom Function that takes directive arguments, and returns function that takes part and returns value Class that extends Directive with update & render methods which accept directive arguments
Declarative rendering Pass value to part.setValue() Return value from render() method
DOM manipulation Implement in directive function Implement in update() method
State Stored in WeakMap keyed on part Stored in class instance fields
Part validation instanceof check on part in every render part.type check in constructor
Async updates part.setValue(v);
part.commit();
Extend AsyncDirective instead of Directive and call this.setValue(v)

Example directive migration

Below is an example of a lit-html 1.x directive, and how to migrate it to the new API:

1.x Directive API:

import {html, directive, Part, NodePart} from 'lit-html';

// State stored in WeakMap
const previousState: WeakMap<Part, number> = new WeakMap();

// Functional-based directive API
export const renderCounter = directive((initialValue: number) => (part: Part) => {
  // When necessary, validate part type each render using `instanceof`
  if (!(part instanceof NodePart)) {
    throw new Error('renderCounter only supports NodePart');
  }
  // Retrieve value from previous state
  let value = previousState.get(part);
  // Update state
  if (value === undefined) {
    value = initialValue;
  } else {
    value++;
  }
  // Store state
  previousState.set(part, value);
  // Update part with new rendering
  part.setValue(html`<p>${value}</p>`);
});

2.0 Directive API:

import {html} from 'lit';
import {directive, Directive, Part, PartInfo, PartType} from 'lit/directive.js';

// Class-based directive API
export class RenderCounter extends Directive {
  // State stored in class field
  value: number | undefined;
  constructor(partInfo: PartInfo) {
    super(partInfo);
    // When necessary, validate part in constructor using `part.type`
    if (partInfo.type !== PartType.CHILD) {
      throw new Error('renderCounter only supports child expressions');
    }
  }
  // Optional: override update to perform any direct DOM manipulation
  update(part: Part, [initialValue]: DirectiveParameters<this>) {
    /* Any imperative updates to DOM/parts would go here */
    return this.render(initialValue);
  }
  // Do SSR-compatible rendering (arguments are passed from call site)
  render(initialValue: number) {
    // Previous state available on class field
    if (this.value === undefined) {
      this.value = initialValue;
    } else {
      this.value++;
    }
    return html`<p>${this.value}</p>`;
  }
}
export const renderCounter = directive(RenderCounter);

Adapt to minor breaking changes

For completeness, the following is a list of minor but notable breaking changes that you may need to adapt your code to. We expect these changes to affect relatively few users.

LitElement

  • The update and render callbacks will only be called when the element is connected to the document. If an element is disconnected while an update is pending, or if an update is requested while the element is disconnected, update callbacks will be called if/when the element is re-connected.
  • For simplicity, requestUpdate no longer returns a Promise. Instead await the updateComplete Promise.
  • Errors that occur during the update cycle were previously squelched to allow subsequent updates to proceed normally. Now errors are re-fired asynchronously so they can be detected. Errors can be observed via an unhandledrejection event handler on window.
  • Creation of shadowRoot via createRenderRoot and support for applying static styles to the shadowRoot has moved from LitElement to ReactiveElement.
  • The createRenderRoot method is now called just before the first update rather than in the constructor. Element code can not assume the renderRoot exists before the element hasUpdated. This change was made for compatibility with server-side rendering.
  • ReactiveElement's initialize method has been removed. This work is now done in the element constructor.
  • The static render method on the LitElement base class has been removed. This was primarily used for implementing ShadyDOM integration, and was not intended as a user-overridable method. ShadyDOM integration is now achieved via the polyfill-support module.
  • When a property declaration is reflect: true and its toAttribute function returns undefined the attribute is now removed where previously it was left unchanged (#872).
  • The dirty check in attributeChangedCallback has been removed. While technically breaking, in practice it should very rarely be (#699).
  • LitElement's adoptStyles method has been removed. Styling is now adopted in createRenderRoot. This method may be overridden to customize this behavior.
  • Removed requestUpdateInternal. The requestUpdate method is now identical to this method and should be used instead.
  • The type of the css function has been changed to CSSResultGroup and is now the same as LitElement.styles. This avoids the need to cast the styles property to any when a subclass sets styles to an Array and its super class set a single value (or visa versa).

lit-html

  • render() no longer clears the container it's rendered to on first render. It now appends to the container by default.
  • Expressions in comments are not rendered or updated.
  • Template caching happens per callsite, not per template-tag/callsize pair. This means some rare forms of highly dynamic template tags are no longer supported.
  • Arrays and other iterables passed to attribute bindings are not specially handled. Arrays will be rendered with their default toString representation. This means that html`<div class=${['a', 'b']}> will render <div class="a,b"> instead of <div class="a b">. To get the old behavior, use array.join(' ').
  • The templateFactory option of RenderOptions has been removed.
  • TemplateProcessor has been removed.
  • Symbols are not converted to a string before mutating DOM, so passing a Symbol to an attribute or text binding will result in an exception.
Clone this wiki locally