@volverjs/form-vue
Version:
Vue 3 Forms with @volverjs/ui-vue
558 lines (459 loc) • 15.2 kB
Markdown
<div align="center">
[](https://volverjs.github.io/form-vue)
## @volverjs/form-vue
`form` `form-field` `form-wrapper` `vue3` `zod` `validation`
[](https://sonarcloud.io/summary/new_code?id=volverjs_form-vue) [](https://sonarcloud.io/summary/new_code?id=volverjs_form-vue) [](https://sonarcloud.io/summary/new_code?id=volverjs_form-vue) [](https://depfu.com) [](https://depfu.com/github/volverjs/form-vue?project_id=38569)
<br>
#### proudly powered by
<br>
[](https://24consulting.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'
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
// continuosValidation: 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](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>
const onSubmit = (formData) => {
// ...
}
const 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)
const onSubmit = (formData) => {
// ...
}
const submitForm = () => {
formEl.value.submit()
}
</script>
<template>
<VvForm @submit="onSubmit" ref="formEl">
<!-- ... -->
</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 **continuos validation** use the `continuosValidation` 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" continuos-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
// continuosValidation: 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**.
```ts
import { useForm } from '@volverjs/form-vue'
import { z } from 'zod'
const schema = z.object({
name: z.string(),
surname: z.string()
})
const {
VvForm,
VvFormWrapper,
VvFormField,
VvFormTemplate,
formData,
status,
errors
} = useForm(schema, {
lazyLoad: true
})
export default {
VvForm,
VvFormWrapper,
VvFormField,
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.
```vue
<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**.
```vue
<template>
<VvForm>
<VvFormWrapper v-slot="{ invalid }">
<div class="form-section">
<span v-if="invalid">There is a validation error</span>
<!-- form fields of section -->
<VvFormWrapper v-slot="{ invalid: groupInvalid }">
<div class="form-section__group">
<span v-if="groupInvalid">There is a validation error</span>
<!-- form fields of the group -->
</div>
</VvFormWrapper>
</div>
</VvFormWrapper>
</VvForm>
</template>
```
### 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" role="alert" id="last-name-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.
## 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
{
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
const templateSchema = (ctx) => [
{
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 Object 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({
name: z.string().default('John'),
surname: z.string().default('Doe')
})
const defaultObject = defaultObjectBySchema(schema)
// defaultObject = { name: 'John', surname: 'Doe' }
const defaultObject = defaultObjectBySchema(schema, { name: 'Jane' })
// defaultObject = { name: 'Jane', surname: 'Doe' }
```
`defaultObjectBySchema` can be used with nested objects.
```ts
import { z } from 'zod'
import { defaultObjectBySchema } from '@volverjs/form-vue'
const schema = z.object({
name: z.string().default('John'),
surname: z.string().default('Doe'),
address: z.object({
street: z.string().default('Main Street'),
number: z.number().default(1)
})
})
const defaultObject = defaultObjectBySchema(schema)
// defaultObject = { name: 'John', surname: '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({
name: z.string().default('John'),
surname: 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 = { name: 'John', surname: 'Doe', address: { street: 'Main Street', number: 1 }, age: null, height: 1.9, weight: 80, email: 'john.doe@test.com' }
```
## License
[MIT](http://opensource.org/licenses/MIT)