@xmachines/play-dom-demo
Examples / @xmachines/play-dom-demo
Vanilla DOM renderer demo for @xmachines/play-dom — actor + createPlayUI without a router.
What This Demonstrates
- Shared auth machine reused without framework-specific business logic
createPlayUIwiringPlayRenderer-equivalent DOM rendering without a framework- Auth machine states (home → login → dashboard) drive DOM updates via TC39 Signal watchers
watchSignalfor reactive nav visibility and debug panel updates- Non-browser invariant tests plus browser renderer coverage
Running the Demo
From the repository root:
npm installnpm run dev -w @xmachines/play-dom-demoThen open http://localhost:5173.
Step-by-Step Code Flow
Use this order to understand the implementation:
src/main.tscallsdefinePlayer({ machine: authMachine })and starts the actor.initShell(actor, app)fromsrc/components/Shell.tsbuilds the HTML scaffold, mounts a reactiveNavBar, wires the debug panel, and returns a disconnect function.defineRegistry(authCatalog, { components, actions })builds the typedregistryResult— real async action handlers dispatching toactor.send().createPlayUI(registryResult)produces amountfunction;mount(actor, container)wires the registry to the actor — view switches happen automatically whenactor.currentViewchanges.createNavBar(actor, headerEl)insrc/components/NavBar.tsuseswatchSignal(actor.state, ...)to update nav button visibility based on auth state.createDebugPanel(actor, shell)insrc/components/DebugPanel.tsuseswatchSignal(actor.state, ...)andwatchSignal(actor.currentRoute, ...)to drive live debug panel updates.- Browser tests in
test/browser/validate startup rendering and auth interactions.
// src/main.ts (shape)const createPlayer = definePlayer({ machine: authMachine });const actor = createPlayer();actor.start();
const app = document.getElementById("app");if (!app) throw new Error("Root element not found");
initShell(actor, app);// src/components/Shell.ts (shape)const registryResult = defineRegistry(authCatalog, { components: { Home, About, Contact, Login, Dashboard, Overview, Stats, Profile, Settings, Navigation, NavBar: NavBarView, }, actions: { login: async (args) => actor.send({ type: "auth.login", username: assertNonNullable(args, "args").username, }), logout: async () => actor.send({ type: "auth.logout" }), route: async (args) => { const { to, params } = assertNonNullable(args, "args"); actor.send({ type: "play.route", to, ...(params != null && { params }), }); }, },});
const disconnectNavBar = createNavBar(actor, headerEl);const mount = createPlayUI(registryResult);const disconnectRenderer = mount(actor, viewContent);const disconnectDebugPanel = createDebugPanel(actor, shell);
return () => { disconnectNavBar(); disconnectRenderer(); disconnectDebugPanel();};Key Files
src/main.ts- actor creation and shell bootstrapsrc/components/Shell.ts- DOM scaffold,DomRegistryconstruction,createPlayUIwiring,createNavBar, andcreateDebugPanelsrc/components/NavBar.ts- reactive nav bar usingwatchSignal(actor.state, ...)for auth-driven button visibilitysrc/components/DebugPanel.ts- live debug footer usingwatchSignalon bothactor.stateandactor.currentRoutetest/library-pattern.test.ts- architecture boundary and invariant assertionstest/browser/renderer-demo.browser.test.ts- browser-mode renderer coverage
State Machine & Architecture Details
The demo utilizes XMachines architectural invariants:
- Actor Authority: Navigation buttons dispatch
play.routeevents to the actor. The actor evaluates guards and transitions to the correct state — the DOM never decides which view to show. - Passive Infrastructure:
createPlayUIandwatchSignalonly react to actor signals. They do not hold business state or make routing decisions. - Signal-Only Reactivity:
watchSignal(from@xmachines/play-signals) wraps the canonical TC39 Signals watcher lifecycle, keeping all reactivity in the actor signal layer rather than in ad-hoc DOM event listeners.
Watcher Lifecycle and Cleanup Contract
This demo follows the canonical watcher lifecycle:
notifyqueueMicrotaskgetPending()- Read actor signals and project DOM state
- Re-arm with
watch()/watch(...signals)
Watcher notifications are one-shot. Cleanup is explicit: disconnectNavBar(), disconnectRenderer(), and disconnectDebugPanel() are all called from the disconnect function returned by initShell. In this demo, cleanup is called on beforeunload via actor.stop().
Adapter Boundaries
createPlayUI (from @xmachines/play-dom) is passive infrastructure. It translates actor view signals into DOM mutations but holds no business logic. The DomRegistry maps catalog component keys to factory functions — the actor owns which key is active.
Available Scripts
These commands are defined in package.json:
| Command | Description |
|---|---|
npm run dev -w @xmachines/play-dom-demo | Start Vite dev server |
npm run build -w @xmachines/play-dom-demo | Build production bundle |
npm run preview -w @xmachines/play-dom-demo | Preview built bundle |
npm run test -w @xmachines/play-dom-demo | Run Vitest test suite |
npm run test:browser -w @xmachines/play-dom-demo | Run browser-focused Vitest suite |
Verification
Use these checks to validate README claims against the current demo implementation:
npm run test -w @xmachines/play-dom-demonpm run test:browser -w @xmachines/play-dom-demoExpected result: library-pattern invariant tests pass and the browser renderer suite completes.
Learn More
Type Aliases
Variables
- About
- authCatalog
- Contact
- Dashboard
- Home
- Login
- NavBarView
- Navigation
- Overview
- Profile
- Settings
- Stats