Getting Started
Introduction & Rationale
Headless form libraries are not scarce. What varies between them is how much of a form's behaviour they model, as opposed to just its state.
@dynamicforms/vue-forms treats behaviour between fields as part of the form definition rather than as wiring you add in your components. A field's visibility, enablement or value can be declared as a condition over other fields. Every change travels through an action pipeline in which each handler receives the previous one and decides whether to call it, transform its result, or abort the event outright. Groups and lists compose recursively, so a nested section or a list row behaves the same way a single field does — and rendering stays entirely yours.
Design Goals
- UI-Agnostic: A logic layer for form state, validation and dynamic behaviour. Works with native HTML controls, Vuetify, Tailwind, or any custom components. The only component the library ships is the optional
MessagesWidgetfor rendering validation errors, and the one deliberate exception isAction, whose value is a label and an icon. - Fields that react to each other: Conditional visibility, enablement and values are declared as statements over other fields, and the action pipeline lets a handler intercept, transform or abort an event.
- Reactive & Type-Safe: Every member of a field, group or list is a tracked read — assign a property and whatever read it re-renders, with no
refto unwrap. A group's value type is inferred from the fields it holds, nested structures included. - Structural serialization: A group's value is the shape of its fields, and
Group.createFromFormData()turns a plain object back into a form. - Lightweight:
vue(^3.5.2) is the only peer dependency andlodash-esthe only runtime one.
Installation
npm install @dynamicforms/vue-formsThe package is ESM-only: it ships one build, which imports lodash-es, its only runtime dependency. Node 22 or newer is required, and vue (^3.5.2) is the only peer dependency. A CommonJS consumer reaches it through require() of an ES module, which Node supports.
Stylesheet
The library ships a small stylesheet used by MessagesWidget. It is not bundled into the JavaScript, so import it once in your app entry point if you use that component:
import '@dynamicforms/vue-forms/style.css';Everything else — Field, Group, List, validators, actions — is UI-agnostic and needs no styles.
Basic Usage
Here's how to create a simple form with @dynamicforms/vue-forms:
import { Field, Group } from '@dynamicforms/vue-forms';
// Create a form with fields
const personForm = new Group({
firstName: new Field({ value: 'John' }),
lastName: new Field({ value: 'Doe' }),
age: new Field({ value: 30 }),
active: new Field({ value: true })
});Every form element — Field, Action, Group and List — is created with new, and every read through it is tracked from that moment on: reading personForm.value or personForm.fields.age.enabled in a template, in a computed or in a watchEffect subscribes to it, and plain assignment re-renders. The element itself is not a Vue proxy, so watch what you read — watch(() => field.value, cb) — rather than passing the element as the source.
Using with Vue Components
You can bind the form fields to any Vue component:
<template>
<form>
<input v-model="personForm.fields.firstName.value" />
<input v-model="personForm.fields.lastName.value" />
<input
type="number"
v-model.number="personForm.fields.age.value"
:disabled="!personForm.fields.age.enabled"
/>
<input type="checkbox" v-model="personForm.fields.active.value" />
</form>
</template>
<script setup>
import { Field, Group } from '@dynamicforms/vue-forms';
const personForm = new Group({
firstName: new Field({ value: 'John' }),
lastName: new Field({ value: 'Doe' }),
age: new Field({ value: 30 }),
active: new Field({ value: true })
});
</script>A disabled field silently ignores writes to value and is omitted from group.value (use group.fullValue if you need every field regardless of enabled).
Validation
Attach validators to a field and render the resulting errors with the MessagesWidget component:
<template>
<input v-model="username.value" />
<messages-widget
v-if="username.touched && username.errors.length > 0"
:message="username.errors"
classes="text-error"
/>
</template>
<script setup>
import { Field, MessagesWidget, Validators } from '@dynamicforms/vue-forms';
const username = new Field({ value: '', validators: [new Validators.Required()] });
</script>Validators run eagerly — the field above is already invalid right after creation, because it has no value. That is why the template checks touched before showing the errors. Required trims a string before measuring it, so a value of spaces alone is no value; pass new Validators.Required({ trim: false }) where the spaces belong to the field.
Plugin Setup
The library ships a Vue plugin for its global options:
import { forms } from '@dynamicforms/vue-forms';
app.use(forms, { useMarkdownInValidators: false });By default the built-in validators produce markdown messages, which MessagesWidget renders through a globally registered vue-markdown component. Set useMarkdownInValidators to false if you don't have one. The same options are reachable without the plugin, through getConfig() and setConfig() — see Configuration.
Versioning and support
The package is in 0.x, where a breaking change goes in the minor version: 0.9.0 → 0.10.0 may break your code, 0.10.0 → 0.10.1 does not. Every such change is listed in the migration guide with before/after code and in the changelog. The public surface is not frozen until 1.0.
| Supported | |
|---|---|
| Vue | ^3.5.2 |
| Node | 22 or newer |
| Module formats | ESM, with type definitions |
| Browsers | whatever your bundler targets — the build is es2015 and uses no browser API of its own |
Next Steps
The model is the whole library in one page — elements, declarations, transactions, validity, and how a List builds its rows. After that, the Examples section shows the patterns in context and the API reference names every member: Field, Group, Validators and Configuration.
Upgrading an existing project? The migration guide walks the whole journey from 0.6.1 onwards, silent breaks first.
