maz-ui 5.0.0 → 5.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{MazDropzone.DJ8au25t.css → MazDropzone.BW2grpTi.css} +1 -1
- package/dist/chunks/{MazDrawer.DANxKYxJ.js → MazDrawer.CC8zomfx.js} +2 -2
- package/dist/chunks/{MazDropzone.POH3pmfi.js → MazDropzone.Dez5j0WP.js} +2 -2
- package/dist/components/MazDrawer.js +1 -1
- package/dist/components/MazDropzone.js +1 -1
- package/dist/components/MazDropzone.vue.d.ts +2 -2
- package/dist/components/MazInputPrice.vue.d.ts +1 -1
- package/dist/components/index.js +2 -2
- package/docs/README.md +207 -0
- package/docs/agents/agents-md.md +52 -0
- package/docs/agents/claude-rule.md +52 -0
- package/docs/components/maz-accordion.md +76 -0
- package/docs/components/maz-alert.md +241 -0
- package/docs/components/maz-animated-counter.md +87 -0
- package/docs/components/maz-animated-element.md +46 -0
- package/docs/components/maz-animated-text.md +53 -0
- package/docs/components/maz-avatar.md +193 -0
- package/docs/components/maz-backdrop.md +178 -0
- package/docs/components/maz-badge.md +167 -0
- package/docs/components/maz-bottom-sheet.md +257 -0
- package/docs/components/maz-btn-group.md +316 -0
- package/docs/components/maz-btn.md +423 -0
- package/docs/components/maz-card-spotlight.md +84 -0
- package/docs/components/maz-card.md +321 -0
- package/docs/components/maz-carousel.md +153 -0
- package/docs/components/maz-chart.md +206 -0
- package/docs/components/maz-checkbox.md +138 -0
- package/docs/components/maz-checklist.md +251 -0
- package/docs/components/maz-circular-progress-bar.md +109 -0
- package/docs/components/maz-code-highlight.md +155 -0
- package/docs/components/maz-container.md +241 -0
- package/docs/components/maz-date-picker.md +791 -0
- package/docs/components/maz-dialog-confirm.md +215 -0
- package/docs/components/maz-dialog.md +143 -0
- package/docs/components/maz-drawer.md +106 -0
- package/docs/components/maz-dropdown.md +419 -0
- package/docs/components/maz-dropzone.md +387 -0
- package/docs/components/maz-expand-animation.md +82 -0
- package/docs/components/maz-fullscreen-loader.md +58 -0
- package/docs/components/maz-gallery.md +79 -0
- package/docs/components/maz-icon.md +203 -0
- package/docs/components/maz-input-code.md +73 -0
- package/docs/components/maz-input-number.md +79 -0
- package/docs/components/maz-input-phone-number.md +536 -0
- package/docs/components/maz-input-price.md +68 -0
- package/docs/components/maz-input-tags.md +112 -0
- package/docs/components/maz-input.md +334 -0
- package/docs/components/maz-lazy-img.md +113 -0
- package/docs/components/maz-link.md +146 -0
- package/docs/components/maz-loading-bar.md +38 -0
- package/docs/components/maz-markdown-editor.md +320 -0
- package/docs/components/maz-pagination.md +82 -0
- package/docs/components/maz-popover.md +794 -0
- package/docs/components/maz-pull-to-refresh.md +222 -0
- package/docs/components/maz-radio-buttons.md +313 -0
- package/docs/components/maz-radio.md +119 -0
- package/docs/components/maz-read-more.md +209 -0
- package/docs/components/maz-reading-progress-bar.md +96 -0
- package/docs/components/maz-select-country.md +513 -0
- package/docs/components/maz-select.md +318 -0
- package/docs/components/maz-sidebar-content.md +25 -0
- package/docs/components/maz-sidebar-footer.md +25 -0
- package/docs/components/maz-sidebar-group.md +27 -0
- package/docs/components/maz-sidebar-header.md +25 -0
- package/docs/components/maz-sidebar-menu-button.md +41 -0
- package/docs/components/maz-sidebar-menu-item.md +25 -0
- package/docs/components/maz-sidebar-menu-sub.md +32 -0
- package/docs/components/maz-sidebar-menu.md +25 -0
- package/docs/components/maz-sidebar-separator.md +21 -0
- package/docs/components/maz-sidebar-trigger.md +25 -0
- package/docs/components/maz-sidebar.md +507 -0
- package/docs/components/maz-skeleton.md +226 -0
- package/docs/components/maz-slider.md +165 -0
- package/docs/components/maz-spinner.md +84 -0
- package/docs/components/maz-stepper.md +301 -0
- package/docs/components/maz-switch.md +86 -0
- package/docs/components/maz-table-cell.md +27 -0
- package/docs/components/maz-table-row.md +28 -0
- package/docs/components/maz-table-title.md +27 -0
- package/docs/components/maz-table-virtualizer.md +31 -0
- package/docs/components/maz-table.md +426 -0
- package/docs/components/maz-tabs-bar.md +43 -0
- package/docs/components/maz-tabs-content-item.md +27 -0
- package/docs/components/maz-tabs-content.md +27 -0
- package/docs/components/maz-tabs.md +308 -0
- package/docs/components/maz-textarea.md +255 -0
- package/docs/components/maz-ticker.md +427 -0
- package/docs/components/maz-timeline.md +304 -0
- package/docs/components/maz-ui-provider.md +26 -0
- package/docs/components/maz-window-mockup.md +182 -0
- package/docs/composables/use-aos.md +41 -0
- package/docs/composables/use-breakpoints.md +42 -0
- package/docs/composables/use-dialog.md +58 -0
- package/docs/composables/use-display-names.md +130 -0
- package/docs/composables/use-drag.md +185 -0
- package/docs/composables/use-form-validator.md +1123 -0
- package/docs/composables/use-idle-timeout.md +172 -0
- package/docs/composables/use-pull-to-refresh.md +86 -0
- package/docs/composables/use-reading-time.md +132 -0
- package/docs/composables/use-string-matching.md +41 -0
- package/docs/composables/use-swipe.md +171 -0
- package/docs/composables/use-timer.md +104 -0
- package/docs/composables/use-toast.md +50 -0
- package/docs/composables/use-user-visibility.md +127 -0
- package/docs/composables/use-wait.md +50 -0
- package/docs/composables/use-window-size.md +25 -0
- package/docs/directives/click-outside.md +182 -0
- package/docs/directives/fullscreen-img.md +83 -0
- package/docs/directives/lazy-img.md +182 -0
- package/docs/directives/tooltip.md +298 -0
- package/docs/directives/zoom-img.md +100 -0
- package/docs/ecosystem/eslint-config.md +289 -0
- package/docs/ecosystem/icons/icon-set.md +25 -0
- package/docs/ecosystem/icons/index.md +584 -0
- package/docs/ecosystem/mcp.md +165 -0
- package/docs/ecosystem/node/exec-promise.md +93 -0
- package/docs/ecosystem/node/index.md +53 -0
- package/docs/ecosystem/node/logger.md +152 -0
- package/docs/ecosystem/node/print-banner.md +101 -0
- package/docs/ecosystem/node/setup-agents.md +102 -0
- package/docs/ecosystem/nuxt.md +551 -0
- package/docs/ecosystem/stylelint-config.md +310 -0
- package/docs/ecosystem/themes.md +966 -0
- package/docs/ecosystem/translations.md +1173 -0
- package/docs/ecosystem/utils/camel-case.md +37 -0
- package/docs/ecosystem/utils/capitalize.md +37 -0
- package/docs/ecosystem/utils/check-availability.md +85 -0
- package/docs/ecosystem/utils/cookie.md +86 -0
- package/docs/ecosystem/utils/country-code-to-unicode-flag.md +128 -0
- package/docs/ecosystem/utils/currency.md +53 -0
- package/docs/ecosystem/utils/date.md +53 -0
- package/docs/ecosystem/utils/debounce-callback.md +44 -0
- package/docs/ecosystem/utils/debounce-id.md +75 -0
- package/docs/ecosystem/utils/debounce.md +71 -0
- package/docs/ecosystem/utils/fetch-locale-ip.md +39 -0
- package/docs/ecosystem/utils/format-json.md +39 -0
- package/docs/ecosystem/utils/format-phone-number.md +43 -0
- package/docs/ecosystem/utils/get-browser-locale.md +35 -0
- package/docs/ecosystem/utils/get-country-flag-url.md +67 -0
- package/docs/ecosystem/utils/get-error-message.md +45 -0
- package/docs/ecosystem/utils/idle-timeout.md +96 -0
- package/docs/ecosystem/utils/index.md +57 -0
- package/docs/ecosystem/utils/is-client.md +38 -0
- package/docs/ecosystem/utils/is-equal.md +44 -0
- package/docs/ecosystem/utils/is-ios.md +43 -0
- package/docs/ecosystem/utils/is-server.md +37 -0
- package/docs/ecosystem/utils/is-standalone-mode.md +49 -0
- package/docs/ecosystem/utils/kebab-case.md +42 -0
- package/docs/ecosystem/utils/normalize-string.md +83 -0
- package/docs/ecosystem/utils/number.md +51 -0
- package/docs/ecosystem/utils/pascal-case.md +41 -0
- package/docs/ecosystem/utils/prevent-ios-input-zoom.md +72 -0
- package/docs/ecosystem/utils/script-loader.md +85 -0
- package/docs/ecosystem/utils/sleep.md +65 -0
- package/docs/ecosystem/utils/snake-case.md +42 -0
- package/docs/ecosystem/utils/swipe-handler.md +97 -0
- package/docs/ecosystem/utils/textarea-autogrow.md +47 -0
- package/docs/ecosystem/utils/throttle-id.md +54 -0
- package/docs/ecosystem/utils/throttle.md +63 -0
- package/docs/ecosystem/utils/truthy-filter.md +37 -0
- package/docs/ecosystem/utils/types/deep-key-of.md +54 -0
- package/docs/ecosystem/utils/types/deep-partial.md +48 -0
- package/docs/ecosystem/utils/types/deep-required.md +45 -0
- package/docs/ecosystem/utils/types/flatten-object-keys.md +50 -0
- package/docs/ecosystem/utils/types/generic-instance-type.md +48 -0
- package/docs/ecosystem/utils/types/infer-maybe-ref.md +41 -0
- package/docs/ecosystem/utils/upper-first.md +38 -0
- package/docs/ecosystem/utils/user-visibility.md +75 -0
- package/docs/guide/ai-agents.md +237 -0
- package/docs/guide/browser-support.md +88 -0
- package/docs/guide/getting-started.md +191 -0
- package/docs/guide/global-defaults.md +103 -0
- package/docs/guide/maz-ui-provider.md +231 -0
- package/docs/guide/migration-v5.md +871 -0
- package/docs/guide/resolvers.md +650 -0
- package/docs/guide/tailwind.md +483 -0
- package/docs/guide/vue.md +286 -0
- package/docs/manifest.json +1 -0
- package/docs/plugins/aos.md +312 -0
- package/docs/plugins/dialog.md +269 -0
- package/docs/plugins/toast.md +221 -0
- package/docs/plugins/wait.md +78 -0
- package/docs/reference/icons.md +46 -0
- package/docs/reference/tailwind-tokens.md +191 -0
- package/docs/rules.md +57 -0
- package/package.json +31 -4
|
@@ -0,0 +1,1123 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "useFormValidator"
|
|
3
|
+
description: "Vue composables for form validation with any Standard Schema library (Valibot, Zod, ArkType...) - useFormValidator and useFormField provide a flexible and typed approach to handle form validation in your Vue applications."
|
|
4
|
+
package: "maz-ui"
|
|
5
|
+
url: "https://maz-ui.com/composables/use-form-validator"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# useFormValidator
|
|
9
|
+
|
|
10
|
+
> Vue composables for form validation with any Standard Schema library (Valibot, Zod, ArkType...) - useFormValidator and useFormField provide a flexible and typed approach to handle form validation in your Vue applications.
|
|
11
|
+
|
|
12
|
+
- Package: `maz-ui`
|
|
13
|
+
- Import: `import { useFormValidator } from 'maz-ui/composables/useFormValidator'`
|
|
14
|
+
- Nuxt: auto-imported by `@maz-ui/nuxt`, no import needed in a Nuxt app
|
|
15
|
+
- Web: https://maz-ui.com/composables/use-form-validator
|
|
16
|
+
|
|
17
|
+
## Introduction
|
|
18
|
+
|
|
19
|
+
`useFormValidator` and `useFormField` are two Vue composables that work together to provide powerful form validation. They are **validation library agnostic**: any library implementing [Standard Schema](https://standardschema.dev) works out of the box, like [Valibot](https://valibot.dev), [Zod](https://zod.dev) or [ArkType](https://arktype.io). See Validation Libraries.
|
|
20
|
+
|
|
21
|
+
- **useFormValidator**: Initializes form validation for your entire form. Use it in your form's parent component.
|
|
22
|
+
- **useFormField**: Manages individual field validation states. Use it when you need fine-grained control over a field or when fields are in child components.
|
|
23
|
+
|
|
24
|
+
**When to use each:**
|
|
25
|
+
|
|
26
|
+
| Composable | Use When |
|
|
27
|
+
|------------|----------|
|
|
28
|
+
| `useFormValidator` only | Simple forms where all fields are in the same component |
|
|
29
|
+
| `useFormValidator` + `useFormField` | Fields in child components, or when using `eager`, `blur`, or `progressive` validation modes |
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
Install the validation library of your choice (Valibot is used in most examples of this page):
|
|
34
|
+
|
|
35
|
+
```bash [valibot]
|
|
36
|
+
pnpm add valibot
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash [zod]
|
|
40
|
+
pnpm add zod
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```bash [arktype]
|
|
44
|
+
pnpm add arktype
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Here's the simplest form you can create with `useFormValidator`:
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<script lang="ts" setup>
|
|
51
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
52
|
+
import { pipe, string, email, nonEmpty, minLength } from 'valibot'
|
|
53
|
+
|
|
54
|
+
// 1. Define your validation schema
|
|
55
|
+
const schema = {
|
|
56
|
+
email: pipe(string(), nonEmpty('Email is required'), email('Invalid email')),
|
|
57
|
+
password: pipe(string(), nonEmpty('Password is required'), minLength(8, 'Min 8 characters')),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. Initialize the form validator
|
|
61
|
+
const {
|
|
62
|
+
model, // Form data (reactive)
|
|
63
|
+
errorMessages, // First error message for each field
|
|
64
|
+
fieldsStates, // Detailed state of each field
|
|
65
|
+
isSubmitting, // Is the form being submitted?
|
|
66
|
+
handleSubmit, // Submit handler wrapper
|
|
67
|
+
} = useFormValidator({ schema })
|
|
68
|
+
|
|
69
|
+
// 3. Handle form submission
|
|
70
|
+
const onSubmit = handleSubmit((data) => {
|
|
71
|
+
// Called only if the form is valid
|
|
72
|
+
console.log('Form submitted:', data)
|
|
73
|
+
})
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<form @submit="onSubmit">
|
|
78
|
+
<MazInput
|
|
79
|
+
v-model="model.email"
|
|
80
|
+
label="Email"
|
|
81
|
+
:hint="errorMessages.email"
|
|
82
|
+
:error="!!errorMessages.email"
|
|
83
|
+
:success="fieldsStates.email.valid"
|
|
84
|
+
/>
|
|
85
|
+
<MazInput
|
|
86
|
+
v-model="model.password"
|
|
87
|
+
label="Password"
|
|
88
|
+
type="password"
|
|
89
|
+
:hint="errorMessages.password"
|
|
90
|
+
:error="!!errorMessages.password"
|
|
91
|
+
:success="fieldsStates.password.valid"
|
|
92
|
+
/>
|
|
93
|
+
<MazBtn type="submit" :loading="isSubmitting">
|
|
94
|
+
Login
|
|
95
|
+
</MazBtn>
|
|
96
|
+
</form>
|
|
97
|
+
</template>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Validation Libraries
|
|
101
|
+
|
|
102
|
+
`useFormValidator` relies on [Standard Schema](https://standardschema.dev), a common interface shared by the main validation libraries. Maz-UI does not ship any validation library: install the one you prefer and pass its schemas. Valibot, Zod and ArkType are declared as optional peer dependencies of `maz-ui`, so your package manager warns you if the installed version is not supported.
|
|
103
|
+
|
|
104
|
+
| Library | Supported versions |
|
|
105
|
+
|---------|--------------------|
|
|
106
|
+
| [Valibot](https://valibot.dev) | `>=1.0.0 <2.0.0` |
|
|
107
|
+
| [Zod](https://zod.dev) | `>=3.24.0 <5.0.0` (v3 and v4) |
|
|
108
|
+
| [ArkType](https://arktype.io) | `>=2.0.0 <3.0.0` |
|
|
109
|
+
| [Others](https://standardschema.dev/#what-schema-libraries-implement-the-spec) | Any library implementing Standard Schema V1 |
|
|
110
|
+
|
|
111
|
+
The `schema` option is always a **plain object** where each key is a field name and each value is a schema of your library. Do not wrap it with `object()` / `z.object()`.
|
|
112
|
+
|
|
113
|
+
### Same form, different libraries
|
|
114
|
+
|
|
115
|
+
```ts [valibot]
|
|
116
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
117
|
+
import { email, minLength, nonEmpty, pipe, string } from 'valibot'
|
|
118
|
+
|
|
119
|
+
const { model, errorMessages, handleSubmit } = useFormValidator({
|
|
120
|
+
schema: {
|
|
121
|
+
email: pipe(string(), nonEmpty('Email is required'), email('Invalid email')),
|
|
122
|
+
password: pipe(string(), minLength(8, 'Min 8 characters')),
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```ts [zod]
|
|
128
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
129
|
+
import { z } from 'zod'
|
|
130
|
+
|
|
131
|
+
const { model, errorMessages, handleSubmit } = useFormValidator({
|
|
132
|
+
schema: {
|
|
133
|
+
email: z.string().min(1, 'Email is required').pipe(z.email('Invalid email')),
|
|
134
|
+
password: z.string().min(8, 'Min 8 characters'),
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```ts [arktype]
|
|
140
|
+
import { type } from 'arktype'
|
|
141
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
142
|
+
|
|
143
|
+
const { model, errorMessages, handleSubmit } = useFormValidator({
|
|
144
|
+
schema: {
|
|
145
|
+
email: type('string.email'),
|
|
146
|
+
password: type('string >= 8'),
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Live example with Zod
|
|
152
|
+
|
|
153
|
+
```vue
|
|
154
|
+
<script lang="ts" setup>
|
|
155
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
156
|
+
import { z } from 'zod'
|
|
157
|
+
|
|
158
|
+
const schema = {
|
|
159
|
+
username: z.string().min(1, 'Username is required').min(3, 'Min 3 characters'),
|
|
160
|
+
age: z.number({ error: 'Age is required' }).min(18, 'Min 18').max(100, 'Max 100'),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const {
|
|
164
|
+
model,
|
|
165
|
+
errorMessages,
|
|
166
|
+
fieldsStates,
|
|
167
|
+
isSubmitting,
|
|
168
|
+
handleSubmit,
|
|
169
|
+
} = useFormValidator({ schema })
|
|
170
|
+
|
|
171
|
+
const onSubmit = handleSubmit((data) => {
|
|
172
|
+
// data is typed as { username: string, age: number }
|
|
173
|
+
console.log('Form submitted:', data)
|
|
174
|
+
})
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<template>
|
|
178
|
+
<form @submit="onSubmit">
|
|
179
|
+
<MazInput
|
|
180
|
+
v-model="model.username"
|
|
181
|
+
label="Username"
|
|
182
|
+
:hint="errorMessages.username"
|
|
183
|
+
:error="!!errorMessages.username"
|
|
184
|
+
:success="fieldsStates.username.valid"
|
|
185
|
+
/>
|
|
186
|
+
<MazInput
|
|
187
|
+
v-model="model.age"
|
|
188
|
+
label="Age"
|
|
189
|
+
type="number"
|
|
190
|
+
:hint="errorMessages.age"
|
|
191
|
+
:error="!!errorMessages.age"
|
|
192
|
+
:success="fieldsStates.age.valid"
|
|
193
|
+
/>
|
|
194
|
+
<MazBtn type="submit" :loading="isSubmitting">
|
|
195
|
+
Submit
|
|
196
|
+
</MazBtn>
|
|
197
|
+
</form>
|
|
198
|
+
</template>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Mixing libraries
|
|
202
|
+
|
|
203
|
+
Each field is validated by its own schema, so you can mix libraries in the same form (useful during a progressive migration from one library to another):
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
207
|
+
import { email, pipe, string } from 'valibot'
|
|
208
|
+
import { z } from 'zod'
|
|
209
|
+
|
|
210
|
+
const { model } = useFormValidator({
|
|
211
|
+
schema: {
|
|
212
|
+
email: pipe(string(), email('Invalid email')),
|
|
213
|
+
age: z.number().min(18, 'Min 18'),
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Custom schema
|
|
219
|
+
|
|
220
|
+
You can also write your own validator by implementing the `StandardSchemaV1` interface (exported by `maz-ui/composables`). `validate` can be synchronous or return a `Promise`:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import type { StandardSchemaV1 } from 'maz-ui/composables'
|
|
224
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
225
|
+
|
|
226
|
+
const frenchZipCode: StandardSchemaV1<string, string> = {
|
|
227
|
+
'~standard': {
|
|
228
|
+
version: 1,
|
|
229
|
+
vendor: 'my-app',
|
|
230
|
+
validate: value =>
|
|
231
|
+
typeof value === 'string' && /^\d{5}$/.test(value)
|
|
232
|
+
? { value }
|
|
233
|
+
: { issues: [{ message: 'Invalid zip code' }] },
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const { model } = useFormValidator({
|
|
238
|
+
schema: { zipCode: frenchZipCode },
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Good to know
|
|
243
|
+
|
|
244
|
+
- **Empty values**: `undefined` and `null` field values are validated as an empty string (`''`). To make a field required, use a length rule (`nonEmpty()` with Valibot, `.min(1)` with Zod). An optional field with a format rule (email, URL...) has to accept the empty string explicitly, for example `z.email().or(z.literal(''))` with Zod or `union([literal(''), pipe(string(), email())])` with Valibot.
|
|
245
|
+
- **Error messages**: `errorMessages` contains the message of the first issue of each field, whatever the library.
|
|
246
|
+
- **Issues type**: `errors` and `fieldsStates[field].errors` are typed as `ValidationIssues`, the Standard Schema issue shape (`message` and `path`). At runtime, they are the native issues of your library, so you can cast them to access library specific properties (for example `BaseIssue<unknown>` from Valibot or `z.core.$ZodIssue` from Zod).
|
|
247
|
+
- **Type inference**: the `model` type (input) and the `handleSubmit` payload type (output, after transformations) are inferred from the schemas whatever the library.
|
|
248
|
+
|
|
249
|
+
## Understanding Form State
|
|
250
|
+
|
|
251
|
+
`useFormValidator` returns several reactive values to help you manage your form:
|
|
252
|
+
|
|
253
|
+
| Property | Type | Description |
|
|
254
|
+
|----------|------|-------------|
|
|
255
|
+
| `model` | `Ref<Model>` | The form data object - bind this to your inputs with `v-model` |
|
|
256
|
+
| `isValid` | `ComputedRef<boolean>` | `true` when all fields pass validation |
|
|
257
|
+
| `isDirty` | `ComputedRef<boolean>` | `true` when any field has been modified from its initial value |
|
|
258
|
+
| `isSubmitting` | `Ref<boolean>` | `true` while the form is being submitted |
|
|
259
|
+
| `isSubmitted` | `Ref<boolean>` | `true` after the form has been submitted at least once |
|
|
260
|
+
| `errorMessages` | `ComputedRef<Record<string, string>>` | The first error message for each field (if any) |
|
|
261
|
+
| `errors` | `ComputedRef<Record<string, ValidationIssues>>` | All validation issues for each field |
|
|
262
|
+
| `fieldsStates` | `Ref<FieldsStates>` | Detailed state object for each field |
|
|
263
|
+
| `handleSubmit` | `Function` | Wrapper function for form submission |
|
|
264
|
+
| `validateForm` | `Function` | Manually trigger form validation |
|
|
265
|
+
| `resetForm` | `Function` | Reset the form to its initial state |
|
|
266
|
+
| `scrollToError` | `Function` | Scroll to the first field with an error |
|
|
267
|
+
|
|
268
|
+
### Field States
|
|
269
|
+
|
|
270
|
+
Each field in `fieldsStates` contains:
|
|
271
|
+
|
|
272
|
+
| Property | Type | Description |
|
|
273
|
+
|----------|------|-------------|
|
|
274
|
+
| `valid` | `boolean` | Field passes validation |
|
|
275
|
+
| `error` | `boolean` | Field has an error that should be displayed |
|
|
276
|
+
| `errors` | `ValidationIssues` | Array of all validation issues |
|
|
277
|
+
| `dirty` | `boolean` | Field value differs from initial value |
|
|
278
|
+
| `blurred` | `boolean` | Field has lost focus at least once |
|
|
279
|
+
| `validated` | `boolean` | Validation has run at least once |
|
|
280
|
+
| `validating` | `boolean` | Async validation is in progress |
|
|
281
|
+
|
|
282
|
+
```vue
|
|
283
|
+
<script lang="ts" setup>
|
|
284
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
285
|
+
import { pipe, string, number, nonEmpty, minLength, minValue, maxValue } from 'valibot'
|
|
286
|
+
|
|
287
|
+
const schema = {
|
|
288
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
289
|
+
age: pipe(number(), minValue(18, 'Min 18'), maxValue(100, 'Max 100')),
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const {
|
|
293
|
+
model,
|
|
294
|
+
errorMessages,
|
|
295
|
+
fieldsStates,
|
|
296
|
+
isValid,
|
|
297
|
+
isDirty,
|
|
298
|
+
isSubmitted,
|
|
299
|
+
handleSubmit,
|
|
300
|
+
} = useFormValidator({ schema })
|
|
301
|
+
|
|
302
|
+
const onSubmit = handleSubmit((data) => {
|
|
303
|
+
console.log('Submitted:', data)
|
|
304
|
+
})
|
|
305
|
+
</script>
|
|
306
|
+
|
|
307
|
+
<template>
|
|
308
|
+
<form @submit="onSubmit">
|
|
309
|
+
<MazInput v-model="model.name" label="Name" />
|
|
310
|
+
<MazInput v-model="model.age" label="Age" type="number" />
|
|
311
|
+
<MazBtn type="submit">Submit</MazBtn>
|
|
312
|
+
</form>
|
|
313
|
+
|
|
314
|
+
<!-- Debug panel -->
|
|
315
|
+
<pre>{{ { isValid, isDirty, isSubmitted } }}</pre>
|
|
316
|
+
<pre>{{ fieldsStates }}</pre>
|
|
317
|
+
</template>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Validation Modes
|
|
321
|
+
|
|
322
|
+
Validation modes control **when** validation runs and **when** errors are displayed. Choose the mode that best fits your UX needs.
|
|
323
|
+
|
|
324
|
+
| Mode | Validates On | Shows Errors | Best For |
|
|
325
|
+
|------|--------------|--------------|----------|
|
|
326
|
+
| `lazy` (default) | Value change | After change (if not empty) | Simple forms |
|
|
327
|
+
| `aggressive` | Immediately + every change | Always | Real-time feedback |
|
|
328
|
+
| `eager` | Blur, then on change | After first blur | Better UX |
|
|
329
|
+
| `blur` | Only on blur | After blur | Minimal interruption |
|
|
330
|
+
| `progressive` | Silently, shows on blur if invalid | After blur or validation | Optimal UX |
|
|
331
|
+
|
|
332
|
+
**Tip:**
|
|
333
|
+
|
|
334
|
+
For `eager`, `blur`, and `progressive` modes, you must use `useFormField` with the `ref` option or `validationEvents` to capture blur events.
|
|
335
|
+
|
|
336
|
+
### Lazy Mode (Default)
|
|
337
|
+
|
|
338
|
+
The default mode. Validates when field values change. Errors only appear if the field is not empty.
|
|
339
|
+
|
|
340
|
+
```vue
|
|
341
|
+
<script lang="ts" setup>
|
|
342
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
343
|
+
import { pipe, string, email, nonEmpty, minLength } from 'valibot'
|
|
344
|
+
|
|
345
|
+
const schema = {
|
|
346
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
347
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const { model, errorMessages, fieldsStates, isSubmitting, handleSubmit } = useFormValidator({
|
|
351
|
+
schema,
|
|
352
|
+
options: {
|
|
353
|
+
mode: 'lazy', // This is the default
|
|
354
|
+
scrollToError: '.has-error-lazy',
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
const onSubmit = handleSubmit((data) => {
|
|
359
|
+
console.log('Submitted:', data)
|
|
360
|
+
})
|
|
361
|
+
</script>
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Aggressive Mode
|
|
365
|
+
|
|
366
|
+
Validates all fields immediately when the form is created and on every change. Errors are always displayed.
|
|
367
|
+
|
|
368
|
+
```vue
|
|
369
|
+
<script lang="ts" setup>
|
|
370
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
371
|
+
import { pipe, string, email, nonEmpty, minLength } from 'valibot'
|
|
372
|
+
|
|
373
|
+
const schema = {
|
|
374
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
375
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const { model, errorMessages, fieldsStates, isSubmitting, handleSubmit } = useFormValidator({
|
|
379
|
+
schema,
|
|
380
|
+
options: {
|
|
381
|
+
mode: 'aggressive', // Validates immediately
|
|
382
|
+
},
|
|
383
|
+
})
|
|
384
|
+
</script>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Eager Mode (Recommended)
|
|
388
|
+
|
|
389
|
+
Validates on blur first (if the field is not empty), then on every change. This provides a good balance between immediate feedback and not overwhelming the user.
|
|
390
|
+
|
|
391
|
+
**Warning:**
|
|
392
|
+
|
|
393
|
+
Requires `useFormField` with `ref` option or `validationEvents`.
|
|
394
|
+
|
|
395
|
+
```vue
|
|
396
|
+
<script lang="ts" setup>
|
|
397
|
+
import { useFormValidator, useFormField } from 'maz-ui/composables'
|
|
398
|
+
import { pipe, string, email, nonEmpty, minLength } from 'valibot'
|
|
399
|
+
import { useTemplateRef } from 'vue'
|
|
400
|
+
|
|
401
|
+
const schema = {
|
|
402
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
403
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const { isSubmitting, handleSubmit, isValid } = useFormValidator({
|
|
407
|
+
schema,
|
|
408
|
+
options: {
|
|
409
|
+
mode: 'eager',
|
|
410
|
+
scrollToError: '.has-error-eager',
|
|
411
|
+
identifier: 'form-eager',
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// useFormField for each field with ref for blur detection
|
|
416
|
+
const {
|
|
417
|
+
value: name,
|
|
418
|
+
hasError: nameHasError,
|
|
419
|
+
errorMessage: nameError,
|
|
420
|
+
isValid: nameValid,
|
|
421
|
+
} = useFormField<string>('name', {
|
|
422
|
+
ref: useTemplateRef('nameRef'),
|
|
423
|
+
formIdentifier: 'form-eager',
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const {
|
|
427
|
+
value: email,
|
|
428
|
+
hasError: emailHasError,
|
|
429
|
+
errorMessage: emailError,
|
|
430
|
+
isValid: emailValid,
|
|
431
|
+
} = useFormField<string>('email', {
|
|
432
|
+
ref: useTemplateRef('emailRef'),
|
|
433
|
+
formIdentifier: 'form-eager',
|
|
434
|
+
})
|
|
435
|
+
</script>
|
|
436
|
+
|
|
437
|
+
<template>
|
|
438
|
+
<form @submit="handleSubmit(onSubmit)">
|
|
439
|
+
<MazInput
|
|
440
|
+
ref="nameRef"
|
|
441
|
+
v-model="name"
|
|
442
|
+
label="Name"
|
|
443
|
+
:hint="nameError"
|
|
444
|
+
:error="nameHasError"
|
|
445
|
+
:success="nameValid"
|
|
446
|
+
/>
|
|
447
|
+
<MazInput
|
|
448
|
+
ref="emailRef"
|
|
449
|
+
v-model="email"
|
|
450
|
+
label="Email"
|
|
451
|
+
:hint="emailError"
|
|
452
|
+
:error="emailHasError"
|
|
453
|
+
:success="emailValid"
|
|
454
|
+
/>
|
|
455
|
+
|
|
456
|
+
<MazBtn type="submit" :loading="isSubmitting" :disabled="!isValid">Submit</MazBtn>
|
|
457
|
+
</form>
|
|
458
|
+
</template>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Blur Mode
|
|
462
|
+
|
|
463
|
+
**Warning:**
|
|
464
|
+
|
|
465
|
+
Requires `useFormField` with `ref` option or `validationEvents`.
|
|
466
|
+
|
|
467
|
+
Validates only when the field loses focus. Errors are only shown after blur.
|
|
468
|
+
|
|
469
|
+
```vue
|
|
470
|
+
<script lang="ts" setup>
|
|
471
|
+
import { useFormValidator, useFormField } from 'maz-ui/composables'
|
|
472
|
+
import { pipe, string, email, nonEmpty, minLength } from 'valibot'
|
|
473
|
+
import { useTemplateRef } from 'vue'
|
|
474
|
+
|
|
475
|
+
const schema = {
|
|
476
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
477
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const { isSubmitting, handleSubmit } = useFormValidator({
|
|
481
|
+
schema,
|
|
482
|
+
options: {
|
|
483
|
+
mode: 'blur',
|
|
484
|
+
identifier: 'form-blur',
|
|
485
|
+
},
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
const { value: name, hasError, errorMessage, isValid } = useFormField<string>('name', {
|
|
489
|
+
ref: useTemplateRef('nameRef'),
|
|
490
|
+
formIdentifier: 'form-blur',
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
const { value: email, hasError: emailHasError, errorMessage: emailError, isValid: emailValid } = useFormField<string>('email', {
|
|
494
|
+
ref: useTemplateRef('emailRef'),
|
|
495
|
+
formIdentifier: 'form-blur',
|
|
496
|
+
})
|
|
497
|
+
</script>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Progressive Mode
|
|
501
|
+
|
|
502
|
+
**Warning:**
|
|
503
|
+
|
|
504
|
+
Requires `useFormField` with `ref` option or `validationEvents`.
|
|
505
|
+
|
|
506
|
+
The most user-friendly mode. Validates silently in the background. Shows errors only on blur if the field is invalid. Once valid, it stays valid until it becomes invalid again.
|
|
507
|
+
|
|
508
|
+
```vue
|
|
509
|
+
<script lang="ts" setup>
|
|
510
|
+
import { useFormValidator, useFormField } from 'maz-ui/composables'
|
|
511
|
+
import { pipe, string, email, nonEmpty, minLength, boolean, literal } from 'valibot'
|
|
512
|
+
import { useTemplateRef } from 'vue'
|
|
513
|
+
|
|
514
|
+
const schema = {
|
|
515
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
516
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
517
|
+
agree: pipe(boolean(), literal(true, 'You must agree')),
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const { isSubmitting, handleSubmit } = useFormValidator({
|
|
521
|
+
schema,
|
|
522
|
+
options: {
|
|
523
|
+
mode: 'progressive',
|
|
524
|
+
identifier: 'form-progressive',
|
|
525
|
+
},
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
const { value: name, hasError, errorMessage, isValid } = useFormField<string>('name', {
|
|
529
|
+
ref: useTemplateRef('nameRef'),
|
|
530
|
+
formIdentifier: 'form-progressive',
|
|
531
|
+
})
|
|
532
|
+
// ... same for other fields
|
|
533
|
+
</script>
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
## useFormField for Child Components
|
|
537
|
+
|
|
538
|
+
`useFormField` is essential when:
|
|
539
|
+
1. Your form fields are in child components
|
|
540
|
+
2. You need the `eager`, `blur`, or `progressive` validation modes
|
|
541
|
+
3. You want fine-grained control over individual field states
|
|
542
|
+
|
|
543
|
+
### Return Values
|
|
544
|
+
|
|
545
|
+
| Property | Type | Description |
|
|
546
|
+
|----------|------|-------------|
|
|
547
|
+
| `value` | `WritableComputedRef<T>` | The field value (use with `v-model`) |
|
|
548
|
+
| `hasError` | `ComputedRef<boolean>` | Field has an error that should be displayed |
|
|
549
|
+
| `errors` | `ComputedRef<ValidationIssues>` | All validation issues |
|
|
550
|
+
| `errorMessage` | `ComputedRef<string>` | First error message |
|
|
551
|
+
| `isValid` | `ComputedRef<boolean>` | Field passes validation |
|
|
552
|
+
| `isDirty` | `ComputedRef<boolean>` | Field has been modified |
|
|
553
|
+
| `isBlurred` | `ComputedRef<boolean>` | Field has lost focus |
|
|
554
|
+
| `isValidated` | `ComputedRef<boolean>` | Validation has run |
|
|
555
|
+
| `isValidating` | `ComputedRef<boolean>` | Async validation in progress |
|
|
556
|
+
| `mode` | `ComputedRef<string>` | The validation mode |
|
|
557
|
+
| `validationEvents` | `ComputedRef<object>` | Blur event handler for `v-bind` |
|
|
558
|
+
|
|
559
|
+
### Two Ways to Bind Validation Events
|
|
560
|
+
|
|
561
|
+
#### Option 1: Using `ref` (Recommended)
|
|
562
|
+
|
|
563
|
+
Pass a template ref to `useFormField`. It will automatically detect interactive elements and attach blur listeners.
|
|
564
|
+
|
|
565
|
+
The `ref` option is **reactive**: if the field is rendered conditionally (e.g. with `v-if`), the blur listeners are attached automatically as soon as the element appears in the DOM, and removed when it is unmounted. You don't need to handle anything special.
|
|
566
|
+
|
|
567
|
+
```vue
|
|
568
|
+
<script setup>
|
|
569
|
+
import { useFormField } from 'maz-ui/composables'
|
|
570
|
+
import { useTemplateRef } from 'vue'
|
|
571
|
+
|
|
572
|
+
const { value, errorMessage, hasError } = useFormField<string>('email', {
|
|
573
|
+
ref: useTemplateRef('emailRef'),
|
|
574
|
+
formIdentifier: 'my-form',
|
|
575
|
+
})
|
|
576
|
+
</script>
|
|
577
|
+
|
|
578
|
+
<template>
|
|
579
|
+
<MazInput
|
|
580
|
+
ref="emailRef"
|
|
581
|
+
v-model="value"
|
|
582
|
+
:hint="errorMessage"
|
|
583
|
+
:error="hasError"
|
|
584
|
+
/>
|
|
585
|
+
</template>
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
Because it is reactive, the same code works even when the input is wrapped in a `v-if`:
|
|
589
|
+
|
|
590
|
+
```vue
|
|
591
|
+
<script setup>
|
|
592
|
+
import { useFormField } from 'maz-ui/composables'
|
|
593
|
+
import { useTemplateRef, ref } from 'vue'
|
|
594
|
+
|
|
595
|
+
const isVisible = ref(false)
|
|
596
|
+
|
|
597
|
+
const { value, errorMessage, hasError } = useFormField<string>('email', {
|
|
598
|
+
ref: useTemplateRef('emailRef'),
|
|
599
|
+
formIdentifier: 'my-form',
|
|
600
|
+
})
|
|
601
|
+
</script>
|
|
602
|
+
|
|
603
|
+
<template>
|
|
604
|
+
<MazInput
|
|
605
|
+
v-if="isVisible"
|
|
606
|
+
ref="emailRef"
|
|
607
|
+
v-model="value"
|
|
608
|
+
:hint="errorMessage"
|
|
609
|
+
:error="hasError"
|
|
610
|
+
/>
|
|
611
|
+
</template>
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
You can also pass a raw `HTMLElement` directly. In that case it won't be reactive, so the element must already exist in the DOM:
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
const { value } = useFormField<string>('email', {
|
|
618
|
+
ref: document.querySelector('input'),
|
|
619
|
+
formIdentifier: 'my-form',
|
|
620
|
+
})
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
#### Option 2: Using `validationEvents`
|
|
624
|
+
|
|
625
|
+
If your component emits a `blur` event, you can use `v-bind` with `validationEvents`.
|
|
626
|
+
|
|
627
|
+
```vue
|
|
628
|
+
<script setup>
|
|
629
|
+
import { useFormField } from 'maz-ui/composables'
|
|
630
|
+
|
|
631
|
+
const { value, errorMessage, hasError, validationEvents } = useFormField<string>('email', {
|
|
632
|
+
formIdentifier: 'my-form',
|
|
633
|
+
})
|
|
634
|
+
</script>
|
|
635
|
+
|
|
636
|
+
<template>
|
|
637
|
+
<MazInput
|
|
638
|
+
v-model="value"
|
|
639
|
+
v-bind="validationEvents"
|
|
640
|
+
:hint="errorMessage"
|
|
641
|
+
:error="hasError"
|
|
642
|
+
/>
|
|
643
|
+
</template>
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
## TypeScript Type Inference
|
|
647
|
+
|
|
648
|
+
The form model is automatically typed based on your schema, whatever the validation library:
|
|
649
|
+
|
|
650
|
+
```ts
|
|
651
|
+
const schema = {
|
|
652
|
+
name: pipe(string(), nonEmpty()),
|
|
653
|
+
age: pipe(number(), minValue(0)),
|
|
654
|
+
email: pipe(string(), email()),
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const { model } = useFormValidator({ schema })
|
|
658
|
+
// model.value is typed as: { name?: string, age?: number, email?: string }
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
For `useFormField`, specify the field type as a generic parameter:
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
// Specify the type for better type safety
|
|
665
|
+
const { value } = useFormField<string>('name', { formIdentifier: 'my-form' })
|
|
666
|
+
// value is typed as WritableComputedRef<string>
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
**Warning: Common TypeScript Errors**
|
|
670
|
+
|
|
671
|
+
If you get circular reference errors with `useTemplateRef`, use the classic `ref()` instead:
|
|
672
|
+
|
|
673
|
+
```ts
|
|
674
|
+
// May cause TypeScript errors
|
|
675
|
+
const { value: email } = useFormField<string>('email', {
|
|
676
|
+
ref: useTemplateRef('emailRef'),
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
// Solution 1: Add generic to useTemplateRef
|
|
680
|
+
const { value: email } = useFormField<string>('email', {
|
|
681
|
+
ref: useTemplateRef<HTMLInputElement>('emailRef'),
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
// Solution 2: Use classic ref
|
|
685
|
+
const emailRef = ref<HTMLInputElement>()
|
|
686
|
+
const { value: email } = useFormField<string>('email', {
|
|
687
|
+
ref: emailRef,
|
|
688
|
+
})
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
## Async Validation
|
|
692
|
+
|
|
693
|
+
Async validations (like checking username availability) are supported by every library: Valibot's `pipeAsync` and `checkAsync`, Zod's async `refine`, etc. The example below uses Valibot, here is the Zod equivalent of the schema:
|
|
694
|
+
|
|
695
|
+
```ts
|
|
696
|
+
import { z } from 'zod'
|
|
697
|
+
|
|
698
|
+
const schema = {
|
|
699
|
+
username: z
|
|
700
|
+
.string()
|
|
701
|
+
.min(1, 'Username is required')
|
|
702
|
+
.min(3, 'Min 3 characters')
|
|
703
|
+
.refine(async (value) => {
|
|
704
|
+
// Simulate API call
|
|
705
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
706
|
+
return value !== 'taken'
|
|
707
|
+
}, 'Username is already taken'),
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
```vue
|
|
712
|
+
<script lang="ts" setup>
|
|
713
|
+
import { useFormValidator, useFormField } from 'maz-ui/composables'
|
|
714
|
+
import { pipeAsync, string, nonEmpty, minLength, checkAsync } from 'valibot'
|
|
715
|
+
import { useTemplateRef } from 'vue'
|
|
716
|
+
|
|
717
|
+
const schema = {
|
|
718
|
+
username: pipeAsync(
|
|
719
|
+
string(),
|
|
720
|
+
nonEmpty('Username is required'),
|
|
721
|
+
minLength(3, 'Min 3 characters'),
|
|
722
|
+
checkAsync(async (value) => {
|
|
723
|
+
// Simulate API call
|
|
724
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
725
|
+
return value !== 'taken' // Return true if valid
|
|
726
|
+
}, 'Username is already taken'),
|
|
727
|
+
),
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const { isSubmitting, handleSubmit } = useFormValidator({
|
|
731
|
+
schema,
|
|
732
|
+
options: { mode: 'eager', identifier: 'form-async' },
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
const {
|
|
736
|
+
value: username,
|
|
737
|
+
hasError,
|
|
738
|
+
errorMessage,
|
|
739
|
+
isValid,
|
|
740
|
+
isValidating, // Shows loading state during async validation
|
|
741
|
+
} = useFormField<string>('username', {
|
|
742
|
+
ref: useTemplateRef('usernameRef'),
|
|
743
|
+
formIdentifier: 'form-async',
|
|
744
|
+
})
|
|
745
|
+
</script>
|
|
746
|
+
|
|
747
|
+
<template>
|
|
748
|
+
<MazInput
|
|
749
|
+
ref="usernameRef"
|
|
750
|
+
v-model="username"
|
|
751
|
+
:hint="errorMessage"
|
|
752
|
+
:error="hasError"
|
|
753
|
+
:success="isValid"
|
|
754
|
+
:loading="isValidating"
|
|
755
|
+
/>
|
|
756
|
+
</template>
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
## Throttling and Debouncing
|
|
760
|
+
|
|
761
|
+
For expensive validations (like API calls), use throttling or debouncing to limit how often validation runs.
|
|
762
|
+
|
|
763
|
+
| Option | Behavior | Default Time | Use Case |
|
|
764
|
+
|--------|----------|--------------|----------|
|
|
765
|
+
| `debouncedFields` | Waits until user stops typing | 300ms | Search fields, API calls |
|
|
766
|
+
| `throttledFields` | Runs at most once per interval | 1000ms | Rate-limited APIs |
|
|
767
|
+
|
|
768
|
+
```vue
|
|
769
|
+
<script lang="ts" setup>
|
|
770
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
771
|
+
import { pipe, string, number, nonEmpty, minLength, minValue } from 'valibot'
|
|
772
|
+
|
|
773
|
+
const schema = {
|
|
774
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
775
|
+
age: pipe(number('Must be a number'), minValue(18, 'Must be 18+')),
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const { model, errorMessages, fieldsStates, isSubmitting, handleSubmit } = useFormValidator({
|
|
779
|
+
schema,
|
|
780
|
+
options: {
|
|
781
|
+
debouncedFields: { name: 500 }, // Wait 500ms after typing stops
|
|
782
|
+
throttledFields: { age: 1000 }, // Validate at most every 1000ms
|
|
783
|
+
// Use `true` for default times: debouncedFields: { name: true } // 300ms
|
|
784
|
+
},
|
|
785
|
+
})
|
|
786
|
+
</script>
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
## Reset Form
|
|
790
|
+
|
|
791
|
+
Use `resetForm()` to reset the form to its initial state, or set `resetOnSuccess` to automatically reset after successful submission.
|
|
792
|
+
|
|
793
|
+
```vue
|
|
794
|
+
<script lang="ts" setup>
|
|
795
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
796
|
+
import { pipe, string, number, nonEmpty, minLength, minValue } from 'valibot'
|
|
797
|
+
|
|
798
|
+
const schema = {
|
|
799
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
800
|
+
age: pipe(number(), minValue(18, 'Must be 18+')),
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const { model, errorMessages, fieldsStates, isSubmitting, handleSubmit, resetForm } = useFormValidator({
|
|
804
|
+
schema,
|
|
805
|
+
defaultValues: { name: 'John', age: 25 },
|
|
806
|
+
options: {
|
|
807
|
+
resetOnSuccess: true, // Auto-reset after successful submission (default: true)
|
|
808
|
+
},
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
const onSubmit = handleSubmit((data) => {
|
|
812
|
+
console.log('Submitted:', data)
|
|
813
|
+
// Form will auto-reset because resetOnSuccess: true
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
// Manual reset
|
|
817
|
+
function handleReset() {
|
|
818
|
+
resetForm()
|
|
819
|
+
}
|
|
820
|
+
</script>
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
## Multiple Forms on Same Page
|
|
824
|
+
|
|
825
|
+
Use the `identifier` option to have multiple independent forms on the same page. Make sure to match the identifier in both `useFormValidator` and `useFormField`.
|
|
826
|
+
|
|
827
|
+
```vue
|
|
828
|
+
<script lang="ts" setup>
|
|
829
|
+
import { useFormValidator, useFormField } from 'maz-ui/composables'
|
|
830
|
+
|
|
831
|
+
// Form 1
|
|
832
|
+
const form1 = useFormValidator({
|
|
833
|
+
schema: schema1,
|
|
834
|
+
options: { identifier: 'login-form' },
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
// Form 2
|
|
838
|
+
const form2 = useFormValidator({
|
|
839
|
+
schema: schema2,
|
|
840
|
+
options: { identifier: 'register-form' },
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
// useFormField must use matching identifier
|
|
844
|
+
const { value: loginEmail } = useFormField<string>('email', {
|
|
845
|
+
formIdentifier: 'login-form',
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
const { value: registerEmail } = useFormField<string>('email', {
|
|
849
|
+
formIdentifier: 'register-form',
|
|
850
|
+
})
|
|
851
|
+
</script>
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
## Error Handling and scrollToError
|
|
855
|
+
|
|
856
|
+
### scrollToError
|
|
857
|
+
|
|
858
|
+
Automatically scroll to the first field with an error when validation fails:
|
|
859
|
+
|
|
860
|
+
```ts
|
|
861
|
+
const { handleSubmit } = useFormValidator({
|
|
862
|
+
schema,
|
|
863
|
+
options: {
|
|
864
|
+
scrollToError: '.has-error', // CSS selector for error elements
|
|
865
|
+
// scrollToError: false, // Disable scrolling
|
|
866
|
+
},
|
|
867
|
+
})
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
Add the matching class to your fields:
|
|
871
|
+
|
|
872
|
+
```html
|
|
873
|
+
<MazInput
|
|
874
|
+
:class="{ 'has-error': hasError }"
|
|
875
|
+
:error="hasError"
|
|
876
|
+
/>
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
### onError Callback
|
|
880
|
+
|
|
881
|
+
Handle validation failures with the `onError` callback:
|
|
882
|
+
|
|
883
|
+
```ts
|
|
884
|
+
const onSubmit = handleSubmit(
|
|
885
|
+
(data) => {
|
|
886
|
+
// Success callback
|
|
887
|
+
console.log('Valid:', data)
|
|
888
|
+
},
|
|
889
|
+
'.has-error', // scrollToError selector (optional)
|
|
890
|
+
{
|
|
891
|
+
onError: ({ model, errorMessages, errors }) => {
|
|
892
|
+
// Called when validation fails
|
|
893
|
+
console.log('Validation failed:', errorMessages)
|
|
894
|
+
},
|
|
895
|
+
}
|
|
896
|
+
)
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
## Performance & Best Practices
|
|
900
|
+
|
|
901
|
+
### Performance Tips
|
|
902
|
+
|
|
903
|
+
1. **Use throttling/debouncing** for expensive validations (API calls, complex logic)
|
|
904
|
+
2. **Prefer `eager` or `progressive` modes** over `aggressive` for better performance
|
|
905
|
+
3. **Use `lazy` mode** for simple forms with minimal validation
|
|
906
|
+
4. **Avoid `aggressive` mode on large forms** - it validates every field on every change
|
|
907
|
+
|
|
908
|
+
### Common Patterns
|
|
909
|
+
|
|
910
|
+
#### Multiple Forms with Identifiers
|
|
911
|
+
|
|
912
|
+
```ts
|
|
913
|
+
const { handleSubmit } = useFormValidator({
|
|
914
|
+
schema,
|
|
915
|
+
options: { identifier: 'my-unique-form' },
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
const { value } = useFormField<string>('email', {
|
|
919
|
+
formIdentifier: 'my-unique-form', // Must match!
|
|
920
|
+
})
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
#### Custom Interactive Elements
|
|
924
|
+
|
|
925
|
+
If your custom component isn't detected for blur events, add `data-interactive`:
|
|
926
|
+
|
|
927
|
+
```vue
|
|
928
|
+
<div data-interactive class="custom-input" tabindex="0">
|
|
929
|
+
Custom Input
|
|
930
|
+
</div>
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
### Common Pitfalls
|
|
934
|
+
|
|
935
|
+
| Pitfall | Solution |
|
|
936
|
+
|---------|----------|
|
|
937
|
+
| Mismatched `formIdentifier` | Ensure `useFormField`'s `formIdentifier` matches `useFormValidator`'s `identifier` |
|
|
938
|
+
| Validation not triggering in eager/blur/progressive modes | Use `ref` option or `v-bind="validationEvents"` |
|
|
939
|
+
| TypeScript errors with `useTemplateRef` | Add generic type or use classic `ref()` |
|
|
940
|
+
| Field not found warning | Make sure the field name exists in your schema |
|
|
941
|
+
|
|
942
|
+
## API Reference
|
|
943
|
+
|
|
944
|
+
### useFormValidator
|
|
945
|
+
|
|
946
|
+
#### Parameters
|
|
947
|
+
|
|
948
|
+
```ts
|
|
949
|
+
useFormValidator<TSchema>({
|
|
950
|
+
schema: TSchema, // Object of Standard Schema field schemas: Valibot, Zod, ArkType... (required)
|
|
951
|
+
model?: Ref<Model>, // External model ref (optional)
|
|
952
|
+
defaultValues?: DeepPartial<Model>, // Initial values (optional)
|
|
953
|
+
options?: {
|
|
954
|
+
mode?: 'lazy' | 'aggressive' | 'eager' | 'blur' | 'progressive', // Default: 'lazy'
|
|
955
|
+
throttledFields?: Record<string, number | true>, // Fields to throttle
|
|
956
|
+
debouncedFields?: Record<string, number | true>, // Fields to debounce
|
|
957
|
+
scrollToError?: string | false, // CSS selector, default: '.has-field-error'
|
|
958
|
+
identifier?: string | symbol, // Form identifier, default: 'main-form-validator'
|
|
959
|
+
resetOnSuccess?: boolean, // Reset after submit, default: true
|
|
960
|
+
}
|
|
961
|
+
})
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
#### Return Values
|
|
965
|
+
|
|
966
|
+
```ts
|
|
967
|
+
{
|
|
968
|
+
identifier: string | symbol
|
|
969
|
+
model: Ref<Model>
|
|
970
|
+
isValid: ComputedRef<boolean>
|
|
971
|
+
isDirty: ComputedRef<boolean>
|
|
972
|
+
isSubmitting: Ref<boolean>
|
|
973
|
+
isSubmitted: Ref<boolean>
|
|
974
|
+
errors: ComputedRef<Record<string, ValidationIssues>>
|
|
975
|
+
errorMessages: ComputedRef<Record<string, string | undefined>>
|
|
976
|
+
fieldsStates: Ref<FieldsStates<Model>>
|
|
977
|
+
validateForm: (setErrors?: boolean) => Promise<void[]>
|
|
978
|
+
scrollToError: (selector?: string) => void
|
|
979
|
+
resetForm: () => void
|
|
980
|
+
handleSubmit: <Func>(
|
|
981
|
+
successCallback: Func,
|
|
982
|
+
scrollToError?: string | false,
|
|
983
|
+
options?: { onError?: Function, resetOnSuccess?: boolean }
|
|
984
|
+
) => (event?: Event) => Promise<ReturnType<Func>>
|
|
985
|
+
}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
### useFormField
|
|
989
|
+
|
|
990
|
+
#### Parameters
|
|
991
|
+
|
|
992
|
+
```ts
|
|
993
|
+
useFormField<FieldType>(
|
|
994
|
+
name: string, // Field name in schema (required)
|
|
995
|
+
options?: {
|
|
996
|
+
defaultValue?: FieldType, // Default value for this field
|
|
997
|
+
mode?: 'lazy' | 'aggressive' | 'eager' | 'blur' | 'progressive', // Override form mode
|
|
998
|
+
ref?: Ref<HTMLElement | ComponentInstance> | HTMLElement, // Reactive template ref (v-if friendly) or raw element
|
|
999
|
+
formIdentifier?: string | symbol, // Must match useFormValidator's identifier
|
|
1000
|
+
}
|
|
1001
|
+
)
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
#### Return Values
|
|
1005
|
+
|
|
1006
|
+
```ts
|
|
1007
|
+
{
|
|
1008
|
+
value: WritableComputedRef<FieldType>
|
|
1009
|
+
hasError: ComputedRef<boolean>
|
|
1010
|
+
errors: ComputedRef<ValidationIssues>
|
|
1011
|
+
errorMessage: ComputedRef<string | undefined>
|
|
1012
|
+
isValid: ComputedRef<boolean>
|
|
1013
|
+
isDirty: ComputedRef<boolean>
|
|
1014
|
+
isBlurred: ComputedRef<boolean>
|
|
1015
|
+
isValidated: ComputedRef<boolean>
|
|
1016
|
+
isValidating: ComputedRef<boolean>
|
|
1017
|
+
mode: ComputedRef<string | undefined>
|
|
1018
|
+
validationEvents: ComputedRef<{ onBlur?: () => void }>
|
|
1019
|
+
}
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
### Types
|
|
1023
|
+
|
|
1024
|
+
```ts
|
|
1025
|
+
// Standard Schema issue (https://standardschema.dev)
|
|
1026
|
+
interface ValidationIssue {
|
|
1027
|
+
readonly message: string
|
|
1028
|
+
readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
type ValidationIssues = ValidationIssue[]
|
|
1032
|
+
|
|
1033
|
+
interface FormValidatorOptions<Model> {
|
|
1034
|
+
mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
|
|
1035
|
+
throttledFields?: Partial<Record<keyof Model, number | true>>
|
|
1036
|
+
debouncedFields?: Partial<Record<keyof Model, number | true>>
|
|
1037
|
+
scrollToError?: string | false
|
|
1038
|
+
identifier?: string | symbol
|
|
1039
|
+
resetOnSuccess?: boolean
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
interface FormFieldOptions<FieldType> {
|
|
1043
|
+
defaultValue?: FieldType
|
|
1044
|
+
mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
|
|
1045
|
+
ref?: Ref<HTMLElement | ComponentInstance> | HTMLElement
|
|
1046
|
+
formIdentifier?: string | symbol
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
interface FieldState<FieldType> {
|
|
1050
|
+
valid: boolean
|
|
1051
|
+
error: boolean
|
|
1052
|
+
errors: ValidationIssues
|
|
1053
|
+
dirty: boolean
|
|
1054
|
+
blurred: boolean
|
|
1055
|
+
validated: boolean
|
|
1056
|
+
validating: boolean
|
|
1057
|
+
initialValue?: Readonly<FieldType>
|
|
1058
|
+
mode?: string
|
|
1059
|
+
}
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
## Troubleshooting
|
|
1063
|
+
|
|
1064
|
+
### Type Errors with useTemplateRef
|
|
1065
|
+
|
|
1066
|
+
**Problem**: TypeScript circular reference errors when using `useTemplateRef`
|
|
1067
|
+
|
|
1068
|
+
**Solutions**:
|
|
1069
|
+
|
|
1070
|
+
```ts
|
|
1071
|
+
// Solution 1: Add generic type
|
|
1072
|
+
const { value } = useFormField<string>('email', {
|
|
1073
|
+
ref: useTemplateRef<HTMLInputElement>('emailRef'),
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
// Solution 2: Use classic ref
|
|
1077
|
+
const emailRef = ref<HTMLInputElement>()
|
|
1078
|
+
const { value } = useFormField<string>('email', { ref: emailRef })
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
### Validation Not Triggering
|
|
1082
|
+
|
|
1083
|
+
**Problem**: `eager`, `blur`, or `progressive` mode not validating
|
|
1084
|
+
|
|
1085
|
+
**Solution**: These modes require blur event detection. Use either:
|
|
1086
|
+
|
|
1087
|
+
```ts
|
|
1088
|
+
// Option 1: ref option
|
|
1089
|
+
const { value } = useFormField<string>('name', {
|
|
1090
|
+
ref: useTemplateRef('inputRef'),
|
|
1091
|
+
})
|
|
1092
|
+
|
|
1093
|
+
// Option 2: validationEvents
|
|
1094
|
+
const { value, validationEvents } = useFormField<string>('name')
|
|
1095
|
+
// Then: v-bind="validationEvents" on your input
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
### Conditionally Rendered Fields (`v-if`)
|
|
1099
|
+
|
|
1100
|
+
The `ref` option is reactive, so a field wrapped in `v-if` works out of the box: blur listeners are attached as soon as the element is rendered and removed when it is unmounted.
|
|
1101
|
+
|
|
1102
|
+
If blur validation still doesn't trigger:
|
|
1103
|
+
1. Ensure the `ref` is bound to an HTML element or a Vue component exposing `$el`
|
|
1104
|
+
2. For custom components, add the `data-interactive` attribute
|
|
1105
|
+
|
|
1106
|
+
### Mismatched Form Identifiers
|
|
1107
|
+
|
|
1108
|
+
**Problem**: `useFormField` not finding the form context
|
|
1109
|
+
|
|
1110
|
+
**Solution**: Ensure identifiers match:
|
|
1111
|
+
|
|
1112
|
+
```ts
|
|
1113
|
+
// In parent
|
|
1114
|
+
const { handleSubmit } = useFormValidator({
|
|
1115
|
+
schema,
|
|
1116
|
+
options: { identifier: 'my-form' }, // This identifier...
|
|
1117
|
+
})
|
|
1118
|
+
|
|
1119
|
+
// In child
|
|
1120
|
+
const { value } = useFormField<string>('email', {
|
|
1121
|
+
formIdentifier: 'my-form', // ...must match this one
|
|
1122
|
+
})
|
|
1123
|
+
```
|