| name | vario-vue |
| description | Expert knowledge for developing with @variojs/vue — the Vue 3 rendering layer of the Vario Schema-First UI system. Covers useVario composable, Schema→VNode rendering, model binding, event handling, directives, expressions, control flow (cond/show/loop), Vue features (refs, lifecycle, provide/inject, teleport, transition, keep-alive), VNode plugin system (VNodePlugin interface, wrapComponent/decorateVNode hooks, built-in & custom plugins), performance optimization (Scope-Weight Hybrid auto-adaptive: path-memo, subtree componentization, loop componentization), schema query API, computed/methods registration, error boundaries, node context, and TypeScript type patterns. Use this skill whenever the user is working with vario-vue, useVario, schema rendering, writing Vario Schema for Vue, configuring renderer options, debugging rendering issues, building forms or dynamic UI with JSON Schema in Vue, or asking about any @variojs/vue API. Also trigger when user mentions Vario + Vue, schema-driven rendering, Action VM events, model path binding, VNode creation from schema, VNode plugins, or custom rendering plugins. |
Vario-Vue Skill
Expert guide for @variojs/vue — the Vue 3 rendering backend of Vario (Schema-First UI Behavior Runtime).
Architecture
Schema (JSON DSL) → @variojs/core (RuntimeContext + ExpressionEngine + Action VM) → @variojs/vue (useVario → VNode)
Dependencies: @variojs/types (shared types), @variojs/core (runtime engine), vue (peer).
Source: packages/vario-vue/src/ — composable.ts (useVario), renderer.ts (VueRenderer, 14-step pipeline), adapter.ts (Vue reactive bridge), bindings.ts (model binding), features/ (feature modules), plugins/ (VNodePlugin system: lifecycle, keep-alive, transition, teleport), composables/ (useSchemaQuery + internal helpers).
useVario Composable
Primary API — converts Schema into Vue VNode.
const {
vnode, state, ctx, refs, error, stats, retry, find, findAll, findById,
} = useVario(schema, options)
Schema input: static object, factory function () => schema, or ComputedRef<Schema> (reactive switching).
Options:
useVario(schema, {
state: { count: 0 },
computed: { total: (s) => s.a + s.b },
methods: {
onClick: defineMethod<MouseEvent, MyState>(({ value, state, ctx }) => { ... }),
},
components: { ElButton },
directives: { vFocus: myFocusDirective },
plugins: [lifecyclePlugin, teleportPlugin],
app: getCurrentInstance()?.appContext.app ?? null,
modelOptions: { separator: '.', lazy: false },
modelBindings: { 'MySwitch': { prop: 'checked', event: 'change' } },
errorBoundary: { enabled: true, fallback: (err) => h('div', err.message) },
onEmit: (event, data) => {},
onError: (err) => {},
})
Rendering pipeline: resolveSchema → reactive(state) → createVueReactiveAdapter → createRuntimeContext → registerComputed → VueRenderer.render → watch(schema/state) for re-render.
Template usage:
<template>
<component :is="vnode" v-if="vnode" />
<div v-else-if="error">{{ error.message }}</div>
</template>
<script setup>
const { vnode, state, error, retry } = useVario(schema, { state: { count: 0 } })
</script>
Schema Node Structure
{
type: string,
id?: string,
props?: Record<string, any>,
children?: string | SchemaNode[],
events?: Record<string, EventAction>,
model?: string | ModelConfig,
cond?: string | boolean,
show?: string | boolean,
loop?: LoopConfig,
ref?: string,
onMounted?: string,
onUnmounted?: string, onUpdated?: string,
onBeforeMount?: string, onBeforeUnmount?: string, onBeforeUpdate?: string,
provide?: Record<string, any>,
inject?: string[] | Record<string, { from: string, default?: any }>,
teleport?: string | boolean,
transition?: string | TransitionConfig,
keepAlive?: boolean | KeepAliveConfig,
directives?: DirectiveConfig,
}
Model Binding
model: 'user.name'
model: 'users[0].email'
model: '{{ dynamicField }}'
model: { path: 'form', scope: true }
model: { path: 'name', default: '张三' }
model: { path: 'search', modifiers: { trim: true, number: true } }
Path stack: Scoped parents push path; children resolve relative to stack (form.user + name → form.user.name).
Named models: 'model:checked': 'isChecked' for multi v-model (Vue 3.4+).
Auto-detection priority: custom configs → native elements → component inspection → { prop: 'modelValue', event: 'update:modelValue' }.
Modifiers: .trim, .number, .lazy (execution order: trim → number → lazy).
Event Handling
events: { click: 'handleClick' }
events: { click: { type: 'call', method: 'fn', params: { id: '{{ item.id }}' } } }
events: { click: ['call', 'fn', ['p1'], ['stop', 'prevent']] }
events: { click: ['validate', { type: 'call', method: 'submit' }] }
events: { 'click.stop.prevent': 'fn' }
Modifiers: stop, prevent, capture, self, once, passive.
Method context: ({ state, params, value, event, ctx }) => { ... } where ctx provides _get/_set/$emit/$self/$parent/$siblings.
Control Flow
cond (v-if): { cond: '{{ isLoggedIn }}' } — removes from VNode tree.
show (v-show): { show: '{{ isVisible }}' } — toggles display: none.
loop:
{ loop: { items: '{{ list }}', itemKey: 'item', indexKey: 'index' },
children: [{ type: 'span', children: '{{ item.name }}' }] }
Key priority: item[itemKey] → item.id → index. Use model: '.' to bind loop item itself.
Expressions
{{ expr }} supported in all dynamic fields. Sandboxed with whitelisted globals (Math.*, Array.*, operators).
children: 'Total: {{ price * quantity }}'
children: '{{ isVIP ? "VIP" : "Regular" }}'
props: { label: '{{ user?.profile?.name }}' }
Vue Features
Refs: { ref: 'myInput' } → refs.myInput.value?.focus(). Dynamic: ref: '{{ \field_${index}` }}'`.
Lifecycle: onMounted/onUnmounted/onUpdated/onBeforeMount/onBeforeUnmount/onBeforeUpdate — values are method names. Node auto-wrapped in defineComponent via lifecyclePlugin.
Provide/Inject: provide: { theme: 'dark' } on parent; inject: ['theme'] or inject: { t: { from: 'theme', default: 'light' } } on consumer.
Teleport: teleport: '#modal-root' or teleport: true (→ body).
Transition: transition: 'fade' or transition: { name: 'slide', appear: true, mode: 'out-in' }.
Keep-Alive: keepAlive: true or keepAlive: { include: [...], max: 10 }.
Wrapping order (outer → inner): Teleport → Transition → Keep-alive → Refs → Directives → Component. Wrapping is handled by VNodePlugin decorateVNode hooks (see Plugin System).
Plugin System
Vue-specific features are decoupled from the core rendering pipeline via VNodePlugin:
interface VNodePlugin {
name: string
wrapComponent?: (component, attrs, children, schema, ctx) => VNode | null
decorateVNode?: (vnode, schema, ctx) => VNode
}
Built-in plugins: lifecyclePlugin (lifecycle hooks + provide/inject), keepAlivePlugin, transitionPlugin, teleportPlugin.
defaultPlugins: all 4 built-in plugins (used when plugins option is omitted).
import { lifecyclePlugin, teleportPlugin, defaultPlugins } from '@variojs/vue'
useVario(schema, { plugins: [lifecyclePlugin, teleportPlugin] })
useVario(schema, { plugins: [] })
const logPlugin: VNodePlugin = {
name: 'log',
decorateVNode(vnode, schema) {
console.log(`render: ${schema.type}`)
return vnode
}
}
useVario(schema, { plugins: [...defaultPlugins, logPlugin] })
Hook semantics:
wrapComponent: first non-null return wins (short-circuit); used when component creation must change (e.g. defineComponent wrapping)
decorateVNode: all plugins chained in order; used for outer wrapping (KeepAlive → Transition → Teleport)
Ordering: wrapComponent plugins first, decorateVNode plugins by wrapping layer (inner → outer).
Directives
directives: { focus: true, tooltip: 'Hello' }
directives: [{ name: 'focus', value: true }]
directives: [['tooltip', 'Hello', 'top', { animate: true }]]
Resolved from UseVarioOptions.directives or app global directives.
Node Context & Schema Query
Node context in methods: ctx.$self (current node), ctx.$parent (chainable), ctx.$siblings, ctx.$children. Uses Proxy + WeakMap for O(1) lookups.
Schema query:
const { find, findAll, findById } = useVario(schema, options)
findById('name-input')?.patch({ props: { disabled: true } })
findAll(n => n.type === 'ElButton').forEach(b => b.patch({ props: { loading: true } }))
NodeWrapper: { path, node, patch(partial), get(key), parent() }. Lazy analysis, cached until schema changes.
Performance Optimization
All optimizations are zero-config via the Scope-Weight Hybrid strategy:
| Strategy | Mechanism | Config |
|---|
| path-memo | Cache static VNode subtrees by path | Always on |
| Scope-Weight subtree | Componentize nodes at scope boundary when weight > COMPONENT_OVERHEAD (5) | Auto-adaptive |
| Scope-Weight loop | Componentize loop items when template weight > COMPONENT_OVERHEAD (5) | Auto-adaptive |
COMPONENT_OVERHEAD = 5 is the cost threshold. computeWeight() calculates subtree weight (cached in WeakMap). isScopeBoundary() checks model/component/lifecycle/provide.
Benchmark highlights (2026-03-15):
- path-memo: avg 11.43x speedup (up to 19.42x on static subtree re-renders)
- Loop componentization: avg 1.41x speedup
- Combined (500 loop): 7.99x speedup
- Plugin refactor: zero perf regression (non-plugin nodes skip unnecessary feature checks)
No rendererOptions needed — all deprecated fields (usePathMemo, loopItemAsComponent, subtreeComponent, schemaFragment) have been removed.
directives is now a top-level option in UseVarioOptions (not nested in rendererOptions).
Error Handling
errorBoundary: { enabled: true, fallback: (err) => h('div', err.message), onRecover: (err) => log(err) }
retry() from useVario re-renders after error. Error types: VarioError, ExpressionError, ActionError, ServiceError.
TypeScript Patterns
const methods = {
onInput: defineMethod<string, MyState>(({ value, state }) => { state.name = value }),
}
const { state } = useVario<FormState>(schema, { state: { name: '', age: 0 } })
Key Constraints
@variojs/core is framework-agnostic — never import Vue APIs in core
@variojs/types is the single source of shared types — no business logic
- All packages: ESM output,
tsup, es2022. Build order: types → core/schema → vue → cli
- Expressions sandboxed — no arbitrary code execution
- State access is flat:
ctx._set('count', 1) not ctx._set('models.count', 1)
- System API prefix:
_get/_set/$emit/$methods/$self/$parent
- Tests:
packages/vario-vue/__tests__/ (433 unit tests), tests/integration/ (42 integration tests across 8 files), Vitest
- Integration tests cover: expression sandbox, error boundaries, schema query, control flow, plugin system
Reference Files
The references/ directory contains detailed deep-dive documents. Read them when the SKILL.md summary above is insufficient for the task at hand.
| File | When to read |
|---|
| model-binding.md | Path stack mechanics, 7 path formats, auto-detection chain, named models, modifier internals, default values, loop context binding |
| events-actions.md | 5 event syntax formats, detection logic, Action VM integration, MethodContext full API, modifier behavior, WeakMap caching, preprocessActionsParams |
| vue-features.md | Refs registry internals, lifecycle wrapper, provide/inject expression evaluation, teleport/transition/keep-alive, directives normalization, VNode wrapping order, VNodePlugin architecture |
| control-flow.md | cond/show implementation details, loop handler full flow, createLoopContext, markLoopSchema, key priority, Fragment wrapping, expression evaluation, text interpolation regex, slot detection and resolution |
| performance.md | PathMemoCache internals (MAX_SIZE=5000), canMemo conditions, subtree detection WeakMap caching, shouldComponentize criteria, SchemaStore (internal), cache size limits, sideEffects:false |
| schema-query-context.md | SchemaQueryApi, NodeWrapper, SchemaAnalyzer lazy analysis, NodeContext interface, ctx variables ($self/$parent/$siblings/$children), Proxy+WeakMap chain |
| patterns.md | Complete code recipes: form, data table, loop cards, scoped slots, computed chains, conditional UI, event patterns, dialog/modal, pagination — with Element Plus examples |