@maz-ui/mcp 4.4.0 → 4.7.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/mcp.mjs +1 -1
- package/docs/generated-docs/maz-accordion.doc.md +5 -5
- package/docs/generated-docs/maz-alert.doc.md +20 -0
- package/docs/generated-docs/maz-container.doc.md +25 -0
- package/docs/generated-docs/maz-input-number.doc.md +21 -19
- package/docs/generated-docs/maz-read-more.doc.md +18 -0
- package/docs/generated-docs/maz-skeleton.doc.md +12 -0
- package/docs/generated-docs/maz-ui-provider.doc.md +12 -0
- package/docs/src/blog/v4.md +2 -2
- package/docs/src/components/maz-accordion.md +1 -1
- package/docs/src/components/maz-alert.md +374 -0
- package/docs/src/components/maz-animated-counter.md +1 -1
- package/docs/src/components/maz-animated-element.md +1 -1
- package/docs/src/components/maz-animated-text.md +1 -1
- package/docs/src/components/maz-avatar.md +1 -1
- package/docs/src/components/maz-backdrop.md +2 -2
- package/docs/src/components/maz-badge.md +1 -1
- package/docs/src/components/maz-bottom-sheet.md +1 -1
- package/docs/src/components/maz-btn-group.md +1 -1
- package/docs/src/components/maz-btn.md +1 -1
- package/docs/src/components/maz-card-spotlight.md +1 -1
- package/docs/src/components/maz-card.md +1 -1
- package/docs/src/components/maz-carousel.md +2 -2
- package/docs/src/components/maz-checkbox.md +1 -1
- package/docs/src/components/maz-checklist.md +2 -2
- package/docs/src/components/maz-circular-progress-bar.md +1 -1
- package/docs/src/components/maz-container.md +348 -0
- package/docs/src/components/maz-date-picker.md +3 -3
- package/docs/src/components/maz-dialog-confirm.md +1 -1
- package/docs/src/components/maz-dialog.md +1 -1
- package/docs/src/components/maz-drawer.md +1 -1
- package/docs/src/components/maz-dropdown.md +2 -2
- package/docs/src/components/maz-dropzone.md +4 -4
- package/docs/src/components/maz-expand-animation.md +1 -1
- package/docs/src/components/maz-fullscreen-loader.md +1 -1
- package/docs/src/components/maz-gallery.md +1 -1
- package/docs/src/components/maz-input-code.md +1 -1
- package/docs/src/components/maz-input-number.md +2 -2
- package/docs/src/components/maz-input-phone-number.md +3 -3
- package/docs/src/components/maz-input-price.md +2 -2
- package/docs/src/components/maz-input-tags.md +2 -2
- package/docs/src/components/maz-input.md +1 -1
- package/docs/src/components/maz-lazy-img.md +1 -1
- package/docs/src/components/maz-link.md +1 -1
- package/docs/src/components/maz-loading-bar.md +1 -1
- package/docs/src/components/maz-pagination.md +2 -2
- package/docs/src/components/maz-pull-to-refresh.md +1 -1
- package/docs/src/components/maz-radio-buttons.md +1 -1
- package/docs/src/components/maz-radio.md +1 -1
- package/docs/src/components/maz-read-more.md +300 -0
- package/docs/src/components/maz-reading-progress-bar.md +1 -1
- package/docs/src/components/maz-select-country.md +1 -1
- package/docs/src/components/maz-select.md +3 -3
- package/docs/src/components/maz-skeleton.md +355 -0
- package/docs/src/components/maz-slider.md +1 -1
- package/docs/src/components/maz-stepper.md +1 -1
- package/docs/src/components/maz-switch.md +1 -1
- package/docs/src/components/maz-table.md +1 -1
- package/docs/src/components/maz-textarea.md +1 -1
- package/docs/src/composables/use-aos.md +1 -1
- package/docs/src/composables/use-form-validator.md +1226 -996
- package/docs/src/composables/use-wait.md +1 -1
- package/docs/src/guide/getting-started.md +13 -10
- package/docs/src/guide/icon-set.md +43 -21
- package/docs/src/guide/icons.md +74 -15
- package/docs/src/guide/maz-ui-provider.md +199 -0
- package/docs/src/guide/migration-v4.md +1 -1
- package/docs/src/guide/nuxt.md +4 -0
- package/docs/src/guide/resolvers.md +20 -0
- package/docs/src/guide/themes.md +4 -0
- package/docs/src/guide/translations.md +4 -0
- package/docs/src/guide/vue.md +7 -2
- package/docs/src/index.md +1 -1
- package/docs/src/plugins/toast.md +1 -1
- package/package.json +8 -8
|
@@ -1,393 +1,435 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: useFormValidator
|
|
3
|
-
description:
|
|
3
|
+
description: Vue composables for form validation with Valibot - useFormValidator and useFormField provide a flexible and typed approach to handle form validation in your Vue applications.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# {{ $frontmatter.title }}
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
{{ $frontmatter.description }}
|
|
9
9
|
|
|
10
10
|
## Introduction
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
`useFormValidator` and `useFormField` are two Vue composables that work together to provide powerful form validation using [Valibot](https://valibot.dev/guides/introduction/).
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
3. Use `useFormField` for fine-grained management of each form field.
|
|
17
|
-
4. Use the `handleSubmit` returned by `useFormValidator` to handle form submission securely.
|
|
18
|
-
5. Leverage computed values like `isValid`, `hasError`, `errorMessage`, and others to control your user interface state.
|
|
14
|
+
- **useFormValidator**: Initializes form validation for your entire form. Use it in your form's parent component.
|
|
15
|
+
- **useFormField**: Manages individual field validation states. Use it when you need fine-grained control over a field or when fields are in child components.
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
**When to use each:**
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
| Composable | Use When |
|
|
20
|
+
|------------|----------|
|
|
21
|
+
| `useFormValidator` only | Simple forms where all fields are in the same component |
|
|
22
|
+
| `useFormValidator` + `useFormField` | Fields in child components, or when using `eager`, `blur`, or `progressive` validation modes |
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
- `aggressive`: Validates all fields immediately and on every change
|
|
26
|
-
- `eager`: (recommended) Validates on initial blur (if not empty), then on every change **(requires `useFormField` to add validation events)**
|
|
27
|
-
- `blur`: Validates only on focus loss **(requires `useFormField` to add validation events)**
|
|
28
|
-
- `progressive`: Validates the field at each user interaction (value change or blur). The field becomes valid after the first successful validation and then validated on input value change. If the field is invalid, the error message on the first blur event **(requires `useFormField` to add validation events)**
|
|
24
|
+
## Quick Start
|
|
29
25
|
|
|
30
|
-
|
|
26
|
+
Here's the simplest form you can create with `useFormValidator`:
|
|
31
27
|
|
|
32
|
-
|
|
28
|
+
<ComponentDemo>
|
|
29
|
+
<form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitQuickStart">
|
|
30
|
+
<MazInput
|
|
31
|
+
v-model="quickStartModel.email"
|
|
32
|
+
label="Email"
|
|
33
|
+
type="email"
|
|
34
|
+
:hint="quickStartErrors.email"
|
|
35
|
+
:error="!!quickStartErrors.email"
|
|
36
|
+
:success="quickStartStates.email.valid"
|
|
37
|
+
/>
|
|
38
|
+
<MazInput
|
|
39
|
+
v-model="quickStartModel.password"
|
|
40
|
+
label="Password"
|
|
41
|
+
type="password"
|
|
42
|
+
:hint="quickStartErrors.password"
|
|
43
|
+
:error="!!quickStartErrors.password"
|
|
44
|
+
:success="quickStartStates.password.valid"
|
|
45
|
+
/>
|
|
46
|
+
<MazBtn type="submit" :loading="quickStartSubmitting">
|
|
47
|
+
Login
|
|
48
|
+
</MazBtn>
|
|
49
|
+
</form>
|
|
33
50
|
|
|
34
|
-
|
|
51
|
+
<template #code>
|
|
35
52
|
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
import { useFormValidator
|
|
53
|
+
```vue
|
|
54
|
+
<script lang="ts" setup>
|
|
55
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
56
|
+
import { pipe, string, email, nonEmpty, minLength } from 'valibot'
|
|
39
57
|
|
|
58
|
+
// 1. Define your validation schema
|
|
40
59
|
const schema = {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
country: pipe(string('Country is required'), nonEmpty('Country is required')),
|
|
60
|
+
email: pipe(string(), nonEmpty('Email is required'), email('Invalid email')),
|
|
61
|
+
password: pipe(string(), nonEmpty('Password is required'), minLength(8, 'Min 8 characters')),
|
|
44
62
|
}
|
|
45
63
|
|
|
46
|
-
//
|
|
47
|
-
const {
|
|
48
|
-
|
|
64
|
+
// 2. Initialize the form validator
|
|
65
|
+
const {
|
|
66
|
+
model, // Form data (reactive)
|
|
67
|
+
errorMessages, // First error message for each field
|
|
68
|
+
fieldsStates, // Detailed state of each field
|
|
69
|
+
isSubmitting, // Is the form being submitted?
|
|
70
|
+
handleSubmit, // Submit handler wrapper
|
|
71
|
+
} = useFormValidator({ schema })
|
|
72
|
+
|
|
73
|
+
// 3. Handle form submission
|
|
74
|
+
const onSubmit = handleSubmit((data) => {
|
|
75
|
+
// Called only if the form is valid
|
|
76
|
+
console.log('Form submitted:', data)
|
|
49
77
|
})
|
|
78
|
+
</script>
|
|
50
79
|
|
|
51
|
-
|
|
52
|
-
|
|
80
|
+
<template>
|
|
81
|
+
<form @submit="onSubmit">
|
|
82
|
+
<MazInput
|
|
83
|
+
v-model="model.email"
|
|
84
|
+
label="Email"
|
|
85
|
+
:hint="errorMessages.email"
|
|
86
|
+
:error="!!errorMessages.email"
|
|
87
|
+
:success="fieldsStates.email.valid"
|
|
88
|
+
/>
|
|
89
|
+
<MazInput
|
|
90
|
+
v-model="model.password"
|
|
91
|
+
label="Password"
|
|
92
|
+
type="password"
|
|
93
|
+
:hint="errorMessages.password"
|
|
94
|
+
:error="!!errorMessages.password"
|
|
95
|
+
:success="fieldsStates.password.valid"
|
|
96
|
+
/>
|
|
97
|
+
<MazBtn type="submit" :loading="isSubmitting">
|
|
98
|
+
Login
|
|
99
|
+
</MazBtn>
|
|
100
|
+
</form>
|
|
101
|
+
</template>
|
|
53
102
|
```
|
|
54
103
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
::: details How to bind validation events with useFormField for eager, blur, or progressive modes?
|
|
58
|
-
|
|
59
|
-
To use the `eager`, `blur`, or `progressive` validation modes, you must use the `useFormField` composable to add the necessary validation events.
|
|
60
|
-
|
|
61
|
-
2 ways to bind validation events:
|
|
104
|
+
</template>
|
|
105
|
+
</ComponentDemo>
|
|
62
106
|
|
|
63
|
-
|
|
107
|
+
## Understanding Form State
|
|
108
|
+
|
|
109
|
+
`useFormValidator` returns several reactive values to help you manage your form:
|
|
110
|
+
|
|
111
|
+
| Property | Type | Description |
|
|
112
|
+
|----------|------|-------------|
|
|
113
|
+
| `model` | `Ref<Model>` | The form data object - bind this to your inputs with `v-model` |
|
|
114
|
+
| `isValid` | `ComputedRef<boolean>` | `true` when all fields pass validation |
|
|
115
|
+
| `isDirty` | `ComputedRef<boolean>` | `true` when any field has been modified from its initial value |
|
|
116
|
+
| `isSubmitting` | `Ref<boolean>` | `true` while the form is being submitted |
|
|
117
|
+
| `isSubmitted` | `Ref<boolean>` | `true` after the form has been submitted at least once |
|
|
118
|
+
| `errorMessages` | `ComputedRef<Record<string, string>>` | The first error message for each field (if any) |
|
|
119
|
+
| `errors` | `ComputedRef<Record<string, ValidationIssues>>` | All validation issues for each field |
|
|
120
|
+
| `fieldsStates` | `Ref<FieldsStates>` | Detailed state object for each field |
|
|
121
|
+
| `handleSubmit` | `Function` | Wrapper function for form submission |
|
|
122
|
+
| `validateForm` | `Function` | Manually trigger form validation |
|
|
123
|
+
| `resetForm` | `Function` | Reset the form to its initial state |
|
|
124
|
+
| `scrollToError` | `Function` | Scroll to the first field with an error |
|
|
125
|
+
|
|
126
|
+
### Field States
|
|
127
|
+
|
|
128
|
+
Each field in `fieldsStates` contains:
|
|
129
|
+
|
|
130
|
+
| Property | Type | Description |
|
|
131
|
+
|----------|------|-------------|
|
|
132
|
+
| `valid` | `boolean` | Field passes validation |
|
|
133
|
+
| `error` | `boolean` | Field has an error that should be displayed |
|
|
134
|
+
| `errors` | `ValidationIssues` | Array of all validation issues |
|
|
135
|
+
| `dirty` | `boolean` | Field value differs from initial value |
|
|
136
|
+
| `blurred` | `boolean` | Field has lost focus at least once |
|
|
137
|
+
| `validated` | `boolean` | Validation has run at least once |
|
|
138
|
+
| `validating` | `boolean` | Async validation is in progress |
|
|
64
139
|
|
|
65
|
-
|
|
140
|
+
<ComponentDemo>
|
|
141
|
+
<div class="maz-flex maz-flex-col maz-gap-4">
|
|
142
|
+
<form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitState">
|
|
143
|
+
<MazInput
|
|
144
|
+
v-model="stateModel.name"
|
|
145
|
+
label="Name (min 3 characters)"
|
|
146
|
+
:hint="stateErrors.name"
|
|
147
|
+
:error="!!stateErrors.name"
|
|
148
|
+
:success="stateFields.name.valid"
|
|
149
|
+
/>
|
|
150
|
+
<MazInput
|
|
151
|
+
v-model="stateModel.age"
|
|
152
|
+
label="Age (18-100)"
|
|
153
|
+
type="number"
|
|
154
|
+
:hint="stateErrors.age"s
|
|
155
|
+
:error="!!stateErrors.age"
|
|
156
|
+
:success="stateFields.age.valid"
|
|
157
|
+
/>
|
|
158
|
+
<MazBtn type="submit">Submit</MazBtn>
|
|
159
|
+
</form>
|
|
160
|
+
<div class="maz-rounded">
|
|
161
|
+
<p class="maz-font-semibold maz-mb-2">Form State:</p>
|
|
162
|
+
<pre class="maz-text-xs maz-bg-surface-600/70 dark:maz-bg-surface-600/60 maz-p-2 maz-rounded">{{ JSON.stringify({ isValid: stateValid, isDirty: stateDirty, isSubmitted: stateSubmitted, isSubmitting: stateSubmitting }, null, 2) }}</pre>
|
|
163
|
+
<p class="maz-font-semibold maz-mb-2 maz-mt-4">Fields States:</p>
|
|
164
|
+
<pre class="maz-text-xs maz-bg-surface-600/70 dark:maz-bg-surface-600/60 maz-p-2 maz-rounded">{{ JSON.stringify(stateFields, null, 2) }}</pre>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
66
167
|
|
|
67
|
-
|
|
168
|
+
<template #code>
|
|
68
169
|
|
|
69
|
-
```vue
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
v-model="value"
|
|
74
|
-
:hint="errorMessage"
|
|
75
|
-
:error="hasError"
|
|
76
|
-
:success="isValid"
|
|
77
|
-
/>
|
|
78
|
-
<!-- Work with HTML input -->
|
|
79
|
-
<input ref="inputRef" v-model="value" />
|
|
80
|
-
</template>
|
|
170
|
+
```vue
|
|
171
|
+
<script lang="ts" setup>
|
|
172
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
173
|
+
import { pipe, string, number, nonEmpty, minLength, minValue, maxValue } from 'valibot'
|
|
81
174
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
175
|
+
const schema = {
|
|
176
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
177
|
+
age: pipe(number(), minValue(18, 'Min 18'), maxValue(100, 'Max 100')),
|
|
178
|
+
}
|
|
85
179
|
|
|
86
|
-
const {
|
|
87
|
-
|
|
180
|
+
const {
|
|
181
|
+
model,
|
|
182
|
+
errorMessages,
|
|
183
|
+
fieldsStates,
|
|
184
|
+
isValid,
|
|
185
|
+
isDirty,
|
|
186
|
+
isSubmitted,
|
|
187
|
+
handleSubmit,
|
|
188
|
+
} = useFormValidator({ schema })
|
|
189
|
+
|
|
190
|
+
const onSubmit = handleSubmit((data) => {
|
|
191
|
+
console.log('Submitted:', data)
|
|
88
192
|
})
|
|
89
193
|
</script>
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
#### 2. Use the `v-bind` directive to bind the validation events
|
|
93
|
-
|
|
94
|
-
You can use the `v-bind` directive to bind the validation events to the component or HTML element.
|
|
95
|
-
|
|
96
|
-
If you use this method with a custom component, the component must emit the `blur` event to trigger the field validation. Otherwise, use the first method.
|
|
97
194
|
|
|
98
|
-
```vue{7,16}
|
|
99
195
|
<template>
|
|
100
|
-
<
|
|
101
|
-
v-model="
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
v-bind="validationEvents"
|
|
106
|
-
/>
|
|
107
|
-
<!-- or -->
|
|
108
|
-
<input v-model="value" v-bind="validationEvents" />
|
|
109
|
-
</template>
|
|
110
|
-
|
|
111
|
-
<script setup lang="ts">
|
|
112
|
-
import { useFormField } from 'maz-ui/composables'
|
|
196
|
+
<form @submit="onSubmit">
|
|
197
|
+
<MazInput v-model="model.name" label="Name" />
|
|
198
|
+
<MazInput v-model="model.age" label="Age" type="number" />
|
|
199
|
+
<MazBtn type="submit">Submit</MazBtn>
|
|
200
|
+
</form>
|
|
113
201
|
|
|
114
|
-
|
|
115
|
-
</
|
|
202
|
+
<!-- Debug panel -->
|
|
203
|
+
<pre>{{ { isValid, isDirty, isSubmitted } }}</pre>
|
|
204
|
+
<pre>{{ fieldsStates }}</pre>
|
|
205
|
+
</template>
|
|
116
206
|
```
|
|
117
207
|
|
|
118
|
-
|
|
208
|
+
</template>
|
|
209
|
+
</ComponentDemo>
|
|
210
|
+
|
|
211
|
+
## Validation Modes
|
|
119
212
|
|
|
120
|
-
|
|
213
|
+
Validation modes control **when** validation runs and **when** errors are displayed. Choose the mode that best fits your UX needs.
|
|
121
214
|
|
|
122
|
-
|
|
215
|
+
| Mode | Validates On | Shows Errors | Best For |
|
|
216
|
+
|------|--------------|--------------|----------|
|
|
217
|
+
| `lazy` (default) | Value change | After change (if not empty) | Simple forms |
|
|
218
|
+
| `aggressive` | Immediately + every change | Always | Real-time feedback |
|
|
219
|
+
| `eager` | Blur, then on change | After first blur | Better UX |
|
|
220
|
+
| `blur` | Only on blur | After blur | Minimal interruption |
|
|
221
|
+
| `progressive` | Silently, shows on blur if invalid | After blur or validation | Optimal UX |
|
|
123
222
|
|
|
124
223
|
::: tip
|
|
125
|
-
|
|
224
|
+
For `eager`, `blur`, and `progressive` modes, you must use `useFormField` with the `ref` option or `validationEvents` to capture blur events.
|
|
126
225
|
:::
|
|
127
226
|
|
|
128
|
-
|
|
129
|
-
<b>Form State</b>
|
|
130
|
-
|
|
131
|
-
<div class="maz-text-xs maz-p-2 maz-bg-surface-600/50 dark:maz-bg-surface-400 maz-rounded maz-mt-2">
|
|
132
|
-
<pre>{{ { isValid, isSubmitting, isDirty, isSubmitted, errorMessages } }}</pre>
|
|
133
|
-
</div>
|
|
227
|
+
### Lazy Mode (Default)
|
|
134
228
|
|
|
135
|
-
|
|
229
|
+
The default mode. Validates when field values change. Errors only appear if the field is not empty.
|
|
136
230
|
|
|
137
|
-
|
|
231
|
+
<ComponentDemo>
|
|
232
|
+
<div class="maz-mb-4">
|
|
233
|
+
<p class="maz-text-sm maz-text-muted">Type in the field and clear it - notice the error appears only when there's content.</p>
|
|
234
|
+
</div>
|
|
235
|
+
<form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitLazy">
|
|
138
236
|
<MazInput
|
|
139
|
-
v-model="
|
|
140
|
-
label="
|
|
141
|
-
:hint="
|
|
142
|
-
:error="!!
|
|
143
|
-
:success="
|
|
144
|
-
:class="{ 'has-error': !!
|
|
237
|
+
v-model="lazyModel.name"
|
|
238
|
+
label="Name (min 3 characters)"
|
|
239
|
+
:hint="lazyErrors.name"
|
|
240
|
+
:error="!!lazyErrors.name"
|
|
241
|
+
:success="lazyStates.name.valid"
|
|
242
|
+
:class="{ 'has-error-lazy': !!lazyErrors.name }"
|
|
145
243
|
/>
|
|
146
244
|
<MazInput
|
|
147
|
-
v-model="
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
:hint="
|
|
151
|
-
:error="!!
|
|
152
|
-
:success="
|
|
153
|
-
:class="{ 'has-error': !!
|
|
245
|
+
v-model="lazyModel.email"
|
|
246
|
+
label="Email"
|
|
247
|
+
type="email"
|
|
248
|
+
:hint="lazyErrors.email"
|
|
249
|
+
:error="!!lazyErrors.email"
|
|
250
|
+
:success="lazyStates.email.valid"
|
|
251
|
+
:class="{ 'has-error-lazy': !!lazyErrors.email }"
|
|
154
252
|
/>
|
|
155
|
-
<
|
|
156
|
-
v-model="model.country"
|
|
157
|
-
:options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
|
|
158
|
-
label="Select your nationality (required)"
|
|
159
|
-
:hint="errorMessages.country"
|
|
160
|
-
:error="!!errorMessages.country"
|
|
161
|
-
:success="fieldsStates.country.valid"
|
|
162
|
-
:class="{ 'has-error': !!errorMessages.country }"
|
|
163
|
-
/>
|
|
164
|
-
<MazCheckbox
|
|
165
|
-
v-model="model.agree"
|
|
166
|
-
:hint="errorMessages.agree"
|
|
167
|
-
:error="fieldsStates.agree.error"
|
|
168
|
-
:success="fieldsStates.agree.valid"
|
|
169
|
-
:class="{ 'has-error': !!errorMessages.agree }"
|
|
170
|
-
>
|
|
171
|
-
I agree to the terms and conditions (required)
|
|
172
|
-
</MazCheckbox>
|
|
173
|
-
<MazBtn type="submit" :loading="isSubmitting">
|
|
174
|
-
Submit
|
|
175
|
-
</MazBtn>
|
|
253
|
+
<MazBtn type="submit" :loading="lazySubmitting">Submit</MazBtn>
|
|
176
254
|
</form>
|
|
255
|
+
|
|
177
256
|
<template #code>
|
|
178
257
|
|
|
179
258
|
```vue
|
|
180
259
|
<script lang="ts" setup>
|
|
181
|
-
import {
|
|
182
|
-
import {
|
|
183
|
-
import { boolean, literal, maxValue, minLength, minValue, nonEmpty, number, pipe, string } from 'valibot'
|
|
184
|
-
|
|
185
|
-
const toast = useToast()
|
|
260
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
261
|
+
import { pipe, string, email, nonEmpty, minLength } from 'valibot'
|
|
186
262
|
|
|
187
263
|
const schema = {
|
|
188
|
-
name: pipe(string(
|
|
189
|
-
|
|
190
|
-
country: pipe(string('Country is required'), nonEmpty('Country is required')),
|
|
191
|
-
agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
|
|
264
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
265
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
192
266
|
}
|
|
193
267
|
|
|
194
|
-
const { model,
|
|
268
|
+
const { model, errorMessages, fieldsStates, isSubmitting, handleSubmit } = useFormValidator({
|
|
195
269
|
schema,
|
|
196
|
-
|
|
197
|
-
|
|
270
|
+
options: {
|
|
271
|
+
mode: 'lazy', // This is the default
|
|
272
|
+
scrollToError: '.has-error-lazy',
|
|
273
|
+
},
|
|
198
274
|
})
|
|
199
275
|
|
|
200
|
-
const onSubmit = handleSubmit(
|
|
201
|
-
|
|
202
|
-
console.log(formData)
|
|
203
|
-
await sleep(2000)
|
|
204
|
-
toast.success('Form submitted', { position: 'top' })
|
|
276
|
+
const onSubmit = handleSubmit((data) => {
|
|
277
|
+
console.log('Submitted:', data)
|
|
205
278
|
})
|
|
206
279
|
</script>
|
|
280
|
+
```
|
|
207
281
|
|
|
208
|
-
|
|
209
|
-
|
|
282
|
+
</template>
|
|
283
|
+
</ComponentDemo>
|
|
284
|
+
|
|
285
|
+
### Aggressive Mode
|
|
286
|
+
|
|
287
|
+
Validates all fields immediately when the form is created and on every change. Errors are always displayed.
|
|
288
|
+
|
|
289
|
+
<ComponentDemo>
|
|
290
|
+
<div class="maz-mb-4">
|
|
291
|
+
<p class="maz-text-sm maz-text-muted">Notice all fields show errors immediately, even before any interaction.</p>
|
|
292
|
+
</div>
|
|
293
|
+
<form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitAggressive">
|
|
210
294
|
<MazInput
|
|
211
|
-
v-model="
|
|
212
|
-
label="
|
|
213
|
-
:hint="
|
|
214
|
-
:error="!!
|
|
215
|
-
:success="
|
|
216
|
-
:class="{ 'has-error': !!errorMessages.name }"
|
|
295
|
+
v-model="aggressiveModel.name"
|
|
296
|
+
label="Name (min 3 characters)"
|
|
297
|
+
:hint="aggressiveErrors.name"
|
|
298
|
+
:error="!!aggressiveErrors.name"
|
|
299
|
+
:success="aggressiveStates.name.valid"
|
|
217
300
|
/>
|
|
218
301
|
<MazInput
|
|
219
|
-
v-model="
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
:hint="
|
|
223
|
-
:error="!!
|
|
224
|
-
:success="
|
|
225
|
-
:class="{ 'has-error': !!errorMessages.age }"
|
|
302
|
+
v-model="aggressiveModel.email"
|
|
303
|
+
label="Email"
|
|
304
|
+
type="email"
|
|
305
|
+
:hint="aggressiveErrors.email"
|
|
306
|
+
:error="!!aggressiveErrors.email"
|
|
307
|
+
:success="aggressiveStates.email.valid"
|
|
226
308
|
/>
|
|
227
|
-
<
|
|
228
|
-
v-model="model.country"
|
|
229
|
-
:options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
|
|
230
|
-
label="Select your nationality"
|
|
231
|
-
:hint="errorMessages.country"
|
|
232
|
-
:error="!!errorMessages.country"
|
|
233
|
-
:success="fieldsStates.country.valid"
|
|
234
|
-
:class="{ 'has-error': !!errorMessages.country }"
|
|
235
|
-
/>
|
|
236
|
-
<MazCheckbox
|
|
237
|
-
v-model="model.agree"
|
|
238
|
-
:hint="errorMessages.agree"
|
|
239
|
-
:error="fieldsStates.agree.error"
|
|
240
|
-
:success="fieldsStates.agree.valid"
|
|
241
|
-
:class="{ 'has-error': !!errorMessages.agree }"
|
|
242
|
-
>
|
|
243
|
-
I agree to the terms and conditions
|
|
244
|
-
</MazCheckbox>
|
|
245
|
-
<MazBtn type="submit" :loading="isSubmitting">
|
|
246
|
-
Submit
|
|
247
|
-
</MazBtn>
|
|
309
|
+
<MazBtn type="submit" :loading="aggressiveSubmitting">Submit</MazBtn>
|
|
248
310
|
</form>
|
|
249
|
-
|
|
311
|
+
|
|
312
|
+
<template #code>
|
|
313
|
+
|
|
314
|
+
```vue
|
|
315
|
+
<script lang="ts" setup>
|
|
316
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
317
|
+
import { pipe, string, email, nonEmpty, minLength } from 'valibot'
|
|
318
|
+
|
|
319
|
+
const schema = {
|
|
320
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
321
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const { model, errorMessages, fieldsStates, isSubmitting, handleSubmit } = useFormValidator({
|
|
325
|
+
schema,
|
|
326
|
+
options: {
|
|
327
|
+
mode: 'aggressive', // Validates immediately
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
</script>
|
|
250
331
|
```
|
|
251
332
|
|
|
252
333
|
</template>
|
|
253
334
|
</ComponentDemo>
|
|
254
335
|
|
|
255
|
-
|
|
336
|
+
### Eager Mode (Recommended)
|
|
256
337
|
|
|
257
|
-
|
|
338
|
+
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.
|
|
258
339
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
340
|
+
::: warning
|
|
341
|
+
Requires `useFormField` with `ref` option or `validationEvents`.
|
|
342
|
+
:::
|
|
262
343
|
|
|
263
344
|
<ComponentDemo>
|
|
345
|
+
<div class="maz-mb-4">
|
|
346
|
+
<p class="maz-text-sm maz-text-muted">Type something, then click outside the field (blur) to see validation. After that, errors update as you type.</p>
|
|
347
|
+
</div>
|
|
264
348
|
<form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitEager">
|
|
265
349
|
<MazInput
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
label="
|
|
269
|
-
:hint="
|
|
270
|
-
:error="
|
|
271
|
-
:
|
|
350
|
+
ref="eagerNameRef"
|
|
351
|
+
v-model="eagerName"
|
|
352
|
+
label="Name (min 3 characters)"
|
|
353
|
+
:hint="eagerNameError"
|
|
354
|
+
:error="eagerNameHasError"
|
|
355
|
+
:success="eagerNameValid"
|
|
356
|
+
:class="{ 'has-error-eager': eagerNameHasError }"
|
|
272
357
|
/>
|
|
273
358
|
<MazInput
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
:hint="
|
|
279
|
-
:error="
|
|
280
|
-
:
|
|
281
|
-
|
|
282
|
-
<MazSelect
|
|
283
|
-
v-model="country"
|
|
284
|
-
ref="countryRef"
|
|
285
|
-
:options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
|
|
286
|
-
label="Select your nationality"
|
|
287
|
-
:hint="countryErrorMessage"
|
|
288
|
-
:error="hasErrorCountry"
|
|
289
|
-
:class="{ 'has-error-form2': hasErrorCountry }"
|
|
359
|
+
ref="eagerEmailRef"
|
|
360
|
+
v-model="eagerEmail"
|
|
361
|
+
label="Email"
|
|
362
|
+
type="email"
|
|
363
|
+
:hint="eagerEmailError"
|
|
364
|
+
:error="eagerEmailHasError"
|
|
365
|
+
:success="eagerEmailValid"
|
|
366
|
+
:class="{ 'has-error-eager': eagerEmailHasError }"
|
|
290
367
|
/>
|
|
291
|
-
<
|
|
292
|
-
v-model="agree"
|
|
293
|
-
ref="agreeRef"
|
|
294
|
-
:hint="agreeErrorMessage"
|
|
295
|
-
:error="hasErrorAgree"
|
|
296
|
-
:class="{ 'has-error': hasErrorAgree }"
|
|
297
|
-
>
|
|
298
|
-
I agree to the terms and conditions
|
|
299
|
-
</MazCheckbox>
|
|
300
|
-
<MazBtn type="submit" :loading="isSubmittingEager">
|
|
301
|
-
Submit
|
|
302
|
-
</MazBtn>
|
|
368
|
+
<MazBtn type="submit" :loading="eagerSubmitting">Submit</MazBtn>
|
|
303
369
|
</form>
|
|
304
370
|
|
|
305
|
-
<template #code>
|
|
371
|
+
<template #code>
|
|
306
372
|
|
|
307
373
|
```vue
|
|
308
|
-
<script
|
|
309
|
-
import {
|
|
310
|
-
import {
|
|
311
|
-
import { boolean, literal, maxValue, minLength, minValue, nonEmpty, number, pipe, string } from 'valibot'
|
|
374
|
+
<script lang="ts" setup>
|
|
375
|
+
import { useFormValidator, useFormField } from 'maz-ui/composables'
|
|
376
|
+
import { pipe, string, email, nonEmpty, minLength } from 'valibot'
|
|
312
377
|
import { useTemplateRef } from 'vue'
|
|
313
378
|
|
|
314
379
|
const schema = {
|
|
315
|
-
name: pipe(string(
|
|
316
|
-
|
|
317
|
-
country: pipe(string('Country is required'), nonEmpty('Country is required')),
|
|
318
|
-
agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
|
|
380
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
381
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
319
382
|
}
|
|
320
383
|
|
|
321
|
-
const { isSubmitting, handleSubmit } = useFormValidator
|
|
384
|
+
const { isSubmitting, handleSubmit } = useFormValidator({
|
|
322
385
|
schema,
|
|
323
|
-
options: {
|
|
386
|
+
options: {
|
|
387
|
+
mode: 'eager',
|
|
388
|
+
scrollToError: '.has-error-eager',
|
|
389
|
+
identifier: 'form-eager',
|
|
390
|
+
},
|
|
324
391
|
})
|
|
325
392
|
|
|
326
|
-
|
|
393
|
+
// useFormField for each field with ref for blur detection
|
|
394
|
+
const {
|
|
395
|
+
value: name,
|
|
396
|
+
hasError: nameHasError,
|
|
397
|
+
errorMessage: nameError,
|
|
398
|
+
isValid: nameValid,
|
|
399
|
+
} = useFormField<string>('name', {
|
|
327
400
|
ref: useTemplateRef('nameRef'),
|
|
328
401
|
formIdentifier: 'form-eager',
|
|
329
402
|
})
|
|
330
|
-
const { value: age, hasError: hasErrorAge, errorMessage: ageErrorMessage } = useFormField<number>('age', {
|
|
331
|
-
ref: useTemplateRef('ageRef'),
|
|
332
|
-
formIdentifier: 'form-eager',
|
|
333
|
-
})
|
|
334
|
-
const { value: agree, hasError: hasErrorAgree, errorMessage: agreeErrorMessage } = useFormField<boolean>('agree', {
|
|
335
|
-
ref: useTemplateRef('agreeRef'),
|
|
336
|
-
formIdentifier: 'form-eager'
|
|
337
|
-
})
|
|
338
|
-
const { value: country, hasError: hasErrorCountry, errorMessage: countryErrorMessage, validationEvents } = useFormField<string>('country', {
|
|
339
|
-
mode: 'lazy',
|
|
340
|
-
formIdentifier: 'form-eager'
|
|
341
|
-
})
|
|
342
403
|
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
404
|
+
const {
|
|
405
|
+
value: email,
|
|
406
|
+
hasError: emailHasError,
|
|
407
|
+
errorMessage: emailError,
|
|
408
|
+
isValid: emailValid,
|
|
409
|
+
} = useFormField<string>('email', {
|
|
410
|
+
ref: useTemplateRef('emailRef'),
|
|
411
|
+
formIdentifier: 'form-eager',
|
|
348
412
|
})
|
|
349
413
|
</script>
|
|
350
414
|
|
|
351
415
|
<template>
|
|
352
|
-
<form @submit="onSubmit">
|
|
416
|
+
<form @submit="handleSubmit(onSubmit)">
|
|
353
417
|
<MazInput
|
|
354
418
|
ref="nameRef"
|
|
355
419
|
v-model="name"
|
|
356
|
-
label="
|
|
357
|
-
:hint="
|
|
358
|
-
:error="
|
|
359
|
-
:
|
|
420
|
+
label="Name"
|
|
421
|
+
:hint="nameError"
|
|
422
|
+
:error="nameHasError"
|
|
423
|
+
:success="nameValid"
|
|
360
424
|
/>
|
|
361
425
|
<MazInput
|
|
362
|
-
ref="
|
|
363
|
-
v-model="
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
:
|
|
367
|
-
:
|
|
368
|
-
:class="{ 'has-error-form2': hasErrorAge }"
|
|
369
|
-
/>
|
|
370
|
-
<MazSelect
|
|
371
|
-
ref="countryRef"
|
|
372
|
-
v-model="country"
|
|
373
|
-
:options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
|
|
374
|
-
label="Select your nationality"
|
|
375
|
-
:hint="countryErrorMessage"
|
|
376
|
-
:error="hasErrorCountry"
|
|
377
|
-
:class="{ 'has-error-form2': hasErrorCountry }"
|
|
426
|
+
ref="emailRef"
|
|
427
|
+
v-model="email"
|
|
428
|
+
label="Email"
|
|
429
|
+
:hint="emailError"
|
|
430
|
+
:error="emailHasError"
|
|
431
|
+
:success="emailValid"
|
|
378
432
|
/>
|
|
379
|
-
<MazCheckbox
|
|
380
|
-
ref="agreeRef"
|
|
381
|
-
v-model="agree"
|
|
382
|
-
:hint="agreeErrorMessage"
|
|
383
|
-
:error="hasErrorAgree"
|
|
384
|
-
:class="{ 'has-error': hasErrorAgree }"
|
|
385
|
-
>
|
|
386
|
-
I agree to the terms and conditions
|
|
387
|
-
</MazCheckbox>
|
|
388
|
-
<MazBtn type="submit" :loading="isSubmitting">
|
|
389
|
-
Submit
|
|
390
|
-
</MazBtn>
|
|
391
433
|
</form>
|
|
392
434
|
</template>
|
|
393
435
|
```
|
|
@@ -395,658 +437,733 @@ const onSubmit = handleSubmit(async (formData) => {
|
|
|
395
437
|
</template>
|
|
396
438
|
</ComponentDemo>
|
|
397
439
|
|
|
398
|
-
###
|
|
440
|
+
### Blur Mode
|
|
399
441
|
|
|
400
|
-
|
|
442
|
+
::: warning
|
|
443
|
+
Requires `useFormField` with `ref` option or `validationEvents`.
|
|
444
|
+
:::
|
|
445
|
+
|
|
446
|
+
Validates only when the field loses focus. Errors are only shown after blur.
|
|
401
447
|
|
|
402
448
|
<ComponentDemo>
|
|
403
|
-
<
|
|
449
|
+
<div class="maz-mb-4">
|
|
450
|
+
<p class="maz-text-sm maz-text-muted">Type in the field, then click outside. Errors only appear after blur, and don't update while typing.</p>
|
|
451
|
+
</div>
|
|
452
|
+
<form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitBlur">
|
|
404
453
|
<MazInput
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
label="
|
|
408
|
-
:hint="
|
|
409
|
-
:error="
|
|
410
|
-
:success="
|
|
411
|
-
:class="{ 'has-error-
|
|
454
|
+
ref="blurNameRef"
|
|
455
|
+
v-model="blurName"
|
|
456
|
+
label="Name (min 3 characters)"
|
|
457
|
+
:hint="blurNameError"
|
|
458
|
+
:error="blurNameHasError"
|
|
459
|
+
:success="blurNameValid"
|
|
460
|
+
:class="{ 'has-error-blur': blurNameHasError }"
|
|
412
461
|
/>
|
|
413
462
|
<MazInput
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
:hint="
|
|
419
|
-
:error="
|
|
420
|
-
:success="
|
|
421
|
-
:class="{ 'has-error-
|
|
463
|
+
ref="blurEmailRef"
|
|
464
|
+
v-model="blurEmail"
|
|
465
|
+
label="Email"
|
|
466
|
+
type="email"
|
|
467
|
+
:hint="blurEmailError"
|
|
468
|
+
:error="blurEmailHasError"
|
|
469
|
+
:success="blurEmailValid"
|
|
470
|
+
:class="{ 'has-error-blur': blurEmailHasError }"
|
|
422
471
|
/>
|
|
423
|
-
<
|
|
424
|
-
v-model="countryProgressive"
|
|
425
|
-
ref="countryProgressiveRef"
|
|
426
|
-
:options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
|
|
427
|
-
label="Select your nationality"
|
|
428
|
-
:hint="countryMessageProgressive"
|
|
429
|
-
:error="!!countryMessageProgressive"
|
|
430
|
-
:success="countryValidProgressive"
|
|
431
|
-
:class="{ 'has-error-progressive': !!countryErrorProgressive }"
|
|
432
|
-
/>
|
|
433
|
-
<MazCheckbox
|
|
434
|
-
v-model="agreeProgressive"
|
|
435
|
-
ref="agreeProgressiveRef"
|
|
436
|
-
:hint="agreeMessageProgressive"
|
|
437
|
-
:error="!!agreeMessageProgressive"
|
|
438
|
-
:class="{ 'has-error-progressive': !!agreeMessageProgressive }"
|
|
439
|
-
>
|
|
440
|
-
I agree to the terms and conditions
|
|
441
|
-
</MazCheckbox>
|
|
442
|
-
<MazBtn type="submit" :loading="isSubmittingProgressive">
|
|
443
|
-
Submit
|
|
444
|
-
</MazBtn>
|
|
472
|
+
<MazBtn type="submit" :loading="blurSubmitting">Submit</MazBtn>
|
|
445
473
|
</form>
|
|
446
474
|
|
|
447
|
-
<template #code>
|
|
475
|
+
<template #code>
|
|
448
476
|
|
|
449
477
|
```vue
|
|
450
|
-
<script
|
|
451
|
-
import {
|
|
452
|
-
import {
|
|
453
|
-
import { boolean, literal, maxValue, minLength, minValue, nonEmpty, number, pipe, string } from 'valibot'
|
|
478
|
+
<script lang="ts" setup>
|
|
479
|
+
import { useFormValidator, useFormField } from 'maz-ui/composables'
|
|
480
|
+
import { pipe, string, email, nonEmpty, minLength } from 'valibot'
|
|
454
481
|
import { useTemplateRef } from 'vue'
|
|
455
482
|
|
|
456
483
|
const schema = {
|
|
457
|
-
name: pipe(string(
|
|
458
|
-
|
|
459
|
-
country: pipe(string('Country is required'), nonEmpty('Country is required')),
|
|
460
|
-
agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
|
|
484
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
485
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
461
486
|
}
|
|
462
487
|
|
|
463
|
-
const { isSubmitting, handleSubmit } = useFormValidator
|
|
488
|
+
const { isSubmitting, handleSubmit } = useFormValidator({
|
|
464
489
|
schema,
|
|
465
|
-
options: {
|
|
490
|
+
options: {
|
|
491
|
+
mode: 'blur',
|
|
492
|
+
identifier: 'form-blur',
|
|
493
|
+
},
|
|
466
494
|
})
|
|
467
495
|
|
|
468
|
-
const { value: name, hasError
|
|
496
|
+
const { value: name, hasError, errorMessage, isValid } = useFormField<string>('name', {
|
|
469
497
|
ref: useTemplateRef('nameRef'),
|
|
470
|
-
formIdentifier: 'form-
|
|
471
|
-
})
|
|
472
|
-
const { value: age, hasError: ageHasError, errorMessage: ageErrorMessage } = useFormField<number>('age', {
|
|
473
|
-
ref: useTemplateRef('ageRef'),
|
|
474
|
-
formIdentifier: 'form-progressive',
|
|
475
|
-
})
|
|
476
|
-
const { value: country, hasError: countryHasError, errorMessage: countryErrorMessage, validationEvents } = useFormField<string>('country', {
|
|
477
|
-
mode: 'lazy',
|
|
478
|
-
formIdentifier: 'form-progressive',
|
|
479
|
-
})
|
|
480
|
-
const { value: agree, hasError: agreeHasError, errorMessage: agreeErrorMessage } = useFormField<boolean>('agree', {
|
|
481
|
-
ref: useTemplateRef('agreeRef'),
|
|
482
|
-
formIdentifier: 'form-progressive',
|
|
498
|
+
formIdentifier: 'form-blur',
|
|
483
499
|
})
|
|
484
500
|
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
await sleep(2000)
|
|
489
|
-
toast.success('Form submitted', { position: 'top' })
|
|
501
|
+
const { value: email, hasError: emailHasError, errorMessage: emailError, isValid: emailValid } = useFormField<string>('email', {
|
|
502
|
+
ref: useTemplateRef('emailRef'),
|
|
503
|
+
formIdentifier: 'form-blur',
|
|
490
504
|
})
|
|
491
505
|
</script>
|
|
492
|
-
|
|
493
|
-
<template>
|
|
494
|
-
<form @submit="onSubmit">
|
|
495
|
-
<MazInput
|
|
496
|
-
ref="nameRef"
|
|
497
|
-
v-model="name"
|
|
498
|
-
label="Enter your name"
|
|
499
|
-
:hint="nameErrorMessage"
|
|
500
|
-
:error="nameHasError"
|
|
501
|
-
:class="{ 'has-error-progressive': nameHasError }"
|
|
502
|
-
/>
|
|
503
|
-
<MazInput
|
|
504
|
-
ref="ageRef"
|
|
505
|
-
v-model="age"
|
|
506
|
-
type="number"
|
|
507
|
-
label="Enter your age"
|
|
508
|
-
:hint="ageErrorMessage"
|
|
509
|
-
:error="ageHasError"
|
|
510
|
-
:class="{ 'has-error-progressive': ageHasError }"
|
|
511
|
-
/>
|
|
512
|
-
<MazSelect
|
|
513
|
-
v-model="country"
|
|
514
|
-
v-bind="validationEvents"
|
|
515
|
-
:options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
|
|
516
|
-
label="Select your nationality"
|
|
517
|
-
:hint="countryErrorMessage"
|
|
518
|
-
:error="countryHasError"
|
|
519
|
-
:class="{ 'has-error-progressive': countryHasError }"
|
|
520
|
-
/>
|
|
521
|
-
<MazCheckbox
|
|
522
|
-
ref="agreeRef"
|
|
523
|
-
v-model="agree"
|
|
524
|
-
:hint="agreeErrorMessage"
|
|
525
|
-
:error="agreeHasError"
|
|
526
|
-
:class="{ 'has-error-progressive': agreeHasError }"
|
|
527
|
-
>
|
|
528
|
-
I agree to the terms and conditions
|
|
529
|
-
</MazCheckbox>
|
|
530
|
-
<MazBtn type="submit" :loading="isSubmitting">
|
|
531
|
-
Submit
|
|
532
|
-
</MazBtn>
|
|
533
|
-
</form>
|
|
534
|
-
</template>
|
|
535
506
|
```
|
|
536
507
|
|
|
537
508
|
</template>
|
|
538
509
|
</ComponentDemo>
|
|
539
510
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
Default: `true`
|
|
511
|
+
### Progressive Mode
|
|
543
512
|
|
|
544
|
-
|
|
513
|
+
::: warning
|
|
514
|
+
Requires `useFormField` with `ref` option or `validationEvents`.
|
|
515
|
+
:::
|
|
545
516
|
|
|
546
|
-
|
|
517
|
+
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.
|
|
547
518
|
|
|
548
519
|
<ComponentDemo>
|
|
549
|
-
<
|
|
520
|
+
<div class="maz-mb-4">
|
|
521
|
+
<p class="maz-text-sm maz-text-muted">Start typing - the field becomes valid (green) as soon as validation passes. Errors only show after blur.</p>
|
|
522
|
+
</div>
|
|
523
|
+
<form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitProgressive">
|
|
550
524
|
<MazInput
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
:
|
|
555
|
-
:
|
|
556
|
-
:
|
|
525
|
+
ref="progressiveNameRef"
|
|
526
|
+
v-model="progressiveName"
|
|
527
|
+
label="Name (min 3 characters)"
|
|
528
|
+
:hint="progressiveNameError"
|
|
529
|
+
:error="progressiveNameHasError"
|
|
530
|
+
:success="progressiveNameValid"
|
|
531
|
+
:class="{ 'has-error-progressive': progressiveNameHasError }"
|
|
557
532
|
/>
|
|
558
533
|
<MazInput
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
label="
|
|
562
|
-
|
|
563
|
-
:
|
|
564
|
-
:
|
|
565
|
-
:
|
|
534
|
+
ref="progressiveEmailRef"
|
|
535
|
+
v-model="progressiveEmail"
|
|
536
|
+
label="Email"
|
|
537
|
+
type="email"
|
|
538
|
+
:hint="progressiveEmailError"
|
|
539
|
+
:error="progressiveEmailHasError"
|
|
540
|
+
:success="progressiveEmailValid"
|
|
541
|
+
:class="{ 'has-error-progressive': progressiveEmailHasError }"
|
|
566
542
|
/>
|
|
567
|
-
<
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
543
|
+
<MazCheckbox
|
|
544
|
+
ref="progressiveAgreeRef"
|
|
545
|
+
v-model="progressiveAgree"
|
|
546
|
+
:hint="progressiveAgreeError"
|
|
547
|
+
:error="progressiveAgreeHasError"
|
|
548
|
+
:class="{ 'has-error-progressive': progressiveAgreeHasError }"
|
|
549
|
+
>
|
|
550
|
+
I agree to the terms and conditions
|
|
551
|
+
</MazCheckbox>
|
|
552
|
+
<MazBtn type="submit" :loading="progressiveSubmitting">Submit</MazBtn>
|
|
573
553
|
</form>
|
|
574
554
|
|
|
575
|
-
<template #code>
|
|
555
|
+
<template #code>
|
|
576
556
|
|
|
577
|
-
```vue
|
|
578
|
-
<
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
<MazInput
|
|
589
|
-
v-model="model.age"
|
|
590
|
-
type="number"
|
|
591
|
-
label="Enter your age"
|
|
592
|
-
:hint="errorMessages.age"
|
|
593
|
-
:error="fieldsStates.age.error"
|
|
594
|
-
:success="fieldsStates.age.valid"
|
|
595
|
-
:class="{ 'has-error-debounced': fieldsStates.age.error }"
|
|
596
|
-
/>
|
|
597
|
-
<MazBtn type="submit" :loading="isSubmitting">
|
|
598
|
-
Submit
|
|
599
|
-
</MazBtn>
|
|
600
|
-
<MazBtn @click="resetForm" color="destructive">
|
|
601
|
-
Reset
|
|
602
|
-
</MazBtn>
|
|
603
|
-
</form>
|
|
604
|
-
</template>
|
|
557
|
+
```vue
|
|
558
|
+
<script lang="ts" setup>
|
|
559
|
+
import { useFormValidator, useFormField } from 'maz-ui/composables'
|
|
560
|
+
import { pipe, string, email, nonEmpty, minLength, boolean, literal } from 'valibot'
|
|
561
|
+
import { useTemplateRef } from 'vue'
|
|
562
|
+
|
|
563
|
+
const schema = {
|
|
564
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
565
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
566
|
+
agree: pipe(boolean(), literal(true, 'You must agree')),
|
|
567
|
+
}
|
|
605
568
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
569
|
+
const { isSubmitting, handleSubmit } = useFormValidator({
|
|
570
|
+
schema,
|
|
571
|
+
options: {
|
|
572
|
+
mode: 'progressive',
|
|
573
|
+
identifier: 'form-progressive',
|
|
574
|
+
},
|
|
575
|
+
})
|
|
610
576
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
options: {
|
|
617
|
-
resetOnSuccess: true,
|
|
618
|
-
scrollToError: '.has-error-reset',
|
|
619
|
-
},
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
const onSubmit = handleSubmit(async (formData) => {
|
|
623
|
-
// Form submission logic
|
|
624
|
-
console.log(formData)
|
|
625
|
-
await sleep(2000)
|
|
626
|
-
toast.success('Form submitted', { position: 'top' })
|
|
627
|
-
})
|
|
577
|
+
const { value: name, hasError, errorMessage, isValid } = useFormField<string>('name', {
|
|
578
|
+
ref: useTemplateRef('nameRef'),
|
|
579
|
+
formIdentifier: 'form-progressive',
|
|
580
|
+
})
|
|
581
|
+
// ... same for other fields
|
|
628
582
|
</script>
|
|
629
583
|
```
|
|
630
584
|
|
|
631
585
|
</template>
|
|
632
586
|
</ComponentDemo>
|
|
633
587
|
|
|
634
|
-
##
|
|
588
|
+
## useFormField for Child Components
|
|
635
589
|
|
|
636
|
-
|
|
590
|
+
`useFormField` is essential when:
|
|
591
|
+
1. Your form fields are in child components
|
|
592
|
+
2. You need the `eager`, `blur`, or `progressive` validation modes
|
|
593
|
+
3. You want fine-grained control over individual field states
|
|
637
594
|
|
|
638
|
-
|
|
595
|
+
### Return Values
|
|
639
596
|
|
|
640
|
-
|
|
597
|
+
| Property | Type | Description |
|
|
598
|
+
|----------|------|-------------|
|
|
599
|
+
| `value` | `WritableComputedRef<T>` | The field value (use with `v-model`) |
|
|
600
|
+
| `hasError` | `ComputedRef<boolean>` | Field has an error that should be displayed |
|
|
601
|
+
| `errors` | `ComputedRef<ValidationIssues>` | All validation issues |
|
|
602
|
+
| `errorMessage` | `ComputedRef<string>` | First error message |
|
|
603
|
+
| `isValid` | `ComputedRef<boolean>` | Field passes validation |
|
|
604
|
+
| `isDirty` | `ComputedRef<boolean>` | Field has been modified |
|
|
605
|
+
| `isBlurred` | `ComputedRef<boolean>` | Field has lost focus |
|
|
606
|
+
| `isValidated` | `ComputedRef<boolean>` | Validation has run |
|
|
607
|
+
| `isValidating` | `ComputedRef<boolean>` | Async validation in progress |
|
|
608
|
+
| `mode` | `ComputedRef<string>` | The validation mode |
|
|
609
|
+
| `validationEvents` | `ComputedRef<object>` | Blur event handler for `v-bind` |
|
|
641
610
|
|
|
642
|
-
|
|
643
|
-
<form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitDebounced">
|
|
644
|
-
<MazInput
|
|
645
|
-
v-model="modelDebounced.name"
|
|
646
|
-
label="Enter your name"
|
|
647
|
-
:hint="errorMessagesDebounced.name"
|
|
648
|
-
:error="fieldsStatesDebounced.name.error"
|
|
649
|
-
:success="fieldsStatesDebounced.name.valid"
|
|
650
|
-
:class="{ 'has-error-debounced': fieldsStatesDebounced.name.error }"
|
|
651
|
-
/>
|
|
652
|
-
<MazInput
|
|
653
|
-
v-model="modelDebounced.age"
|
|
654
|
-
type="number"
|
|
655
|
-
label="Enter your age"
|
|
656
|
-
:hint="errorMessagesDebounced.age"
|
|
657
|
-
:error="fieldsStatesDebounced.age.error"
|
|
658
|
-
:success="fieldsStatesDebounced.age.valid"
|
|
659
|
-
:class="{ 'has-error-debounced': fieldsStatesDebounced.age.error }"
|
|
660
|
-
/>
|
|
661
|
-
<MazBtn type="submit" :loading="isSubmittingDebounced">
|
|
662
|
-
Submit
|
|
663
|
-
</MazBtn>
|
|
664
|
-
</form>
|
|
611
|
+
### Two Ways to Bind Validation Events
|
|
665
612
|
|
|
666
|
-
|
|
613
|
+
#### Option 1: Using `ref` (Recommended)
|
|
667
614
|
|
|
668
|
-
|
|
669
|
-
<template>
|
|
670
|
-
<form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmit">
|
|
671
|
-
<MazInput
|
|
672
|
-
v-model="model.name"
|
|
673
|
-
label="Enter your name"
|
|
674
|
-
:hint="errorMessages.name"
|
|
675
|
-
:error="fieldsStates.name.error"
|
|
676
|
-
:success="fieldsStates.name.valid"
|
|
677
|
-
:class="{ 'has-error-debounced': fieldsStates.name.error }"
|
|
678
|
-
/>
|
|
679
|
-
<MazInput
|
|
680
|
-
v-model="model.age"
|
|
681
|
-
type="number"
|
|
682
|
-
label="Enter your age"
|
|
683
|
-
:hint="errorMessages.age"
|
|
684
|
-
:error="fieldsStates.age.error"
|
|
685
|
-
:success="fieldsStates.age.valid"
|
|
686
|
-
:class="{ 'has-error-debounced': fieldsStates.age.error }"
|
|
687
|
-
/>
|
|
688
|
-
<MazBtn type="submit" :loading="isSubmitting">
|
|
689
|
-
Submit
|
|
690
|
-
</MazBtn>
|
|
691
|
-
</form>
|
|
692
|
-
</template>
|
|
615
|
+
Pass a template ref to `useFormField`. It will automatically detect interactive elements and attach blur listeners.
|
|
693
616
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
617
|
+
```vue
|
|
618
|
+
<script setup>
|
|
619
|
+
import { useFormField } from 'maz-ui/composables'
|
|
620
|
+
import { useTemplateRef } from 'vue'
|
|
698
621
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
},
|
|
704
|
-
options: {
|
|
705
|
-
debouncedFields: { name: 500 },
|
|
706
|
-
throttledFields: { age: true },
|
|
707
|
-
scrollToError: '.has-error-debounced',
|
|
708
|
-
},
|
|
709
|
-
})
|
|
710
|
-
|
|
711
|
-
const onSubmit = handleSubmit(async (formData) => {
|
|
712
|
-
// Form submission logic
|
|
713
|
-
console.log(formData)
|
|
714
|
-
await sleep(2000)
|
|
715
|
-
toast.success('Form submitted', { position: 'top' })
|
|
716
|
-
})
|
|
622
|
+
const { value, errorMessage, hasError } = useFormField<string>('email', {
|
|
623
|
+
ref: useTemplateRef('emailRef'),
|
|
624
|
+
formIdentifier: 'my-form',
|
|
625
|
+
})
|
|
717
626
|
</script>
|
|
627
|
+
|
|
628
|
+
<template>
|
|
629
|
+
<MazInput
|
|
630
|
+
ref="emailRef"
|
|
631
|
+
v-model="value"
|
|
632
|
+
:hint="errorMessage"
|
|
633
|
+
:error="hasError"
|
|
634
|
+
/>
|
|
635
|
+
</template>
|
|
718
636
|
```
|
|
719
637
|
|
|
720
|
-
|
|
721
|
-
|
|
638
|
+
#### Option 2: Using `validationEvents`
|
|
639
|
+
|
|
640
|
+
If your component emits a `blur` event, you can use `v-bind` with `validationEvents`.
|
|
641
|
+
|
|
642
|
+
```vue
|
|
643
|
+
<script setup>
|
|
644
|
+
import { useFormField } from 'maz-ui/composables'
|
|
645
|
+
|
|
646
|
+
const { value, errorMessage, hasError, validationEvents } = useFormField<string>('email', {
|
|
647
|
+
formIdentifier: 'my-form',
|
|
648
|
+
})
|
|
649
|
+
</script>
|
|
650
|
+
|
|
651
|
+
<template>
|
|
652
|
+
<MazInput
|
|
653
|
+
v-model="value"
|
|
654
|
+
v-bind="validationEvents"
|
|
655
|
+
:hint="errorMessage"
|
|
656
|
+
:error="hasError"
|
|
657
|
+
/>
|
|
658
|
+
</template>
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## TypeScript Type Inference
|
|
662
|
+
|
|
663
|
+
The form model is automatically typed based on your schema:
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
const schema = {
|
|
667
|
+
name: pipe(string(), nonEmpty()),
|
|
668
|
+
age: pipe(number(), minValue(0)),
|
|
669
|
+
email: pipe(string(), email()),
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const { model } = useFormValidator({ schema })
|
|
673
|
+
// model.value is typed as: { name?: string, age?: number, email?: string }
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
For `useFormField`, specify the field type as a generic parameter:
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
// Specify the type for better type safety
|
|
680
|
+
const { value } = useFormField<string>('name', { formIdentifier: 'my-form' })
|
|
681
|
+
// value is typed as WritableComputedRef<string>
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
::: warning Common TypeScript Errors
|
|
685
|
+
If you get circular reference errors with `useTemplateRef`, use the classic `ref()` instead:
|
|
686
|
+
|
|
687
|
+
```ts
|
|
688
|
+
// May cause TypeScript errors
|
|
689
|
+
const { value: email } = useFormField<string>('email', {
|
|
690
|
+
ref: useTemplateRef('emailRef'),
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
// Solution 1: Add generic to useTemplateRef
|
|
694
|
+
const { value: email } = useFormField<string>('email', {
|
|
695
|
+
ref: useTemplateRef<HTMLInputElement>('emailRef'),
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
// Solution 2: Use classic ref
|
|
699
|
+
const emailRef = ref<HTMLInputElement>()
|
|
700
|
+
const { value: email } = useFormField<string>('email', {
|
|
701
|
+
ref: emailRef,
|
|
702
|
+
})
|
|
703
|
+
```
|
|
722
704
|
|
|
723
|
-
|
|
705
|
+
:::
|
|
724
706
|
|
|
725
|
-
|
|
707
|
+
## Async Validation
|
|
708
|
+
|
|
709
|
+
Use Valibot's `pipeAsync` and `checkAsync` for async validations like checking username availability:
|
|
726
710
|
|
|
727
711
|
<ComponentDemo>
|
|
712
|
+
<div class="maz-mb-4">
|
|
713
|
+
<p class="maz-text-sm maz-text-muted">Try typing "taken" - the async validator will reject it after a 2-second delay.</p>
|
|
714
|
+
</div>
|
|
728
715
|
<form class="maz-flex maz-gap-4" @submit="onSubmitAsync">
|
|
729
716
|
<MazInput
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
:
|
|
735
|
-
:
|
|
736
|
-
:
|
|
737
|
-
|
|
738
|
-
:class="{ 'has-error-async': fieldsStatesAsync.name.error }"
|
|
717
|
+
ref="asyncUsernameRef"
|
|
718
|
+
v-model="asyncUsername"
|
|
719
|
+
label="Username"
|
|
720
|
+
:hint="asyncUsernameError"
|
|
721
|
+
:error="asyncUsernameHasError"
|
|
722
|
+
:success="asyncUsernameValid"
|
|
723
|
+
:loading="asyncUsernameValidating"
|
|
724
|
+
class="maz-flex-1"
|
|
739
725
|
/>
|
|
740
|
-
<MazBtn type="submit" :loading="
|
|
741
|
-
Submit
|
|
742
|
-
</MazBtn>
|
|
726
|
+
<MazBtn type="submit" :loading="asyncSubmitting">Submit</MazBtn>
|
|
743
727
|
</form>
|
|
744
728
|
|
|
745
|
-
<template #code>
|
|
729
|
+
<template #code>
|
|
746
730
|
|
|
747
731
|
```vue
|
|
732
|
+
<script lang="ts" setup>
|
|
733
|
+
import { useFormValidator, useFormField } from 'maz-ui/composables'
|
|
734
|
+
import { pipeAsync, string, nonEmpty, minLength, checkAsync } from 'valibot'
|
|
735
|
+
import { useTemplateRef } from 'vue'
|
|
736
|
+
|
|
737
|
+
const schema = {
|
|
738
|
+
username: pipeAsync(
|
|
739
|
+
string(),
|
|
740
|
+
nonEmpty('Username is required'),
|
|
741
|
+
minLength(3, 'Min 3 characters'),
|
|
742
|
+
checkAsync(async (value) => {
|
|
743
|
+
// Simulate API call
|
|
744
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
745
|
+
return value !== 'taken' // Return true if valid
|
|
746
|
+
}, 'Username is already taken'),
|
|
747
|
+
),
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const { isSubmitting, handleSubmit } = useFormValidator({
|
|
751
|
+
schema,
|
|
752
|
+
options: { mode: 'eager', identifier: 'form-async' },
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
const {
|
|
756
|
+
value: username,
|
|
757
|
+
hasError,
|
|
758
|
+
errorMessage,
|
|
759
|
+
isValid,
|
|
760
|
+
isValidating, // Shows loading state during async validation
|
|
761
|
+
} = useFormField<string>('username', {
|
|
762
|
+
ref: useTemplateRef('usernameRef'),
|
|
763
|
+
formIdentifier: 'form-async',
|
|
764
|
+
})
|
|
765
|
+
</script>
|
|
766
|
+
|
|
748
767
|
<template>
|
|
749
|
-
<
|
|
768
|
+
<MazInput
|
|
769
|
+
ref="usernameRef"
|
|
770
|
+
v-model="username"
|
|
771
|
+
:hint="errorMessage"
|
|
772
|
+
:error="hasError"
|
|
773
|
+
:success="isValid"
|
|
774
|
+
:loading="isValidating"
|
|
775
|
+
/>
|
|
776
|
+
</template>
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
</template>
|
|
780
|
+
</ComponentDemo>
|
|
781
|
+
|
|
782
|
+
## Throttling and Debouncing
|
|
783
|
+
|
|
784
|
+
For expensive validations (like API calls), use throttling or debouncing to limit how often validation runs.
|
|
785
|
+
|
|
786
|
+
| Option | Behavior | Default Time | Use Case |
|
|
787
|
+
|--------|----------|--------------|----------|
|
|
788
|
+
| `debouncedFields` | Waits until user stops typing | 300ms | Search fields, API calls |
|
|
789
|
+
| `throttledFields` | Runs at most once per interval | 1000ms | Rate-limited APIs |
|
|
790
|
+
|
|
791
|
+
<ComponentDemo>
|
|
792
|
+
<div class="maz-mb-4">
|
|
793
|
+
<p class="maz-text-sm maz-text-muted">Name has 500ms debounce, Age has 1000ms throttle. Watch the console to see validation timing.</p>
|
|
794
|
+
</div>
|
|
795
|
+
<form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitDebounced">
|
|
750
796
|
<MazInput
|
|
751
|
-
v-model="
|
|
752
|
-
label="
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
:
|
|
756
|
-
:error="fieldsStates.name.error"
|
|
757
|
-
:success="fieldsStates.name.valid"
|
|
758
|
-
:loading="fieldsStates.name.validating"
|
|
759
|
-
:class="{ 'has-error-debounced': fieldsStates.name.error }"
|
|
797
|
+
v-model="debouncedModel.name"
|
|
798
|
+
label="Name (debounced 500ms)"
|
|
799
|
+
:hint="debouncedErrors.name"
|
|
800
|
+
:error="debouncedStates.name.error"
|
|
801
|
+
:success="debouncedStates.name.valid"
|
|
760
802
|
/>
|
|
761
|
-
<
|
|
762
|
-
|
|
763
|
-
|
|
803
|
+
<MazInput
|
|
804
|
+
v-model="debouncedModel.age"
|
|
805
|
+
label="Age (throttled 1000ms)"
|
|
806
|
+
type="number"
|
|
807
|
+
:hint="debouncedErrors.age"
|
|
808
|
+
:error="debouncedStates.age.error"
|
|
809
|
+
:success="debouncedStates.age.valid"
|
|
810
|
+
/>
|
|
811
|
+
<MazBtn type="submit" :loading="debouncedSubmitting">Submit</MazBtn>
|
|
764
812
|
</form>
|
|
765
|
-
</template>
|
|
766
813
|
|
|
767
|
-
<
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
await sleep(2000)
|
|
788
|
-
return false
|
|
789
|
-
},
|
|
790
|
-
'Name is already taken',
|
|
791
|
-
)),
|
|
792
|
-
},
|
|
793
|
-
options: { mode: 'eager', scrollToError: '.has-error-async', identifier: 'form-async' },
|
|
794
|
-
})
|
|
795
|
-
|
|
796
|
-
const onSubmit = handleSubmit((formData) => {
|
|
797
|
-
// Form submission logic
|
|
798
|
-
console.log(formData)
|
|
799
|
-
toast.success('Form submitted', { position: 'top' })
|
|
800
|
-
})
|
|
814
|
+
<template #code>
|
|
815
|
+
|
|
816
|
+
```vue
|
|
817
|
+
<script lang="ts" setup>
|
|
818
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
819
|
+
import { pipe, string, number, nonEmpty, minLength, minValue } from 'valibot'
|
|
820
|
+
|
|
821
|
+
const schema = {
|
|
822
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
823
|
+
age: pipe(number('Must be a number'), minValue(18, 'Must be 18+')),
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const { model, errorMessages, fieldsStates, isSubmitting, handleSubmit } = useFormValidator({
|
|
827
|
+
schema,
|
|
828
|
+
options: {
|
|
829
|
+
debouncedFields: { name: 500 }, // Wait 500ms after typing stops
|
|
830
|
+
throttledFields: { age: 1000 }, // Validate at most every 1000ms
|
|
831
|
+
// Use `true` for default times: debouncedFields: { name: true } // 300ms
|
|
832
|
+
},
|
|
833
|
+
})
|
|
801
834
|
</script>
|
|
802
835
|
```
|
|
803
836
|
|
|
804
|
-
</template>
|
|
805
|
-
|
|
837
|
+
</template>
|
|
806
838
|
</ComponentDemo>
|
|
807
839
|
|
|
808
|
-
##
|
|
809
|
-
|
|
810
|
-
`useFormValidator` is the main composable for initializing form validation.
|
|
840
|
+
## Reset Form
|
|
811
841
|
|
|
812
|
-
|
|
842
|
+
Use `resetForm()` to reset the form to its initial state, or set `resetOnSuccess` to automatically reset after successful submission.
|
|
813
843
|
|
|
814
|
-
|
|
844
|
+
<ComponentDemo>
|
|
845
|
+
<form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitReset">
|
|
846
|
+
<MazInput
|
|
847
|
+
v-model="resetModel.name"
|
|
848
|
+
label="Name"
|
|
849
|
+
:hint="resetErrors.name"
|
|
850
|
+
:error="resetStates.name.error"
|
|
851
|
+
:success="resetStates.name.valid"
|
|
852
|
+
/>
|
|
853
|
+
<MazInput
|
|
854
|
+
v-model="resetModel.age"
|
|
855
|
+
label="Age"
|
|
856
|
+
type="number"
|
|
857
|
+
:hint="resetErrors.age"
|
|
858
|
+
:error="resetStates.age.error"
|
|
859
|
+
:success="resetStates.age.valid"
|
|
860
|
+
/>
|
|
861
|
+
<div class="maz-flex maz-gap-2">
|
|
862
|
+
<MazBtn type="submit" :loading="resetSubmitting">Submit</MazBtn>
|
|
863
|
+
<MazBtn type="button" color="danger" @click="resetFormFn">Reset</MazBtn>
|
|
864
|
+
</div>
|
|
865
|
+
</form>
|
|
815
866
|
|
|
816
|
-
|
|
867
|
+
<template #code>
|
|
817
868
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
- `mode`: `'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'` (optional) - Form validation mode. (default: 'lazy') - To use the `eager`, `blur`, or `progressive` validation modes, you must use the `useFormField` composable to add the necessary validation events. - [see validation modes](#introduction)
|
|
823
|
-
- `throttledFields`: `Partial<Record<ModelKey, number | true>>` (optional) - Fields to validate with throttling. It's an object where the key is the field name and the value is the throttle time in milliseconds or `true` for the default throttle time (1000ms).
|
|
824
|
-
- `debouncedFields`: `Partial<Record<ModelKey, number | true>>` (optional) - Fields to validate with debouncing. It's an object where the key is the field name and the value is the debounce time in milliseconds or `true` for the default debounce time (300ms).
|
|
825
|
-
- `scrollToError`: `string | false` (optional) - Disable or provide a CSS selector for scrolling to errors (default '.has-field-error')
|
|
826
|
-
- `identifier`: `string | symbol` (optional) - Identifier for the form (useful when you have multiple forms on the same component)
|
|
869
|
+
```vue
|
|
870
|
+
<script lang="ts" setup>
|
|
871
|
+
import { useFormValidator } from 'maz-ui/composables'
|
|
872
|
+
import { pipe, string, number, nonEmpty, minLength, minValue } from 'valibot'
|
|
827
873
|
|
|
828
|
-
|
|
874
|
+
const schema = {
|
|
875
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
876
|
+
age: pipe(number(), minValue(18, 'Must be 18+')),
|
|
877
|
+
}
|
|
829
878
|
|
|
830
|
-
|
|
879
|
+
const { model, errorMessages, fieldsStates, isSubmitting, handleSubmit, resetForm } = useFormValidator({
|
|
880
|
+
schema,
|
|
881
|
+
defaultValues: { name: 'John', age: 25 },
|
|
882
|
+
options: {
|
|
883
|
+
resetOnSuccess: true, // Auto-reset after successful submission (default: true)
|
|
884
|
+
},
|
|
885
|
+
})
|
|
831
886
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
- `errors`: `ComputedRef<Record<ModelKey, ValidationIssues>>` - Validation errors for each field.
|
|
837
|
-
- `errorsMessages`: `ComputedRef<Record<string, string>>` - The first validation error message for each field.
|
|
838
|
-
- `model`: `Ref<Model>` - The form's data model.
|
|
839
|
-
- `fieldsStates`: `FieldsStates` - The validation state of each field.
|
|
840
|
-
- `validateForm`: `(setErrors?: boolean) => Promise<boolean>` - Function to validate the entire form.
|
|
841
|
-
- `scrollToError`: `(selector?: string, options?: { offset?: number }) => void` - Function to scroll to the first field with an error.
|
|
842
|
-
- `handleSubmit`: `(successCallback: (model: Model) => Promise<unknown> | unknown, scrollToError?: false | string, options?: { resetOnSuccess?: boolean }) => Promise<void>` - Form submission handler, the callback is called if the form is valid and passes the complete payload as an argument. The second argument is optional and can be used to disable or provide a CSS selector for scrolling to errors (default '.has-field-error'). The third argument is an optional object with options `resetOnSuccess` that is a boolean and is optional.
|
|
843
|
-
- `resetForm`: `() => void` - Function to reset the form to its initial state.
|
|
887
|
+
const onSubmit = handleSubmit((data) => {
|
|
888
|
+
console.log('Submitted:', data)
|
|
889
|
+
// Form will auto-reset because resetOnSuccess: true
|
|
890
|
+
})
|
|
844
891
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
isDirty: import('vue').ComputedRef<boolean>;
|
|
849
|
-
isSubmitting: Ref<boolean, boolean>;
|
|
850
|
-
isSubmitted: Ref<boolean, boolean>;
|
|
851
|
-
isValid: import('vue').ComputedRef<boolean>;
|
|
852
|
-
errors: import('vue').ComputedRef<Record<ExtractModelKey<FormSchema<InferSchemaFormValidator<TSchema>>>, import('./useFormValidator/types').ValidationIssues>>;
|
|
853
|
-
model: Ref<InferSchemaFormValidator<TSchema>, InferSchemaFormValidator<TSchema>>;
|
|
854
|
-
fieldsStates: Ref<FieldsStates<InferSchemaFormValidator<TSchema>, ExtractModelKey<FormSchema<InferSchemaFormValidator<TSchema>>>>, FieldsStates<InferSchemaFormValidator<TSchema>, ExtractModelKey<FormSchema<InferSchemaFormValidator<TSchema>>>>>;
|
|
855
|
-
validateForm: (setErrors?: boolean) => Promise<void[]>;
|
|
856
|
-
scrollToError: typeof scrollToError;
|
|
857
|
-
handleSubmit: <Func extends (model: InferOutputSchemaFormValidator<TSchema>) => Promise<Awaited<ReturnType<Func>>> | ReturnType<Func>>(successCallback: Func, enableScrollOrSelector?: FormValidatorOptions["scrollToError"], options?: { resetOnSuccess?: boolean }) => (event?: Event) => Promise<ReturnType<Func> | undefined>;
|
|
858
|
-
errorMessages: import('vue').ComputedRef<Record<ExtractModelKey<FormSchema<InferSchemaFormValidator<TSchema>>>, string | undefined>>;
|
|
859
|
-
resetForm: () => void;
|
|
892
|
+
// Manual reset
|
|
893
|
+
function handleReset() {
|
|
894
|
+
resetForm()
|
|
860
895
|
}
|
|
896
|
+
</script>
|
|
861
897
|
```
|
|
862
898
|
|
|
863
|
-
|
|
899
|
+
</template>
|
|
900
|
+
</ComponentDemo>
|
|
864
901
|
|
|
865
|
-
|
|
866
|
-
Before using `useFormField`, make sure you have initialized the form with `useFormValidator`.
|
|
867
|
-
:::
|
|
902
|
+
## Multiple Forms on Same Page
|
|
868
903
|
|
|
869
|
-
`
|
|
904
|
+
Use the `identifier` option to have multiple independent forms on the same page. Make sure to match the identifier in both `useFormValidator` and `useFormField`.
|
|
870
905
|
|
|
871
|
-
|
|
872
|
-
|
|
906
|
+
```vue
|
|
907
|
+
<script lang="ts" setup>
|
|
908
|
+
import { useFormValidator, useFormField } from 'maz-ui/composables'
|
|
873
909
|
|
|
874
|
-
|
|
910
|
+
// Form 1
|
|
911
|
+
const form1 = useFormValidator({
|
|
912
|
+
schema: schema1,
|
|
913
|
+
options: { identifier: 'login-form' },
|
|
914
|
+
})
|
|
875
915
|
|
|
876
|
-
|
|
916
|
+
// Form 2
|
|
917
|
+
const form2 = useFormValidator({
|
|
918
|
+
schema: schema2,
|
|
919
|
+
options: { identifier: 'register-form' },
|
|
920
|
+
})
|
|
877
921
|
|
|
878
|
-
|
|
922
|
+
// useFormField must use matching identifier
|
|
923
|
+
const { value: loginEmail } = useFormField<string>('email', {
|
|
924
|
+
formIdentifier: 'login-form',
|
|
925
|
+
})
|
|
879
926
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
- `formIdentifier`: `string | symbol` (optional) - Identifier for the form (must match the one used in `useFormValidator`)
|
|
927
|
+
const { value: registerEmail } = useFormField<string>('email', {
|
|
928
|
+
formIdentifier: 'register-form',
|
|
929
|
+
})
|
|
930
|
+
</script>
|
|
931
|
+
```
|
|
886
932
|
|
|
887
|
-
|
|
933
|
+
## Error Handling and scrollToError
|
|
888
934
|
|
|
889
|
-
|
|
935
|
+
### scrollToError
|
|
890
936
|
|
|
891
|
-
|
|
892
|
-
- `errorMessage`: `ComputedRef<string>` - The first validation error message.
|
|
893
|
-
- `isValid`: `ComputedRef<boolean>` - Indicates if the field is valid.
|
|
894
|
-
- `isDirty`: `ComputedRef<boolean>` - Indicates if the field has been modified.
|
|
895
|
-
- `isBlurred`: `ComputedRef<boolean>` - Indicates if the field has lost focus.
|
|
896
|
-
- `hasError`: `ComputedRef<boolean>` - Indicates if the field has errors.
|
|
897
|
-
- `isValidated`: `ComputedRef<boolean>` - Indicates if the field has been validated.
|
|
898
|
-
- `isValidating`: `ComputedRef<boolean>` - Indicates if the field is currently being validated.
|
|
899
|
-
- `mode`: `ComputedRef<StrictOptions['mode']>` - The validation mode for the field.
|
|
900
|
-
- `value`: `WritableComputedRef<T>` - The reactive value of the field with proper TypeScript typing.
|
|
901
|
-
- `validationEvents`: `ComputedRef<{ onBlur?: () => void; }>` - Validation events to bind to the field. They are used to trigger field validation, to be used like this `v-bind="validationEvents"` (components must emit `blur` event to trigger field validation) - Not necessary for `lazy`, `aggressive` validation modes or if you use the component reference when initializing the composable.
|
|
937
|
+
Automatically scroll to the first field with an error when validation fails:
|
|
902
938
|
|
|
903
939
|
```ts
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
isValidated: import('vue').ComputedRef<boolean>;
|
|
912
|
-
isValidating: import('vue').ComputedRef<boolean>;
|
|
913
|
-
mode: import('vue').ComputedRef<"blur" | "eager" | "lazy" | "aggressive" | "progressive" | undefined>;
|
|
914
|
-
value: import('vue').WritableComputedRef<FieldType, FieldType>;
|
|
915
|
-
validationEvents: import('vue').ComputedRef<{
|
|
916
|
-
onBlur: () => void;
|
|
917
|
-
} | undefined>;
|
|
918
|
-
}
|
|
940
|
+
const { handleSubmit } = useFormValidator({
|
|
941
|
+
schema,
|
|
942
|
+
options: {
|
|
943
|
+
scrollToError: '.has-error', // CSS selector for error elements
|
|
944
|
+
// scrollToError: false, // Disable scrolling
|
|
945
|
+
},
|
|
946
|
+
})
|
|
919
947
|
```
|
|
920
948
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
### 🚀 Enhanced Type Safety
|
|
924
|
-
|
|
925
|
-
- **Automatic schema inference**: Use `typeof schema` for precise TypeScript types
|
|
926
|
-
- **Field-level type safety**: `useFormField<T>` provides exact field types
|
|
927
|
-
- **Improved reactivity**: Optimized watchers with better performance and memory management
|
|
949
|
+
Add the matching class to your fields:
|
|
928
950
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
- Custom interactive elements: `data-interactive`, `data-clickable`, `.interactive`
|
|
936
|
-
|
|
937
|
-
### 🔧 Improved Memory Management
|
|
951
|
+
```html
|
|
952
|
+
<MazInput
|
|
953
|
+
:class="{ 'has-error': hasError }"
|
|
954
|
+
:error="hasError"
|
|
955
|
+
/>
|
|
956
|
+
```
|
|
938
957
|
|
|
939
|
-
|
|
940
|
-
- WeakMap-based tracking for better garbage collection
|
|
941
|
-
- Race condition protection in async validation
|
|
958
|
+
### onError Callback
|
|
942
959
|
|
|
943
|
-
|
|
960
|
+
Handle validation failures with the `onError` callback:
|
|
944
961
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
962
|
+
```ts
|
|
963
|
+
const onSubmit = handleSubmit(
|
|
964
|
+
(data) => {
|
|
965
|
+
// Success callback
|
|
966
|
+
console.log('Valid:', data)
|
|
967
|
+
},
|
|
968
|
+
'.has-error', // scrollToError selector (optional)
|
|
969
|
+
{
|
|
970
|
+
onError: ({ model, errorMessages, errors }) => {
|
|
971
|
+
// Called when validation fails
|
|
972
|
+
console.log('Validation failed:', errorMessages)
|
|
973
|
+
},
|
|
974
|
+
}
|
|
975
|
+
)
|
|
976
|
+
```
|
|
948
977
|
|
|
949
978
|
## Performance & Best Practices
|
|
950
979
|
|
|
951
|
-
###
|
|
980
|
+
### Performance Tips
|
|
952
981
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
982
|
+
1. **Use throttling/debouncing** for expensive validations (API calls, complex logic)
|
|
983
|
+
2. **Prefer `eager` or `progressive` modes** over `aggressive` for better performance
|
|
984
|
+
3. **Use `lazy` mode** for simple forms with minimal validation
|
|
985
|
+
4. **Avoid `aggressive` mode on large forms** - it validates every field on every change
|
|
957
986
|
|
|
958
|
-
###
|
|
987
|
+
### Common Patterns
|
|
959
988
|
|
|
960
|
-
#### Multiple Forms
|
|
989
|
+
#### Multiple Forms with Identifiers
|
|
961
990
|
|
|
962
991
|
```ts
|
|
963
|
-
const
|
|
964
|
-
schema
|
|
965
|
-
options: { identifier: 'form
|
|
966
|
-
})
|
|
967
|
-
|
|
968
|
-
const form2 = useFormValidator<typeof schema2>({
|
|
969
|
-
schema: schema2,
|
|
970
|
-
options: { identifier: 'form-2' }
|
|
992
|
+
const { handleSubmit } = useFormValidator({
|
|
993
|
+
schema,
|
|
994
|
+
options: { identifier: 'my-unique-form' },
|
|
971
995
|
})
|
|
972
996
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
formIdentifier: 'form-1'
|
|
997
|
+
const { value } = useFormField<string>('email', {
|
|
998
|
+
formIdentifier: 'my-unique-form', // Must match!
|
|
976
999
|
})
|
|
977
1000
|
```
|
|
978
1001
|
|
|
979
1002
|
#### Custom Interactive Elements
|
|
980
1003
|
|
|
1004
|
+
If your custom component isn't detected for blur events, add `data-interactive`:
|
|
1005
|
+
|
|
981
1006
|
```vue
|
|
982
|
-
<
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
Custom Input
|
|
986
|
-
</div>
|
|
987
|
-
</template>
|
|
1007
|
+
<div data-interactive class="custom-input" tabindex="0">
|
|
1008
|
+
Custom Input
|
|
1009
|
+
</div>
|
|
988
1010
|
```
|
|
989
1011
|
|
|
990
|
-
###
|
|
1012
|
+
### Common Pitfalls
|
|
991
1013
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1014
|
+
| Pitfall | Solution |
|
|
1015
|
+
|---------|----------|
|
|
1016
|
+
| Mismatched `formIdentifier` | Ensure `useFormField`'s `formIdentifier` matches `useFormValidator`'s `identifier` |
|
|
1017
|
+
| Validation not triggering in eager/blur/progressive modes | Use `ref` option or `v-bind="validationEvents"` |
|
|
1018
|
+
| TypeScript errors with `useTemplateRef` | Add generic type or use classic `ref()` |
|
|
1019
|
+
| Field not found warning | Make sure the field name exists in your schema |
|
|
995
1020
|
|
|
996
|
-
##
|
|
1021
|
+
## API Reference
|
|
997
1022
|
|
|
998
|
-
###
|
|
1023
|
+
### useFormValidator
|
|
999
1024
|
|
|
1000
|
-
|
|
1025
|
+
#### Parameters
|
|
1001
1026
|
|
|
1002
1027
|
```ts
|
|
1003
|
-
|
|
1004
|
-
|
|
1028
|
+
useFormValidator<TSchema>({
|
|
1029
|
+
schema: TSchema, // Valibot validation schema (required)
|
|
1030
|
+
model?: Ref<Model>, // External model ref (optional)
|
|
1031
|
+
defaultValues?: DeepPartial<Model>, // Initial values (optional)
|
|
1032
|
+
options?: {
|
|
1033
|
+
mode?: 'lazy' | 'aggressive' | 'eager' | 'blur' | 'progressive', // Default: 'lazy'
|
|
1034
|
+
throttledFields?: Record<string, number | true>, // Fields to throttle
|
|
1035
|
+
debouncedFields?: Record<string, number | true>, // Fields to debounce
|
|
1036
|
+
scrollToError?: string | false, // CSS selector, default: '.has-field-error'
|
|
1037
|
+
identifier?: string | symbol, // Form identifier, default: 'main-form-validator'
|
|
1038
|
+
resetOnSuccess?: boolean, // Reset after submit, default: true
|
|
1039
|
+
}
|
|
1040
|
+
})
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
#### Return Values
|
|
1005
1044
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1045
|
+
```ts
|
|
1046
|
+
{
|
|
1047
|
+
identifier: string | symbol
|
|
1048
|
+
model: Ref<Model>
|
|
1049
|
+
isValid: ComputedRef<boolean>
|
|
1050
|
+
isDirty: ComputedRef<boolean>
|
|
1051
|
+
isSubmitting: Ref<boolean>
|
|
1052
|
+
isSubmitted: Ref<boolean>
|
|
1053
|
+
errors: ComputedRef<Record<string, ValidationIssues>>
|
|
1054
|
+
errorMessages: ComputedRef<Record<string, string | undefined>>
|
|
1055
|
+
fieldsStates: Ref<FieldsStates<Model>>
|
|
1056
|
+
validateForm: (setErrors?: boolean) => Promise<void[]>
|
|
1057
|
+
scrollToError: (selector?: string) => void
|
|
1058
|
+
resetForm: () => void
|
|
1059
|
+
handleSubmit: <Func>(
|
|
1060
|
+
successCallback: Func,
|
|
1061
|
+
scrollToError?: string | false,
|
|
1062
|
+
options?: { onError?: Function, resetOnSuccess?: boolean }
|
|
1063
|
+
) => (event?: Event) => Promise<ReturnType<Func>>
|
|
1064
|
+
}
|
|
1008
1065
|
```
|
|
1009
1066
|
|
|
1010
|
-
###
|
|
1067
|
+
### useFormField
|
|
1011
1068
|
|
|
1012
|
-
|
|
1069
|
+
#### Parameters
|
|
1013
1070
|
|
|
1014
|
-
|
|
1071
|
+
```ts
|
|
1072
|
+
useFormField<FieldType>(
|
|
1073
|
+
name: string, // Field name in schema (required)
|
|
1074
|
+
options?: {
|
|
1075
|
+
defaultValue?: FieldType, // Default value for this field
|
|
1076
|
+
mode?: 'lazy' | 'aggressive' | 'eager' | 'blur' | 'progressive', // Override form mode
|
|
1077
|
+
ref?: Ref<HTMLElement | ComponentInstance>, // Template ref for blur detection
|
|
1078
|
+
formIdentifier?: string | symbol, // Must match useFormValidator's identifier
|
|
1079
|
+
}
|
|
1080
|
+
)
|
|
1081
|
+
```
|
|
1015
1082
|
|
|
1016
|
-
|
|
1017
|
-
// ❌ May cause TypeScript errors
|
|
1018
|
-
const { value: email } = useFormField<string>('email', {
|
|
1019
|
-
ref: useTemplateRef('emailRef'),
|
|
1020
|
-
})
|
|
1083
|
+
#### Return Values
|
|
1021
1084
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1085
|
+
```ts
|
|
1086
|
+
{
|
|
1087
|
+
value: WritableComputedRef<FieldType>
|
|
1088
|
+
hasError: ComputedRef<boolean>
|
|
1089
|
+
errors: ComputedRef<ValidationIssues>
|
|
1090
|
+
errorMessage: ComputedRef<string | undefined>
|
|
1091
|
+
isValid: ComputedRef<boolean>
|
|
1092
|
+
isDirty: ComputedRef<boolean>
|
|
1093
|
+
isBlurred: ComputedRef<boolean>
|
|
1094
|
+
isValidated: ComputedRef<boolean>
|
|
1095
|
+
isValidating: ComputedRef<boolean>
|
|
1096
|
+
mode: ComputedRef<string | undefined>
|
|
1097
|
+
validationEvents: ComputedRef<{ onBlur?: () => void }>
|
|
1098
|
+
}
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
### Types
|
|
1102
|
+
|
|
1103
|
+
```ts
|
|
1104
|
+
interface FormValidatorOptions<Model> {
|
|
1105
|
+
mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
|
|
1106
|
+
throttledFields?: Partial<Record<keyof Model, number | true>>
|
|
1107
|
+
debouncedFields?: Partial<Record<keyof Model, number | true>>
|
|
1108
|
+
scrollToError?: string | false
|
|
1109
|
+
identifier?: string | symbol
|
|
1110
|
+
resetOnSuccess?: boolean
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
interface FormFieldOptions<FieldType> {
|
|
1114
|
+
defaultValue?: FieldType
|
|
1115
|
+
mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
|
|
1116
|
+
ref?: Ref<HTMLElement | ComponentInstance>
|
|
1117
|
+
formIdentifier?: string | symbol
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
interface FieldState<FieldType> {
|
|
1121
|
+
valid: boolean
|
|
1122
|
+
error: boolean
|
|
1123
|
+
errors: ValidationIssues
|
|
1124
|
+
dirty: boolean
|
|
1125
|
+
blurred: boolean
|
|
1126
|
+
validated: boolean
|
|
1127
|
+
validating: boolean
|
|
1128
|
+
initialValue?: Readonly<FieldType>
|
|
1129
|
+
mode?: string
|
|
1130
|
+
}
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
## Troubleshooting
|
|
1134
|
+
|
|
1135
|
+
### Type Errors with useTemplateRef
|
|
1136
|
+
|
|
1137
|
+
**Problem**: TypeScript circular reference errors when using `useTemplateRef`
|
|
1138
|
+
|
|
1139
|
+
**Solutions**:
|
|
1140
|
+
|
|
1141
|
+
```ts
|
|
1142
|
+
// Solution 1: Add generic type
|
|
1143
|
+
const { value } = useFormField<string>('email', {
|
|
1144
|
+
ref: useTemplateRef<HTMLInputElement>('emailRef'),
|
|
1025
1145
|
})
|
|
1026
1146
|
|
|
1027
|
-
//
|
|
1147
|
+
// Solution 2: Use classic ref
|
|
1028
1148
|
const emailRef = ref<HTMLInputElement>()
|
|
1029
|
-
const { value
|
|
1030
|
-
ref: emailRef,
|
|
1031
|
-
})
|
|
1149
|
+
const { value } = useFormField<string>('email', { ref: emailRef })
|
|
1032
1150
|
```
|
|
1033
1151
|
|
|
1034
1152
|
### Validation Not Triggering
|
|
1035
1153
|
|
|
1036
|
-
**Problem**:
|
|
1154
|
+
**Problem**: `eager`, `blur`, or `progressive` mode not validating
|
|
1037
1155
|
|
|
1038
|
-
|
|
1039
|
-
// ❌ Missing ref or validation events
|
|
1040
|
-
const { value } = useFormField<string>('name')
|
|
1156
|
+
**Solution**: These modes require blur event detection. Use either:
|
|
1041
1157
|
|
|
1042
|
-
|
|
1158
|
+
```ts
|
|
1159
|
+
// Option 1: ref option
|
|
1043
1160
|
const { value } = useFormField<string>('name', {
|
|
1044
|
-
ref: useTemplateRef('inputRef')
|
|
1161
|
+
ref: useTemplateRef('inputRef'),
|
|
1045
1162
|
})
|
|
1046
1163
|
|
|
1047
|
-
//
|
|
1164
|
+
// Option 2: validationEvents
|
|
1048
1165
|
const { value, validationEvents } = useFormField<string>('name')
|
|
1049
|
-
// Then: v-bind="validationEvents" on your
|
|
1166
|
+
// Then: v-bind="validationEvents" on your input
|
|
1050
1167
|
```
|
|
1051
1168
|
|
|
1052
1169
|
### Element Not Found Warning
|
|
@@ -1054,233 +1171,346 @@ const { value, validationEvents } = useFormField<string>('name')
|
|
|
1054
1171
|
**Problem**: `No element found for ref in field 'name'`
|
|
1055
1172
|
|
|
1056
1173
|
**Solutions**:
|
|
1057
|
-
1. Ensure the ref is
|
|
1058
|
-
2. Make sure the component has a `$el` property
|
|
1059
|
-
3.
|
|
1174
|
+
1. Ensure the ref is bound to an HTML element or Vue component
|
|
1175
|
+
2. Make sure the component has a `$el` property
|
|
1176
|
+
3. For custom components, add `data-interactive` attribute
|
|
1177
|
+
|
|
1178
|
+
### Mismatched Form Identifiers
|
|
1060
1179
|
|
|
1061
|
-
|
|
1180
|
+
**Problem**: `useFormField` not finding the form context
|
|
1062
1181
|
|
|
1063
|
-
|
|
1182
|
+
**Solution**: Ensure identifiers match:
|
|
1064
1183
|
|
|
1065
1184
|
```ts
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
|
|
1077
|
-
/**
|
|
1078
|
-
* Fields to validate with throttling
|
|
1079
|
-
* Useful for fields that require a network request to avoid spamming the server
|
|
1080
|
-
* @example { name: 1000 } or { name: true } for the default throttle time (1000ms)
|
|
1081
|
-
*/
|
|
1082
|
-
throttledFields?: Partial<Record<ModelKey, number | true>>
|
|
1083
|
-
/**
|
|
1084
|
-
* Fields to validate with debouncing
|
|
1085
|
-
* Useful to wait for the user to finish typing before validating
|
|
1086
|
-
* Useful for fields that require a network request to avoid spamming the server
|
|
1087
|
-
* @example { name: 300 } or { name: true } for the default debounce time (300ms)
|
|
1088
|
-
*/
|
|
1089
|
-
debouncedFields?: Partial<Record<ModelKey, number | true>>
|
|
1090
|
-
/**
|
|
1091
|
-
* Scroll to the first error found
|
|
1092
|
-
* @default '.has-field-error'
|
|
1093
|
-
*/
|
|
1094
|
-
scrollToError?: string | false
|
|
1095
|
-
/**
|
|
1096
|
-
* Identifier to use for the form
|
|
1097
|
-
* Useful to have multiple forms on the same page
|
|
1098
|
-
* @default `main-form-validator`
|
|
1099
|
-
*/
|
|
1100
|
-
identifier?: string | symbol
|
|
1101
|
-
}
|
|
1185
|
+
// In parent
|
|
1186
|
+
const { handleSubmit } = useFormValidator({
|
|
1187
|
+
schema,
|
|
1188
|
+
options: { identifier: 'my-form' }, // This identifier...
|
|
1189
|
+
})
|
|
1190
|
+
|
|
1191
|
+
// In child
|
|
1192
|
+
const { value } = useFormField<string>('email', {
|
|
1193
|
+
formIdentifier: 'my-form', // ...must match this one
|
|
1194
|
+
})
|
|
1102
1195
|
```
|
|
1103
1196
|
|
|
1104
|
-
|
|
1197
|
+
<script lang="ts" setup>
|
|
1198
|
+
import { ref, useTemplateRef } from 'vue'
|
|
1199
|
+
import { useFormValidator } from 'maz-ui/src/composables/useFormValidator'
|
|
1200
|
+
import { useFormField } from 'maz-ui/src/composables/useFormField'
|
|
1201
|
+
import { useToast } from 'maz-ui/src/composables/useToast'
|
|
1202
|
+
import { sleep } from '@maz-ui/utils'
|
|
1203
|
+
import {
|
|
1204
|
+
string,
|
|
1205
|
+
nonEmpty,
|
|
1206
|
+
pipe,
|
|
1207
|
+
number,
|
|
1208
|
+
minValue,
|
|
1209
|
+
maxValue,
|
|
1210
|
+
boolean,
|
|
1211
|
+
literal,
|
|
1212
|
+
minLength,
|
|
1213
|
+
email,
|
|
1214
|
+
pipeAsync,
|
|
1215
|
+
checkAsync,
|
|
1216
|
+
} from 'valibot'
|
|
1105
1217
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
defaultValue?: T
|
|
1113
|
-
/**
|
|
1114
|
-
* Validation mode
|
|
1115
|
-
* To override the form validation mode
|
|
1116
|
-
*/
|
|
1117
|
-
mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
|
|
1118
|
-
/**
|
|
1119
|
-
* Vue ref to the component or HTML element for automatic event binding
|
|
1120
|
-
* Use useTemplateRef() for type safety
|
|
1121
|
-
* Automatically detects interactive elements (input, select, textarea, button, ARIA elements, etc.)
|
|
1122
|
-
* Necessary for 'eager', 'progressive' and 'blur' validation modes
|
|
1123
|
-
*/
|
|
1124
|
-
ref?: Ref<HTMLElement | ComponentInstance>
|
|
1125
|
-
/**
|
|
1126
|
-
* Identifier for the form
|
|
1127
|
-
* Useful when you have multiple forms on the same component
|
|
1128
|
-
* Should be the same as the one used in `useFormValidator`
|
|
1129
|
-
*/
|
|
1130
|
-
formIdentifier?: string | symbol
|
|
1218
|
+
const toast = useToast()
|
|
1219
|
+
|
|
1220
|
+
// Quick Start Demo
|
|
1221
|
+
const quickStartSchema = {
|
|
1222
|
+
email: pipe(string(), nonEmpty('Email is required'), email('Invalid email')),
|
|
1223
|
+
password: pipe(string(), nonEmpty('Password is required'), minLength(8, 'Min 8 characters')),
|
|
1131
1224
|
}
|
|
1132
|
-
```
|
|
1133
1225
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
|
|
1147
|
-
country: pipe(string('Country is required'), nonEmpty('Country is required')),
|
|
1148
|
-
agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
|
|
1149
|
-
})
|
|
1150
|
-
|
|
1151
|
-
const { model, isValid, isSubmitting, isDirty, isSubmitted, handleSubmit, errorMessages, fieldsStates, resetForm } = useFormValidator<typeof schema>({
|
|
1152
|
-
schema,
|
|
1153
|
-
defaultValues: { name: 'John Doe', age: 10 },
|
|
1154
|
-
options: { mode: 'lazy', scrollToError: '.has-error' },
|
|
1155
|
-
})
|
|
1156
|
-
|
|
1157
|
-
const onSubmit = handleSubmit(async (formData) => {
|
|
1158
|
-
// Form submission logic
|
|
1159
|
-
console.log(formData)
|
|
1160
|
-
await sleep(2000)
|
|
1161
|
-
toast.success('Form submitted', { position: 'top' })
|
|
1162
|
-
})
|
|
1163
|
-
|
|
1164
|
-
const eagerSchema = {
|
|
1165
|
-
name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
|
|
1166
|
-
age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
|
|
1167
|
-
country: pipe(string('Country is required'), nonEmpty('Country is required')),
|
|
1168
|
-
agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
|
|
1169
|
-
}
|
|
1226
|
+
const {
|
|
1227
|
+
model: quickStartModel,
|
|
1228
|
+
errorMessages: quickStartErrors,
|
|
1229
|
+
fieldsStates: quickStartStates,
|
|
1230
|
+
isSubmitting: quickStartSubmitting,
|
|
1231
|
+
handleSubmit: handleQuickStart,
|
|
1232
|
+
} = useFormValidator({ schema: quickStartSchema })
|
|
1233
|
+
|
|
1234
|
+
const onSubmitQuickStart = handleQuickStart(async () => {
|
|
1235
|
+
await sleep(1000)
|
|
1236
|
+
toast.success('Login successful!', { position: 'top' })
|
|
1237
|
+
})
|
|
1170
1238
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
const { value: name, hasError: hasErrorName, errorMessage: nameErrorMessage } = useFormField<string>('name', { ref: useTemplateRef('nameRef'), formIdentifier: 'form-eager' })
|
|
1177
|
-
const { value: age, hasError: hasErrorAge, errorMessage: ageErrorMessage } = useFormField<number>('age', { ref: useTemplateRef('ageRef'), formIdentifier: 'form-eager' })
|
|
1178
|
-
const { value: country, hasError: hasErrorCountry, errorMessage: countryErrorMessage, validationEvents } = useFormField<string>('country', { mode: 'lazy', formIdentifier: 'form-eager' })
|
|
1179
|
-
const { value: agree, hasError: hasErrorAgree, errorMessage: agreeErrorMessage } = useFormField<boolean>('agree', { ref: useTemplateRef('agreeRef'), formIdentifier: 'form-eager' })
|
|
1180
|
-
|
|
1181
|
-
const onSubmitEager = handleSubmitEager(async (formData) => {
|
|
1182
|
-
// Form submission logic
|
|
1183
|
-
console.log(formData)
|
|
1184
|
-
await sleep(2000)
|
|
1185
|
-
toast.success('Form submitted', { position: 'top' })
|
|
1186
|
-
})
|
|
1187
|
-
|
|
1188
|
-
const { isValid: isValidProgressive, isSubmitting: isSubmittingProgressive, handleSubmit: handleSubmitProgressive } = useFormValidator<Model>({
|
|
1189
|
-
schema: {
|
|
1190
|
-
name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
|
|
1191
|
-
age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
|
|
1192
|
-
country: pipe(string('Country is required'), nonEmpty('Country is required')),
|
|
1193
|
-
agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
|
|
1194
|
-
},
|
|
1195
|
-
options: { mode: 'progressive', scrollToError: '.has-error-progressive', identifier: 'form-progressive' },
|
|
1196
|
-
})
|
|
1197
|
-
|
|
1198
|
-
const progressiveSchema = {
|
|
1199
|
-
name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
|
|
1200
|
-
age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
|
|
1201
|
-
country: pipe(string('Country is required'), nonEmpty('Country is required')),
|
|
1202
|
-
agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
|
|
1203
|
-
}
|
|
1239
|
+
// Form State Demo
|
|
1240
|
+
const stateSchema = {
|
|
1241
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
1242
|
+
age: pipe(number('Must be a number'), minValue(18, 'Min 18'), maxValue(100, 'Max 100')),
|
|
1243
|
+
}
|
|
1204
1244
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
|
|
1220
|
-
age: pipe(number('Age is required'), nonEmpty('Age is required'), minValue(18, 'Age must be greater than 18')),
|
|
1221
|
-
},
|
|
1222
|
-
options: {
|
|
1223
|
-
resetOnSuccess: true,
|
|
1224
|
-
scrollToError: '.has-error-reset',
|
|
1225
|
-
},
|
|
1226
|
-
})
|
|
1245
|
+
const {
|
|
1246
|
+
model: stateModel,
|
|
1247
|
+
errorMessages: stateErrors,
|
|
1248
|
+
fieldsStates: stateFields,
|
|
1249
|
+
isValid: stateValid,
|
|
1250
|
+
isDirty: stateDirty,
|
|
1251
|
+
isSubmitted: stateSubmitted,
|
|
1252
|
+
isSubmitting: stateSubmitting,
|
|
1253
|
+
handleSubmit: handleState,
|
|
1254
|
+
} = useFormValidator({ schema: stateSchema })
|
|
1255
|
+
|
|
1256
|
+
const onSubmitState = handleState(() => {
|
|
1257
|
+
toast.success('Submitted!', { position: 'top' })
|
|
1258
|
+
})
|
|
1227
1259
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
})
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1260
|
+
// Lazy Mode Demo
|
|
1261
|
+
const lazySchema = {
|
|
1262
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
1263
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const {
|
|
1267
|
+
model: lazyModel,
|
|
1268
|
+
errorMessages: lazyErrors,
|
|
1269
|
+
fieldsStates: lazyStates,
|
|
1270
|
+
isSubmitting: lazySubmitting,
|
|
1271
|
+
handleSubmit: handleLazy,
|
|
1272
|
+
} = useFormValidator({
|
|
1273
|
+
schema: lazySchema,
|
|
1274
|
+
options: { mode: 'lazy', scrollToError: '.has-error-lazy' },
|
|
1275
|
+
})
|
|
1276
|
+
|
|
1277
|
+
const onSubmitLazy = handleLazy(async () => {
|
|
1278
|
+
await sleep(1000)
|
|
1279
|
+
toast.success('Submitted!', { position: 'top' })
|
|
1280
|
+
})
|
|
1281
|
+
|
|
1282
|
+
// Aggressive Mode Demo
|
|
1283
|
+
const aggressiveSchema = {
|
|
1284
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
1285
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const {
|
|
1289
|
+
model: aggressiveModel,
|
|
1290
|
+
errorMessages: aggressiveErrors,
|
|
1291
|
+
fieldsStates: aggressiveStates,
|
|
1292
|
+
isSubmitting: aggressiveSubmitting,
|
|
1293
|
+
handleSubmit: handleAggressive,
|
|
1294
|
+
} = useFormValidator({
|
|
1295
|
+
schema: aggressiveSchema,
|
|
1296
|
+
options: { mode: 'aggressive' },
|
|
1297
|
+
})
|
|
1298
|
+
|
|
1299
|
+
const onSubmitAggressive = handleAggressive(async () => {
|
|
1300
|
+
await sleep(1000)
|
|
1301
|
+
toast.success('Submitted!', { position: 'top' })
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
// Eager Mode Demo
|
|
1305
|
+
const eagerSchema = {
|
|
1306
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
1307
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const {
|
|
1311
|
+
isSubmitting: eagerSubmitting,
|
|
1312
|
+
handleSubmit: handleEager,
|
|
1313
|
+
} = useFormValidator({
|
|
1314
|
+
schema: eagerSchema,
|
|
1315
|
+
options: { mode: 'eager', scrollToError: '.has-error-eager', identifier: 'form-eager' },
|
|
1316
|
+
})
|
|
1317
|
+
|
|
1318
|
+
const {
|
|
1319
|
+
value: eagerName,
|
|
1320
|
+
hasError: eagerNameHasError,
|
|
1321
|
+
errorMessage: eagerNameError,
|
|
1322
|
+
isValid: eagerNameValid,
|
|
1323
|
+
} = useFormField<string>('name', {
|
|
1324
|
+
ref: useTemplateRef<HTMLInputElement>('eagerNameRef'),
|
|
1325
|
+
formIdentifier: 'form-eager',
|
|
1326
|
+
})
|
|
1327
|
+
|
|
1328
|
+
const {
|
|
1329
|
+
value: eagerEmail,
|
|
1330
|
+
hasError: eagerEmailHasError,
|
|
1331
|
+
errorMessage: eagerEmailError,
|
|
1332
|
+
isValid: eagerEmailValid,
|
|
1333
|
+
} = useFormField<string>('email', {
|
|
1334
|
+
ref: useTemplateRef<HTMLInputElement>('eagerEmailRef'),
|
|
1335
|
+
formIdentifier: 'form-eager',
|
|
1336
|
+
})
|
|
1337
|
+
|
|
1338
|
+
const onSubmitEager = handleEager(async () => {
|
|
1339
|
+
await sleep(1000)
|
|
1340
|
+
toast.success('Submitted!', { position: 'top' })
|
|
1341
|
+
})
|
|
1342
|
+
|
|
1343
|
+
// Blur Mode Demo
|
|
1344
|
+
const blurSchema = {
|
|
1345
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
1346
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const {
|
|
1350
|
+
isSubmitting: blurSubmitting,
|
|
1351
|
+
handleSubmit: handleBlur,
|
|
1352
|
+
} = useFormValidator({
|
|
1353
|
+
schema: blurSchema,
|
|
1354
|
+
options: { mode: 'blur', scrollToError: '.has-error-blur', identifier: 'form-blur' },
|
|
1355
|
+
})
|
|
1356
|
+
|
|
1357
|
+
const {
|
|
1358
|
+
value: blurName,
|
|
1359
|
+
hasError: blurNameHasError,
|
|
1360
|
+
errorMessage: blurNameError,
|
|
1361
|
+
isValid: blurNameValid,
|
|
1362
|
+
} = useFormField<string>('name', {
|
|
1363
|
+
ref: useTemplateRef<HTMLInputElement>('blurNameRef'),
|
|
1364
|
+
formIdentifier: 'form-blur',
|
|
1365
|
+
})
|
|
1366
|
+
|
|
1367
|
+
const {
|
|
1368
|
+
value: blurEmail,
|
|
1369
|
+
hasError: blurEmailHasError,
|
|
1370
|
+
errorMessage: blurEmailError,
|
|
1371
|
+
isValid: blurEmailValid,
|
|
1372
|
+
} = useFormField<string>('email', {
|
|
1373
|
+
ref: useTemplateRef<HTMLInputElement>('blurEmailRef'),
|
|
1374
|
+
formIdentifier: 'form-blur',
|
|
1375
|
+
})
|
|
1376
|
+
|
|
1377
|
+
const onSubmitBlur = handleBlur(async () => {
|
|
1378
|
+
await sleep(1000)
|
|
1379
|
+
toast.success('Submitted!', { position: 'top' })
|
|
1380
|
+
})
|
|
1381
|
+
|
|
1382
|
+
// Progressive Mode Demo
|
|
1383
|
+
const progressiveSchema = {
|
|
1384
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
1385
|
+
email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
|
|
1386
|
+
agree: pipe(boolean(), literal(true, 'You must agree')),
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const {
|
|
1390
|
+
isSubmitting: progressiveSubmitting,
|
|
1391
|
+
handleSubmit: handleProgressive,
|
|
1392
|
+
} = useFormValidator({
|
|
1393
|
+
schema: progressiveSchema,
|
|
1394
|
+
options: { mode: 'progressive', scrollToError: '.has-error-progressive', identifier: 'form-progressive' },
|
|
1395
|
+
})
|
|
1396
|
+
|
|
1397
|
+
const {
|
|
1398
|
+
value: progressiveName,
|
|
1399
|
+
hasError: progressiveNameHasError,
|
|
1400
|
+
errorMessage: progressiveNameError,
|
|
1401
|
+
isValid: progressiveNameValid,
|
|
1402
|
+
} = useFormField<string>('name', {
|
|
1403
|
+
ref: useTemplateRef<HTMLInputElement>('progressiveNameRef'),
|
|
1404
|
+
formIdentifier: 'form-progressive',
|
|
1405
|
+
})
|
|
1406
|
+
|
|
1407
|
+
const {
|
|
1408
|
+
value: progressiveEmail,
|
|
1409
|
+
hasError: progressiveEmailHasError,
|
|
1410
|
+
errorMessage: progressiveEmailError,
|
|
1411
|
+
isValid: progressiveEmailValid,
|
|
1412
|
+
} = useFormField<string>('email', {
|
|
1413
|
+
ref: useTemplateRef<HTMLInputElement>('progressiveEmailRef'),
|
|
1414
|
+
formIdentifier: 'form-progressive',
|
|
1415
|
+
})
|
|
1416
|
+
|
|
1417
|
+
const {
|
|
1418
|
+
value: progressiveAgree,
|
|
1419
|
+
hasError: progressiveAgreeHasError,
|
|
1420
|
+
errorMessage: progressiveAgreeError,
|
|
1421
|
+
} = useFormField<boolean>('agree', {
|
|
1422
|
+
ref: useTemplateRef<HTMLInputElement>('progressiveAgreeRef'),
|
|
1423
|
+
formIdentifier: 'form-progressive',
|
|
1424
|
+
})
|
|
1425
|
+
|
|
1426
|
+
const onSubmitProgressive = handleProgressive(async () => {
|
|
1427
|
+
await sleep(1000)
|
|
1428
|
+
toast.success('Submitted!', { position: 'top' })
|
|
1429
|
+
})
|
|
1430
|
+
|
|
1431
|
+
// Async Validation Demo
|
|
1432
|
+
const asyncSchema = {
|
|
1433
|
+
username: pipeAsync(
|
|
1434
|
+
string(),
|
|
1435
|
+
nonEmpty('Username is required'),
|
|
1436
|
+
minLength(3, 'Min 3 characters'),
|
|
1437
|
+
checkAsync(async (value) => {
|
|
1438
|
+
await sleep(2000)
|
|
1439
|
+
return value !== 'taken'
|
|
1440
|
+
}, 'Username is already taken'),
|
|
1441
|
+
),
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const {
|
|
1445
|
+
isSubmitting: asyncSubmitting,
|
|
1446
|
+
handleSubmit: handleAsync,
|
|
1447
|
+
} = useFormValidator({
|
|
1448
|
+
schema: asyncSchema,
|
|
1449
|
+
options: { mode: 'eager', identifier: 'form-async' },
|
|
1450
|
+
})
|
|
1451
|
+
|
|
1452
|
+
const {
|
|
1453
|
+
value: asyncUsername,
|
|
1454
|
+
hasError: asyncUsernameHasError,
|
|
1455
|
+
errorMessage: asyncUsernameError,
|
|
1456
|
+
isValid: asyncUsernameValid,
|
|
1457
|
+
isValidating: asyncUsernameValidating,
|
|
1458
|
+
} = useFormField<string>('username', {
|
|
1459
|
+
ref: useTemplateRef<HTMLInputElement>('asyncUsernameRef'),
|
|
1460
|
+
formIdentifier: 'form-async',
|
|
1461
|
+
})
|
|
1462
|
+
|
|
1463
|
+
const onSubmitAsync = handleAsync(async () => {
|
|
1464
|
+
await sleep(1000)
|
|
1465
|
+
toast.success('Submitted!', { position: 'top' })
|
|
1466
|
+
})
|
|
1467
|
+
|
|
1468
|
+
// Debounce/Throttle Demo
|
|
1469
|
+
const debouncedSchema = {
|
|
1470
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
1471
|
+
age: pipe(number('Must be a number'), minValue(18, 'Must be 18+')),
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const {
|
|
1475
|
+
model: debouncedModel,
|
|
1476
|
+
errorMessages: debouncedErrors,
|
|
1477
|
+
fieldsStates: debouncedStates,
|
|
1478
|
+
isSubmitting: debouncedSubmitting,
|
|
1479
|
+
handleSubmit: handleDebounced,
|
|
1480
|
+
} = useFormValidator({
|
|
1481
|
+
schema: debouncedSchema,
|
|
1482
|
+
options: {
|
|
1483
|
+
debouncedFields: { name: 500 },
|
|
1484
|
+
throttledFields: { age: 1000 },
|
|
1485
|
+
},
|
|
1486
|
+
})
|
|
1487
|
+
|
|
1488
|
+
const onSubmitDebounced = handleDebounced(async () => {
|
|
1489
|
+
await sleep(1000)
|
|
1490
|
+
toast.success('Submitted!', { position: 'top' })
|
|
1491
|
+
})
|
|
1492
|
+
|
|
1493
|
+
// Reset Form Demo
|
|
1494
|
+
const resetSchema = {
|
|
1495
|
+
name: pipe(string(), nonEmpty('Required'), minLength(3, 'Min 3 characters')),
|
|
1496
|
+
age: pipe(number('Must be a number'), minValue(18, 'Must be 18+')),
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const {
|
|
1500
|
+
model: resetModel,
|
|
1501
|
+
errorMessages: resetErrors,
|
|
1502
|
+
fieldsStates: resetStates,
|
|
1503
|
+
isSubmitting: resetSubmitting,
|
|
1504
|
+
handleSubmit: handleReset,
|
|
1505
|
+
resetForm: resetFormFn,
|
|
1506
|
+
} = useFormValidator({
|
|
1507
|
+
schema: resetSchema,
|
|
1508
|
+
defaultValues: { name: 'John', age: 25 },
|
|
1509
|
+
options: { resetOnSuccess: false },
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
const onSubmitReset = handleReset(async () => {
|
|
1513
|
+
await sleep(1000)
|
|
1514
|
+
toast.success('Submitted!', { position: 'top' })
|
|
1515
|
+
})
|
|
1286
1516
|
</script>
|