or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

core-components.mdcustom-directives.mddom-query-decorators.mdindex.mdproperty-decorators.mdstatic-templates.mdtemplate-directives.md
tile.json

custom-directives.mddocs/

Custom Directives

API for creating custom directives to extend template functionality with reusable template logic and stateful behavior.

Capabilities

Directive Function

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 Directive Class

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>
    `;
  }
}

Async Directive Class

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>
    `;
  }
}

Advanced Async Directive Example

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>
    `;
  }
}

Part Information and Context

Part Info Interface

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);

Directive Helper Functions

Value Type Checking

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;

Part Management

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;

Best Practices

Directive Design Principles

  1. Single Responsibility: Each directive should have one clear purpose
  2. Reusability: Design directives to work in multiple contexts
  3. Performance: Avoid unnecessary work during re-renders
  4. Type Safety: Provide good TypeScript types for directive parameters

Performance Considerations

// 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);
  }
}

Memory Management

// 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;
  }
}

Error Handling

// 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}`);
    }
  }
}

Types

Core Directive Types

/** 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;
}

Helper Types

/** 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;