<div align="center">

[![volverjs](docs/static/volverjs-form.svg)](https://volverjs.github.io/form-vue)

## @volverjs/form-vue

`form` `form-field` `form-wrapper` `vue3` `zod` `validation`

[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=volverjs_form-vue&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=volverjs_form-vue) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=volverjs_form-vue&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=volverjs_form-vue) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=volverjs_form-vue&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=volverjs_form-vue) [![Depfu](https://badges.depfu.com/badges/e2c464e3cb95f98ee6a9a566dd44e0a9/status.svg)](https://depfu.com) [![Depfu](https://badges.depfu.com/badges/e2c464e3cb95f98ee6a9a566dd44e0a9/overview.svg)](https://depfu.com/github/volverjs/form-vue?project_id=38569)

<br>

maintained with ❤️ by

<br>

[![8 Wave](docs/static/8wave.svg)](https://8wave.it)

<br>

</div>

## Install

```bash
# pnpm
pnpm add @volverjs/form-vue

# yarn
yarn add @volverjs/form-vue

# npm
npm install @volverjs/form-vue --save
```

## Usage

`@volverjs/form-vue` allow you to create a Vue 3 form with [`@volverjs/ui-vue`](https://github.com/volverjs/ui-vue) components from a [Zod Object](https://zod.dev/?id=objects) schema. It provides two functions: `createForm()` and `useForm()`.

## Plugin

`createForm()` defines globally three components `VvForm`, `VvFormWrapper`, and `VvFormField` through a [Vue 3 Plugin](https://vuejs.org/guide/reusability/plugins.html).

```typescript
import { createApp } from 'vue'
import { createForm } from '@volverjs/form-vue'
// Zod 4. The library also supports Zod 3.25+ via `import { z } from 'zod/v3'`.
import { z } from 'zod'

const schema = z.object({
    firstName: z.string(),
    lastName: z.string()
})

const app = createApp(App)
const form = createForm({
    schema
    // lazyLoad: boolean - default false
    // updateThrottle: number - default 500
    // continuousValidation: boolean - default false
    // sideEffects?: (data: any) => void
    // scope?: string - Defines a unique scope for the form instance (singletons)
    // class?: new (data?: any) => Type - Type constructor for form data
    // Example:
    // class: class User { constructor(data?: any) { Object.assign(this, data) } }
})

app.use(form)
app.mount('#app')
```

If the schema is omitted, the plugin only share the options to the forms created with the [composable](https://github.com/volverjs/form-vue/#composable).

### VvForm

`VvForm` render a `form` tag and emit a `submit` event. Form data are validated on submit.
A `valid` or `invalid` event is emitted when the form status changes.

```vue
<script lang="ts" setup>
function onSubmit(formData) {
    // ...
}
function onInvalid(errors) {
    // ...
}
</script>

<template>
    <VvForm @submit="onSubmit" @invalid="onInvalid">
        <!-- ... -->
        <button type="submit">
            Submit
        </button>
    </VvForm>
</template>
```

The submit can be triggered programmatically with the `submit()` method.

```vue
<script lang="ts" setup>
import { ref } from 'vue'
import type { FormComponent } from '@volverjs/form-vue'

const formEl = ref<InstanceType<FormComponent>>(null)
function onSubmit(formData) {
    // ...
}
function submitForm() {
    formEl.value.submit()
}
</script>

<template>
    <VvForm ref="formEl" @submit="onSubmit">
    <!-- ... -->
    </VvForm>
    <button type="button" @click.stop="submitForm">
        Submit
    </button>
</template>
```

Use the `v-model` directive (or only `:model-value` to set the initial value of form data) or bind the form data.

The form data two way binding is **throttled** by default (500ms) to avoid performance issues. The throttle can be changed with the `updateThrottle` option or prop.

By default form validation **stops** when a **valid state** is reached.
To activate **continuous validation** use the `continuousValidation` option or prop.

```vue
<script lang="ts" setup>
import { ref } from 'vue'

const formData = ref({
    firstName: '',
    lastName: ''
})
</script>

<template>
    <VvForm v-model="formData" :update-throttle="1000" continuous-validation>
    <!-- ... -->
    </VvForm>
</template>
```

## Composable

`useForm()` can be used to create a form programmatically inside a Vue 3 Component.
The **default settings** are **inherited** from the plugin (if it's installed).

```vue
<script lang="ts" setup>
import { useForm } from '@volverjs/form-vue'
import { z } from 'zod'

const schema = z.object({
    firstName: z.string(),
    lastName: z.string()
})

const { VvForm, VvFormWrapper, VvFormField } = useForm(schema, {
    // lazyLoad: boolean - default false
    // updateThrottle: number - default 500
    // continuousValidation: boolean - default false
    // sideEffects?: (formData: any) => void
    // scope?: string
    // class?: new (data?: any) => Type
})
</script>

<template>
    <VvForm>
        <VvFormField type="text" name="firstName" label="First Name" />
        <VvFormField type="text" name="lastName" label="Last Name" />
    </VvForm>
</template>
```

### Outside a Vue 3 Component

`useForm()` can create a form also outside a Vue 3 Component, plugin settings are **not inherited**.

```ts
import { useForm } from '@volverjs/form-vue'
import { z } from 'zod'

const schema = z.object({
    firstName: z.string(),
    lastName: z.string()
})

const {
    VvForm,
    VvFormWrapper,
    VvFormField,
    VvFormFieldsGroup,
    VvFormTemplate,
    formData,
    status,
    errors,
    wrappers
} = useForm(schema, {
    lazyLoad: true
})

export default {
    VvForm,
    VvFormWrapper,
    VvFormField,
    VvFormFieldsGroup,
    VvFormTemplate,
    formData,
    status,
    errors,
    wrappers
}
```

### VvFormWrapper

`VvFormWrapper` gives you the validation status of a part of your form.
The wrapper status is invalid if at least one of the fields inside it is invalid.

```vue
<template>
    <VvForm>
        <VvFormWrapper v-slot="{ invalid }" name="firstSection">
            <div class="form-section-1">
                <span v-if="invalid">There is a validation error</span>
                <!-- form fields of section 1 -->
            </div>
        </VvFormWrapper>
        <VvFormWrapper v-slot="{ invalid }" name="secondSection">
            <div class="form-section-2">
                <span v-if="invalid">There is a validation error</span>
                <!-- form fields of the section 2 -->
            </div>
        </VvFormWrapper>
    </VvForm>
</template>
```

`VvFormWrapper` can be used recursively to create a validation tree. The wrapper status is invalid if **at least one of the fields** inside it or one of its children **is invalid**.

```vue
<template>
    <VvForm>
        <!-- main VvFormWrapper -->
        <VvFormWrapper v-slot="{ invalid }" name="firstSection">
            <!-- add VvFormFields to wrapper -->
            <div class="form-section">
                <span v-if="invalid">There is a validation error</span>
                <!-- nested VvFormWrapper -->
                <VvFormWrapper v-slot="{ invalid: groupInvalid }">
                    <div class="form-section__group">
                        <span v-if="groupInvalid">There is a validation error</span>
                        <!-- add VvFormFields to nested wrapper -->
                    </div>
                </VvFormWrapper>
            </div>
        </VvFormWrapper>
    </VvForm>
</template>
```

The  `wrappers` map provides access to form wrapper data.
This allows for better control over form validation state and data management.

```vue
<script setup>
const { wrappers } = useForm(schema)

// Access wrapper data
const isFirstSectionInvalid = computed(() => wrappers.get('firstSection').invalid)
</script>
```

### VvFormField

`VvFormField` allow you to render a form field or a [`@volverjs/ui-vue`](https://github.com/volverjs/ui-vue) input component inside a form.

It automatically bind the form data through the `name` attribute. For nested objects, use the `name` attribute with **dot notation**.

```vue
<template>
    <VvForm>
        <VvFormField
            v-slot="{ modelValue, invalid, invalidLabel, onUpdate }"
            name="lastName"
        >
            <label for="lastName">Last Name</label>
            <input
                id="lastName"
                type="text"
                name="lastName"
                :value="modelValue"
                :aria-invalid="invalid"
                :aria-errormessage="invalid ? 'last-name-alert' : undefined"
                @input="onUpdate"
            >
            <small v-if="invalid" id="last-name-alert" role="alert">
                {{ invalidLabel }}
            </small>
        </VvFormField>
    </VvForm>
</template>
```

To render a [`@volverjs/ui-vue`](https://github.com/volverjs/ui-vue) input component, use the `type` attribute.
By default UI components must be installed globally, they can be lazy-loaded with `lazyLoad` option or prop.

```vue
<template>
    <VvForm>
        <VvFormField type="text" name="username" label="Username" lazy-load />
        <VvFormField type="password" name="password" label="Password" lazy-load />
    </VvForm>
</template>
```

Check the [`VvFormField` documentation](./docs/VvFormField.md) to learn more about form fields.

### VvFormFieldsGroup

`VvFormFieldsGroup` allow you to render a group of form fields inside a form.

It automatically bind the form data through the `names` attribute. For nested objects, use the `names` attribute with **dot notation**.

```vue
<template>
    <VvForm>
        <VvFormFieldsGroup
            v-slot="{ modelValue, invalids, invalidLabels, onUpdateField }"
            :names="['firstName', 'lastName']"
        >
            <fieldset>
                <p>
                    <label for="firstName">First Name</label>
                    <input
                        id="firstName"
                        type="text"
                        name="firstName"
                        :value="modelValue.firstName"
                        :aria-invalid="invalids.firstName"
                        :aria-errormessage="invalids.firstName ? 'first-name-alert' : undefined"
                        @input="onUpdateField('firstName', $event)"
                    >
                    <small v-if="invalids.firstName" id="first-name-alert" role="alert">
                        {{ invalidLabels?.firstName }}
                    </small>
                </p>
                <p>
                    <label for="lastName">Last Name</label>
                    <input
                        id="lastName"
                        type="text"
                        name="lastName"
                        :value="modelValue.lastName"
                        :aria-invalid="invalids.lastName"
                        :aria-errormessage="invalids.lastName ? 'last-name-alert' : undefined"
                        @input="onUpdateField('lastName', $event)"
                    >
                    <small v-if="invalids.lastName" id="last-name-alert" role="alert">
                        {{ invalidLabels?.lastName }}
                    </small>
                </p>
            </fieldset>
        </VvFormFieldsGroup>
    </VvForm>
</template>
```

Alternatively, you can create a custom component to render the group of form fields.

```vue
// MyFieldsGroup.vue
<script lang="ts" setup>
defineProps<{
    invalids: Record<string, boolean>
    invalidLabels?: Record<string, string[]>
}>()
// v-model:first-name
const firstName = defineModel<string>('firstName', { default: '' })
// v-model:last-name
const lastName = defineModel<string>('lastName', { default: '' })
</script>

<template>
    <fieldset>
        <p>
            <label for="firstName">First Name</label>
            <input
                id="firstName"
                v-model="firstName"
                type="text"
                name="firstName"
                :aria-invalid="invalids.firstName"
                :aria-errormessage="invalids.firstName ? 'first-name-alert' : undefined"
            >
            <small v-if="invalids.firstName" id="first-name-alert" role="alert">
                {{ invalidLabels?.firstName }}
            </small>
        </p>
        <p>
            <label for="lastName">Last Name</label>
            <input
                id="lastName"
                v-model="lastName"
                type="text"
                name="lastName"
                :aria-invalid="invalids.lastName"
                :aria-errormessage="invalids.lastName ? 'last-name-alert' : undefined"
            >
            <small v-if="invalids.lastName" id="last-name-alert" role="alert">
                {{ invalidLabels?.lastName }}
            </small>
        </p>
    </fieldset>
</template>
```

An than use it inside the `VvFormFieldsGroup` with the `:is` attribute.

```vue
<script>
import MyFieldsGroup from './MyFieldsGroup.vue'
</script>

<template>
    <VvForm>
        <VvFormFieldsGroup
            :is="MyFieldsGroup"
            :names="['firstName', 'lastName']"
        />
    </VvForm>
</template>
```

You can also map the form fields to the components v-models. The `:names` attribute can be an object with the component v-models as keys and the form fields names as values.

```vue
<script>
import MyCustomComponent from './MyCustomComponent.vue'
</script>

<template>
    <VvForm>
        <VvFormFieldsGroup
            :is="MyCustomComponent"
            :names="{
                myCustomComponentVModel: 'path.to.form.field',
            }"
        />
    </VvForm>
</template>
```

## VvFormTemplate

Forms can also be created using a template. A template is an **array of objects** that describes the form fields. All properties that are **not listed** below are passed to the component **as props**.

```vue
<script lang="ts" setup>
import { useForm } from '@volverjs/form-vue'
import { z } from 'zod'

const schema = z.object({
    firstName: z.string(),
    lastName: z.string(),
    address: z.object({
        street: z.string(),
        number: z.string(),
        city: z.string(),
        zip: z.number()
    })
})

const templateSchema = [
    {
        vvName: 'firstName',
        vvType: 'text',
        label: 'First Name'
    },
    {
        vvName: 'lastName',
        vvType: 'text',
        label: 'Last Name'
    },
    {
        vvIs: 'div',
        class: 'grid grid-col-3 gap-4',
        vvChildren: [
            {
                vvName: 'address.street',
                vvType: 'text',
                label: 'Street',
                class: 'col-span-2'
            },
            {
                vvName: 'address.number',
                vvType: 'text',
                label: 'Number'
            },
            {
                vvName: 'address.city',
                vvType: 'text',
                label: 'City',
                class: 'col-span-2',
            },
            {
                vvName: 'address.zip',
                vvType: 'number',
                label: 'Zip'
            }
        ]
    }
]

const { VvForm, VvFormTemplate } = useForm(schema)
</script>

<template>
    <VvForm>
        <VvFormTemplate :schema="templateSchema" />
    </VvForm>
</template>
```

Template items, by default, are rendered as a `VvFormField` component but this can be changed using the `vvIs` property. The `vvIs` property can be a string or a component.

`vvName` refers to the name of the field in the schema and can be a nested property using **dot notation**.
`vvType` refers to the type of the field and can be any of the supported [types](./docs/VvFormField.md#ui-components).
`vvDefaultValue` can be used to set default values for the form item.
`vvShowValid` can be used to show the valid state of the form item.
`vvSlots` can be used to pass a slots to the template item.
`vvChildren` is an array of template items which will be wrapped in the parent item.

Conditional rendering can be achieved using the `vvIf` and `vvElseIf` properties.

```vue
<script lang="ts" setup>
import { useForm } from '@volverjs/form-vue'
import { z } from 'zod'

const schema = z.object({
    firstName: z.string(),
    lastName: z.string(),
    hasUsername: z.boolean(),
    username: z.string().optional(),
    email: z.string().email().optional()
}).superRefine((value, ctx) => {
    if (value.hasUsername && !value.username) {
        ctx.addIssue({
            code: z.ZodIssueCode.custom,
            message: 'Username is required'
        })
    }
    if (!value.hasUsername && !value.email) {
        ctx.addIssue({
            code: z.ZodIssueCode.custom,
            message: 'Email is required'
        })
    }
})

const templateSchema = [
    {
        vvName: 'firstName',
        vvType: 'text',
        label: 'First Name'
    },
    {
        vvName: 'lastName',
        vvType: 'text',
        label: 'Last Name'
    },
    {
        vvName: 'hasUsername',
        vvType: 'checkbox',
        label: 'Has Username',
        value: true,
        uncheckedValue: false
    },
    {
        vvIf: 'hasUsername',
        vvName: 'username',
        vvType: 'text',
        label: 'Username'
    },
    {
        vvElseIf: true,
        vvName: 'email',
        vvType: 'email',
        label: 'Email'
    }
]

const { VvForm, VvFormTemplate } = useForm(schema)
</script>

<template>
    <VvForm>
        <VvFormTemplate :schema="templateSchema" />
    </VvForm>
</template>
```

`vvElseIf` can be used multiple times. `vvElseIf: true` is like an `else` statement and will be rendered if all previous `vvIf` and `vvElseIf` conditions are false.

`vvIf` and `vvElseIf` can be a string or a function. If it is a string it will be evaluated as a **property** of the form data. If it is a function it will be called with the **form context** as the **first argument** and must return a boolean.

```ts
const templateSchema = [
    {
        vvIf: ctx => ctx.formData.value.hasUsername,
        vvName: 'username',
        vvType: 'text',
        label: 'Username'
    }
]
```

Also the template schema and all template items can be a function.
The function will be called with the **form context** as the **first argument**.

```ts
function templateSchema(ctx) {
    return [
        {
            vvName: 'firstName',
            vvType: 'text',
            label: `Hi ${ctx.formData.value.firstName}!`
        }
    ]
}
```

```ts
const templateSchema = [
    ctx => ({
        vvName: 'firstName',
        vvType: 'text',
        label: `Hi ${ctx.formData.value.firstName}!`
    }),
    {
        vvName: 'username',
        type: 'text',
        label: 'username'
    }
]
```

## Default Object by Zod Schema

`defaultObjectBySchema` creates an object by a [Zod Object Schema](https://zod.dev/?id=objects).
It can be useful to create a **default object** for a **form**. The default object is created by the default values of the schema and can be merged with an other object passed as parameter.

```ts
import { z } from 'zod'
import { defaultObjectBySchema } from '@volverjs/form-vue'

const schema = z.object({
    firstName: z.string().default('John'),
    lastName: z.string().default('Doe')
})

const defaultObject = defaultObjectBySchema(schema)
// defaultObject = { firstName: 'John', lastName: 'Doe' }

const defaultObject = defaultObjectBySchema(schema, { name: 'Jane' })
// defaultObject = { firstName: 'Jane', lastName: 'Doe' }
```

`defaultObjectBySchema` can be used with nested objects.

```ts
import { z } from 'zod'
import { defaultObjectBySchema } from '@volverjs/form-vue'

const schema = z.object({
    firstName: z.string().default('John'),
    lastName: z.string().default('Doe'),
    address: z.object({
        street: z.string().default('Main Street'),
        number: z.number().default(1)
    })
})

const defaultObject = defaultObjectBySchema(schema)
// defaultObject = { firstName: 'John', lastName: 'Doe', address: { street: 'Main Street', number: 1 } }
```

Other Zod methods are also supported: [`z.nullable()`](https://github.com/colinhacks/zod#nullable), [`z.coerce`](https://github.com/colinhacks/zod#coercion-for-primitives) and [`z.passthrough()`](https://github.com/colinhacks/zod#passthrough).

```ts
import { z } from 'zod'
import { defaultObjectBySchema } from '@volverjs/form-vue'

const schema = z
    .object({
        firstName: z.string().default('John'),
        lastName: z.string().default('Doe'),
        address: z.object({
            street: z.string().default('Main Street'),
            number: z.number().default(1)
        }),
        age: z.number().nullable().default(null),
        height: z.coerce.number().default(1.8),
        weight: z.number().default(80)
    })
    .passthrough()

const defaultObject = defaultObjectBySchema(schema, {
    height: '1.9',
    email: 'john.doe@test.com'
})
// defaultObject = { firstName: 'John', lastName: 'Doe', address: { street: 'Main Street', number: 1 }, age: null, height: 1.9, weight: 80, email: 'john.doe@test.com' }
```

## Zod 4
`@volverjs/form-vue` supports Zod 4 from `zod@3.25.x` and `zod@4.x.x`. All features and methods are compatible and the usage remains the same, the library automatically detects the Zod version and adapts accordingly.

```typescript
import * as z from 'zod/v4'
import { createForm, useForm, defaultObjectBySchema } from '@volverjs/form-vue'

const schema = z.object({
    firstName: z.string(),
    lastName: z.string()
})

// Plugin
const form = createForm({
    schema
})

// Composable
const { VvForm, VvFormWrapper, VvFormField } = useForm(schema)

// Default Object by Zod Schema
const defaultObject = defaultObjectBySchema(schema, { firstName: 'Jane' })
```

## License

[MIT](http://opensource.org/licenses/MIT)
