Creating directives
Directives are functions that can customize how lit-html renders values. Template authors can use directives in their templates like other functions:
However, instead of returning a value to render, the directive controls what gets rendered to its location in the DOM.
Internally, lit-html uses the Part
interface to represent the dynamic DOM associated with a binding. A directive has access to the Part
associated with its binding. For example, it can find the current value of the part and set a new value for the part.
To create a directive, pass a factory function to lit-html's directive
function:
The factory function can take optional arguments for configuration and values to be passed in by the template author.
The returned function is called each time the part is rendered. The part
argument is a Part
object with an API for directly managing the dynamic DOM associated with expressions. Each type of binding has its own specific Part object:
NodePart
for content bindings.AttributePart
for standard attribute bindings.BooleanAttributePart
for boolean attribute bindings.EventPart
for event bindings.PropertyPart
for property bindings.
Each of these part types implement a common API:
value
. Holds the current value of the part.setValue
. Sets the pending value of the part.commit
. Writes the pending value to the DOM. In most cases this happens automatically—this method is only required for advanced use cases, like asynchronous directives. See Asynchronous directives for more information.
Here's an example of a directive that takes a function, and evaluates it in a try/catch block to implement exception-safe expressions:
Now the safe
directive can be used to wrap a function:
This example increments a counter on every render:
The user uses it in a template by passing in an initial value:
Limiting a directive to one binding type
Permalink to “Limiting a directive to one binding type”Some directives are only useful in one context, such as an attribute binding or a content binding. If placed in the wrong context, the directive should throw an appropriate error.
This example shows a directive that should only work in a content binding (that is, a NodePart
).
Asynchronous directives
Permalink to “Asynchronous directives”Directives are invoked during the render process. The previous example directives are synchronous: they call setValue
on their parts before returning, so their results are written to the DOM during the render call.
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.
When a directive sets a value asynchronously, it needs to call the part's commit
method to write the updated value to the DOM.
Here's a trivial example of an asynchronous directive:
Here's an equally trivial example of the directive in use:
Here, the rendered template shows "Waiting for promise to resolve," followed one second later by "Promise is resolved."
Maintaining state between renders
Permalink to “Maintaining state between renders”If your directive needs to maintain state between renders, you can rely on the fact that the Part
object representing a given location in the DOM stays the same between calls to render
. In the renderCounter
example, the part's value serves as the state.
If you need to store more complicated state, you can can use a WeakMap
, using the Part
as a key.
Why a WeakMap? Using a weak map ensures that the Part
objects and state data can be garbage collected when they're no longer in use, preventing a memory leak. For more information, see the MDN page on WeakMap.
Repeating directives in content bindings
Permalink to “Repeating directives in content bindings”Sometimes you want a directive to manage multiple nested parts. For example, a directive that renders a list of items (like repeat
) might create a nested part for each item. Keeping separate parts lets you manipulate them efficiently: for example, you can change the value of a single part without re-rendering the entire list.
To create nested parts, you construct NodePart
instances and associate them with specific locations in the DOM. The section of DOM controlled by a given NodePart
needs to be delimited by static nodes that serve as markers. (lit-html usually uses comment nodes for these markers.)
As shown in the diagram, the nodes managed by the NodePart
appear between its startNode
and endNode
. The following code creates and adds a new, nested part inside an existing part (the "container part").
The end result looks something like this:
The appendIntoPart
method creates the marker nodes and inserts the nested part for you. In some cases, you may need to manually manage the marker nodes (for example, if you're inserting a nested part into the middle of the child list). In this case, you can use code like this:
Putting it all together—the following example directive takes a value and inserts it into the DOM twice by creating two nested parts. As shown in Maintaining state between renders, it uses a WeakMap
to store these nested parts.
The NodePart
class provides a number of other convenience methods, including other methods for adding nested parts, and a clear
method to remove all of the DOM associated with a part. See the NodePart
API docs for details.