Back to components
Components

Form

Build structured and accessible forms with support for inputs, validation, and flexible layouts.

Component

Additional Information

Architecture

The Form component provides a clean and consistent layout structure for building forms.

It works with the FormRow, FormFieldGroup, and FormActions components to help organize fields and actions intuitively.

 <template>
    <Form 
        class="max-w-[600px]"
        @submit="handleSubmit" 
        @reset="resetForm"
    >
        <FormRow>
            <InputField 
                id="full-name"
                v-model="formData.fullName"
                v-model:error="formErrors.fullName"
                label="Full name"
                placeholder="Enter full name"
                required
                :validator="validateField"
            />
        </FormRow>
        <FormRow>
            <InputField 
                id="email"
                v-model="formData.email"
                v-model:error="formErrors.email"
                label="Email"
                placeholder="Enter email"
                :validator="validateEmail"
                type="email"
                required
            />
        </FormRow>
        <FormFieldGroup title="Additional Information">
            <FormRow>
                <InputField
                    id="phone"
                    v-model="formData.phone"
                    label="Phone"
                    placeholder="Enter phone number"
                />
                <InputField
                    id="address"
                    v-model="formData.address"
                    label="Address"
                    placeholder="Enter address"
                />
            </FormRow>
        </FormFieldGroup>
        <FormActions>
            <ActionButton 
                type="submit" 
                text="Submit"
                :styleType="ButtonStyleType.PRIMARY_BRAND_FILLED"
            />
            <ActionButton 
                type="reset" 
                text="Reset"
            />
        </FormActions>
    </Form>
</template>
<script setup lang="ts">
// Initialize toast
const { $toast } = useNuxtApp()

// States
const formData = reactive({
    fullName: '',
    email: '',
    phone: '',
    address: '',
})

// Validation
const { formErrors, resetForm, validateFormFields } = useForm({
    formData,
    requiredFields: [
        'fullName', 
        'email', 
    ],
    validators: {
        fullName: validateField,
        email: validateEmail,
    },
})

// Methods
const handleSubmit = () => {
    const isValid = validateFormFields()

    const hasErrors = !isValid

    if (hasErrors) {
        $toast.error('Some fields contain errors', {
            toastId: 'form-error',
        })
        return
    }

    $toast.success('Form submitted successfully', {
        toastId: 'form-success',
    })

    resetForm()
}
</script>
 

Components

This set of components are used to create the layout of a form.

Name Description
<FormRow>

Wraps form field components, ensuring proper spacing and alignment.On smaller screens, fields are automatically stacked vertically for better readability.

<FormFieldGroup>

Groups related form fields under a common title.

<FormActions>

Groups form action buttons, such as Submit or Cancel. On mobile devices, buttons stack vertically and their order can be customized via props.

Usage

FormRow

The FormRow component is used to wrap form field components, ensuring proper spacing and alignment.

 <template>
    <FormRow>
        <InputField 
            id="first-name"
            v-model="formData.firstName"
            label="Full name"
        />
    </FormRow>
    <FormRow>
        <InputField 
            id="full-name"
            v-model="formData.lastName"
            label="Full name"
        />
    </FormRow>
</template>
 
Props Default Type
spacedfalseboolean

spaced

Adds some extra vertifcal padding to the row.

 <template>
    <FormRow spaced>
        ...
    </FormRow>
</template>
 
  • Type: boolean
  • Default: false

FormFieldGroup

The FormFieldGroup component groups related form fields under a common title.

 <template>
    <FormFieldGroup title="Personal Information">
        <FormRow>
            <InputField 
                id="first-name"
                v-model="formData.firstName"
                label="First name"
            />
            <InputField 
                id="last-name"
                v-model="formData.lastName"
                label="Last name"
            />
        </FormRow>
    </FormFieldGroup>
</template>
 
Props Default Type
title'Group title'string
titleClass — string
headingTag'h3'HeadingTag

title

Sets the title of the form field group.

 <template>
    <FormFieldGroup title="Personal Information">
        ...
    </FormFieldGroup>
</template>
 
  • Type: string
  • Default: 'Group title'

FormActions

The FormActions component groups form action buttons, such as Submit or Cancel. On mobile devices, buttons stack vertically and their order can be customized via props.

 <template>
    <FormActions>
        <ActionButton 
            type="submit" 
            text="Submit"
            :styleType="ButtonStyleType.PRIMARY_BRAND_FILLED"
        />
        <ActionButton 
            type="reset" 
            text="Reset"
        />
    </FormActions>
</template>
 
Props Default Type
reverseOnMobilefalseboolean

reverseOnMobile

Reverses the order of the buttons on mobile devices.

 <template>
    <FormActions reverseOnMobile>
        ...
    </FormActions>
</template>
 
  • Type: boolean
  • Default: true

Triggers

submit

To submit a form, there two options:

Via @submit

  1. Attach the @submit emitter to the Form and set the handle submit function. It automatically prevents default behaviour.
  2. Set one of the form buttonstype prop to submit.
  3. The @submit event will be triggered when the form is submitted.
 <template>
    <Form @submit="handleSubmit">
        <FormRow>
            <InputField 
                id="full-name"
                v-model="formData.fullName"
                label="Full name"
            />
        </FormRow>
        <FormActions>
            <ActionButton 
                type="submit" 
                text="Submit"
                :styleType="ButtonStyleType.PRIMARY_BRAND_FILLED"
            />
        </FormActions>
    </Form>
</template>
<script setup lang="ts">
// Methods
const handleSubmit = () => {
    // Submit logic here
}
</script>
 

Via button @click

@submit emitter can be omitted when the submit function is being set directly to the submit button @click.

In this case, the button does not require the type prop.

 <template>
    <Form>
        <FormRow>
            <InputField 
                id="full-name"
                v-model="formData.fullName"
                label="Full name"
            />
        </FormRow>
        <FormActions>
            <ActionButton 
                text="Submit"
                :styleType="ButtonStyleType.PRIMARY_BRAND_FILLED"
                @click="handleSubmit"
            />
        </FormActions>
    </Form>
</template>
<script setup lang="ts">
// Methods
const handleSubmit = () => {
    // Submit logic here
}
</script>
 

reset

To reset a form, there two options:

Via @reset

  1. Attach the @reset emitter to the Form and set the handle submit function. It automatically prevents default behaviour.
  2. Set one of the form buttonstype prop to reset.
  3. The @reset event will be triggered when the form is submitted.
 <template>
    <Form @reset="resetForm">
        <FormRow>
            <InputField 
                id="full-name"
                v-model="formData.fullName"
                label="Full name"
            />
        </FormRow>
        <FormActions>
            <ActionButton 
                type="reset" 
                text="Reset"
            />
        </FormActions>
    </Form>
</template>
<script setup lang="ts">
// States
const formData = reactive({
    fullName: '',
})

// Composables
const { resetForm } = useForm({
    formData,
})
</script>
 

Via button @click

@reset emitter can be omitted when the submit function is being set directly to the submit button @click.

In this case, the button does not require the type prop.

 <template>
    <Form>
        <FormRow>
            <InputField 
                id="full-name"
                v-model="formData.fullName"
                label="Full name"
            />
        </FormRow>
        <FormActions>
            <ActionButton 
                text="Reset"
                @click="resetForm"
            />
        </FormActions>
    </Form>
</template>
<script setup lang="ts">
// States
const formData = reactive({
    fullName: '',
})

// Composables
const { resetForm } = useForm({
    formData,
})
</script>
 

Validation

To properly validate a form, consider the following aspects:

formData

A reactive object named formData is required to store the form’s field values. This object should be defined inside the <script> block.

Form field requirements

Must use this two props:

  • required attribute
  • v-model:error binding (linked to formErrors)

Validation composable

Use the useForm composable to manage validation logic:

 const { formErrors, resetForm, validateFormFields } = useForm({
    formData,
    requiredFields: [
        'fullName', 
        'email', 
    ],
    validators: {
        fullName: validateField,
        email: validateEmail,
    },
    validateOn: FormValidationMode.SUBMIT // Optional (Default: Submit)
})
 
  • formErrors: An object that holds error messages for each form field.
  • resetForm: A function to reset all fields and error states.
  • validateFormFields: A function to validate all required fields using the provided validators.
  • validateOn (optional):
    Defines when validation should be triggered before form submission. By default, the validation mode is set to FormValidationMode.SUBMIT, which means fields are only validated when the form is submitted.

    If you prefer to validate fields when they lose focus, you can set validateOn to FormValidationMode.BLUR. Once the form has been submitted at least once, validation will automatically switch to BLUR mode for subsequent changes.

    When calling resetForm, the validation mode will be reset to the initially defined validateOn value.

Usage example

 <template>
    <Form @submit="handleSubmit">
        <FormRow>
            <InputField 
                id="full-name"
                v-model="formData.fullName"
                v-model:error="formErrors.fullName"
                label="Full name"
                placeholder="Enter full name"
                required
                :validator="validateField"
            />
        </FormRow>
        <FormActions>
            <ActionButton 
                type="submit" 
                text="Submit"
                :styleType="ButtonStyleType.PRIMARY_BRAND_FILLED"
            />
        </FormActions>
    </Form>
</template>
<script setup lang="ts">
// Initialize toast
const { $toast } = useNuxtApp()

// States
const formData = reactive({
    fullName: '',
})

// Validation
const { formErrors, resetForm, validateFormFields } = useForm({
    formData,
    requiredFields: [
        'fullName', 
    ],
    validators: {
        fullName: validateField,
    },
})

// Methods
const handleSubmit = () => {
    const isValid = validateFormFields()

    if (!isValid) {
        $toast.error('Some fields contain errors', {
            toastId: 'form-error',
        })
        return
    }

    $toast.success('Form submitted successfully', {
        toastId: 'form-success',
    })

    resetForm()
}
</script>
 

Key takeaways

  • Define a formData object using reactive() to manage field values.
  • Bind each field’s error to formErrors using v-model:error, matching the corresponding key from formData.
  • Use the useForm composable to define:
    • Required fields
    • Custom validation logic for each field
  • Call validateFormFields() inside the submit handler to check if all fields pass validation.
  • Use resetForm() after a successful submission to clear the form.

Validation functions

validateField

Checks if the field value has a value and is not empty.

 validators: {
    field: validateField,
}
 

validateEmail

Validates whether a given value is a properly formatted email address and not empty.

 validators: {
    email: validateEmail,
}
 

validatePasswordMatch

Validates that the repeated password matches the original one.

 validators: {
    passwordRepeat: value => validatePasswordMatch(formData.password, value),
}
 

validateUrl

Validates whether the value is a valid URL and is not empty.

 validators: {
    url: validateUrl,
}
 

validateBooleanField

Validates that a boolean field (checkbox or switch) is true.

 validators: {
    acceptConditions: validateBooleanField,
}
 

validateArrayField

Validates that the field is a non-empty array, optionally checking minimum and/or maximum item counts.

 validators: {
    technologies: validateArrayField,
}
 

validateDateRange

Validates that the end date is after or equal to the start date. If either date is missing or invalid, it returns an error message.

 validators: {
    startDate: value => validateField(value),
    endDate: value => validateDateRange(formData.startDate, value),
}
 

composeArrayValidators

Composes multiple array validators and returns the first error found.

 const validateRules = composeArrayValidators([
    validateRuleCompleteness,
    validateAtLeastOneRule,
])

validators: {
    rules: validateRules,
}
 

validateRuleCompleteness

Validates that rule rows are not partially completed. Empty rows are allowed; partially filled rows are rejected.

 validators: {
    rules: validateRuleCompleteness,
}
 

validateAtLeastOneRule

Validates that at least one complete rule exists.

 validators: {
    rules: validateAtLeastOneRule,
}
 

validateMaxRules

Validates that a rules array does not exceed a maximum length.

 validators: {
    rules: value => validateMaxRules(value, 10, 'You can add up to 10 rules.'),
}
 

createRulesFieldValidator

Creates a ready-to-use validator for RulesField, combining:

  • incomplete row validation
  • at least one complete rule validation
  • max rules validation (optional)
  • optional custom validators
 const validateRules = createRulesFieldValidator({
    required: true,
    maxRules: 10,
    requiredFieldMessage: 'Add at least one complete rule.',
    incompleteRuleMessage: 'Complete all rule fields before continuing.',
    maxRulesMessage: 'You can add up to 10 rules.',
})

const { formErrors, validateFormFields } = useForm({
    formData,
    requiredFields: ['rules'],
    validators: {
        rules: validateRules,
    },
})