@xosen/vuetify-form
v0.0.4
Published
Dynamic form builder for Vuetify 3 with VineJS validation
Maintainers
Readme
@xosen/vuetify-form
Dynamic form builder for Vuetify 3 with VineJS validation and extensible component registry.
Features
- Dynamic Form Generation: Build forms from schema definitions
- VineJS Validation: Integrated validation with VineJS
- Component Registry: Register custom field components at runtime
- Type-Safe: Full TypeScript support
- Tree-Shakeable: Only bundle what you use
- SSR Compatible: Works with server-side rendering
- Extensible: Easy to add custom field types
Installation
npm install @xosen/vuetify-form
# or
pnpm add @xosen/vuetify-form
# or
yarn add @xosen/vuetify-formPeer Dependencies
npm install vue@^3.3.0 vuetify@^3.4.0Basic Usage
<template>
<XFormBuilder
v-model="formData"
:schema="schema"
@submit="handleSubmit"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { XFormBuilder } from '@xosen/vuetify-form'
import type { FormField } from '@xosen/vuetify-form'
const formData = ref({})
const schema: FormField[] = [
{
name: 'email',
type: 'email',
label: 'Email',
required: true,
},
{
name: 'password',
type: 'password',
label: 'Password',
required: true,
},
]
function handleSubmit(data: Record<string, any>) {
console.log('Form submitted:', data)
}
</script>Core Field Types
The following field types are supported out of the box:
text,email,number- Text input fieldspassword- Password field with visibility toggletextarea- Multi-line text inputselect- Dropdown selectautocomplete- Autocomplete field with async loadingcheckbox- Checkbox inputswitch- Toggle switchradio- Radio button groupcolor- Color pickerdate- Date pickercomponent- Custom component field
Registering Custom Components
The package uses a registry system to allow applications to register custom field types at runtime. This keeps the core package lightweight while allowing full extensibility.
Method 1: Using the Vue Plugin (Recommended)
// main.ts
import { createApp } from 'vue'
import { createFormBuilder } from '@xosen/vuetify-form'
import PhoneInput from './components/PhoneInput.vue'
import CustomWidget from './components/CustomWidget.vue'
const app = createApp(App)
app.use(createFormBuilder({
components: {
phone: PhoneInput,
'custom-widget': CustomWidget,
}
}))Method 2: Direct Registry API
// main.ts or setup file
import { registerFieldComponent, registerFieldComponents } from '@xosen/vuetify-form'
import PhoneInput from './components/PhoneInput.vue'
// Register a single component
registerFieldComponent('phone', PhoneInput)
// Or register multiple at once
registerFieldComponents({
phone: PhoneInput,
'custom-widget': CustomWidget,
})Using Registered Components in Your Form
Once registered, use them like any other field type:
const schema: FormField[] = [
{
name: 'phoneNumber',
type: 'phone', // This will use the registered PhoneInput component
label: 'Phone Number',
required: true,
},
{
name: 'customData',
type: 'custom-widget',
label: 'Custom Widget',
},
]Custom Component Requirements
Custom components should follow the standard v-model pattern:
<template>
<div>
<v-text-field
:model-value="modelValue"
@update:model-value="$emit('update:modelValue', $event)"
v-bind="$attrs"
/>
</div>
</template>
<script setup lang="ts">
defineProps<{
modelValue?: any
label?: string
placeholder?: string
errorMessages?: string[]
error?: boolean
disabled?: boolean
readonly?: boolean
// ... other common field props
}>()
defineEmits<{
'update:modelValue': [value: any]
'blur': []
}>()
</script>Using the Component Type (Alternative)
For one-off custom components, you can use the component field type without registering:
<script setup lang="ts">
import CustomInput from './CustomInput.vue'
const schema: FormField[] = [
{
name: 'custom',
type: 'component',
component: CustomInput,
label: 'Custom Field',
props: {
// Custom props for your component
customProp: 'value',
},
},
]
</script>VineJS Validation
Integrate VineJS schemas for validation:
import { vine } from '@vinejs/vine'
const vineSchema = vine.object({
email: vine.string().email(),
password: vine.string().minLength(8),
})
const formSchema = {
fields: [
{ name: 'email', type: 'email', label: 'Email' },
{ name: 'password', type: 'password', label: 'Password' },
],
vineSchema,
}Advanced Features
Conditional Fields
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 show if role is admin
},
]Async Select Items
{
name: 'country',
type: 'select',
label: 'Country',
items: async () => {
const response = await fetch('/api/countries')
return response.json()
},
}Grid Layout
const schema: FormField[] = [
{
name: 'firstName',
type: 'text',
label: 'First Name',
cols: 6, // Half width
},
{
name: 'lastName',
type: 'text',
label: 'Last Name',
cols: 6, // Half width
},
]TypeScript Support
Extending Field Types
You can extend the field types for better type safety:
// types.d.ts
declare module '@xosen/vuetify-form' {
interface CustomFieldTypes {
'phone': PhoneFormField
'custom-widget': CustomWidgetFormField
}
}
// Now TypeScript knows about your custom types
export interface PhoneFormField extends BaseFormField {
type: 'phone'
countryCode?: string
// ... other phone-specific props
}API Reference
Components
XFormBuilder
Main form builder component.
Props:
schema: FormField[] | FormSchema- Form schema definitionmodelValue?: Record<string, any>- Form data (v-model)readonly?: boolean- Make all fields readonlydisabled?: boolean- Disable all fieldsvariant?: string- Default Vuetify variant for all fieldsdensity?: string- Default Vuetify density for all fieldsvineSchema?: any- VineJS validation schema
Events:
update:modelValue- Emitted when form data changessubmit- Emitted when form is submitted
Exposed Methods:
validate()- Validate the formreset()- Reset the formresetValidation()- Clear validation errors
Registry Functions
registerFieldComponent(type, component)
Register a single field component.
registerFieldComponent('phone', PhoneInput)registerFieldComponents(components)
Register multiple field components.
registerFieldComponents({
phone: PhoneInput,
custom: CustomWidget,
})getFieldComponent(type)
Get a registered component by type.
const PhoneComponent = getFieldComponent('phone')hasFieldComponent(type)
Check if a component is registered.
if (hasFieldComponent('phone')) {
// Phone component is registered
}Plugin
createFormBuilder(options)
Create the Vue plugin.
app.use(createFormBuilder({
components: {
phone: PhoneInput,
},
registerGlobally: true, // Default: true
}))Options:
components?: Record<string, Component>- Components to registerregisterGlobally?: boolean- Register components globally (default: true)
Examples
Complete Form with Validation
<template>
<XFormBuilder
ref="formRef"
v-model="formData"
:schema="formSchema"
@submit="handleSubmit"
/>
<v-btn @click="submitForm">Submit</v-btn>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { XFormBuilder } from '@xosen/vuetify-form'
import { vine } from '@vinejs/vine'
const formRef = ref()
const formData = ref({
name: '',
email: '',
age: null,
})
const vineSchema = vine.object({
name: vine.string().minLength(2),
email: vine.string().email(),
age: vine.number().min(18),
})
const formSchema = {
fields: [
{ name: 'name', type: 'text', label: 'Name', required: true },
{ name: 'email', type: 'email', label: 'Email', required: true },
{ name: 'age', type: 'number', label: 'Age', required: true },
],
vineSchema,
}
async function submitForm() {
const { valid } = await formRef.value.validate()
if (valid) {
handleSubmit(formData.value)
}
}
function handleSubmit(data: Record<string, any>) {
console.log('Submitting:', data)
}
</script>Migration from libs/vuetify-shared
If you were using XFormBuilder from libs/vuetify-shared, here's what changed:
- PhoneInput and GroupSelect are no longer bundled - Register them in your app:
import { registerFieldComponents } from '@xosen/vuetify-form'
import PhoneInput from 'libs/vuetify-shared/src/components/PhoneInput.vue'
import GroupSelect from 'libs/vuetify-shared/src/components/GroupSelect.vue'
registerFieldComponents({
phone: PhoneInput,
'group-select': GroupSelect,
})- Import paths changed:
// Before
import XFormBuilder from 'libs/vuetify-shared/src/form/XFormBuilder.vue'
// After
import { XFormBuilder } from '@xosen/vuetify-form'- Everything else works the same - All field types, validation, and APIs remain compatible.
License
MIT
Contributing
Contributions are welcome! Please open an issue or pull request.
