Skip to content

volverjs/form-vue

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

volverjs

@volverjs/form-vue

form form-field form-wrapper vue3 zod validation

Quality Gate Status Maintainability Rating Security Rating Depfu Depfu


maintained with ❤️ by


8 Wave


Install

# 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 components from a Zod Object schema. It provides two functions: createForm() and useForm().

Plugin

createForm() defines globally three components VvForm, VvFormWrapper, and VvFormField through a Vue 3 Plugin.

import { createApp } from 'vue'
import { createForm } from '@volverjs/form-vue'
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
})

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

If the schema is omitted, the plugin only share the options to the forms created with the 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.

<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.

<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.

<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).

<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
})
</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.

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
} = useForm(schema, {
    lazyLoad: true
})

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

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.

<template>
    <VvForm>
        <VvFormWrapper v-slot="{ invalid }">
            <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 }">
            <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.

<template>
    <VvForm>
        <!-- main VvFormWrapper -->
        <VvFormWrapper v-slot="{ invalid }">
            <!-- 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>

VvFormField

VvFormField allow you to render a form field or a @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.

<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 input component, use the type attribute. By default UI components must be installed globally, they can be lazy-loaded with lazyLoad option or prop.

<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 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.

<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.

// 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.

<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.

<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.

<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. 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.

<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.

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.

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

Default Object by Zod Object Schema

defaultObjectBySchema creates an object by a Zod Object Schema. 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.

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.

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(), z.coerce and z.passthrough().

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' }

License

MIT