Directives are objects that customize how Lit renders expressions. Using a directive in your template is like calling a function:
However, instead of simply returning a value to render, a directive gets special access to the underlying DOM associated with its expression. And a directive instance is persisted across multiple renders so it can maintain state. A directive can even update the DOM asynchronously, outside of the main update cycle.
- Implement the directive as a class that extends the Directive class.
- Pass your class to the directive() factory to create a directive function that can be used in Lit template expressions.
When this template is evaluated, the directive function (
hello()) returns a
DirectiveResult object, which instructs Lit to create or update an instance of the directive class (
HelloDirective). Lit then calls methods on the directive instance to run its update logic.
Some directives need to update the DOM asynchronously, outside of the normal update cycle. To create an async directive, extend the
AsyncDirective base class instead of
Directive. See Async directives for details.
Lifecycle of a directivePermalink to “Lifecycle of a directive”
The directive class has a few built-in lifecycle methods:
- The class constructor, for one-time initialization.
render(), for declarative rendering.
update(), for imperative DOM access.
You must implement the
render() callback for all directives. Implementing
update() is optional. The default implementation of
update() calls and returns the value from
Async directives, which can update the DOM outside of the normal update cycle, use some additional lifecycle callbacks. See Async directives for details.
One-time setup: constructor()Permalink to “One-time setup: constructor()”
When Lit encounters a
DirectiveResult in an expression for the first time, it will construct an instance of the corresponding directive class (causing the directive's constructor and any class field initializers to run):
As long as the same directive function is used in the same expression each render, the previous instance is reused, thus the state of the instance persists between renders.
The constructor receives a single
PartInfo object, which provides metadata about the expression the directive was used in. This can be useful for providing error checking in the cases where a directive is designed to be used only in specific types of expressions (see Limiting a directive to one expression type).
Declarative rendering: render()Permalink to “Declarative rendering: render()”
render() method should return the value to render into the DOM. It can return any renderable value, including another
In addition to referring to state on the directive instance, the
render() method can also accept arbitrary arguments passed in to the directive function:
The parameters defined for the
render() method determine the signature of the directive function:
Imperative DOM access: update()Permalink to “Imperative DOM access: update()”
In more advanced use cases, your directive may need to access the underlying DOM and imperatively read from or mutate it. You can achieve this by overriding the
update() callback receives two arguments:
Partobject with an API for directly managing the DOM associated with the expression.
- An array containing the
update() method should return something Lit can render, or the special value
noChange if no re-rendering is required. The
update() callback is quite flexible, but typical uses include:
- Reading data from the DOM, and using it to generate a value to render.
- Imperatively updating the DOM using the
parentNodereference on the
Partobject. In this case,
noChange, indicating that Lit doesn't need to take any further action to render the directive.
Each expression position has its own specific
- ChildPart for expressions in HTML child position.
- AttributePart for expressions in HTML attribute value position.
- BooleanAttributePart for expressions in a boolean attribute value (name prefixed with
- EventPart for expressions in an event listener position (name prefixed with
- PropertyPart for expressions in property value position (name prefixed with
- ElementPart for expressions on the element tag.
In addition to the part-specific metadata contained in
Part types provide access to the DOM
element associated with the expression (or
parentNode, in the case of
ChildPart), which may be directly accessed in
update(). For example:
In addition, the
directive-helpers.js module includes a number of helper functions which act on
Part objects, and can be used to dynamically create, insert, and move parts within a directive's
Calling render() from update()
The default implementation of
update() simply calls and returns the value from
render(). If you override
update() and still want to call
render() to generate a value, you need to call
render() arguments are passed into
update() as an array. You can pass the arguments to
render() like this:
Differences between update() and render()Permalink to “Differences between update() and render()”
update() callback is more powerful than the
render() callback, there is an important distinction: When using the
@lit-labs/ssr package for server-side rendering (SSR), only the
render() method is called on the server. To be compatible with SSR, directives should return values from
render() and only use
update() for logic that requires access to the DOM.
Signaling no changePermalink to “Signaling no change”
Sometimes a directive may have nothing new for Lit to render. You signal this by returning
noChange from the
render() method. This is different from returning
undefined, which causes Lit to clear the
Part associated with the directive. Returning
noChange leaves the previously rendered value in place.
There are several common reasons for returning
- Based on the input values, there's nothing new to render.
update()method updated the DOM imperatively.
- In an async directive, a call to
noChangebecause there's nothing to render yet.
For example, a directive can keep track of the previous values passed in to it, and perform its own dirty checking to determine whether the directive's output needs to be updated. The
render() method can return
noChange to signal that the directive's output doesn't need to be re-rendered.
Limiting a directive to one expression typePermalink to “Limiting a directive to one expression type”
Some directives are only useful in one context, such as an attribute expression or a child expression. If placed in the wrong context, the directive should throw an appropriate error.
For example, the
classMap directive validates that it is only used in an
AttributePart and only for the
Async directivesPermalink to “Async directives”
The previous example directives are synchronous: they return values synchronously from their
update() lifecycle callbacks, so their results are written to the DOM during the component's
Sometimes, you want a directive to be able to update the DOM asynchronously—for example, if it depends on an asynchronous event like a network request.
To set a value asynchronously, a directive needs to extend the AsyncDirective base class, which provides a
Here's an example of a simple async directive that renders a Promise value:
Here, the rendered template shows "Waiting for promise to resolve," followed by the resolved value of the promise, whenever it resolves.
Async directives often need to subscribe to external resources. To prevent memory leaks async directives should unsubscribe or dispose of resources when the directive instance is no longer in use. For this purpose,
AsyncDirective provides the following extra lifecycle callbacks and API:
disconnected(): Called when a directive is no longer in use. Directive instances are disconnected in three cases:
- When the DOM tree the directive is contained in is removed from the DOM
- When the directive's host element is disconnected
- When the expression that produced the directive no longer resolves to the same directive.
After a directive receives a
disconnectedcallback, it should release all resources it may have subscribed to during
renderto prevent memory leaks.
reconnected(): Called when a previously disconnected directive is being returned to use. Because DOM subtrees can be temporarily disconnected and then reconnected again later, a disconnected directive may need to react to being reconnected. Examples of this include when DOM is removed and cached for later use, or when a host element is moved causing a disconnection and reconnection. The
reconnected()callback should always be implemented alongside
disconnected(), in order to restore a disconnected directive back to its working state.
isConnected: Reflects the current connection state of the directive.
Note that it is possible for an
AsyncDirective to continue receiving updates while it is disconnected if its containing tree is re-rendered. Because of this,
render should always check the
this.isConnected flag before subscribing to any long-held resources to prevent memory leaks.
Below is an example of a directive that subscribes to an
Observable and handles disconnection and reconnection appropriately: