@texaryn/vue

Schema-driven forms and display documents for Vue 3, with composables and default widgets
2,589 2
Install
npm install @texaryn/vue
Latest Version:0.8.1
Vue:3
License:Apache-2.0
Last Updated:Oct 17, 2026
Links: GitHub  ·  npm
Maintainer: hamzahamidi

@texaryn/vue

Vue 3 bindings for Texaryn: composables, provide/inject context, a renderer and a small set of native widgets.

Status: pre-1.0. Public APIs may change before 1.0.

Install

pnpm add @texaryn/core @texaryn/schema-json @texaryn/vue vue

@texaryn/vue requires Vue 3.5 or newer, for useId.

Quick start

main.ts creates the adapter before mounting and passes it to App as a prop, because awaiting createJsonSchemaAdapter inside <script setup> would make App an async component, which renders nothing as the root without a <Suspense> boundary.

// schema.ts
export const schema = {
  $schema: 'https://json-schema.org/draft/2020-12/schema',
  type: 'object',
  properties: { name: { type: 'string', title: 'Name' } },
  required: ['name'],
}
// main.ts
import { createApp } from 'vue'
import { createJsonSchemaAdapter } from '@texaryn/schema-json'
import App from './App.vue'
import { schema } from './schema'

const adapter = await createJsonSchemaAdapter(schema)

createApp(App, { adapter }).mount('#app')
<!-- App.vue -->
<script setup lang="ts">
import type { SchemaEvaluationPort } from '@texaryn/core'
import { ErrorSummary, FormRoot, createDefaultRegistry, provideFormRuntime, useForm } from '@texaryn/vue'

const props = defineProps<{ adapter: SchemaEvaluationPort }>()

const form = useForm(props.adapter, { initialData: { name: '' } })
provideFormRuntime(form.runtime)

const registry = createDefaultRegistry()
function submit() {
  form.dispatch({ type: 'Submit' })
}
</script>

<template>
  <!-- Both have to be descendants, not this component itself -->
  <form novalidate @submit.prevent="submit">
    <ErrorSummary />
    <FormRoot :registry="registry" />
    <button type="submit">Submit</button>
  </form>
</template>

FormRoot renders the tree only. The application owns the native <form> and connects its submit event to the runtime, which keeps the renderer safe to compose inside an existing form.

provideFormRuntime(runtime, { messages }) takes a whole FormMessages set from @texaryn/core, as a value, a ref or a getter, and replaces every word the built-in widgets invent. Custom widgets read it with useFormMessages(), which returns a computed.

ErrorSummary renders a named group, headed by an h2, listing every visible error with a link to the input the renderer mounted, #<prefix>-<nodeId>-input, and nothing while there are none. It takes focus once a failed submit settles, once per attempt; pass :focus="false" on all but one summary when one runtime is rendered twice. It is not a live region: the fields already announce their own errors, and the focus move is what speaks the heading.

useForm creates the runtime and ties it to the calling scope. Providing is a separate call rather than a side effect of construction: React's equivalent is <FormProvider> in the template, and Vue's is a setup call.

A component cannot inject what it provided itself, so FormRoot and anything calling useField has to be a descendant of whichever component called provideFormRuntime.

That call also opens the DOM id namespace for the form, which is why it sits there rather than in FormRoot: a sibling added later shares the same scope. Generated ids are opaque relationship identifiers rather than styling hooks, so use classes or data attributes for CSS. Uniqueness is per Vue application, so two independent apps on one page need distinct app.config.idPrefix values.

Lifetime is a requirement, not a convention

useForm and useStore throw when called outside setup() or an active effectScope. Both own something that only teardown releases, a runtime with live validation timers and a store subscription, and neither returns a handle the caller could stop. Requiring a scope is what makes that ownership real: the check runs before anything is created, so a misuse cannot leak the thing it was about to allocate. Stopping the scope destroys the runtime and unsubscribes.

What it does not depend on

@texaryn/core and Vue. Not @texaryn/react. The point of a second binding is to find out what core actually promises, and reaching into the first binding for a helper would answer a different question.

Node ids are accepted as refs or getters, and that matters

useField and useFieldArray take MaybeRefOrGetter<NodeId>, not NodeId.

Node ids follow position in the document. Array item identity is carried separately as StableItemId, and a row keyed on that keeps its component instance through a move while the node underneath it becomes a different one. React re-reads the id on every render, so it follows for free. A Vue composable that captured the id in setup would keep reading the old position and keep writing to it, so typing into a moved row would overwrite the row that took its place.

The same reasoning applies to reads. getNodeState is a map lookup with no reactivity of its own, and the runtime replaces a node's state bundle when its id leaves the document and returns. useField reads the document ref when it resolves the bundle so a recompile re-resolves it.

Both are covered by tests that fail when the behaviour is removed.

Conformance

tests/example-conformance/vue.test.ts runs the whole catalog through this package and makes the same two claims the React renderers make: every example mounts without a console error or warning, and every active node in every example resolves a widget. The framework-neutral half of that harness is shared; mounting is not, because the two frameworks do not share a mounting model.

No single file components

The package ships plain TypeScript with render functions, so it builds with the same tsc --build as every other package in the workspace and needs no Vue-specific toolchain to compile. Consumers are free to use SFCs; nothing here requires them to.

Related packages

See the architecture guide for the package boundaries and runtime contracts.

License

Apache-2.0

Related Packages

@narrative.io/jsonforms-provider-protocols

Dynamic data provider capabilities for JSONForms with Vue 3 integration

18,467
@laravilt/forms

Complete form builder with 30+ field types, validation, and dynamic fields. Buil...

1,003 0
former-ui

Former is a headless library for building and handling forms for Vue using an UI...

3,214 7

Version History

Version Released License
0.8.1 Apache-2.0
0.8.0 Apache-2.0
0.7.0 Apache-2.0
0.6.0 Apache-2.0
0.5.0 Apache-2.0