HTML templates literals in JavaScript that enable efficient, expressive HTML templating with incremental DOM updates
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Framework for creating custom directives to extend template functionality with lifecycle management and state.
Creates directive functions from directive classes for use in templates.
/**
* Creates a user-facing directive function from a Directive class.
* @param c - Directive class constructor
* @returns Function that creates DirectiveResult instances
*/
function directive<C extends DirectiveClass>(
c: C
): (...values: DirectiveParameters<InstanceType<C>>) => DirectiveResult<C>;Usage Examples:
import { directive, Directive } from 'lit-html/directive.js';
import { html } from 'lit-html';
class MyDirective extends Directive {
render(value: string) {
return value.toUpperCase();
}
}
const myDirective = directive(MyDirective);
const template = html`<div>${myDirective('hello world')}</div>`;
// Renders: <div>HELLO WORLD</div>Abstract base class for creating custom directives with render lifecycle.
/**
* Base class for creating custom directives. Users should extend this class,
* implement `render` and/or `update`, and then pass their subclass to `directive`.
*/
abstract class Directive implements Disconnectable {
constructor(partInfo: PartInfo);
/** Required render method that returns the directive's output */
abstract render(...props: Array<unknown>): unknown;
/** Optional update method called before render with the current Part */
update(part: Part, props: Array<unknown>): unknown;
/** Connection state from the parent part/directive */
get _$isConnected(): boolean;
}Usage Examples:
import { directive, Directive, PartInfo, PartType } from 'lit-html/directive.js';
import { html } from 'lit-html';
class HighlightDirective extends Directive {
constructor(partInfo: PartInfo) {
super(partInfo);
if (partInfo.type !== PartType.CHILD) {
throw new Error('highlight() can only be used in text expressions');
}
}
render(text: string, color: string = 'yellow') {
return html`<mark style="background-color: ${color}">${text}</mark>`;
}
}
const highlight = directive(HighlightDirective);
// Usage in templates
const template = html`
<p>This is ${highlight('important text', 'lightblue')} in a paragraph.</p>
`;Extended directive class with async capabilities and connection lifecycle management.
/**
* An abstract `Directive` base class whose `disconnected` method will be
* called when the part containing the directive is cleared or disconnected.
*/
abstract class AsyncDirective extends Directive {
/** The connection state for this Directive */
isConnected: boolean;
/** Sets the value of the directive's Part outside the normal update/render lifecycle */
setValue(value: unknown): void;
/** Called when the directive is disconnected from the DOM */
protected disconnected(): void;
/** Called when the directive is reconnected to the DOM */
protected reconnected(): void;
}Usage Examples:
import { AsyncDirective, directive } from 'lit-html/async-directive.js';
import { html } from 'lit-html';
class TimerDirective extends AsyncDirective {
private timer?: number;
render(interval: number) {
return 0; // Initial value
}
override update(part: Part, [interval]: [number]) {
// Set up timer when connected
if (this.isConnected && !this.timer) {
let count = 0;
this.timer = setInterval(() => {
this.setValue(++count);
}, interval);
}
return this.render(interval);
}
protected override disconnected() {
// Clean up timer when disconnected
if (this.timer) {
clearInterval(this.timer);
this.timer = undefined;
}
}
protected override reconnected() {
// Timer will be recreated in update() when reconnected
}
}
const timer = directive(TimerDirective);
// Usage
const template = html`<div>Timer: ${timer(1000)}</div>`;Information about the part a directive is bound to for validation and behavior.
interface PartInfo {
readonly type: PartType;
}
interface ChildPartInfo {
readonly type: typeof PartType.CHILD;
}
interface AttributePartInfo {
readonly type:
| typeof PartType.ATTRIBUTE
| typeof PartType.PROPERTY
| typeof PartType.BOOLEAN_ATTRIBUTE
| typeof PartType.EVENT;
readonly strings?: ReadonlyArray<string>;
readonly name: string;
readonly tagName: string;
}
interface ElementPartInfo {
readonly type: typeof PartType.ELEMENT;
}
const PartType = {
ATTRIBUTE: 1,
CHILD: 2,
PROPERTY: 3,
BOOLEAN_ATTRIBUTE: 4,
EVENT: 5,
ELEMENT: 6,
} as const;Usage Examples:
import { directive, Directive, PartInfo, PartType } from 'lit-html/directive.js';
class AttributeOnlyDirective extends Directive {
constructor(partInfo: PartInfo) {
super(partInfo);
// Validate this directive is only used on attributes
if (partInfo.type !== PartType.ATTRIBUTE) {
throw new Error('This directive can only be used on attributes');
}
}
render(value: string) {
return value.toLowerCase();
}
}
class ElementDirective extends Directive {
constructor(partInfo: PartInfo) {
super(partInfo);
if (partInfo.type !== PartType.ELEMENT) {
throw new Error('This directive can only be used on elements');
}
}
update(part: ElementPart, [className]: [string]) {
part.element.className = className;
return this.render(className);
}
render(className: string) {
return undefined; // Element directives typically don't render content
}
}Utility functions for working with directives and parts.
/**
* Tests if a value is a primitive value.
* @param value - Value to test
* @returns True if the value is null, undefined, boolean, number, string, symbol, or bigint
*/
function isPrimitive(value: unknown): value is Primitive;
/**
* Tests if a part represents a single expression.
* @param partInfo - Part information to test
* @returns True if the part has no static strings or only empty strings
*/
function isSingleExpression(partInfo: PartInfo): boolean;
/**
* Inserts a ChildPart into the given container ChildPart's DOM.
* @param containerPart - Container part to insert into
* @param refPart - Optional reference part to insert before
* @param part - Optional part to insert, if not provided creates a new one
* @returns The inserted ChildPart
*/
function insertPart(
containerPart: ChildPart,
refPart?: ChildPart,
part?: ChildPart
): ChildPart;
/**
* Gets the committed value from a ChildPart.
* @param part - ChildPart to get value from
* @returns The last committed value
*/
function getCommittedValue(part: ChildPart): unknown;
/**
* Removes a ChildPart from its parent and clears its DOM.
* @param part - ChildPart to remove
*/
function removePart(part: ChildPart): void;
/**
* Sets the committed value on a part.
* @param part - Part to set value on
* @param value - Value to commit
*/
function setCommittedValue(part: Part, value?: unknown): void;
/**
* Sets a value on a ChildPart.
* @param part - ChildPart to set value on
* @param value - Value to set
* @param directiveParent - Optional directive parent for context
* @returns The same ChildPart instance
*/
function setChildPartValue<T extends ChildPart>(
part: T,
value: unknown,
directiveParent?: DirectiveParent
): T;Usage Examples:
import { directive, Directive } from 'lit-html/directive.js';
import {
isPrimitive,
isSingleExpression,
insertPart,
getCommittedValue,
removePart
} from 'lit-html/directive-helpers.js';
class AdvancedDirective extends Directive {
constructor(partInfo: PartInfo) {
super(partInfo);
// Check if this is a single expression binding
if (!isSingleExpression(partInfo)) {
throw new Error('This directive only works with single expressions');
}
}
render(value: unknown) {
// Use helper to check if value is primitive
if (isPrimitive(value)) {
return `Primitive value: ${value}`;
}
return 'Complex value detected';
}
update(part: ChildPart, [value]: [unknown]) {
// Get the previously committed value
const previousValue = getCommittedValue(part);
if (previousValue !== value) {
// Value changed, process update
return this.render(value);
}
// No change, return noChange to skip update
return noChange;
}
}
// Advanced directive that manages child parts
class ListManagerDirective extends Directive {
private childParts: ChildPart[] = [];
render(items: unknown[]) {
// This directive manages its own child parts
return '';
}
update(part: ChildPart, [items]: [unknown[]]) {
// Add new parts if needed
while (this.childParts.length < items.length) {
const newPart = insertPart(part);
this.childParts.push(newPart);
}
// Remove excess parts
while (this.childParts.length > items.length) {
const partToRemove = this.childParts.pop()!;
removePart(partToRemove);
}
// Update remaining parts
for (let i = 0; i < items.length; i++) {
setChildPartValue(this.childParts[i], items[i]);
}
return this.render(items);
}
}Directives can maintain internal state across renders:
import { directive, Directive } from 'lit-html/directive.js';
class CounterDirective extends Directive {
private count = 0;
render(increment: number = 1) {
this.count += increment;
return `Count: ${this.count}`;
}
}
const counter = directive(CounterDirective);
// Each use maintains separate state
const template = html`
<div>${counter()}</div> <!-- Count: 1 -->
<div>${counter(2)}</div> <!-- Count: 3 -->
<div>${counter()}</div> <!-- Count: 4 -->
`;Directives that fetch or subscribe to external data:
import { AsyncDirective, directive } from 'lit-html/async-directive.js';
class FetchDirective extends AsyncDirective {
private url?: string;
private abortController?: AbortController;
render(url: string, fallback: unknown = 'Loading...') {
if (url !== this.url) {
this.url = url;
this.fetchData(url);
}
return fallback;
}
private async fetchData(url: string) {
if (!this.isConnected) return;
// 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 rendered value
if (this.isConnected) {
this.setValue(data);
}
} catch (error) {
if (error.name !== 'AbortError' && this.isConnected) {
this.setValue(`Error: ${error.message}`);
}
}
}
protected override disconnected() {
this.abortController?.abort();
}
}
const fetchData = directive(FetchDirective);Directives that work with attribute interpolations:
import { directive, Directive, AttributePart } from 'lit-html/directive.js';
class PrefixDirective extends Directive {
render(prefix: string, value: string) {
return `${prefix}:${value}`;
}
override update(part: AttributePart, [prefix, value]: [string, string]) {
// Handle multi-part attribute bindings
if (part.strings && part.strings.length > 2) {
// This is a multi-part attribute binding
return `${prefix}:${value}`;
}
return this.render(prefix, value);
}
}
const prefix = directive(PrefixDirective);
// Usage in multi-part attribute
const template = html`<div class="base ${prefix('theme', 'dark')} extra"></div>`;interface DirectiveClass {
new (part: PartInfo): Directive;
}
interface DirectiveResult<C extends DirectiveClass = DirectiveClass> {
['_$litDirective$']: C;
values: DirectiveParameters<InstanceType<C>>;
}
type DirectiveParameters<C extends Directive> = Parameters<C['render']>;
interface Disconnectable {
_$parent?: Disconnectable;
_$disconnectableChildren?: Set<Disconnectable>;
_$isConnected: boolean;
}
type Primitive = null | undefined | boolean | number | string | symbol | bigint;// Basic directive system
import { directive, Directive } from 'lit-html/directive.js';
// Async directive system
import { AsyncDirective } from 'lit-html/async-directive.js';
// Directive helpers
import { isPrimitive, isSingleExpression } from 'lit-html/directive-helpers.js';
// Part types and info
import { PartInfo, PartType, AttributePartInfo } from 'lit-html/directive.js';