@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.
Files changed (75) hide show
  1. package/dist/mcp.mjs +1 -1
  2. package/docs/generated-docs/maz-accordion.doc.md +5 -5
  3. package/docs/generated-docs/maz-alert.doc.md +20 -0
  4. package/docs/generated-docs/maz-container.doc.md +25 -0
  5. package/docs/generated-docs/maz-input-number.doc.md +21 -19
  6. package/docs/generated-docs/maz-read-more.doc.md +18 -0
  7. package/docs/generated-docs/maz-skeleton.doc.md +12 -0
  8. package/docs/generated-docs/maz-ui-provider.doc.md +12 -0
  9. package/docs/src/blog/v4.md +2 -2
  10. package/docs/src/components/maz-accordion.md +1 -1
  11. package/docs/src/components/maz-alert.md +374 -0
  12. package/docs/src/components/maz-animated-counter.md +1 -1
  13. package/docs/src/components/maz-animated-element.md +1 -1
  14. package/docs/src/components/maz-animated-text.md +1 -1
  15. package/docs/src/components/maz-avatar.md +1 -1
  16. package/docs/src/components/maz-backdrop.md +2 -2
  17. package/docs/src/components/maz-badge.md +1 -1
  18. package/docs/src/components/maz-bottom-sheet.md +1 -1
  19. package/docs/src/components/maz-btn-group.md +1 -1
  20. package/docs/src/components/maz-btn.md +1 -1
  21. package/docs/src/components/maz-card-spotlight.md +1 -1
  22. package/docs/src/components/maz-card.md +1 -1
  23. package/docs/src/components/maz-carousel.md +2 -2
  24. package/docs/src/components/maz-checkbox.md +1 -1
  25. package/docs/src/components/maz-checklist.md +2 -2
  26. package/docs/src/components/maz-circular-progress-bar.md +1 -1
  27. package/docs/src/components/maz-container.md +348 -0
  28. package/docs/src/components/maz-date-picker.md +3 -3
  29. package/docs/src/components/maz-dialog-confirm.md +1 -1
  30. package/docs/src/components/maz-dialog.md +1 -1
  31. package/docs/src/components/maz-drawer.md +1 -1
  32. package/docs/src/components/maz-dropdown.md +2 -2
  33. package/docs/src/components/maz-dropzone.md +4 -4
  34. package/docs/src/components/maz-expand-animation.md +1 -1
  35. package/docs/src/components/maz-fullscreen-loader.md +1 -1
  36. package/docs/src/components/maz-gallery.md +1 -1
  37. package/docs/src/components/maz-input-code.md +1 -1
  38. package/docs/src/components/maz-input-number.md +2 -2
  39. package/docs/src/components/maz-input-phone-number.md +3 -3
  40. package/docs/src/components/maz-input-price.md +2 -2
  41. package/docs/src/components/maz-input-tags.md +2 -2
  42. package/docs/src/components/maz-input.md +1 -1
  43. package/docs/src/components/maz-lazy-img.md +1 -1
  44. package/docs/src/components/maz-link.md +1 -1
  45. package/docs/src/components/maz-loading-bar.md +1 -1
  46. package/docs/src/components/maz-pagination.md +2 -2
  47. package/docs/src/components/maz-pull-to-refresh.md +1 -1
  48. package/docs/src/components/maz-radio-buttons.md +1 -1
  49. package/docs/src/components/maz-radio.md +1 -1
  50. package/docs/src/components/maz-read-more.md +300 -0
  51. package/docs/src/components/maz-reading-progress-bar.md +1 -1
  52. package/docs/src/components/maz-select-country.md +1 -1
  53. package/docs/src/components/maz-select.md +3 -3
  54. package/docs/src/components/maz-skeleton.md +355 -0
  55. package/docs/src/components/maz-slider.md +1 -1
  56. package/docs/src/components/maz-stepper.md +1 -1
  57. package/docs/src/components/maz-switch.md +1 -1
  58. package/docs/src/components/maz-table.md +1 -1
  59. package/docs/src/components/maz-textarea.md +1 -1
  60. package/docs/src/composables/use-aos.md +1 -1
  61. package/docs/src/composables/use-form-validator.md +1226 -996
  62. package/docs/src/composables/use-wait.md +1 -1
  63. package/docs/src/guide/getting-started.md +13 -10
  64. package/docs/src/guide/icon-set.md +43 -21
  65. package/docs/src/guide/icons.md +74 -15
  66. package/docs/src/guide/maz-ui-provider.md +199 -0
  67. package/docs/src/guide/migration-v4.md +1 -1
  68. package/docs/src/guide/nuxt.md +4 -0
  69. package/docs/src/guide/resolvers.md +20 -0
  70. package/docs/src/guide/themes.md +4 -0
  71. package/docs/src/guide/translations.md +4 -0
  72. package/docs/src/guide/vue.md +7 -2
  73. package/docs/src/index.md +1 -1
  74. package/docs/src/plugins/toast.md +1 -1
  75. package/package.json +8 -8
@@ -1,393 +1,435 @@
1
1
  ---
2
2
  title: useFormValidator
3
- description: useFormValidator and useFormField are two Vue composables designed to simplify form validation using Valibot as the validation library. These composables offer a flexible and typed approach to handle form validation in your Vue applications.
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
- `useFormValidator` and `useFormField` are two Vue composables designed to simplify form validation using [Valibot](https://valibot.dev/guides/introduction/) as the validation library. These composables offer a flexible and typed approach to handle form validation in your Vue applications.
8
+ {{ $frontmatter.description }}
9
9
 
10
10
  ## Introduction
11
11
 
12
- ::: details Best Practices
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
- 1. Use typed Valibot schemas to ensure type consistency.
15
- 2. Choose the appropriate validation mode based on your form's needs.
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
- ::: details Validation modes details
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
- - `lazy`: (default) Validates only on value changes
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
- ::: details How to get TypeScript type safety?
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
- The model is typed automatically from the schema.
51
+ <template #code>
35
52
 
36
- ```ts{11,16}
37
- import { pipe, string, nonEmpty, number, minValue, maxValue, minLength } from 'valibot'
38
- import { useFormValidator, useFormField } from 'maz-ui/composables'
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
- name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
42
- age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
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
- // Automatic type inference from schema
47
- const { model } = useFormValidator({
48
- schema,
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
- // For useFormField, specify both schema and field name for precise typing
52
- const { value: name } = useFormField<string>('name', { formIdentifier: 'form' })
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
- #### 1. Use the `ref` attribute on the component to get the reference
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
- You can use the `ref` attribute on the component and pass the reference to the `useFormField` composable.
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
- This method will automatically detect interactive elements (input, select, textarea, button, elements with ARIA roles, etc.) within the component and add the necessary validation events.
168
+ <template #code>
68
169
 
69
- ```vue{3,10,17}
70
- <template>
71
- <MazInput
72
- ref="inputRef"
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
- <script setup lang="ts">
83
- import { useFormField } from 'maz-ui/composables'
84
- import { useTemplateRef } from 'vue'
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 { value, errorMessage, isValid, hasError } = useFormField<string>('name', {
87
- ref: useTemplateRef<HTMLInputElement>('inputRef'),
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
- <MazInput
101
- v-model="value"
102
- :hint="errorMessage"
103
- :error="hasError"
104
- :success="isValid"
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
- const { value, errorMessage, isValid, hasError, validationEvents } = useFormField<string>('name')
115
- </script>
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
- ## Basic Usage with lazy mode
213
+ Validation modes control **when** validation runs and **when** errors are displayed. Choose the mode that best fits your UX needs.
121
214
 
122
- In this example, we will create a simple form with four fields: `name`, `age`, `agree` and `country`. The form will be validated in `lazy` mode, which means that the fields will be validated on every change.
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
- Submit the form to show the validation errors
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
- <ComponentDemo>
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
- <br />
229
+ The default mode. Validates when field values change. Errors only appear if the field is not empty.
136
230
 
137
- <form novalidate class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmit">
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="model.name"
140
- label="Enter your name (min 3 characters)"
141
- :hint="errorMessages.name"
142
- :error="!!errorMessages.name"
143
- :success="fieldsStates.name.valid"
144
- :class="{ 'has-error': !!errorMessages.name }"
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="model.age"
148
- type="number"
149
- label="Enter your age (18-100)"
150
- :hint="errorMessages.age"
151
- :error="!!errorMessages.age"
152
- :success="fieldsStates.age.valid"
153
- :class="{ 'has-error': !!errorMessages.age }"
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
- <MazSelect
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 { sleep } from 'maz-ui'
182
- import { useFormValidator, useToast } from 'maz-ui/composables'
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('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
189
- age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
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, isSubmitting, handleSubmit, errorMessages, fieldsStates } = useFormValidator<typeof schema>({
268
+ const { model, errorMessages, fieldsStates, isSubmitting, handleSubmit } = useFormValidator({
195
269
  schema,
196
- defaultValues: { name: 'John Doe', age: 10 },
197
- options: { mode: 'lazy', scrollToError: '.has-error' },
270
+ options: {
271
+ mode: 'lazy', // This is the default
272
+ scrollToError: '.has-error-lazy',
273
+ },
198
274
  })
199
275
 
200
- const onSubmit = handleSubmit(async (formData) => {
201
- // Form submission logic
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
- <template>
209
- <form @submit="onSubmit">
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="model.name"
212
- label="Enter your name"
213
- :hint="errorMessages.name"
214
- :error="!!errorMessages.name"
215
- :success="fieldsStates.name.valid"
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="model.age"
220
- type="number"
221
- label="Enter your age"
222
- :hint="errorMessages.age"
223
- :error="!!errorMessages.age"
224
- :success="fieldsStates.age.valid"
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
- <MazSelect
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
- </template>
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
- ## Usage with useFormField
336
+ ### Eager Mode (Recommended)
256
337
 
257
- In this example, we will use the `useFormField` composable to handle the validation of each field individually.
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
- ### Eager mode
260
-
261
- With eager mode, each form field is validated on blur (if not empty) and then on every change. This mode is made for a better user experience, as the user will see the validation errors only after they have finished typing.
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
- v-model="name"
267
- ref="nameRef"
268
- label="Enter your name"
269
- :hint="nameErrorMessage"
270
- :error="hasErrorName"
271
- :class="{ 'has-error-form2': hasErrorName }"
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
- v-model="age"
275
- ref="ageRef"
276
- type="number"
277
- label="Enter your age"
278
- :hint="ageErrorMessage"
279
- :error="hasErrorAge"
280
- :class="{ 'has-error-form2': hasErrorAge }"
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
- <MazCheckbox
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 setup lang="ts">
309
- import { sleep } from 'maz-ui'
310
- import { useFormField, useFormValidator, useToast } from 'maz-ui/composables'
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('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
316
- age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
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<typeof schema>({
384
+ const { isSubmitting, handleSubmit } = useFormValidator({
322
385
  schema,
323
- options: { mode: 'eager', scrollToError: '.has-error-form2', identifier: 'form-eager' },
386
+ options: {
387
+ mode: 'eager',
388
+ scrollToError: '.has-error-eager',
389
+ identifier: 'form-eager',
390
+ },
324
391
  })
325
392
 
326
- const { value: name, hasError: hasErrorName, errorMessage: nameErrorMessage } = useFormField<string>('name', {
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 onSubmit = handleSubmit(async (formData) => {
344
- // Form submission logic
345
- console.log(formData)
346
- await sleep(2000)
347
- toast.success('Form submitted', { position: 'top' })
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="Enter your name"
357
- :hint="nameErrorMessage"
358
- :error="hasErrorName"
359
- :class="{ 'has-error-form2': hasErrorName }"
420
+ label="Name"
421
+ :hint="nameError"
422
+ :error="nameHasError"
423
+ :success="nameValid"
360
424
  />
361
425
  <MazInput
362
- ref="ageRef"
363
- v-model="age"
364
- type="number"
365
- label="Enter your age"
366
- :hint="ageErrorMessage"
367
- :error="hasErrorAge"
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
- ### Progressive mode
440
+ ### Blur Mode
399
441
 
400
- With progressive mode, the field becomes valid after the first successful validation and then validated on input value change. If the field is invalid, the error message is shown on the first blur event.
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
- <form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitProgressive">
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
- v-model="nameProgressive"
406
- ref="nameProgressiveRef"
407
- label="Enter your name"
408
- :hint="nameMessageProgressive"
409
- :error="!!nameMessageProgressive"
410
- :success="nameValidProgressive"
411
- :class="{ 'has-error-progressive': !!nameMessageProgressive }"
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
- v-model="ageProgressive"
415
- ref="ageProgressiveRef"
416
- type="number"
417
- label="Enter your age"
418
- :hint="ageMessageProgressive"
419
- :error="!!ageMessageProgressive"
420
- :success="ageValidProgressive"
421
- :class="{ 'has-error-progressive': !!ageMessageProgressive }"
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
- <MazSelect
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 setup lang="ts">
451
- import { sleep } from 'maz-ui'
452
- import { useFormField, useFormValidator, useToast } from 'maz-ui/composables'
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('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
458
- age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
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<typeof schema>({
488
+ const { isSubmitting, handleSubmit } = useFormValidator({
464
489
  schema,
465
- options: { mode: 'progressive', scrollToError: '.has-error-progressive', identifier: 'form-progressive' },
490
+ options: {
491
+ mode: 'blur',
492
+ identifier: 'form-blur',
493
+ },
466
494
  })
467
495
 
468
- const { value: name, hasError: nameHasError, errorMessage: nameErrorMessage } = useFormField<string>('name', {
496
+ const { value: name, hasError, errorMessage, isValid } = useFormField<string>('name', {
469
497
  ref: useTemplateRef('nameRef'),
470
- formIdentifier: 'form-progressive',
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 onSubmit = handleSubmit(async (formData) => {
486
- // Form submission logic
487
- console.log(formData)
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
- ## Reset Form
541
-
542
- Default: `true`
511
+ ### Progressive Mode
543
512
 
544
- You can use the `resetForm` function to reset the form to its initial state.
513
+ ::: warning
514
+ Requires `useFormField` with `ref` option or `validationEvents`.
515
+ :::
545
516
 
546
- You must use `handleSubmit` to handle the form submission and reset the form.
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
- <form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitReset">
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
- v-model="modelReset.name"
552
- label="Enter your name"
553
- :hint="errorMessagesReset.name"
554
- :error="fieldsStatesReset.name.error"
555
- :success="fieldsStatesReset.name.valid"
556
- :class="{ 'has-error-reset': fieldsStatesReset.name.error }"
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
- v-model="modelReset.age"
560
- type="number"
561
- label="Enter your age"
562
- :hint="errorMessagesReset.age"
563
- :error="fieldsStatesReset.age.error"
564
- :success="fieldsStatesReset.age.valid"
565
- :class="{ 'has-error-reset': fieldsStatesReset.age.error }"
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
- <MazBtn type="submit" :loading="isSubmittingReset">
568
- Submit
569
- </MazBtn>
570
- <MazBtn @click="resetFormReset" color="destructive">
571
- Reset
572
- </MazBtn>
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{37,38}
578
- <template>
579
- <form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmit">
580
- <MazInput
581
- v-model="model.name"
582
- label="Enter your name"
583
- :hint="errorMessages.name"
584
- :error="fieldsStates.name.error"
585
- :success="fieldsStates.name.valid"
586
- :class="{ 'has-error-debounced': fieldsStates.name.error }"
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
- <script setup lang="ts">
607
- import { sleep } from 'maz-ui'
608
- import { useFormValidator, useToast, InferFormValidatorSchema } from 'maz-ui/composables'
609
- import { string, nonEmpty, pipe, number, minValue, minLength } from 'valibot'
569
+ const { isSubmitting, handleSubmit } = useFormValidator({
570
+ schema,
571
+ options: {
572
+ mode: 'progressive',
573
+ identifier: 'form-progressive',
574
+ },
575
+ })
610
576
 
611
- const { model, fieldsStates, isValid, isSubmitting, errorMessages, handleSubmit, resetForm } = useFormValidator({
612
- schema: {
613
- name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
614
- age: pipe(number('Age is required'), nonEmpty('Age is required'), minValue(18, 'Age must be greater than 18')),
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
- ## Throlling and Debouncing
588
+ ## useFormField for Child Components
635
589
 
636
- You can use the `throttledFields` and `debouncedFields` options to throttle or debounce the validation of specific fields.
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
- The fields are validated with throttling or debouncing to avoid spamming the server or to wait for the user to finish typing before validating.
595
+ ### Return Values
639
596
 
640
- You can set the throttle or debounce time in milliseconds or use `true` for the default throttle time (1000ms) or debounce time (300ms).
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
- <ComponentDemo>
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
- <template #code>
613
+ #### Option 1: Using `ref` (Recommended)
667
614
 
668
- ```vue{37,38}
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
- <script setup lang="ts">
695
- import { sleep } from 'maz-ui'
696
- import { useFormValidator, useToast, InferFormValidatorSchema } from 'maz-ui/composables'
697
- import { string, nonEmpty, pipe, number, minValue, minLength } from 'valibot'
617
+ ```vue
618
+ <script setup>
619
+ import { useFormField } from 'maz-ui/composables'
620
+ import { useTemplateRef } from 'vue'
698
621
 
699
- const { model, fieldsStates, isValid, isSubmitting, errorMessages, handleSubmit } = useFormValidator({
700
- schema: {
701
- name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
702
- age: pipe(number('Age is required'), nonEmpty('Age is required'), minValue(18, 'Age must be greater than 18')),
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
- </template>
721
- </ComponentDemo>
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
- ## Validation with async function
705
+ :::
724
706
 
725
- You can use async functions in the validation schema.
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
- v-model="modelAsync.name"
731
- label="Enter your name"
732
- ref="nameAsyncRef"
733
- v-bind="validationEventsAsync"
734
- :hint="errorMessagesAsync.name"
735
- :error="fieldsStatesAsync.name.error"
736
- :success="fieldsStatesAsync.name.valid"
737
- :loading="fieldsStatesAsync.name.validating"
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="isSubmittingAsync">
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
- <form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmit">
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="model.name"
752
- label="Enter your name"
753
- ref="nameRef"
754
- v-bind="validationEvents"
755
- :hint="errorMessages.name"
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
- <MazBtn type="submit" :loading="isSubmitting">
762
- Submit
763
- </MazBtn>
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
- <script setup lang="ts">
768
- import { sleep } from 'maz-ui'
769
- import { useFormValidator, useToast, InferFormValidatorSchema } from 'maz-ui/composables'
770
- import { string, nonEmpty, pipe, number, minValue, minLength, pipeAsync, checkAsync } from 'valibot'
771
-
772
- const {
773
- model,
774
- fieldsStates,
775
- isValid,
776
- isSubmitting,
777
- errorMessages,
778
- handleSubmit,
779
- } = useFormValidator({
780
- schema: {
781
- name: pipeAsync(
782
- string('Name is required'),
783
- nonEmpty('Name is required'),
784
- minLength(3, 'Name must be at least 3 characters'),
785
- checkAsync(
786
- async (name) => {
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
- ## useFormValidator
809
-
810
- `useFormValidator` is the main composable for initializing form validation.
840
+ ## Reset Form
811
841
 
812
- It accepts a validation schema, default values, and configuration options to handle form validation. You can also provide a model reference to bind the form data.
842
+ Use `resetForm()` to reset the form to its initial state, or set `resetOnSuccess` to automatically reset after successful submission.
813
843
 
814
- ### Parameters
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
- `useFormValidator<TSchema>` accepts an object with the following properties:
867
+ <template #code>
817
868
 
818
- - `schema`: `TSchema` - The Valibot validation schema for the form.
819
- - `model`: `Ref<Model>` (optional) - A reference to the form's data model.
820
- - `defaultValues`: `DeepPartial<Model>` (optional) - Default values for the form fields.
821
- - `options`: `FormValidatorOptions` (optional) - Configuration options for the form validation behavior.
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
- ### Return
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
- `useFormValidator` returns an object containing:
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
- - `isDirty`: `ComputedRef<boolean>` - Indicates if the form has been modified.
833
- - `isSubmitting`: `Ref<boolean>` - Indicates if the form is currently being submitted.
834
- - `isSubmitted`: `Ref<boolean>` - Indicates if the form has been submitted.
835
- - `isValid`: `ComputedRef<boolean>` - Indicates if the form is valid.
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
- ```ts
846
- interface useFormValidationReturn {
847
- identifier: string | symbol;
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
- ## useFormField
899
+ </template>
900
+ </ComponentDemo>
864
901
 
865
- ::: warning
866
- Before using `useFormField`, make sure you have initialized the form with `useFormValidator`.
867
- :::
902
+ ## Multiple Forms on Same Page
868
903
 
869
- `useFormField` is a composable for handling validation at the individual form field level.
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
- Useful for fine-grained control over form fields, `useFormField` provides computed properties for validation state, error messages, and more.
872
- Can be very useful when you are using fields in child components of form.
906
+ ```vue
907
+ <script lang="ts" setup>
908
+ import { useFormValidator, useFormField } from 'maz-ui/composables'
873
909
 
874
- To use the modes `eager`, `progressive` or `blur`, you must use this `useFormField` composable to add the [necessary validation events](#introduction).
910
+ // Form 1
911
+ const form1 = useFormValidator({
912
+ schema: schema1,
913
+ options: { identifier: 'login-form' },
914
+ })
875
915
 
876
- ### Parameters
916
+ // Form 2
917
+ const form2 = useFormValidator({
918
+ schema: schema2,
919
+ options: { identifier: 'register-form' },
920
+ })
877
921
 
878
- `useFormField<T>` takes the following parameters:
922
+ // useFormField must use matching identifier
923
+ const { value: loginEmail } = useFormField<string>('email', {
924
+ formIdentifier: 'login-form',
925
+ })
879
926
 
880
- - `name`: `string` - The name of the field in the validation schema (must be a key from the schema).
881
- - `options`: `FormFieldOptions<T>` (optional) - Field-specific options.
882
- - `defaultValue`: `T` (optional) - The default value of the field.
883
- - `mode`: `'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'` (optional) - The validation mode for the field - [see validation modes](#introduction)
884
- - `ref`: `Ref<HTMLElement | ComponentInstance>` (optional) - Vue ref to the component/element for automatic event binding - use `useTemplateRef()` for type safety
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
- ### Return
933
+ ## Error Handling and scrollToError
888
934
 
889
- `useFormField` returns an object containing:
935
+ ### scrollToError
890
936
 
891
- - `errors`: `ComputedRef<ValidationIssues>` - Validation errors for this field.
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
- interface useFormFieldReturn {
905
- hasError: import('vue').ComputedRef<boolean>;
906
- errors: import('vue').ComputedRef<import('./useFormValidator/types').ValidationIssues>;
907
- errorMessage: import('vue').ComputedRef<Record<ModelKey, string | undefined>[ModelKey]>;
908
- isValid: import('vue').ComputedRef<boolean>;
909
- isDirty: import('vue').ComputedRef<boolean>;
910
- isBlurred: import('vue').ComputedRef<boolean>;
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
- ## Recent Improvements (v4.0.0)
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
- ### 🎯 Better Interactive Element Detection
930
-
931
- The `ref` option in `useFormField` now automatically detects and binds events to:
932
- - Standard form elements: `input`, `select`, `textarea`, `button`
933
- - Focusable elements: links with `href`, elements with `tabindex`
934
- - ARIA interactive elements: `role="button"`, `role="textbox"`, etc.
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
- - Automatic cleanup of event listeners to prevent memory leaks
940
- - WeakMap-based tracking for better garbage collection
941
- - Race condition protection in async validation
958
+ ### onError Callback
942
959
 
943
- ### 📝 Better Development Experience
960
+ Handle validation failures with the `onError` callback:
944
961
 
945
- - More informative warning messages
946
- - Improved error handling and validation states
947
- - Enhanced debugging capabilities
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
- ### 🚀 Performance Tips
980
+ ### Performance Tips
952
981
 
953
- - **Use `throttledFields` or `debouncedFields`** for expensive validations or network requests
954
- - **Prefer `eager` or `progressive` modes** for better UX instead of `aggressive`
955
- - **Use `lazy` mode** for simple forms with minimal validation
956
- - **Leverage TypeScript**: Always use `typeof schema` for automatic type inference
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
- ### 💡 Common Patterns
987
+ ### Common Patterns
959
988
 
960
- #### Multiple Forms on Same Page
989
+ #### Multiple Forms with Identifiers
961
990
 
962
991
  ```ts
963
- const form1 = useFormValidator<typeof schema1>({
964
- schema: schema1,
965
- options: { identifier: 'form-1' }
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
- // Use matching identifiers in useFormField
974
- const { value } = useFormField<string>('name', {
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
- <template>
983
- <!-- Add data-interactive for custom components -->
984
- <div data-interactive class="custom-input" tabindex="0">
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
- ### ⚠️ Common Pitfalls
1012
+ ### Common Pitfalls
991
1013
 
992
- - **Mismatched form identifiers**: Ensure `useFormField` uses the same `formIdentifier` as `useFormValidator`
993
- - **Missing refs for interactive modes**: `eager`, `blur`, and `progressive` modes require either `ref` or `validationEvents`
994
- - **Incorrect TypeScript generics**: Always specify both schema and field name: `useFormField<T>`
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
- ## Troubleshooting
1021
+ ## API Reference
997
1022
 
998
- ### Type Errors
1023
+ ### useFormValidator
999
1024
 
1000
- **Problem**: `WritableComputedRef<string | number | boolean | undefined>`
1025
+ #### Parameters
1001
1026
 
1002
1027
  ```ts
1003
- // ❌ Wrong - loses type precision
1004
- const { value } = useFormField('name')
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
- // ✅ Correct - precise typing
1007
- const { value } = useFormField<string>('name')
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
- ### Using `useTemplateRef` with `useFormField` cause TypeScript errors
1067
+ ### useFormField
1011
1068
 
1012
- **Cause:** `useTemplateRef` can create TypeScript circular references when the destructured variable name resembles the template ref name.
1069
+ #### Parameters
1013
1070
 
1014
- If you encounter TypeScript errors when using `useFormField` with `useTemplateRef`, use classic `ref()` instead:
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
- ```typescript
1017
- // ❌ May cause TypeScript errors
1018
- const { value: email } = useFormField<string>('email', {
1019
- ref: useTemplateRef('emailRef'),
1020
- })
1083
+ #### Return Values
1021
1084
 
1022
- // ✅ Correct - precise typing
1023
- const { value: email } = useFormField<string>('email', {
1024
- ref: useTemplateRef<string>('emailRef'),
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
- // Use classic `ref()` instead
1147
+ // Solution 2: Use classic ref
1028
1148
  const emailRef = ref<HTMLInputElement>()
1029
- const { value: email } = useFormField<string>('email', {
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**: Field validation doesn't work with `eager`/`blur`/`progressive` modes
1154
+ **Problem**: `eager`, `blur`, or `progressive` mode not validating
1037
1155
 
1038
- ```ts
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
- // ✅ Use ref for automatic detection
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
- // Or use validation events manually
1164
+ // Option 2: validationEvents
1048
1165
  const { value, validationEvents } = useFormField<string>('name')
1049
- // Then: v-bind="validationEvents" on your component
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 properly bound to an HTML element or Vue component
1058
- 2. Make sure the component has a `$el` property if it's a Vue component
1059
- 3. Use `data-interactive` attribute for custom interactive elements
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
- ## Types
1180
+ **Problem**: `useFormField` not finding the form context
1062
1181
 
1063
- ### FormValidatorOptions
1182
+ **Solution**: Ensure identifiers match:
1064
1183
 
1065
1184
  ```ts
1066
- interface FormValidatorOptions {
1067
- /**
1068
- * Validation mode
1069
- * - lazy: validate on input value change
1070
- * - aggressive: validate all fields immediately on form creation and on input value change
1071
- * - blur: validate on blur
1072
- * - eager: validate on blur at first (only if the field is not empty) and then on input value change
1073
- * - progressive: 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.
1074
- * @default 'lazy'
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
- ### FormFieldOptions
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
- ```ts
1107
- interface FormFieldOptions<T> {
1108
- /**
1109
- * Default value of the field
1110
- * @default undefined
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
- <script lang="ts" setup>
1135
- import { ref, useTemplateRef } from 'vue'
1136
- import { useFormValidator } from 'maz-ui/src/composables/useFormValidator'
1137
- import { useFormField } from 'maz-ui/src/composables/useFormField'
1138
- import { useToast } from 'maz-ui/src/composables/useToast'
1139
- import { sleep } from 'maz-ui'
1140
- import { string, nonEmpty, pipe, number, minValue, maxValue, boolean, literal, minLength, pipeAsync, checkAsync } from 'valibot'
1141
-
1142
- const toast = useToast()
1143
-
1144
- const schema = ref({
1145
- name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
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
- const { isValid: isValidEager, isSubmitting: isSubmittingEager, handleSubmit: handleSubmitEager } = useFormValidator<typeof eagerSchema>({
1172
- schema: eagerSchema,
1173
- options: { mode: 'eager', scrollToError: '.has-error-form2', identifier: 'form-eager' },
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
- const { value: nameProgressive, isValid: nameValidProgressive, hasError: nameErrorProgressive, errorMessage: nameMessageProgressive } = useFormField<string>('name', { ref: useTemplateRef('nameProgressiveRef'), formIdentifier: 'form-progressive' })
1206
- const { value: ageProgressive, isValid: ageValidProgressive, hasError: ageErrorProgressive, errorMessage: ageMessageProgressive } = useFormField<number>('age', { ref: useTemplateRef('ageProgressiveRef'), formIdentifier: 'form-progressive' })
1207
- const { value: countryProgressive, isValid: countryValidProgressive, hasError: countryErrorProgressive, errorMessage: countryMessageProgressive, validationEventsProgressive } = useFormField<string>('country', { ref: useTemplateRef('countryProgressiveRef'), formIdentifier: 'form-progressive' })
1208
- const { value: agreeProgressive, isValid: agreeValidProgressive, hasError: agreeErrorProgressive, errorMessage: agreeMessageProgressive } = useFormField<boolean>('agree', { ref: useTemplateRef('agreeProgressiveRef'), formIdentifier: 'form-progressive' })
1209
-
1210
- const onSubmitProgressive = handleSubmitProgressive(async (formData) => {
1211
- // Form submission logic
1212
- console.log(formData)
1213
- await sleep(2000)
1214
- toast.success('Form submitted', { position: 'top' })
1215
- })
1216
-
1217
- const { model: modelReset, fieldsStates: fieldsStatesReset, isValid: isValidReset, isSubmitting: isSubmittingReset, errorMessages: errorMessagesReset, handleSubmit: handleSubmitReset, resetForm: resetFormReset } = useFormValidator({
1218
- schema: {
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
- const { model: modelDebounced, fieldsStates: fieldsStatesDebounced, isValid: isValidDebounced, isSubmitting: isSubmittingDebounced, errorMessages: errorMessagesDebounced, handleSubmit: handleSubmitDebounced } = useFormValidator({
1229
- schema: {
1230
- name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
1231
- age: pipe(number('Age is required'), nonEmpty('Age is required'), minValue(18, 'Age must be greater than 18')),
1232
- },
1233
- options: {
1234
- debouncedFields: { name: 500 },
1235
- throttledFields: { age: true },
1236
- scrollToError: '.has-error-debounced',
1237
- },
1238
- })
1239
-
1240
- const onSubmitReset = handleSubmitReset(async (formData) => {
1241
- // Form submission logic
1242
- console.log(formData)
1243
- await sleep(2000)
1244
- toast.success(`Form submitted with ${JSON.stringify(formData)}`, { position: 'top' })
1245
- })
1246
-
1247
- const onSubmitDebounced = handleSubmitDebounced(async (formData) => {
1248
- // Form submission logic
1249
- console.log(formData)
1250
- await sleep(2000)
1251
- toast.success(`Form submitted with ${JSON.stringify(formData)}`, { position: 'top' })
1252
- })
1253
-
1254
- const { model: modelAsync, fieldsStates: fieldsStatesAsync, isValid: isValidAsync, isSubmitting: isSubmittingAsync, errorMessages: errorMessagesAsync, handleSubmit: handleSubmitAsync } = useFormValidator({
1255
- schema: {
1256
- name: pipeAsync(
1257
- string('Name is required'),
1258
- nonEmpty('Name is required'),
1259
- minLength(3, 'Name must be at least 3 characters'),
1260
- checkAsync(
1261
- async (name) => {
1262
- await sleep(2000)
1263
- return false
1264
- },
1265
- 'Name is already taken',
1266
- )),
1267
- },
1268
- options: { mode: 'eager', scrollToError: '.has-error-async', identifier: 'form-async' },
1269
- })
1270
-
1271
- const {
1272
- value: nameAsync,
1273
- hasError: hasErrorNameAsync,
1274
- errorMessage: nameErrorMessageAsync,
1275
- validationEvents: validationEventsAsync,
1276
- } = useFormField<string>('name', {
1277
- ref: useTemplateRef('nameAsyncRef'),
1278
- formIdentifier: 'form-async',
1279
- })
1280
-
1281
- const onSubmitAsync = handleSubmitAsync((formData) => {
1282
- // Form submission logic
1283
- console.log(formData)
1284
- toast.success('Form submitted', { position: 'top' })
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>