Appearance
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 field | Purpose |
|---|---|
static accepts | Pattern A: the ops this element handles. Pattern B: ignored (actor's accepts is authoritative). |
static emits | Same. |
static tokens | Theme tokens supported ({ tokenName: { default, description } }). Surfaced in $introspect and oracle snapshots. |
static description | Human-readable self-description. |
static template | Shadow-DOM template string. |
static styles | Optional CSS for reactive render() shells. |
static state | Optional initial state seed copied into actor state on mount. |
static factDomains | Fact domains to hydrate and subscribe to on mount. |
static tracks | Tracked component IDs for the CQRS view pattern. |
static moduleUrl | import.meta.url of the defining module. Set this for ghost-rendering support. |
static contextTimeoutMs | How long to wait for context before warning. Default 5000. |
static promptTimeoutMs | End-to-end timeout for $prompt round trip. Default 300_000 (5 min). |
static _layoutOnly | Set 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 observedAttributes | Per-Web-Components standard. Auto-derived from actor class in Pattern B. |
The instance surface
| Instance member | What it gives you |
|---|---|
readonly ready: Promise<void> | Resolves when the element has its context and actor bootstrapped. |
get name | Element id / name attribute. |
get mountPath | Computed mount path. |
get root | Authority root (current realm is a deprecated alias). |
get actor: MatrixActor | null | The wrapped actor instance. |
get state | Readonly 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
- Web Components — what custom elements buy us.
- UI component lifecycle — when each callback fires.
- Shadow DOM and rendering —
templateandrender(state). - Define an actor — Pattern B's actor half.
Source:
projects/matrix-3/packages/core/src/framework/MatrixActorHtmlElement.ts:202-447(statics + register),:216-360(class skeleton, instance fields, constructor).