@xmachines/play-vue-demo
Examples / @xmachines/play-vue-demo
Vue 3 renderer demo for @xmachines/play-vue — actor + PlayRenderer without a router.
What This Demonstrates
- Shared auth machine reused without framework-specific business logic
PlayRendererrendering actor-projected views with a typeddefineRegistrycatalog- Auth machine states drive view switching via
auth.login/auth.logoutevents only — no URL routing - Vue Composition API mapping to atoms lifecycle
- Non-browser invariant tests plus browser renderer coverage
- Stately inspector attached at creation via
options.inspect— the debug panel’s Show Inspector button opens the live machine in its own window - json-render 0.20 showcase pages driven purely by spec:
- Overview — two data planes side by side:
/context/tasks(machine context via the read-only/contextprojection;repeat+key,$item-filtered lists, per-row actions sending inspectable machine events) next to ephemeral store state (pushState/removeState/setStatebuilt-ins driving filter chips and toggles that survive context updates) - Stats — nested repeats (
repeat.statePath: { $item: "members" }) over/context/teams - Settings — declarative field validation, the
validateFormbuilt-in, a confirm-gatedresetFormhost action, andonSuccess/onErrorchains - Contact — a chained named action: submit validates, then routes home on success or writes
"$error.message"to state on failure
- Overview — two data planes side by side:
Running the Demo
From the repository root:
pnpm installpnpm --filter @xmachines/play-vue-demo run devThen open http://localhost:3004.
Step-by-Step Code Flow
Use this order to understand the implementation:
src/main.tscreates the page’s inspector withcreateDemoInspector(), callsdefinePlayer({ machine: authMachine, options: { inspect: inspector.inspect } }), starts the actor, provides bothactorandinspectorto the app, and mounts it.- The actor is provided to all components via
app.provide("actor", actor). src/App.vueinjects the actor and builds theregistryResultinsidecomputedwithdefineRegistry(authCatalog, { components: demoComponents, actions: createAuthActionHandlers(typedActor) })— the component map lives once insrc/components/demo-components.ts(shared with the router demos) and the handlers dispatch toactor.send().<PlayUIProvider :actor="typedActor" :registryResult="registryResult"><PlayRenderer /></PlayUIProvider>observesactor.currentViewand renders the active spec.- A
NavBarSFC observesactoratoms directly for nav visibility. <DebugPanel :actor="typedActor" :inspector="inspector" />shows live state, auth status, and current route, plus the Show Inspector button.- HMR cleanup calls
actor.stop()viaimport.meta.hot.dispose. - Browser tests in
test/browser/validate startup and interaction behavior.
const inspector = createDemoInspector();const createPlayer = definePlayer({ machine: authMachine, options: { inspect: inspector.inspect }, actor: compose(PlayerActor, withRouting, withView),});const actor = createPlayer() as AuthActor;actor.start();
const app = createApp(App);app.provide("actor", actor);app.provide("inspector", inspector);app.mount("#app");<script setup lang="ts">import type { ComponentsMap } from "@xmachines/play-vue";import type { ActionFn } from "@xmachines/json-render-vue";
const typedActor = assertNonNullable(inject<AuthActor>("actor"), "actor");const inspector = assertNonNullable(inject<DemoInspector>("inspector"), "inspector");
const registryResult = computed(() => defineRegistry(authCatalog, { components: demoComponents, // one shared map — Home…NavBar plus the 0.20 Panel/Field/Button leaves actions: createAuthActionHandlers(typedActor), // login/logout/route + task events + form actions }),);</script>
<template> <div class="demo-app" data-demo-shell> <header class="demo-header"> <h1 class="demo-title">XMachines Play Vue Demo</h1> <NavBar :actor="typedActor" /> </header> <main class="demo-content" data-demo-content> <PlayUIProvider :actor="typedActor" :registryResult="registryResult"> <PlayRenderer /> </PlayUIProvider> </main> <DebugPanel :actor="typedActor" :inspector="inspector" /> </div></template>Key Files
src/main.ts- actor creation/start and Vue app mount with actor injectionsrc/App.vue- typedComponentsMapdeclaration, registry construction, andPlayUIProvider+PlayRenderercompositionsrc/catalog.ts-authCatalogbuilt viadefineCatalogwith the@xmachines/json-render-vueschemasrc/components/- demo view components bound to catalog component keys (Home, Login, Dashboard, Profile, etc.)src/index.ts- re-exports all components and catalog for library-pattern testingtest/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: View components dispatch
auth.loginandauth.logoutevents to the actor. The actor evaluates guards and transitions — Vue never decides which view to render. - Passive Infrastructure:
PlayRendererobservesactor.currentViewatoms only. It holds no business state and makes no routing decisions. - Atom-Only Reactivity: The bridge leverages Vue’s reactivity model internally to react precisely when actor atoms update, without polluting the component tree with reactive refs holding business state.
Watcher Lifecycle and Cleanup Contract
This demo follows the canonical watcher lifecycle used across all @xmachines framework adapters:
- The atom notifies the subscription, synchronously, from the
setthat changed it watchAtomqueues one microtask, and it drops each duplicate notification of that batch- The microtask reads the actor atoms and syncs Vue-local render state
One subscription lives for the whole watch, and no caller arms a watcher again. Cleanup is explicit: actor.stop() is called during HMR disposal, and PlayRenderer handles internal watcher teardown natively on component unmount.
Adapter Boundaries
PlayRenderer and defineRegistry stay passive infrastructure. Business validity remains actor-owned. The registry maps catalog component keys to Vue SFC implementations — the actor owns which key is active. The demo intentionally omits URL routing to isolate the renderer contract.
Available Scripts
These commands are defined in package.json:
| Command | Description |
|---|---|
pnpm --filter @xmachines/play-vue-demo run dev | Start Vite dev server (http://localhost:3004/) |
pnpm --filter @xmachines/play-vue-demo run build | Build production bundle |
pnpm --filter @xmachines/play-vue-demo run preview | Preview built bundle |
pnpm --filter @xmachines/play-vue-demo run test | Run Vitest test suite |
pnpm --filter @xmachines/play-vue-demo run test:browser | Run browser-focused Vitest suite |