API for creating custom directives to extend template functionality with reusable template logic and stateful behavior.
Creates directive functions from directive classes for use in templates.
/**
* Creates a directive function from a directive class
* The returned function can be used in templates
*/
function directive<T extends DirectiveClass>(directiveClass: T): DirectiveFn<T>;
/** Directive class interface */
interface DirectiveClass {
new (partInfo: PartInfo): Directive;
}
/** Directive function type */
type DirectiveFn<T extends DirectiveClass> = (...args: DirectiveParameters<InstanceType<T>>) => DirectiveResult<T>;
/** Directive parameters type */
type DirectiveParameters<T extends Directive> = Parameters<T['render']>;Usage Examples:
import { directive, Directive } from "lit/directive.js";
import { html } from "lit";
// Simple directive that adds a timestamp
class TimestampDirective extends Directive {
render() {
return new Date().toISOString();
}
}
const timestamp = directive(TimestampDirective);
// Usage in templates
class TimestampComponent extends LitElement {
render() {
return html`
<p>Current time: ${timestamp()}</p>
`;
}
}Base class for creating custom directives with access to template part information.
/**
* Base class for all directives
* Provides access to part information and update lifecycle
*/
class Directive {
/** Constructor receives part information */
constructor(partInfo: PartInfo);
/** Called during template rendering with directive arguments */
render(...args: unknown[]): unknown;
/** Called when directive is updated with new arguments */
update(part: Part, args: unknown[]): unknown;
}Usage Examples:
import { directive, Directive } from "lit/directive.js";
// Directive that formats numbers with locale
class LocaleNumberDirective extends Directive {
render(value: number, locale: string = 'en-US', options?: Intl.NumberFormatOptions) {
return new Intl.NumberFormat(locale, options).format(value);
}
}
const localeNumber = directive(LocaleNumberDirective);
// Usage
class PriceComponent extends LitElement {
@property({ type: Number }) price = 1234.56;
render() {
return html`
<div class="price">
${localeNumber(this.price, 'en-US', {
style: 'currency',
currency: 'USD'
})}
</div>
`;
}
}Extended directive class for asynchronous operations with the ability to update after initial render.
/**
* Base class for directives that update asynchronously
* Provides setValue method for updating after initial render
*/
class AsyncDirective extends Directive {
/** Constructor receives part information */
constructor(partInfo: PartInfo);
/** Updates the directive's value asynchronously */
setValue(value: unknown): void;
/** Called when directive is disconnected from DOM */
disconnected(): void;
/** Called when directive is reconnected to DOM */
reconnected(): void;
}Usage Examples:
import { directive, AsyncDirective } from "lit/async-directive.js";
// Directive that loads and displays remote content
class FetchDirective extends AsyncDirective {
private _url?: string;
private _abortController?: AbortController;
render(url: string) {
if (url !== this._url) {
this._url = url;
this._fetchData(url);
}
return this._url ? "Loading..." : "";
}
private async _fetchData(url: string) {
// Cancel previous request
this._abortController?.abort();
this._abortController = new AbortController();
try {
const response = await fetch(url, {
signal: this._abortController.signal
});
const data = await response.text();
// Update the directive's value
this.setValue(data);
} catch (error) {
if (error.name !== 'AbortError') {
this.setValue(`Error: ${error.message}`);
}
}
}
disconnected() {
// Clean up when directive is removed
this._abortController?.abort();
}
}
const fetch = directive(FetchDirective);
// Usage
class RemoteContent extends LitElement {
@property() apiUrl = "";
render() {
return html`
<div class="content">
${this.apiUrl ? fetch(this.apiUrl) : "No URL provided"}
</div>
`;
}
}import { directive, AsyncDirective } from "lit/async-directive.js";
import { html } from "lit";
// Directive that handles promise states with loading/error UI
class PromiseDirective extends AsyncDirective {
private _promise?: Promise<unknown>;
private _state: 'pending' | 'fulfilled' | 'rejected' = 'pending';
private _value?: unknown;
private _error?: Error;
render(
promise: Promise<unknown>,
loadingTemplate?: unknown,
errorTemplate?: (error: Error) => unknown
) {
if (promise !== this._promise) {
this._promise = promise;
this._state = 'pending';
this._handlePromise(promise, loadingTemplate, errorTemplate);
}
switch (this._state) {
case 'pending':
return loadingTemplate ?? "Loading...";
case 'fulfilled':
return this._value;
case 'rejected':
return errorTemplate?.(this._error!) ?? `Error: ${this._error?.message}`;
}
}
private async _handlePromise(
promise: Promise<unknown>,
loadingTemplate?: unknown,
errorTemplate?: (error: Error) => unknown
) {
try {
const value = await promise;
// Only update if this is still the current promise
if (promise === this._promise) {
this._state = 'fulfilled';
this._value = value;
this.setValue(value);
}
} catch (error) {
if (promise === this._promise) {
this._state = 'rejected';
this._error = error as Error;
const errorContent = errorTemplate?.(this._error) ?? `Error: ${this._error.message}`;
this.setValue(errorContent);
}
}
}
}
const promiseState = directive(PromiseDirective);
// Usage
class AsyncDataComponent extends LitElement {
@property() dataId?: string;
private _loadData(id: string) {
return fetch(`/api/data/${id}`).then(r => r.json());
}
render() {
return html`
<div class="async-content">
${this.dataId ? promiseState(
this._loadData(this.dataId),
html`<div class="spinner">Loading data...</div>`,
(error) => html`<div class="error">Failed to load: ${error.message}</div>`
) : "No data ID provided"}
</div>
`;
}
}Information about the template part where the directive is used.
/**
* Information about the template part passed to directive constructors
*/
interface PartInfo {
/** Type of template part */
readonly type: PartType;
/** Name of attribute/property (for attribute/property parts) */
readonly name?: string;
/** Template strings array (for element parts) */
readonly strings?: ReadonlyArray<string>;
}
/** Template part types */
enum PartType {
ATTRIBUTE = 1, // <div attr=${directive()}>
CHILD = 2, // <div>${directive()}</div>
PROPERTY = 3, // <div .prop=${directive()}>
BOOLEAN_ATTRIBUTE = 4, // <div ?attr=${directive()}>
EVENT = 5, // <div @event=${directive()}>
ELEMENT = 6 // <div ${directive()}>
}Usage Examples:
import { directive, Directive, PartType } from "lit/directive.js";
// Directive that behaves differently based on part type
class ContextAwareDirective extends Directive {
render(value: unknown) {
const { type, name } = this.constructor.partInfo;
switch (type) {
case PartType.ATTRIBUTE:
return `attr-${name}: ${value}`;
case PartType.PROPERTY:
return { [`prop-${name}`]: value };
case PartType.CHILD:
return html`<span>Child: ${value}</span>`;
case PartType.BOOLEAN_ATTRIBUTE:
return Boolean(value);
case PartType.EVENT:
return (event: Event) => {
console.log(`Event ${name}:`, value, event);
};
default:
return value;
}
}
}
const contextAware = directive(ContextAwareDirective);Helper functions for checking value types within directives.
/**
* Checks if a value is a primitive type
*/
function isPrimitive(value: unknown): value is string | number | boolean | bigint | symbol | null | undefined;
/**
* Checks if a value is a template result
*/
function isTemplateResult(value: unknown): value is TemplateResult;
/**
* Checks if a value is a directive result
*/
function isDirectiveResult(value: unknown): value is DirectiveResult;
/**
* Gets the directive class from a directive result
*/
function getDirectiveClass(result: DirectiveResult): DirectiveClass | undefined;Functions for managing template parts within custom directives.
/**
* Inserts a child part into a parent part
*/
function insertPart(containerPart: ChildPart, refPart?: ChildPart): ChildPart;
/**
* Sets the value of a child part
*/
function setChildPartValue<T extends ChildPart>(part: T, value: unknown): T;
/**
* Removes a child part from its parent
*/
function removePart(part: ChildPart): void;
/**
* Clears the content of a child part
*/
function clearPart(part: ChildPart): void;// Good: Avoid work when inputs haven't changed
class EfficientDirective extends Directive {
private _lastValue?: unknown;
private _result?: unknown;
render(value: unknown) {
if (value !== this._lastValue) {
this._lastValue = value;
this._result = this._expensiveOperation(value);
}
return this._result;
}
private _expensiveOperation(value: unknown) {
// Expensive computation here
return processValue(value);
}
}// Good: Clean up resources in async directives
class ResourceDirective extends AsyncDirective {
private _cleanup?: () => void;
render(resource: Resource) {
this._cleanup?.();
this._cleanup = resource.subscribe(value => {
this.setValue(value);
});
return "Loading...";
}
disconnected() {
this._cleanup?.();
this._cleanup = undefined;
}
}// Good: Handle errors gracefully
class SafeDirective extends AsyncDirective {
render(operation: () => Promise<unknown>) {
this._performOperation(operation);
return "Processing...";
}
private async _performOperation(operation: () => Promise<unknown>) {
try {
const result = await operation();
this.setValue(result);
} catch (error) {
console.error("Directive error:", error);
this.setValue(`Error: ${error.message}`);
}
}
}/** Base directive result interface */
interface DirectiveResult<T extends DirectiveClass = DirectiveClass> {
_$litDirective$: T;
values: DirectiveParameters<InstanceType<T>>;
}
/** Directive class constructor interface */
interface DirectiveClass {
new (partInfo: PartInfo): Directive;
}
/** Template part interface */
interface Part {
type: PartType;
element: Element;
name: string | undefined;
strings: ReadonlyArray<string> | undefined;
}
/** Child part interface for content parts */
interface ChildPart extends Part {
type: PartType.CHILD;
startNode: Node;
endNode: Node;
}/** Extract directive parameters from directive class */
type DirectiveParameters<T extends Directive> = Parameters<T['render']>;
/** Union of all part types */
type AnyPart = AttributePart | ChildPart | PropertyPart | BooleanAttributePart | EventPart | ElementPart;
/** Template result union type */
type TemplateResult = HTMLTemplateResult | SVGTemplateResult | MathMLTemplateResult;