Dynamic form builder for Vuetify 3 with VineJS validation
npm install @xosen/vuetify-formDynamic form builder for Vuetify 3 with VineJS validation and an extensible component registry.
``bash`
pnpm add @xosen/vuetify-form
Peer dependencies: vue ^3.3.0, vuetify ^3.4.0.
`ts
import { createApp } from 'vue';
import { createFormBuilder } from '@xosen/vuetify-form';
import PhoneInput from './components/PhoneInput.vue';
const app = createApp(App);
app.use(createFormBuilder({
locale: () => i18n.global.locale.value, // for VineJS validation messages
components: {
phone: PhoneInput, // register custom field types
},
}));
`
Plugin options:
| Option | Type | Default | Description |
|---|---|---|---|
| locale | () => string | — | Locale getter for VineJS validation messages |components
| | Record | {} | Custom field components to register |registerGlobally
| | boolean | true | Register components in global registry |
`vue
:schema="schema"
@submit="handleSubmit"
/>
`
| Prop | Type | Default | Description |
|---|---|---|---|
| schema | FormField[] \| FormSchema | required | Form schema definition |modelValue
| | Record | {} | Form data (v-model) |readonly
| | boolean | false | Make all fields readonly |disabled
| | boolean | false | Disable all fields |variant
| | string | 'outlined' | Default Vuetify variant |density
| | string | 'default' | Default Vuetify density |vineSchema
| | any | — | VineJS validation schema |
| Event | Payload | Description |
|---|---|---|
| update:modelValue | Record | Form data changed |submit
| | Record | Form submitted |
`ts
const formRef = ref
// Validate the form (Vuetify rules + VineJS)
const { valid } = await formRef.value.validate()
// Reset form data and validation
formRef.value.reset()
// Clear validation errors only
formRef.value.resetValidation()
// Direct access
formRef.value.formData // Ref
formRef.value.isValid // ComputedRef
`
`ts`
const schema: FormField[] = [
{ name: 'email', type: 'email', label: 'Email' },
{ name: 'password', type: 'password', label: 'Password' },
]
`ts
import type { FormSchema } from '@xosen/vuetify-form'
const schema: FormSchema = {
cols: 6, // default column width for all fields
variant: 'outlined', // default variant
density: 'comfortable', // default density
vineSchema: vine.object({ // VineJS validation
email: vine.string().email(),
password: vine.string().minLength(8),
}),
fields: [
{ name: 'email', type: 'email', label: 'Email' },
{ name: 'password', type: 'password', label: 'Password', cols: 12 },
],
}
`
`ts`
{
name: 'username',
type: 'text', // 'text' | 'email' | 'number'
label: 'Username',
maxlength: 50,
counter: true,
clearable: true,
prependIcon: 'mdi-account',
appendIcon: 'mdi-check',
autocomplete: 'username',
}
Password field with eye toggle for visibility.
`ts`
{
name: 'password',
type: 'password',
label: 'Password',
maxlength: 128,
clearable: true,
}
`ts`
{
name: 'bio',
type: 'textarea',
label: 'Bio',
rows: 4,
autoGrow: true,
maxlength: 500,
counter: true,
}
`ts`
{
name: 'country',
type: 'select',
label: 'Country',
items: ['USA', 'Canada', 'Mexico'], // static array
// items: () => fetch('/api/countries').then(r => r.json()), // async
itemTitle: 'name',
itemValue: 'id',
multiple: false,
chips: false,
clearable: true,
}
Enhanced autocomplete with debounced server-side search.
`ts`
{
name: 'customer',
type: 'autocomplete',
label: 'Customer',
itemTitle: 'name',
itemValue: 'id',
multiple: false,
chips: false,
clearable: true,
// Static or async items (loaded once on mount)
items: async () => api.getCustomers(),
// OR: server-side search (debounced 500ms)
onLoad: async ({ search, connector }) => {
return connector.searchCustomers(search)
},
connector: apiClient,
}
`ts`
{
name: 'agree',
type: 'checkbox',
label: 'I agree to the terms',
trueValue: 'yes',
falseValue: 'no',
}
`ts`
{
name: 'active',
type: 'switch',
label: 'Active',
inset: true,
color: 'primary',
trueValue: 1,
falseValue: 0,
}
`ts`
{
name: 'priority',
type: 'radio',
label: 'Priority',
items: [
{ title: 'Low', value: 'low' },
{ title: 'Medium', value: 'medium' },
{ title: 'High', value: 'high' },
],
itemTitle: 'title',
itemValue: 'value',
inline: true,
}
`ts`
{
name: 'theme',
type: 'color',
label: 'Theme Color',
mode: 'hex',
hideSliders: false,
hideInputs: false,
hideCanvas: false,
}
`ts`
{
name: 'birthday',
type: 'date',
label: 'Birthday',
min: '1900-01-01',
max: '2026-12-31',
clearable: true,
}
`ts`
{
name: 'phone',
type: 'mask-input',
label: 'Phone',
mask: '+# (###) ###-####',
clearable: true,
}
Grouped select/autocomplete with divider headers.
`ts`
{
name: 'department',
type: 'group-select',
label: 'Department',
items: [
{ text: 'Engineering', value: 'eng', group: 'tech' },
{ text: 'Design', value: 'design', group: 'tech' },
{ text: 'Sales', value: 'sales', group: 'business' },
],
itemTitle: 'text',
itemValue: 'value',
itemGroup: 'group',
groupsText: { tech: 'Technology', business: 'Business' },
searchable: true, // uses VAutocomplete instead of VSelect
chips: true,
multiple: true,
}
Render any Vue component as a form field.
`ts
// Inline component
{
name: 'custom',
type: 'component',
component: MyCustomInput,
props: { customProp: 'value' },
}
// Reactive props (computed from form data)
{
name: 'custom',
type: 'component',
component: MyCustomInput,
props: (formData) => ({
options: formData.category === 'premium' ? premiumOptions : standardOptions,
}),
}
// Render function (no v-model)
{
type: 'component',
render: (h, formData) => h('div', Hello ${formData.name}),`
}
Custom components should follow the v-model pattern:
`vue`
All field types share these base properties:
`ts`
{
label?: string,
placeholder?: string,
hint?: string,
persistentHint?: boolean,
rules?: Array<(v: any) => boolean | string>,
disabled?: boolean | (() => boolean), // static or reactive
readonly?: boolean,
required?: boolean,
hideDetails?: boolean | 'auto',
variant?: 'filled' | 'outlined' | 'plain' | 'underlined' | 'solo' | 'solo-inverted' | 'solo-filled',
density?: 'default' | 'comfortable' | 'compact',
color?: string,
bgColor?: string,
autofocus?: boolean,
cols?: number | string, // grid column width (1-12)
class?: string,
visible?: boolean | ((data: any) => boolean),
}
`ts`
const schema: FormField[] = [
{ name: 'role', type: 'select', label: 'Role', items: ['admin', 'user'] },
{
name: 'adminKey',
type: 'text',
label: 'Admin Key',
visible: (data) => data.role === 'admin', // only shown when role is admin
},
]
`ts
const schema: FormField[] = [
{ name: 'firstName', type: 'text', label: 'First Name', cols: 6 },
{ name: 'lastName', type: 'text', label: 'Last Name', cols: 6 },
{ name: 'email', type: 'email', label: 'Email' }, // full width (12)
]
// Or set a default for all fields via FormSchema:
const schema: FormSchema = {
cols: 6,
fields: [
{ name: 'firstName', type: 'text', label: 'First Name' }, // 6
{ name: 'lastName', type: 'text', label: 'Last Name' }, // 6
{ name: 'email', type: 'email', label: 'Email', cols: 12 }, // override to 12
],
}
`
Select, autocomplete, radio, and group-select fields support async item loading:
`ts`
{
name: 'country',
type: 'select',
label: 'Country',
items: async () => {
const res = await fetch('/api/countries')
return res.json()
},
}
Items are loaded once on mount. A loading spinner is shown automatically while the promise resolves.
For autocomplete with server-side search, use onLoad instead:
`ts/api/customers?q=${search}
{
name: 'customer',
type: 'autocomplete',
label: 'Customer',
onLoad: async ({ search }) => {
const res = await fetch()`
return res.json()
},
}
`ts
import { vine } from '@xosen/vuetify-form'
const vineSchema = vine.object({
email: vine.string().email(),
password: vine.string().minLength(8),
age: vine.number().min(18),
})
// Pass as prop
// Or embed in FormSchema
const schema: FormSchema = {
vineSchema,
fields: [
{ name: 'email', type: 'email', label: 'Email' },
{ name: 'password', type: 'password', label: 'Password' },
{ name: 'age', type: 'number', label: 'Age' },
],
}
`
Validation runs on field blur and on explicit validate() calls. Errors display inline via Vuetify's error-messages system.
`ts
import { vineFieldRules } from '@xosen/vuetify-form'
const schema = vine.object({
email: vine.string().email(),
})
const fields: FormField[] = [
{
name: 'email',
type: 'email',
label: 'Email',
rules: vineFieldRules(schema, 'email', () => formData.value),
},
]
`
`ts`
import {
vineFieldRules, // convert VineJS field to Vuetify rules
vineSchemaToRules, // convert entire schema to rules map
createVineRule, // create rules from a single VineJS field schema
validateWithVine, // standalone validation (returns { valid, data?, errors? })
configureVineJS, // configure VineJS with i18n
jsonRule, // custom VineJS rule for JSON strings
vine, // re-exported VineJS instance
} from '@xosen/vuetify-form'
`ts`
const schema = vine.object({
config: vine.string().optional().use(jsonRule()),
})
Built-in translations for en, ru, uk, hu. Locale is configured via the plugin:
`ts`
app.use(createFormBuilder({
locale: () => i18n.global.locale.value,
}))
Or directly:
`ts`
import { configureVineJS } from '@xosen/vuetify-form'
configureVineJS(() => i18n.global.locale.value)
Custom error transformation:
`tsvalidation.${error.rule}
const schema: FormSchema = {
vineSchema,
onValidationError: (error) => {
return i18n.global.t(, { field: error.field })`
},
fields: [...],
}
Register custom field types at runtime.
`ts
import { registerFieldType } from '@xosen/vuetify-form'
import PhoneInput from './components/PhoneInput.vue'
registerFieldType('phone', {
component: PhoneInput,
needsValidation: true,
needsBlurHandler: true,
needsInputHandler: true,
extractProps: ['defaultCountry', 'preferredCountries'],
})
`
`ts
import { registerFieldTypes } from '@xosen/vuetify-form'
registerFieldTypes({
phone: { component: PhoneInput, extractProps: ['defaultCountry'] },
'rich-text': { component: RichTextEditor, needsValidation: false },
})
`
`ts`
interface FieldTypeConfig {
component: Component;
needsValidation?: boolean; // show error messages (default: true)
needsBlurHandler?: boolean; // wire @blur for validation (default: true)
needsInputHandler?: boolean; // wire @update:model-value (default: true)
needsType?: boolean; // pass :type prop (default: false)
needsItems?: boolean; // pass :items prop (default: false)
needsLoading?: boolean; // pass :loading prop (default: false)
extractProps?: string[]; // field-specific props to extract
}
`ts`
import {
getFieldComponent, // get registered component by type
hasFieldComponent, // check if type is registered
getRegisteredFieldTypes, // list all registered type names
} from '@xosen/vuetify-form'
Extend the type system for custom field types:
`ts
// types.d.ts
import type { BaseFormField } from '@xosen/vuetify-form'
declare module '@xosen/vuetify-form' {
interface CustomFieldTypes {
phone: PhoneFormField
'rich-text': RichTextFormField
}
}
interface PhoneFormField extends BaseFormField {
type: 'phone'
defaultCountry?: string
preferredCountries?: string[]
}
interface RichTextFormField extends BaseFormField {
type: 'rich-text'
toolbar?: string[]
}
`
`ts`
import {
defineFormSchema, // type-safe object schema creation
formFieldsObjectToArray, // convert object schema to array
defineSwitchField, // narrowed type helper for switch fields
defineGroupSelectField, // narrowed type helper for group-select
defineComponentField, // narrowed type helper for component fields
} from '@xosen/vuetify-form'
Standalone components exported for direct use:
`ts`
import {
XFormBuilder, // main form builder
XPasswordField, // password with eye toggle
XAutocompleteField, // enhanced autocomplete with onLoad
XGroupSelect, // grouped select/autocomplete
} from '@xosen/vuetify-form'
`ts
// Component
export { XFormBuilder, XPasswordField, XAutocompleteField, XGroupSelect } from '@xosen/vuetify-form'
// Plugin
export { createFormBuilder, FormBuilderComponentsSymbol } from '@xosen/vuetify-form'
export type { FormBuilderPluginOptions } from '@xosen/vuetify-form'
// Registry
export {
registerFieldComponent, registerFieldComponents,
getFieldComponent, hasFieldComponent,
getRegisteredFieldTypes,
} from '@xosen/vuetify-form'
// Validation
export {
useVineValidation, configureVineJS, createVineRule,
jsonRule, validateWithVine, vine, vineFieldRules, vineSchemaToRules,
} from '@xosen/vuetify-form'
// Types
export type {
FormFieldType, CoreFieldType, CustomFieldTypes, RenderFunction, BaseFormField,
TextFormField, TextareaFormField, SelectFormField, AutocompleteFormField,
CheckboxFormField, SwitchFormField, RadioFormField, ColorFormField,
DateFormField, MaskInputFormField, PhoneFormField, GroupSelectFormField,
ComponentFormField, CoreFormField, FormField, FormFieldWithName,
FormFieldsObject, FormFieldWithoutName, FormSchema, FormProps, FormBuilderExposed,
} from '@xosen/vuetify-form'
// Helpers
export {
defineFormSchema, formFieldsObjectToArray,
defineSwitchField, defineGroupSelectField, defineComponentField,
} from '@xosen/vuetify-form'
// Locales
export { locales } from '@xosen/vuetify-form'
`
- @xosen/vuetify-dialog — Dialog, snackbar, and alert system for Vuetify 3
- @xosen/vuetify-dialog-schema` — Schema-based dialogs with form integration and wizards
MIT