# @json-render/vue Vue 3 renderer for json-render. Turn JSON specs into Vue components with data binding, visibility, and actions. ## Installation ```bash npm install @json-render/vue @json-render/core zod ``` Peer dependencies: `vue ^3.5.0` and `zod ^4.0.0`. ## Quick Start ### 1. Create a Catalog ```typescript import { defineCatalog } from "@json-render/core"; import { schema } from "@json-render/vue/schema"; import { z } from "zod"; export const catalog = defineCatalog(schema, { components: { Card: { props: z.object({ title: z.string(), description: z.string().nullable(), }), description: "A card container", }, Button: { props: z.object({ label: z.string(), action: z.string(), }), description: "A clickable button", }, Input: { props: z.object({ value: z.union([z.string(), z.record(z.unknown())]).nullable(), label: z.string(), placeholder: z.string().nullable(), }), description: "Text input field with optional value binding", }, }, actions: { submit: { description: "Submit the form" }, cancel: { description: "Cancel and close" }, }, }); ``` ### 2. Define Component Implementations Components are written using Vue's `h()` render function. `children` is a `VNode | VNode[]` — pass it directly to your container element. `defineRegistry` conditionally requires the `actions` field only when the catalog declares actions. Catalogs with `actions: {}` can omit it entirely. ```typescript import { h } from "vue"; import { defineRegistry } from "@json-render/vue"; import { catalog } from "./catalog"; export const { registry } = defineRegistry(catalog, { components: { Card: ({ props, children }) => h("div", { class: "card" }, [ h("h3", null, props.title), props.description ? h("p", null, props.description) : null, children, ]), Button: ({ props, emit }) => h("button", { onClick: () => emit("press") }, props.label), Input: ({ props, bindings }) => { // Use bindings?.value with a watcher to implement two-way binding return h("label", null, [ props.label, h("input", { placeholder: props.placeholder ?? "", value: props.value ?? "", }), ]); }, }, // actions stubs are required when the catalog declares actions: actions: { submit: async () => {}, cancel: async () => {}, }, }); ``` > **Note:** Vue does not have `useBoundProp`. For two-way binding, use `{ "$bindState": "/path" }` on the value prop and handle the `bindings` object in your component. ### 3. Render Specs ```vue ``` ## Spec Format The Vue renderer uses the same flat element map format as the React renderer: ```typescript interface Spec { root: string; // Key of the root element elements: Record; // Flat map of elements by key state?: Record; // Optional initial state } interface UIElement { type: string; // Component name from catalog props: Record; // Component props children?: string[]; // Keys of child elements visible?: VisibilityCondition; // Visibility condition } ``` Example spec: ```json { "root": "card-1", "elements": { "card-1": { "type": "Card", "props": { "title": "Welcome" }, "children": ["input-1", "btn-1"] }, "input-1": { "type": "Input", "props": { "value": { "$bindState": "/form/name" }, "label": "Name", "placeholder": "Enter name" } }, "btn-1": { "type": "Button", "props": { "label": "Submit" }, "children": [] } } } ``` ## Providers ### StateProvider Share data across components with JSON Pointer paths: ```vue ``` ```typescript // In composables: const { state, get, set } = useStateStore(); const name = get("/user/name"); // "John" set("/user/age", 25); // state is a ShallowRef — access with state.value console.log(state.value); ``` #### External Store (Controlled Mode) Pass a `StateStore` to bypass the internal state and wire json-render to any state management library (Pinia, VueUse, etc.): ```typescript import { createStateStore, type StateStore } from "@json-render/vue"; // Option 1: Use the built-in store outside of Vue const store = createStateStore({ count: 0 }); ``` ```vue ``` ```typescript // Mutate from anywhere — Vue will re-render automatically: store.set("/count", 1); // Option 2: Implement the StateStore interface with your own backend const piniaStore: StateStore = { get: (path) => getByPath(myStore.$state, path), set: (path, value) => myStore.$patch(/* ... */), update: (updates) => myStore.$patch(/* ... */), getSnapshot: () => myStore.$state, subscribe: (listener) => myStore.$subscribe(listener), }; ``` When `store` is provided, `initialState` and `onStateChange` are ignored. ### ActionProvider Handle actions from components: ```vue ``` ### VisibilityProvider Control element visibility based on data: ```vue ``` ```json { "type": "Alert", "props": { "message": "Error!" }, "visible": { "$state": "/form/hasError" } } ``` ### ValidationProvider Add field validation: ```vue ``` ```typescript // Use validation composable: const { errors, validate } = useFieldValidation("/form/email", { checks: [ { type: "required", message: "Email required" }, { type: "email", message: "Invalid email" }, ], }); ``` ## Composables | Composable | Purpose | |------------|---------| | `useStateStore()` | Access state context (`state` as `ShallowRef`, `get`, `set`, `update`) | | `useStateValue(path)` | Get single value from state | | `useStateBinding(path)` | Two-way data binding (deprecated — use `$bindState` instead) | | `useIsVisible(condition)` | Check if a visibility condition is met | | `useActions()` | Access action context | | `useAction(binding)` | Get a single action dispatch function | | `useFieldValidation(path, config)` | Field validation state | > **Note:** `useStateStore().state` returns a `ShallowRef` — use `state.value` to access the underlying object. This differs from the React renderer where `state` is a plain object. ## Visibility Conditions ```typescript // Truthiness check { "$state": "/user/isAdmin" } // Auth state (use state path) { "$state": "/auth/isSignedIn" } // Comparisons (flat style) { "$state": "/status", "eq": "active" } { "$state": "/count", "gt": 10 } // Negation { "$state": "/maintenance", "not": true } // Multiple conditions (implicit AND) [ { "$state": "/feature/enabled" }, { "$state": "/maintenance", "not": true } ] // Always / never true // always visible false // never visible ``` TypeScript helpers from `@json-render/core`: ```typescript import { visibility } from "@json-render/core"; visibility.when("/path") // { $state: "/path" } visibility.unless("/path") // { $state: "/path", not: true } visibility.eq("/path", val) // { $state: "/path", eq: val } visibility.neq("/path", val) // { $state: "/path", neq: val } visibility.and(cond1, cond2) // { $and: [cond1, cond2] } visibility.always // true visibility.never // false ``` ## Dynamic Prop Expressions Any prop value can use data-driven expressions that resolve at render time. The renderer resolves these transparently before passing props to components. ```json { "type": "Badge", "props": { "label": { "$state": "/user/role" }, "color": { "$cond": { "$state": "/user/role", "eq": "admin" }, "$then": "red", "$else": "gray" } } } ``` For two-way binding, use `{ "$bindState": "/path" }` on the natural value prop. Inside repeat scopes, use `{ "$bindItem": "field" }` instead. Components receive resolved `bindings` with the state path for each bound prop. See [@json-render/core](../core/README.md) for full expression syntax. ## Built-in Actions The `setState`, `pushState`, and `removeState` actions are built into the Vue schema and handled automatically by `ActionProvider`. They are injected into AI prompts without needing to be declared in your catalog's `actions`. They update the state model, which triggers re-evaluation of visibility conditions and dynamic prop expressions: ```json { "type": "Button", "props": { "label": "Switch Tab" }, "on": { "press": { "action": "setState", "params": { "statePath": "/activeTab", "value": "settings" } } }, "children": [] } ``` ## Component Props When using `defineRegistry`, components receive these props via their render function: ```typescript import type { VNode } from "vue"; interface ComponentContext

{ props: P; // Typed props from the catalog (expressions resolved) children?: VNode | VNode[]; // Rendered children (for container components) emit: (event: string) => void; // Emit a named event (always defined) on: (event: string) => EventHandle; // Get event handle with metadata loading?: boolean; // Whether the parent is loading bindings?: Record; // State paths for $bindState/$bindItem expressions } interface EventHandle { emit: () => void; // Fire the event shouldPreventDefault: boolean; // Whether any binding requested preventDefault bound: boolean; // Whether any handler is bound } ``` Use `emit("press")` for simple event firing. Use `on("click")` when you need to check metadata like `shouldPreventDefault` or `bound`: ```typescript Link: ({ props, on }) => { const click = on("click"); return h("a", { href: props.href, onClick: (e: MouseEvent) => { if (click.shouldPreventDefault) e.preventDefault(); click.emit(); }, }, props.label); }, ``` ### `BaseComponentProps` For building reusable component libraries that are not tied to a specific catalog, use the catalog-agnostic `BaseComponentProps` type: ```typescript import type { BaseComponentProps } from "@json-render/vue"; const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => h("div", null, [props.title, children]); ``` ## Generate AI Prompts ```typescript const systemPrompt = catalog.prompt(); // Returns detailed prompt with component/action descriptions ``` ## Full Example ```typescript import { h } from "vue"; import { defineCatalog } from "@json-render/core"; import { schema } from "@json-render/vue/schema"; import { defineRegistry, Renderer, StateProvider } from "@json-render/vue"; import { z } from "zod"; const catalog = defineCatalog(schema, { components: { Greeting: { props: z.object({ name: z.string() }), description: "Displays a greeting", }, }, actions: {}, }); const { registry } = defineRegistry(catalog, { components: { Greeting: ({ props }) => h("h1", null, `Hello, ${props.name}!`), }, }); const spec = { root: "greeting-1", elements: { "greeting-1": { type: "Greeting", props: { name: "World" }, children: [], }, }, }; // In your App.vue: // // // ``` ## Key Exports | Export | Purpose | |--------|---------| | `defineRegistry` | Create a type-safe component registry from a catalog | | `Renderer` | Render a spec using a registry | | `schema` | Element tree schema (includes built-in actions: `setState`, `pushState`, `removeState`) | | `useStateStore` | Access state context (`state` is `ShallowRef`) | | `useStateValue` | Get single value from state | | `useActions` | Access actions context | | `useAction` | Get a single action dispatch function | | `createStateStore` | Create a framework-agnostic in-memory `StateStore` | ### Types | Export | Purpose | |--------|---------| | `ComponentContext` | Typed component render function context (catalog-aware) | | `BaseComponentProps` | Catalog-agnostic base type for reusable component libraries | | `EventHandle` | Event handle with `emit()`, `shouldPreventDefault`, `bound` | | `ActionProviderProps` | Props for `ActionProvider` | | `ValidationProviderProps` | Props for `ValidationProvider` | | `ComponentFn` | Component render function type | | `SetState` | State setter type | | `StateModel` | State model type | | `StateStore` | Interface for plugging in external state management | ## Differences from `@json-render/react` | API | React | Vue | Note | |-----|-------|-----|------| | `useStateStore().state` | `StateModel` | `ShallowRef` | Vue reactivity; use `state.value` | | `children` type | `React.ReactNode` | `VNode \| VNode[]` | Platform-specific | | `useBoundProp` | exported | not available | React-specific; use `bindings` directly in Vue | | Streaming hooks | `useUIStream`, `useChatUI` | not available | Vue package is UI-only for now |