Skip to content

@xmachines/play-vue-demo

Examples / @xmachines/play-vue-demo

Vue 3 renderer demo for @xmachines/play-vue — actor + PlayRenderer without a router.

License: MIT

What This Demonstrates

  • Shared auth machine reused without framework-specific business logic
  • PlayRenderer rendering actor-projected views with a typed defineRegistry catalog
  • Auth machine states drive view switching via auth.login / auth.logout events 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 /context projection; repeat + key, $item-filtered lists, per-row actions sending inspectable machine events) next to ephemeral store state (pushState/removeState/setState built-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 validateForm built-in, a confirm-gated resetForm host action, and onSuccess/onError chains
    • Contact — a chained named action: submit validates, then routes home on success or writes "$error.message" to state on failure

Running the Demo

From the repository root:

Terminal window
pnpm install
pnpm --filter @xmachines/play-vue-demo run dev

Then open http://localhost:3004.

Step-by-Step Code Flow

Use this order to understand the implementation:

  1. src/main.ts creates the page’s inspector with createDemoInspector(), calls definePlayer({ machine: authMachine, options: { inspect: inspector.inspect } }), starts the actor, provides both actor and inspector to the app, and mounts it.
  2. The actor is provided to all components via app.provide("actor", actor).
  3. src/App.vue injects the actor and builds the registryResult inside computed with defineRegistry(authCatalog, { components: demoComponents, actions: createAuthActionHandlers(typedActor) }) — the component map lives once in src/components/demo-components.ts (shared with the router demos) and the handlers dispatch to actor.send().
  4. <PlayUIProvider :actor="typedActor" :registryResult="registryResult"><PlayRenderer /></PlayUIProvider> observes actor.currentView and renders the active spec.
  5. A NavBar SFC observes actor atoms directly for nav visibility.
  6. <DebugPanel :actor="typedActor" :inspector="inspector" /> shows live state, auth status, and current route, plus the Show Inspector button.
  7. HMR cleanup calls actor.stop() via import.meta.hot.dispose.
  8. Browser tests in test/browser/ validate startup and interaction behavior.
src/main.ts
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");
src/App.vue
<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 injection
  • src/App.vue - typed ComponentsMap declaration, registry construction, and PlayUIProvider + PlayRenderer composition
  • src/catalog.ts - authCatalog built via defineCatalog with the @xmachines/json-render-vue schema
  • src/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 testing
  • test/library-pattern.test.ts - architecture boundary and invariant assertions
  • test/browser/renderer-demo.browser.test.ts - browser-mode renderer coverage

State Machine & Architecture Details

The demo utilizes XMachines architectural invariants:

  1. Actor Authority: View components dispatch auth.login and auth.logout events to the actor. The actor evaluates guards and transitions — Vue never decides which view to render.
  2. Passive Infrastructure: PlayRenderer observes actor.currentView atoms only. It holds no business state and makes no routing decisions.
  3. 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:

  1. The atom notifies the subscription, synchronously, from the set that changed it
  2. watchAtom queues one microtask, and it drops each duplicate notification of that batch
  3. 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:

CommandDescription
pnpm --filter @xmachines/play-vue-demo run devStart Vite dev server (http://localhost:3004/)
pnpm --filter @xmachines/play-vue-demo run buildBuild production bundle
pnpm --filter @xmachines/play-vue-demo run previewPreview built bundle
pnpm --filter @xmachines/play-vue-demo run testRun Vitest test suite
pnpm --filter @xmachines/play-vue-demo run test:browserRun browser-focused Vitest suite

Learn More

Type Aliases

Variables

Functions