Form
Flexible form component with built-in validation, customizable fields, and action handling. Supports text, email, password, textarea, markdown, number, select, checkbox, switch, rating, file, and date/time inputs.
Overview
VOvForm provides a comprehensive form solution with configurable fields, validation rules, custom actions, and error handling. It supports 14+ field types with Vuetify integration, automatic layout management, and responsive grid columns. The component handles form submission, validation, and custom actions with TypeScript support throughout.
Features:
- 14+ field types (text, email, password, textarea, markdown, number, select, checkbox, switch, rating, file, date, time, datetime)
- Built-in validation with 20+ rule types
- Customizable actions with formatters
- Responsive grid layout with configurable columns
- Error display and server-side error handling
- v-model support for form data binding
- Auto-focus on first field or validation errors
- Loading state with overlay indicator
- Full TypeScript support
Usage
Examples
<template>
<v-ov-form
:data
:options
:loading
:t
@action="action"
@cancel="cancel"
@reset="reset"
@submit="submit"
@validate="validate"
/>
<v-checkbox v-model="loading" label="Loading" class="ma-1" />
<v-btn class="ma-1" @click="update()">Update!</v-btn>
</template>
<script setup lang="ts">
definePage({
meta: {
visibility: 'with-role',
access: 'with-role',
roles: ['developer'],
},
})
const { t } = useI18n()
const loading = ref(false)
function update() {
data.value.text2 = '{}'
data.value.text3 = 'Hello World, some custom more'
data.value.number = 5
data.value.datetime = new Date().toISOString().slice(0, 16)
}
const data = ref({
text1: 'Hello World!',
text2: 'Hello World, again!',
text3: 'Hello World, some more!',
number: 2,
password: '',
rating: 4,
checkbox: false,
datetime: '',
})
const customValidation = (value: string) => {
return value.includes('custom') ? true : "Value must contain 'custom'"
}
const options = ref<OvFormOptions>({
fields: [
{
type: 'text',
name: 'text1',
label: 'Text 1',
placeholder: 'Enter some text',
value: 'Hello World - default!',
},
{
type: 'text',
name: 'text2',
label: 'Text 2',
placeholder: 'Enter some text',
rules: [
{ type: 'required', params: true, message: 'Text 2 is required' },
{ type: 'is-json', params: true, message: 'Text 2 must be a valid JSON' },
],
},
{
type: 'text',
name: 'text3',
label: 'Text 3',
placeholder: 'Enter some text',
variant: 'underlined',
rules: [
{ type: 'required', params: true, message: 'Text 3 is required' },
{ type: 'custom', params: customValidation, message: 'Text 3 must contain "custom"' },
],
},
{
type: 'email',
name: 'email',
label: 'Email',
placeholder: 'Enter valid e-mail address',
},
{
type: 'number',
name: 'number',
label: 'Number',
placeholder: 'Enter some number',
color: 'green',
rules: [
{ type: 'required', params: true, message: 'Number is required' },
{ type: 'less-than', params: 10, message: 'Number must be at least 10' },
],
},
{
type: 'password',
name: 'password',
label: 'Password',
placeholder: 'Enter password, at least 12 characters',
required: true,
},
{
type: 'textarea',
name: 'textarea',
label: 'Textarea',
placeholder: 'Enter some larger text here',
rows: 3,
counter: 200,
noResize: true,
},
{
type: 'switch',
name: 'acceptTerms',
label: 'Accept Terms and Conditions',
required: true,
value: false,
},
{
type: 'rating',
name: 'rating',
label: 'Rating',
length: 6,
color: 'amber',
},
{
type: 'checkbox',
name: 'checkbox',
label: 'Checkbox',
},
{
type: 'select',
name: 'select',
label: 'Select',
items: ['Alfa', 'Bravo', 'Charlie', 'Delta'],
chips: true,
multiple: true,
},
{
type: 'combobox',
name: 'combobox',
label: 'Combo',
items: ['Alfa', 'Bravo', 'Charlie', 'Delta'],
chips: true,
multiple: true,
},
{
type: 'autocomplete',
name: 'autocomplete',
label: 'Autocomplete',
items: ['Alfa', 'Bravo', 'Charlie', 'Delta'],
chips: true,
multiple: true,
},
{
type: 'file',
name: 'file',
label: 'File!',
},
{
type: 'date',
name: 'date',
label: 'Date',
placeholder: 'Select date',
},
{
type: 'time',
name: 'time',
label: 'Time',
placeholder: 'Select time',
},
{
type: 'datetime',
name: 'datetime',
label: 'Date and Time',
placeholder: 'Select date and time',
},
],
cols: 3,
actions: ['validate', 'cancel', 'submit', 'reset', { name: 'custom', format: { color: 'red' } }],
actionFormat: {
color: 'lime',
},
actionAlign: 'center',
actionSubmit: 'submit',
actionReset: 'reset',
actionValidate: 'validate',
actionCancel: 'cancel',
errors: [{ name: 'text1', message: 'Error from outside' }],
focusFirst: true,
})
function action(actionName: string, newData: unknown) {
console.log('action', actionName, newData)
}
function cancel() {
console.log('cancel')
}
function reset() {
console.log('reset')
}
async function submit(newData: unknown) {
loading.value = true
await new Promise((resolve) => setTimeout(resolve, 2000))
data.value.datetime = new Date().toISOString().slice(0, 16)
options.value.errors = [
{ name: 'datetime', message: 'Server-side validation failed for datetime' },
]
console.log('submit', newData)
loading.value = false
}
function validate(newData: unknown, errors: unknown) {
console.log('validate', newData, errors)
}
</script>API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | OvFormOptions | - | Required. Form configuration with fields, actions, and validation |
data | OvFormData | undefined | Form data object for initializing field values |
loading | boolean | false | Show loading overlay with spinner |
t | (text?: string) => string | (text) => text | Translation/localization function for labels and messages |
Events
| Event | Payload | Description |
|---|---|---|
submit | OvFormData | Emitted when form is submitted and validation passes |
validate | OvFormData, errors?: unknown | Emitted when validate action is triggered |
cancel | - | Emitted when cancel action is clicked |
reset | - | Emitted when form is reset to initial values |
action | actionName: string, OvFormData | Emitted when custom action is triggered |
change | fieldName: string, value: unknown, allValues: OvFormData | Emitted whenever a field value changes |
Slots
| Slot | Scope | Description |
|---|---|---|
field-{fieldName} | { value, field } | Customize field label rendering (e.g., field-consent, field-email). Slot is located in the field's label area. Name format: field-{fieldName} where {fieldName} matches the field's name property. Scope provides value (current field value) and field (field configuration object). |
Form Options
interface OvFormOptions {
// Required
fields: OvFormField[]
// Optional - Actions
actions?: OvAction[]
actionFormat?: OvFormat | OvFormat[]
actionAlign?: 'left' | 'center' | 'right'
actionSubmit?: string
actionReset?: string
actionValidate?: string
actionCancel?: string
// Optional - Form behavior
autocomplete?: 'on' | 'off'
disabled?: boolean
readonly?: boolean
fastFail?: boolean
cols?: number
focusFirst?: boolean
// Optional - Error handling
errors?: OvFormFieldError[]
}Field Types
Base Field Properties
All field types share these properties:
| Property | Type | Default | Description |
|---|---|---|---|
type | string | - | Required. Field type identifier |
name | string | - | Required. Unique field identifier for data binding |
label | string | - | Field label text |
placeholder | string | - | Input placeholder text |
value | unknown | - | Initial/default field value |
required | boolean | - | Mark field as required |
disabled | boolean | - | Disable field input |
readonly | boolean | - | Make field read-only |
hidden | boolean | - | Hide field from form |
variant | string | - | Vuetify input variant (underlined, outlined, filled, etc.) |
density | string | - | Input density (default, comfortable, compact) |
color | string | - | Field color |
clearable | boolean | - | Show clear button |
counter | number | - | Character counter limit |
hint | string | - | Helper text below field |
prefix | string | - | Text prefix in field (text types) |
suffix | string | - | Text suffix in field (text types) |
prependIcon | string | - | Icon before input |
appendIcon | string | - | Icon after input |
prependInnerIcon | string | - | Icon inside at start |
appendInnerIcon | string | - | Icon inside at end |
autocomplete | 'on' | 'off' | - | Auto-complete behavior |
rules | OvRule[] | - | Validation rules |
errors | string[] | - | Field-specific error messages |
Text Input Fields
Basic text input with optional prefix/suffix:
{
type: 'text',
name: 'username',
label: 'Username',
placeholder: 'Enter username',
counter: 50
}Email Field
Email input with built-in email validation:
{
type: 'email',
name: 'email',
label: 'Email Address',
placeholder: 'Enter your email'
}Password Field
Password input with show/hide toggle:
{
type: 'password',
name: 'password',
label: 'Password',
placeholder: 'Enter password',
appendInnerIcon: '$mdiEye'
}Number Field
Numeric input field:
{
type: 'number',
name: 'quantity',
label: 'Quantity',
placeholder: 'Enter amount'
}Textarea Field
Multi-line text input with optional auto-grow:
{
type: 'textarea',
name: 'description',
label: 'Description',
placeholder: 'Enter description',
rows: 5,
counter: 500,
autoGrow: true,
noResize: true
}Textarea-specific properties:
rows(number) - Initial row countautoGrow(boolean) - Auto-expand as user typesnoResize(boolean) - Disable manual resizing
Markdown Field
Rich text editor with markdown output using TipTap:
{
type: 'markdown',
name: 'content',
label: 'Content',
hint: 'Use the toolbar to format your text',
toolbar: ['bold', 'italic', 'heading1', 'heading2', 'bulletList', 'orderedList'],
minHeight: '150px',
maxHeight: '300px'
}Markdown-specific properties:
toolbar(string[]) - Toolbar buttons to display. Available options:bold,italic,underline,strike,bulletList,orderedList,heading1,heading2,heading3. Default:['bold', 'italic', 'heading1', 'heading2', 'bulletList', 'orderedList']toolbarClass(string) - CSS class for toolbar containereditorClass(string) - CSS class for editor content areaminHeight(string) - Minimum height of the editor (e.g.,'150px')maxHeight(string) - Maximum height before scrolling (e.g.,'300px')
The markdown field stores content as markdown format, making it ideal for rich text that needs to be rendered or stored as markdown.
Switch Field
Toggle switch input:
{
type: 'switch',
name: 'acceptTerms',
label: 'I accept terms and conditions',
value: false
}Checkbox Field
Checkbox input:
{
type: 'checkbox',
name: 'newsletter',
label: 'Subscribe to newsletter'
}Rating Field
Star rating input:
{
type: 'rating',
name: 'satisfaction',
label: 'How satisfied are you?',
length: 5,
size: 24,
color: 'amber'
}Rating-specific properties:
length(number) - Number of stars (default: 5)size(number) - Star size in pixelsitemLabels(string[]) - Labels for each rating level
Select Field
Dropdown select with single or multiple selection:
{
type: 'select',
name: 'country',
label: 'Select Country',
items: [
{ title: 'USA', value: 'us' },
{ title: 'Canada', value: 'ca' },
{ title: 'Mexico', value: 'mx' }
],
multiple: false,
chips: false
}Selection-specific properties:
items(OvFormSelectItem[]) - Array of selectable items with{ title: string, value: unknown }structuremultiple(boolean) - Allow multiple selectionschips(boolean) - Display selected items as chipsitemTitle(string) - Property name for display text (default:'title')itemValue(string) - Property name for the value (default:'value')fetchItems((search: string) => Promise<OvFormSelectItem[]>) - Async function to fetch items dynamicallydebounce(number) - Debounce delay in milliseconds for async search (default: 300)minSearchLength(number) - Minimum characters required before triggering search (default: 0)
Combobox Field
Autocomplete-enabled select with custom input:
{
type: 'combobox',
name: 'tags',
label: 'Tags',
items: [
{ title: 'React', value: 'react' },
{ title: 'Vue', value: 'vue' },
{ title: 'Angular', value: 'angular' }
],
multiple: true,
chips: true
}Autocomplete Field
Autocomplete with filtering:
{
type: 'autocomplete',
name: 'city',
label: 'Select City',
items: [
{ title: 'New York', value: 'ny' },
{ title: 'Los Angeles', value: 'la' },
{ title: 'Chicago', value: 'chi' }
],
multiple: false
}Autocomplete with Async Data
Autocomplete with dynamic API-driven items:
{
type: 'autocomplete',
name: 'assignee',
label: 'Assignee',
clearable: true,
fetchItems: async (search: string) => {
const response = await http.get('/api/users', { params: { search } })
return response.data.map(user => ({
title: user.fullname,
value: user.uuid
}))
},
itemValue: 'value',
itemTitle: 'title',
debounce: 300,
minSearchLength: 1
}The fetchItems function is called with the search string whenever the user types (after debounce delay). The component automatically:
- Debounces API calls to avoid excessive requests
- Shows a loading indicator while fetching
- Preserves selected items so they display correctly even when search results change
- Supports custom
itemValueanditemTitleproperty mappings
File Field
File input field:
{
type: 'file',
name: 'attachment',
label: 'Upload File',
clearable: true
}Date Field
Date picker input:
{
type: 'date',
name: 'birthDate',
label: 'Date of Birth',
placeholder: 'YYYY-MM-DD'
}Time Field
Time picker input:
{
type: 'time',
name: 'eventTime',
label: 'Event Time',
placeholder: 'HH:MM'
}Datetime Field
Combined date and time picker:
{
type: 'datetime',
name: 'eventDateTime',
label: 'Event Date and Time',
placeholder: 'YYYY-MM-DDTHH:MM'
}Validation Rules
Validation rules are applied to fields through the rules array:
{
type: 'text',
name: 'username',
rules: [
{
type: 'required',
params: true,
message: 'Username is required'
},
{
type: 'min-length',
params: 3,
message: 'Username must be at least 3 characters'
}
]
}Available Rule Types
| Rule Type | Params | Description |
|---|---|---|
required | true | Value must not be empty |
min-length | number | String length must be >= params |
max-length | number | String length must be <= params |
equals | any | Value must equal params |
equals-not | any | Value must not equal params |
starts-with | string | String must start with params |
ends-with | string | String must end with params |
contains | string | String must contain params |
greater-than | number | Number must be > params |
less-than | number | Number must be < params |
in-range | [min, max] | Number must be >= min and <= max |
includes | any[] | Value must be in params array |
set | any[] | Value must be in params array |
password | true | Strong password (letters + digits, 8+ chars) |
email | true | Valid email format |
url | true | Valid URL format |
ip | true | Valid IPv4 address |
regexp | RegExp string | Value must match regexp |
same-as | any | Value must equal params |
is-json | true | Valid JSON string |
custom | (value) => boolean | Custom validation function |
Actions
Actions are buttons displayed at the bottom of the form:
// Built-in actions (string shorthand)
actions: ['submit', 'reset', 'cancel', 'validate']
// Custom actions with formatting
actions: [
{ name: 'submit' },
{
name: 'export',
format: {
color: 'blue',
icon: '$mdiDownload',
text: 'Export Data'
}
}
]Built-in Action Types
| Action | Handler | Event |
|---|---|---|
submit | Validates all fields and submits if valid | submit |
validate | Validates all fields without submitting | validate |
reset | Resets form to initial values | reset |
cancel | Cancels form editing | cancel |
| Custom | User-defined action | action |
Field Errors
Server-side or programmatic errors can be displayed on fields:
errors: [
{
name: 'email',
message: 'This email is already registered'
},
{
name: 'username',
message: 'Username already taken'
}
]Responsive Behavior
The form uses Vuetify's grid system for responsive layout:
colsprop determines column count on desktop (1-12)- Automatically switches to single column on mobile devices
- Use
OvFormprops to configure layout
Styling
The form uses Vuetify defaults provider for consistent styling:
.v-form- Main form container.v-container- Form content wrapper (class:position-relative).v-overlay- Loading overlay (class:rounded)- Form action buttons have
ma-1class by default
Notes
- The form validates on invalid input after first interaction (
validate-on="invalid-input") - Password fields automatically show/hide toggle button
- Rating and checkbox fields have primary color by default if not specified
- Server-side errors override built-in validation messages
- Form state resets when
dataprop is cleared - All labels and messages respect the
ttranslation function - Long-running form submissions should use the
loadingprop to prevent double-submission - Mobile devices always render single-column layout regardless of
colssetting - The
changeevent is emitted for each field when its value changes, enabling real-time reactivity and dynamic form behavior