Skip to content

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
vue
<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 ​

PropTypeDefaultDescription
optionsOvFormOptions-Required. Form configuration with fields, actions, and validation
dataOvFormDataundefinedForm data object for initializing field values
loadingbooleanfalseShow loading overlay with spinner
t(text?: string) => string(text) => textTranslation/localization function for labels and messages

Events ​

EventPayloadDescription
submitOvFormDataEmitted when form is submitted and validation passes
validateOvFormData, errors?: unknownEmitted when validate action is triggered
cancel-Emitted when cancel action is clicked
reset-Emitted when form is reset to initial values
actionactionName: string, OvFormDataEmitted when custom action is triggered
changefieldName: string, value: unknown, allValues: OvFormDataEmitted whenever a field value changes

Slots ​

SlotScopeDescription
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 ​

typescript
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:

PropertyTypeDefaultDescription
typestring-Required. Field type identifier
namestring-Required. Unique field identifier for data binding
labelstring-Field label text
placeholderstring-Input placeholder text
valueunknown-Initial/default field value
requiredboolean-Mark field as required
disabledboolean-Disable field input
readonlyboolean-Make field read-only
hiddenboolean-Hide field from form
variantstring-Vuetify input variant (underlined, outlined, filled, etc.)
densitystring-Input density (default, comfortable, compact)
colorstring-Field color
clearableboolean-Show clear button
counternumber-Character counter limit
hintstring-Helper text below field
prefixstring-Text prefix in field (text types)
suffixstring-Text suffix in field (text types)
prependIconstring-Icon before input
appendIconstring-Icon after input
prependInnerIconstring-Icon inside at start
appendInnerIconstring-Icon inside at end
autocomplete'on' | 'off'-Auto-complete behavior
rulesOvRule[]-Validation rules
errorsstring[]-Field-specific error messages

Text Input Fields ​

Basic text input with optional prefix/suffix:

typescript
{
  type: 'text',
  name: 'username',
  label: 'Username',
  placeholder: 'Enter username',
  counter: 50
}

Email Field ​

Email input with built-in email validation:

typescript
{
  type: 'email',
  name: 'email',
  label: 'Email Address',
  placeholder: 'Enter your email'
}

Password Field ​

Password input with show/hide toggle:

typescript
{
  type: 'password',
  name: 'password',
  label: 'Password',
  placeholder: 'Enter password',
  appendInnerIcon: '$mdiEye'
}

Number Field ​

Numeric input field:

typescript
{
  type: 'number',
  name: 'quantity',
  label: 'Quantity',
  placeholder: 'Enter amount'
}

Textarea Field ​

Multi-line text input with optional auto-grow:

typescript
{
  type: 'textarea',
  name: 'description',
  label: 'Description',
  placeholder: 'Enter description',
  rows: 5,
  counter: 500,
  autoGrow: true,
  noResize: true
}

Textarea-specific properties:

  • rows (number) - Initial row count
  • autoGrow (boolean) - Auto-expand as user types
  • noResize (boolean) - Disable manual resizing

Markdown Field ​

Rich text editor with markdown output using TipTap:

typescript
{
  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 container
  • editorClass (string) - CSS class for editor content area
  • minHeight (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:

typescript
{
  type: 'switch',
  name: 'acceptTerms',
  label: 'I accept terms and conditions',
  value: false
}

Checkbox Field ​

Checkbox input:

typescript
{
  type: 'checkbox',
  name: 'newsletter',
  label: 'Subscribe to newsletter'
}

Rating Field ​

Star rating input:

typescript
{
  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 pixels
  • itemLabels (string[]) - Labels for each rating level

Select Field ​

Dropdown select with single or multiple selection:

typescript
{
  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 } structure
  • multiple (boolean) - Allow multiple selections
  • chips (boolean) - Display selected items as chips
  • itemTitle (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 dynamically
  • debounce (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:

typescript
{
  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:

typescript
{
  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:

typescript
{
  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 itemValue and itemTitle property mappings

File Field ​

File input field:

typescript
{
  type: 'file',
  name: 'attachment',
  label: 'Upload File',
  clearable: true
}

Date Field ​

Date picker input:

typescript
{
  type: 'date',
  name: 'birthDate',
  label: 'Date of Birth',
  placeholder: 'YYYY-MM-DD'
}

Time Field ​

Time picker input:

typescript
{
  type: 'time',
  name: 'eventTime',
  label: 'Event Time',
  placeholder: 'HH:MM'
}

Datetime Field ​

Combined date and time picker:

typescript
{
  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:

typescript
{
  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 TypeParamsDescription
requiredtrueValue must not be empty
min-lengthnumberString length must be >= params
max-lengthnumberString length must be <= params
equalsanyValue must equal params
equals-notanyValue must not equal params
starts-withstringString must start with params
ends-withstringString must end with params
containsstringString must contain params
greater-thannumberNumber must be > params
less-thannumberNumber must be < params
in-range[min, max]Number must be >= min and <= max
includesany[]Value must be in params array
setany[]Value must be in params array
passwordtrueStrong password (letters + digits, 8+ chars)
emailtrueValid email format
urltrueValid URL format
iptrueValid IPv4 address
regexpRegExp stringValue must match regexp
same-asanyValue must equal params
is-jsontrueValid JSON string
custom(value) => booleanCustom validation function

Actions ​

Actions are buttons displayed at the bottom of the form:

typescript
// 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 ​

ActionHandlerEvent
submitValidates all fields and submits if validsubmit
validateValidates all fields without submittingvalidate
resetResets form to initial valuesreset
cancelCancels form editingcancel
CustomUser-defined actionaction

Field Errors ​

Server-side or programmatic errors can be displayed on fields:

typescript
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:

  • cols prop determines column count on desktop (1-12)
  • Automatically switches to single column on mobile devices
  • Use OvForm props 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-1 class 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 data prop is cleared
  • All labels and messages respect the t translation function
  • Long-running form submissions should use the loading prop to prevent double-submission
  • Mobile devices always render single-column layout regardless of cols setting
  • The change event is emitted for each field when its value changes, enabling real-time reactivity and dynamic form behavior