or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

browser-apis.mdbrowser-window.mdcss-styling.mdcustom-elements.mddom-core.mdevent-system.mdfetch-http.mdform-file.mdhtml-elements.mdindex.mdmedia-av.md
tile.json

custom-elements.mddocs/

Custom Elements & Web Components

Web Components support including Custom Element registry, Shadow DOM, and slot functionality. Enables creation of reusable, encapsulated HTML elements.

Capabilities

Custom Element Registry

CustomElementRegistry Class

Registry for custom element definitions.

/**
 * Registry for custom element definitions
 */
class CustomElementRegistry {
  /** Define custom element */
  define(name: string, constructor: CustomElementConstructor, options?: ElementDefinitionOptions): void;
  
  /** Get custom element constructor */
  get(name: string): CustomElementConstructor | undefined;
  
  /** Wait for element definition */
  whenDefined(name: string): Promise<CustomElementConstructor>;
  
  /** Upgrade element */
  upgrade(root: Node): void;
}

interface ElementDefinitionOptions {
  extends?: string;
}

type CustomElementConstructor = new () => HTMLElement;

Shadow DOM

ShadowRoot Class

Shadow DOM root for encapsulation.

/**
 * Shadow DOM root for encapsulation
 */
class ShadowRoot extends DocumentFragment {
  /** Shadow root mode */
  readonly mode: ShadowRootMode;
  
  /** Host element */
  readonly host: Element;
  
  /** Delegates focus flag */
  readonly delegatesFocus: boolean;
  
  /** Slot assignment mode */
  readonly slotAssignment: SlotAssignmentMode;
  
  /** Inner HTML */
  innerHTML: string;
  
  /** Active element */
  readonly activeElement: Element | null;
  
  /** Style sheets */
  readonly styleSheets: StyleSheetList;
  
  /** Get element by ID */
  getElementById(elementId: string): Element | null;
  
  /** Get elements by tag name */
  getElementsByTagName(qualifiedName: string): HTMLCollection;
  
  /** Get elements by class name */
  getElementsByClassName(classNames: string): HTMLCollection;
}

type ShadowRootMode = 'open' | 'closed';
type SlotAssignmentMode = 'manual' | 'named';

Slot Elements

HTMLSlotElement Class

Slot element for content projection.

/**
 * Slot element for content projection
 */
class HTMLSlotElement extends HTMLElement {
  /** Slot name */
  name: string;
  
  /** Get assigned nodes */
  assignedNodes(options?: AssignedNodesOptions): Node[];
  
  /** Get assigned elements */
  assignedElements(options?: AssignedNodesOptions): Element[];
  
  /** Assign nodes (manual slot assignment) */
  assign(...nodes: (Element | Text)[]): void;
}

interface AssignedNodesOptions {
  flatten?: boolean;
}

Usage Examples

Basic Custom Element

import { Window, HTMLElement } from "happy-dom";

const window = new Window();
const document = window.document;

// Define custom element class
class MyCustomElement extends HTMLElement {
  constructor() {
    super();
    this.innerHTML = '<p>Hello from custom element!</p>';
  }
  
  connectedCallback() {
    console.log('Custom element added to page');
    this.addEventListener('click', this.handleClick);
  }
  
  disconnectedCallback() {
    console.log('Custom element removed from page');
    this.removeEventListener('click', this.handleClick);
  }
  
  attributeChangedCallback(name: string, oldValue: string, newValue: string) {
    console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
  }
  
  static get observedAttributes() {
    return ['data-value'];
  }
  
  private handleClick = () => {
    console.log('Custom element clicked!');
  }
}

// Register custom element
window.customElements.define('my-custom-element', MyCustomElement);

// Use custom element
const customElement = document.createElement('my-custom-element');
document.body.appendChild(customElement);

// Or use in HTML
document.body.innerHTML = '<my-custom-element data-value="test"></my-custom-element>';

Shadow DOM with Slots

import { Window, HTMLElement } from "happy-dom";

const window = new Window();
const document = window.document;

// Custom element with shadow DOM
class CardElement extends HTMLElement {
  constructor() {
    super();
    
    // Attach shadow root
    const shadow = this.attachShadow({ mode: 'open' });
    
    // Define shadow DOM structure
    shadow.innerHTML = `
      <style>
        :host {
          display: block;
          border: 1px solid #ccc;
          border-radius: 8px;
          padding: 16px;
          margin: 8px;
        }
        .header {
          font-weight: bold;
          margin-bottom: 8px;
        }
        .content {
          color: #666;
        }
      </style>
      <div class="header">
        <slot name="title">Default Title</slot>
      </div>
      <div class="content">
        <slot>Default content</slot>
      </div>
    `;
  }
}

// Register card element
window.customElements.define('card-element', CardElement);

// Use card with slotted content
document.body.innerHTML = `
  <card-element>
    <span slot="title">My Card Title</span>
    <p>This is the card content that goes in the default slot.</p>
  </card-element>
`;

// Access shadow DOM
const card = document.querySelector('card-element');
if (card && card.shadowRoot) {
  const titleSlot = card.shadowRoot.querySelector('slot[name="title"]');
  const assignedNodes = titleSlot?.assignedNodes();
  console.log('Assigned title nodes:', assignedNodes);
}

Custom Element with Properties

import { Window, HTMLElement } from "happy-dom";

const window = new Window();
const document = window.document;

// Custom element with reactive properties
class CounterElement extends HTMLElement {
  private _count = 0;
  private _button!: HTMLButtonElement;
  private _display!: HTMLSpanElement;
  
  constructor() {
    super();
    
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        button {
          padding: 8px 16px;
          font-size: 16px;
          cursor: pointer;
        }
        .count {
          font-weight: bold;
          margin-left: 8px;
        }
      </style>
      <button id="increment">Increment</button>
      <span class="count" id="display">0</span>
    `;
    
    this._button = shadow.querySelector('#increment')!;
    this._display = shadow.querySelector('#display')!;
    
    this._button.addEventListener('click', () => {
      this.count++;
    });
  }
  
  get count() {
    return this._count;
  }
  
  set count(value: number) {
    const oldValue = this._count;
    this._count = value;
    this._display.textContent = value.toString();
    
    // Dispatch custom event
    this.dispatchEvent(new CustomEvent('count-changed', {
      detail: { oldValue, newValue: value },
      bubbles: true
    }));
  }
  
  static get observedAttributes() {
    return ['count'];
  }
  
  attributeChangedCallback(name: string, oldValue: string, newValue: string) {
    if (name === 'count') {
      this.count = parseInt(newValue) || 0;
    }
  }
}

// Register counter element
window.customElements.define('counter-element', CounterElement);

// Use counter element
const counter = document.createElement('counter-element');
counter.setAttribute('count', '5');
document.body.appendChild(counter);

// Listen for count changes
counter.addEventListener('count-changed', (event) => {
  console.log('Count changed:', event.detail);
});