Skip to content

Embedding the runtime in your app

@jxsuite/runtime renders a Jx document into any node your application owns. The mount() function is the entry point for a host that keeps its own state in TypeScript and wants a document to render and react to it. This is how Jx Studio draws its own chrome.

Mount a document

import { mount } from "@jxsuite/runtime";
import menu from "./surfaces/menu.json" with { type: "json" };

const handle = await mount(menu, document.getElementById("menu-host")!, {
  scope: { rows, run, formatBinding },
});

mount() appends the rendered root to the target and returns a handle. The handle carries the live scope , the root node, the set of custom-element tags the render used as elements , and a dispose() function.

The document is an object. Import it as JSON when it ships in your bundle, or call resolve(url) first when it lives on a server.

Pass state in

Everything you put in scope is visible to the document as state.<name> . Reactive records, refs and computed values stay live, so a change on the host side updates the render.

import { reactive } from "@vue/reactivity";

const model = reactive({ label: "Untitled" });
await mount(doc, host, { scope: { model } });
model.label = "Home"; 

Two rules apply. A state entry declared by the document wins over a host member of the same name. Keys that begin with $ or # belong to the runtime and are refused.

Note

The host and the runtime need one copy of @vue/reactivity . Pin the same exact version the runtime uses and bundle once. A second copy renders fine but never tracks.

Call host functions

A handler can name a host function directly. It receives the scope and the event, like any function in state .

{ "tagName": "button", "onclick": { "$ref": "#/state/onPress" } }

To pass arguments, use a call expression. The values arrive positionally, and a method keeps the object it belongs to as this .

{
  "tagName": "button",
  "onclick": {
    "$expression": {
      "operator": "call",
      "target": { "$ref": "#/state/commands/run" },
      "value": ["file.save", { "mode": "preview" }]
    }
  }
}

Listen for events

A document reports back with a dispatchEvent statement. When the body runs from a DOM event, the event dispatches from the element that handled it. When it runs without one, for example a function called with arguments, the event dispatches from the mounted root. Listen on the node you passed as the target and set bubbles in the document.

{
  "notify": {
    "$prototype": "Function",
    "parameters": ["what"],
    "body": [{ "dispatchEvent": "jx-notify", "detail": { "$ref": "$args/what" }, "bubbles": true }]
  }
}
host.addEventListener("jx-notify", (event) => {
  console.log((event as CustomEvent).detail);
});

Bundle documents and sidecars

A bundled document still names the things it depends on by URL and by $src . Register them before you mount, and nothing is fetched.

import { preloadDocument, preloadModule } from "@jxsuite/runtime";
import button from "./components/jx-button.json" with { type: "json" };
import * as behaviors from "./behaviors/menu.ts";

preloadDocument("jx-ui:/components/jx-button.json", button);
preloadModule("jx-ui:/behaviors/menu.ts", behaviors);

A document that lists { "$ref": "jx-ui:/components/jx-button.json" } in $elements , or "$src": "jx-ui:/behaviors/menu.ts" on a function, now resolves from those registrations.

A sidecar you would rather not load until something needs it takes a function instead of a namespace. The loader runs once, the first time a document names the specifier, and the bundler splits the import into its own chunk:

preloadModule("jx-ui:/behaviors/menu.ts", () => import("./behaviors/menu.ts"));

The kit ships this table ready-made as KIT_LOADERS from @jxsuite/ui/loaders , the same specifiers as KIT_MODULES with every entry a thunk.

Configure each mount

Options that used to be page-wide settings are read per mount:

Option What it controls
base The URL that relative references in the document resolve against.
media Named breakpoints for @--name style blocks.
resolver A function that answers a document URL for this mount, before fetching.
skipServerFunctions Suppress timing: "server" entries.
skipAutoRequests Suppress automatic Request fetches.
signal An AbortSignal ; aborting disposes the mount.

A resolver answer is private to its mount. It never enters the shared cache, so two mounts can answer the same URL differently.

Redefine an element

A host that lets people edit a component while it is in use can replace its definition without reloading the page:

import { redefineElement, elementDefinition } from "@jxsuite/runtime";

await redefineElement(updatedDoc, "jx-ui:/components/");
elementDefinition("jx-button"); // { doc: updatedDoc, base: "jx-ui:/components/" }

An instance created after the call renders the new definition. One already on the page keeps what it rendered, with its bindings still live, until you re-mount it. Use mount() 's elements to find the roots that used the tag and dispose and mount them again. A tag that was never defined is defined by the same call.

One thing does not change: observedAttributes . The browser fixes that list when a custom element is first defined, so a changed list is reported on the console and only takes effect after a page load.

Dispose

Call handle.dispose() when the host removes the surface. It stops every effect the render created, runs the document's onUnmount if it declares one, removes the root, and releases the styles the mount adopted. Calling it twice is safe. Passing a signal does the same when the signal aborts.