Skip to content

MatrixActorHtmlElement

MatrixActorHtmlElement is the abstract base class every Matrix UI component extends. It is a custom element (extends HTMLElement) that owns an internal MatrixActor via composition-with-delegation. The browser side of the actor model.

The class lives at projects/matrix-3/packages/core/src/framework/MatrixActorHtmlElement.ts (~3200 lines). This page documents the surface package authors actually touch.

The two patterns

Every component picks one of two patterns to declare its behaviour.

Pattern A — all-in-one

accepts, emits, and handlers live on the element class itself. No separate actor class. The framework creates an internal DynamicActor to satisfy the bus contract, but you don't see it.

ts
class MyWidget extends MatrixActorHtmlElement {
  static accepts = { 'widget.do': { description: 'Do', schema: {}, returns: {} } };
  static emits = { 'widget.done': { description: 'Done', schema: {} } };
  static template = `<div>...</div>`;

  onWidgetDo(payload: unknown) {
    this.emit('widget.done', {});
  }
}

customElements.define('my-widget', MyWidget);

Pattern A is what you want for 90% of UI components. The element IS the actor as far as your code is concerned.

Pattern B — separate actor class

accepts, emits, and handlers live on a MatrixActor subclass. The element is a thin wrapper. Use this when:

  • The actor logic is reused outside the browser (also runs on the daemon).
  • You want one actor implementation behind multiple element shells.
  • You're using the DSL compiler, which produces actor + element pairs.
ts
class MyWidgetActor extends MatrixActor {
  static accepts = { 'widget.do': { description: 'Do', schema: {}, returns: {} } };
  static emits = { 'widget.done': { description: 'Done', schema: {} } };
  onWidgetDo(payload: unknown) { this.emit('widget.done', {}); }
}

class MyWidgetElement extends MatrixActorHtmlElement {
  static getActorClass() { return MyWidgetActor; }
  static template = `<div>...</div>`;
}
customElements.define('my-widget', MyWidgetElement);

// Or via the static helper:
MatrixActorHtmlElement.register('my-widget', MyWidgetActor, { template: '<div>...</div>' });

getActorClass() returns null by default (MatrixActorHtmlElement.ts:398-400). Returning a class flips the framework into Pattern B. The element class still receives DOM events, but bus dispatch routes through the inner actor's handlers.

MatrixActorHtmlElement.register(tag, ActorClass, options) is a sugar for the common Pattern B case (MatrixActorHtmlElement.ts:418-447). It builds a Pattern B element class, copies accepts/emits/tracks from the actor onto the element class for child-event discovery, and calls customElements.define.

The static surface

Static fieldPurpose
static acceptsPattern A: the ops this element handles. Pattern B: ignored (actor's accepts is authoritative).
static emitsSame.
static tokensTheme tokens supported ({ tokenName: { default, description } }). Surfaced in $introspect and oracle snapshots.
static descriptionHuman-readable self-description.
static templateShadow-DOM template string.
static stylesOptional CSS for reactive render() shells.
static stateOptional initial state seed copied into actor state on mount.
static factDomainsFact domains to hydrate and subscribe to on mount.
static tracksTracked component IDs for the CQRS view pattern.
static moduleUrlimport.meta.url of the defining module. Set this for ghost-rendering support.
static contextTimeoutMsHow long to wait for context before warning. Default 5000.
static promptTimeoutMsEnd-to-end timeout for $prompt round trip. Default 300_000 (5 min).
static _layoutOnlySet true for transparent containers (mx-split) so children skip past you when finding their parent shell.
static getActorClass()Override → Pattern B with that actor class.
static getChildTag(name)Override → enable auto-creating DOM elements for dynamic children.
static observedAttributesPer-Web-Components standard. Auto-derived from actor class in Pattern B.

The instance surface

Instance memberWhat it gives you
readonly ready: Promise<void>Resolves when the element has its context and actor bootstrapped.
get nameElement id / name attribute.
get mountPathComputed mount path.
get rootAuthority root (current realm is a deprecated alias).
get actor: MatrixActor | nullThe wrapped actor instance.
get stateReadonly view of the actor's state.
setState(patch)Mutate state — same semantics as on the actor.
emit(op, payload)Publish to $events. Inherited from the actor.
sendCommand(target, op, payload) or sendCommand(op, payload)Send to child or tracked domain.
_receiveContext(ctx, existingActor?)Framework-internal — receive context from parent or runtime.
connectedCallback() / disconnectedCallback()Standard custom-element lifecycle. The framework hooks both.

Security request hook

CapabilityRequestDetail (exported from this module) is the detail payload for mx-capability-request CustomEvents. When a component fails because it lacks a capability, the framework dispatches that event up the DOM tree so a parent shell can drive escalation UX (e.g., "Allow this component to subscribe to fact domain X?"). See MatrixActorHtmlElement.ts:60-68 and :100-150.

This is the only standard upward channel beyond regular CustomEvents. Most component authors do not emit mx-capability-request directly — the framework does it on their behalf when fact-domain access is denied.

Worked example: a chat input

ts
import { MatrixActorHtmlElement } from '@open-matrix/core';

class ChatInput extends MatrixActorHtmlElement {
  static accepts = {
    'chat-input.set-disabled': {
      description: 'Disable or enable the input.',
      schema: { disabled: { type: 'boolean', description: 'true to disable' } },
      returns: {},
    },
  };

  static emits = {
    'chat-input.submit': {
      description: 'User pressed Enter or clicked Send.',
      schema: { text: { type: 'string', description: 'Message text' } },
    },
  };

  static template = `
    <style>
      :host { display: flex; gap: 0.5rem; }
      textarea { flex: 1; font: inherit; resize: vertical; }
      button { font: inherit; }
    </style>
    <textarea data-action="onInput" placeholder="Type a message..."></textarea>
    <button data-action="onSubmit">Send</button>
  `;

  protected async _onContextUpdated() {
    const ta = this.shadowRoot!.querySelector('textarea')!;
    ta.disabled = !!this.state.disabled;
  }

  onInput(event: Event) {
    const ta = event.target as HTMLTextAreaElement;
    if (event instanceof KeyboardEvent && event.key === 'Enter' && !event.shiftKey) {
      event.preventDefault();
      this.submit(ta.value);
    }
  }

  onSubmit() {
    const ta = this.shadowRoot!.querySelector('textarea')!;
    this.submit(ta.value);
  }

  private submit(text: string) {
    if (!text.trim()) return;
    this.emit('chat-input.submit', { text });
    (this.shadowRoot!.querySelector('textarea') as HTMLTextAreaElement).value = '';
  }

  onChatInputSetDisabled(payload: { disabled: boolean }) {
    this.setState({ disabled: payload.disabled });
  }
}

customElements.define('chat-input', ChatInput);

Mount this anywhere inside a <matrix-dsl-host>:

html
<chat-input id="composer" name="composer"></chat-input>

It registers with the parent shell, gets a context, and is reachable as the actor at <root>.<page>.composer.

See also

Source: projects/matrix-3/packages/core/src/framework/MatrixActorHtmlElement.ts:202-447 (statics + register), :216-360 (class skeleton, instance fields, constructor).