Skip to content

FormFields

Dynamic form generator that renders fields from a configuration array. Supports vertical/horizontal layouts, nested rows, dividers, and validation integration.

Composition

Generates a form from a config array. For how it relates to FormGroup and the bare inputs, see Composing Forms.

Import

typescript
import { FormFields } from 'vuiii'

Props

PropTypeDefaultDescription
modelValueData-The form data (v-model, required)
fieldsFormFieldOrRow<Data>[]-Field config; a nested array becomes a row, FORM_DIVIDER a separator
validationResultsRecord<keyof Data, ValidationFieldResults>-Per-field invalid state and error message
orientation'vertical' | 'horizontal''vertical'Layout direction; nesting alternates it

Field config

KeyTypeDescription
namekeyof Data | stringProperty of the model this field binds to
componentComponent | AsyncComponentLoader | stringThe control to render
labelstringPassed to the wrapping FormGroup
descriptionstringPassed to the wrapping FormGroup
hintstringPassed to the wrapping FormGroup
propsRecord<string, unknown> | ((data: Data) => Record<string, unknown>)Forwarded to the control; as a function, of the whole form data
requiredboolean | ((data: Data) => boolean)Static, or derived from the whole form data
disabledboolean | ((data: Data) => boolean)Static, or derived from the whole form data
value{ getter, setter }Maps the field to something other than one property

Slots

SlotDescription
field:{name}Replaces the control for one field, keeping its label and error handling. Props: the field config plus index

Events

Exposes the form data through v-model (update:modelValue). It emits no other custom events.

Basic Usage

vue
<script setup>
import { FormFields, Input, Select } from 'vuiii'
import type { FormField } from 'vuiii'

type UserData = { email: string; name: string; role: string }

const formData = ref<UserData>({ email: '', name: '', role: 'user' })

const fields: FormField<UserData>[] = [
  { name: 'email', component: Input, label: 'Email', props: { type: 'email' } },
  { name: 'name', component: Input, label: 'Name' },
  { name: 'role', component: Select, label: 'Role', props: { options: ['admin', 'user'] } },
]
</script>

<template>
  <FormFields v-model="formData" :fields="fields" />
</template>

component takes a component, not a name from a fixed list — VUIII's, your own, or an async one — and everything in props is forwarded to it.

Rows

Nest an array to lay fields out side by side. Nesting alternates the orientation, so a row inside a vertical form is horizontal.

vue
const fields: FormFieldOrRow<UserData>[] = [
  [
    { name: 'firstName', component: Input, label: 'First name' },
    { name: 'lastName', component: Input, label: 'Last name' },
  ],
  { name: 'email', component: Input, label: 'Email' },
]

Dividers

FORM_DIVIDER in the field list renders a Divider, so sections stay in the config rather than in the template.


vue
import { FORM_DIVIDER } from 'vuiii'

const fields: FormFieldOrRow<UserData>[] = [
  { name: 'name', component: Input, label: 'Name' },
  FORM_DIVIDER,
  { name: 'email', component: Input, label: 'Email' },
]

Fields That Depend on Other Fields

props, required and disabled each accept a function of the whole form data, so a field can react to the rest of the form. Pick a country below and the state select fills in.

vue
const fields: FormField<Address>[] = [
  { name: 'country', component: Select, label: 'Country', props: { options: countries } },
  {
    name: 'state',
    component: Select,
    label: 'State',
    props: (data) => ({ options: statesByCountry[data.country] ?? [] }),
    disabled: (data) => !data.country,
  },
]

Validation

validation-results takes the shape useValidation produces, so the two connect without adapter code: each field gets its error message on the FormGroup and the invalid state on the control.

Email is required
vue
<script setup>
const { validate, validatedFields } = useValidation(validateForm)
</script>

<template>
  <FormFields v-model="data" :fields="fields" :validation-results="validatedFields" />
</template>

See Composing Forms for the full validation and submit flow.

Overriding a Single Field

field:{name} replaces the control for one entry while keeping its label, layout and error handling — so a generated form is not all-or-nothing.

vue
<FormFields v-model="form" :fields="fields">
  <template #field:avatar>
    <FilePicker accept="image/*" @files="([file]) => (form.avatar = file)" />
  </template>
</FormFields>

Transforming a Value

When a field does not map to a single model property, give it a value getter/setter pair. Both receive the whole form data.

ts
const fields: FormField<UserData>[] = [
  {
    name: 'fullName',
    component: Input,
    label: 'Full name',
    value: {
      getter: (data) => `${data.firstName} ${data.lastName}`,
      setter: (value, data) => {
        const [firstName, lastName] = value.split(' ')
        return { ...data, firstName, lastName }
      },
    },
  },
]

Storybook

For interactive examples with all variants, see FormFields in Storybook.

Released under the MIT License.