@maz-ui/mcp 4.0.0-beta.26

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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +264 -0
  3. package/bin/maz-ui-mcp.mjs +7 -0
  4. package/dist/mcp.d.mts +13 -0
  5. package/dist/mcp.d.ts +13 -0
  6. package/dist/mcp.mjs +586 -0
  7. package/docs/generated-docs/maz-accordion.doc.md +21 -0
  8. package/docs/generated-docs/maz-animated-counter.doc.md +17 -0
  9. package/docs/generated-docs/maz-animated-element.doc.md +14 -0
  10. package/docs/generated-docs/maz-animated-text.doc.md +14 -0
  11. package/docs/generated-docs/maz-avatar.doc.md +44 -0
  12. package/docs/generated-docs/maz-backdrop.doc.md +61 -0
  13. package/docs/generated-docs/maz-badge.doc.md +16 -0
  14. package/docs/generated-docs/maz-bottom-sheet.doc.md +21 -0
  15. package/docs/generated-docs/maz-btn.doc.md +30 -0
  16. package/docs/generated-docs/maz-card-spotlight.doc.md +16 -0
  17. package/docs/generated-docs/maz-card.doc.md +39 -0
  18. package/docs/generated-docs/maz-carousel.doc.md +16 -0
  19. package/docs/generated-docs/maz-chart.doc.md +10 -0
  20. package/docs/generated-docs/maz-checkbox.doc.md +34 -0
  21. package/docs/generated-docs/maz-checklist.doc.md +30 -0
  22. package/docs/generated-docs/maz-circular-progress-bar.doc.md +27 -0
  23. package/docs/generated-docs/maz-date-picker.doc.md +52 -0
  24. package/docs/generated-docs/maz-dialog-confirm.doc.md +24 -0
  25. package/docs/generated-docs/maz-dialog.doc.md +22 -0
  26. package/docs/generated-docs/maz-drawer.doc.md +26 -0
  27. package/docs/generated-docs/maz-dropdown.doc.md +42 -0
  28. package/docs/generated-docs/maz-dropzone.doc.md +82 -0
  29. package/docs/generated-docs/maz-expand-animation.doc.md +12 -0
  30. package/docs/generated-docs/maz-fullscreen-loader.doc.md +13 -0
  31. package/docs/generated-docs/maz-gallery.doc.md +17 -0
  32. package/docs/generated-docs/maz-icon.doc.md +18 -0
  33. package/docs/generated-docs/maz-input-code.doc.md +25 -0
  34. package/docs/generated-docs/maz-input-number.doc.md +31 -0
  35. package/docs/generated-docs/maz-input-phone-number.doc.md +56 -0
  36. package/docs/generated-docs/maz-input-price.doc.md +26 -0
  37. package/docs/generated-docs/maz-input-tags.doc.md +24 -0
  38. package/docs/generated-docs/maz-input.doc.md +54 -0
  39. package/docs/generated-docs/maz-lazy-img.doc.md +31 -0
  40. package/docs/generated-docs/maz-link.doc.md +31 -0
  41. package/docs/generated-docs/maz-loading-bar.doc.md +6 -0
  42. package/docs/generated-docs/maz-pagination.doc.md +22 -0
  43. package/docs/generated-docs/maz-popover.doc.md +70 -0
  44. package/docs/generated-docs/maz-pull-to-refresh.doc.md +31 -0
  45. package/docs/generated-docs/maz-radio-buttons.doc.md +33 -0
  46. package/docs/generated-docs/maz-radio.doc.md +33 -0
  47. package/docs/generated-docs/maz-reading-progress-bar.doc.md +18 -0
  48. package/docs/generated-docs/maz-select-country.doc.md +44 -0
  49. package/docs/generated-docs/maz-select.doc.md +65 -0
  50. package/docs/generated-docs/maz-slider.doc.md +20 -0
  51. package/docs/generated-docs/maz-spinner.doc.md +6 -0
  52. package/docs/generated-docs/maz-stepper.doc.md +29 -0
  53. package/docs/generated-docs/maz-switch.doc.md +31 -0
  54. package/docs/generated-docs/maz-table-cell.doc.md +5 -0
  55. package/docs/generated-docs/maz-table-row.doc.md +11 -0
  56. package/docs/generated-docs/maz-table-title.doc.md +5 -0
  57. package/docs/generated-docs/maz-table.doc.md +66 -0
  58. package/docs/generated-docs/maz-tabs-bar.doc.md +18 -0
  59. package/docs/generated-docs/maz-tabs-content-item.doc.md +11 -0
  60. package/docs/generated-docs/maz-tabs-content.doc.md +5 -0
  61. package/docs/generated-docs/maz-tabs.doc.md +17 -0
  62. package/docs/generated-docs/maz-textarea.doc.md +41 -0
  63. package/docs/src/components/index.md +8 -0
  64. package/docs/src/components/maz-accordion.md +80 -0
  65. package/docs/src/components/maz-animated-counter.md +124 -0
  66. package/docs/src/components/maz-animated-element.md +36 -0
  67. package/docs/src/components/maz-animated-text.md +36 -0
  68. package/docs/src/components/maz-avatar.md +179 -0
  69. package/docs/src/components/maz-backdrop.md +16 -0
  70. package/docs/src/components/maz-badge.md +222 -0
  71. package/docs/src/components/maz-bottom-sheet.md +398 -0
  72. package/docs/src/components/maz-btn.md +526 -0
  73. package/docs/src/components/maz-card-spotlight.md +163 -0
  74. package/docs/src/components/maz-card.md +447 -0
  75. package/docs/src/components/maz-carousel.md +127 -0
  76. package/docs/src/components/maz-chart.md +346 -0
  77. package/docs/src/components/maz-checkbox.md +168 -0
  78. package/docs/src/components/maz-checklist.md +414 -0
  79. package/docs/src/components/maz-circular-progress-bar.md +147 -0
  80. package/docs/src/components/maz-date-picker.md +1078 -0
  81. package/docs/src/components/maz-dialog-confirm.md +240 -0
  82. package/docs/src/components/maz-dialog.md +208 -0
  83. package/docs/src/components/maz-drawer.md +177 -0
  84. package/docs/src/components/maz-dropdown.md +650 -0
  85. package/docs/src/components/maz-dropzone.md +442 -0
  86. package/docs/src/components/maz-expand-animation.md +99 -0
  87. package/docs/src/components/maz-fullscreen-loader.md +58 -0
  88. package/docs/src/components/maz-gallery.md +85 -0
  89. package/docs/src/components/maz-icon.md +85 -0
  90. package/docs/src/components/maz-input-code.md +61 -0
  91. package/docs/src/components/maz-input-number.md +81 -0
  92. package/docs/src/components/maz-input-phone-number.md +867 -0
  93. package/docs/src/components/maz-input-price.md +58 -0
  94. package/docs/src/components/maz-input-tags.md +114 -0
  95. package/docs/src/components/maz-input.md +453 -0
  96. package/docs/src/components/maz-lazy-img.md +24 -0
  97. package/docs/src/components/maz-link.md +156 -0
  98. package/docs/src/components/maz-loading-bar.md +26 -0
  99. package/docs/src/components/maz-pagination.md +81 -0
  100. package/docs/src/components/maz-popover.md +1414 -0
  101. package/docs/src/components/maz-pull-to-refresh.md +49 -0
  102. package/docs/src/components/maz-radio-buttons.md +456 -0
  103. package/docs/src/components/maz-radio.md +141 -0
  104. package/docs/src/components/maz-reading-progress-bar.md +74 -0
  105. package/docs/src/components/maz-select-country.md +636 -0
  106. package/docs/src/components/maz-select.md +439 -0
  107. package/docs/src/components/maz-slider.md +191 -0
  108. package/docs/src/components/maz-spinner.md +93 -0
  109. package/docs/src/components/maz-stepper.md +418 -0
  110. package/docs/src/components/maz-switch.md +92 -0
  111. package/docs/src/components/maz-table.md +571 -0
  112. package/docs/src/components/maz-tabs.md +231 -0
  113. package/docs/src/components/maz-textarea.md +218 -0
  114. package/docs/src/composables/use-aos.md +34 -0
  115. package/docs/src/composables/use-breakpoints.md +35 -0
  116. package/docs/src/composables/use-dialog.md +88 -0
  117. package/docs/src/composables/use-display-names.md +174 -0
  118. package/docs/src/composables/use-form-validator.md +1149 -0
  119. package/docs/src/composables/use-idle-timeout.md +256 -0
  120. package/docs/src/composables/use-reading-time.md +168 -0
  121. package/docs/src/composables/use-string-matching.md +63 -0
  122. package/docs/src/composables/use-swipe.md +223 -0
  123. package/docs/src/composables/use-timer.md +130 -0
  124. package/docs/src/composables/use-toast.md +71 -0
  125. package/docs/src/composables/use-user-visibility.md +169 -0
  126. package/docs/src/composables/use-wait.md +62 -0
  127. package/docs/src/composables/use-window-size.md +18 -0
  128. package/docs/src/demo/DemoAuthPage.vue +178 -0
  129. package/docs/src/demo/DemoDashboardPage.vue +298 -0
  130. package/docs/src/demo/DemoProductPage.vue +135 -0
  131. package/docs/src/directives/click-outside.md +275 -0
  132. package/docs/src/directives/fullscreen-img.md +101 -0
  133. package/docs/src/directives/lazy-img.md +184 -0
  134. package/docs/src/directives/tooltip.md +458 -0
  135. package/docs/src/directives/zoom-img.md +127 -0
  136. package/docs/src/guide/cli.md +144 -0
  137. package/docs/src/guide/getting-started.md +284 -0
  138. package/docs/src/guide/icon-set.md +60 -0
  139. package/docs/src/guide/icons.md +481 -0
  140. package/docs/src/guide/mcp.md +210 -0
  141. package/docs/src/guide/migration-v4.md +898 -0
  142. package/docs/src/guide/nuxt.md +411 -0
  143. package/docs/src/guide/resolvers.md +697 -0
  144. package/docs/src/guide/themes.md +789 -0
  145. package/docs/src/guide/translations.md +1173 -0
  146. package/docs/src/guide/vue.md +243 -0
  147. package/docs/src/helpers/camel-case.md +14 -0
  148. package/docs/src/helpers/capitalize.md +51 -0
  149. package/docs/src/helpers/check-availability.md +14 -0
  150. package/docs/src/helpers/country-code-to-unicode-flag.md +213 -0
  151. package/docs/src/helpers/currency.md +67 -0
  152. package/docs/src/helpers/date.md +67 -0
  153. package/docs/src/helpers/debounce-callback.md +14 -0
  154. package/docs/src/helpers/debounce-id.md +14 -0
  155. package/docs/src/helpers/debounce.md +14 -0
  156. package/docs/src/helpers/get-country-flag-url.md +156 -0
  157. package/docs/src/helpers/is-client.md +14 -0
  158. package/docs/src/helpers/is-equal.md +14 -0
  159. package/docs/src/helpers/is-standalone-mode.md +14 -0
  160. package/docs/src/helpers/kebab-case.md +14 -0
  161. package/docs/src/helpers/normalize-string.md +14 -0
  162. package/docs/src/helpers/number.md +65 -0
  163. package/docs/src/helpers/pascal-case.md +14 -0
  164. package/docs/src/helpers/script-loader.md +14 -0
  165. package/docs/src/helpers/sleep.md +14 -0
  166. package/docs/src/helpers/snake-case.md +14 -0
  167. package/docs/src/helpers/throttle-id.md +14 -0
  168. package/docs/src/helpers/throttle.md +14 -0
  169. package/docs/src/index.md +555 -0
  170. package/docs/src/made-with-maz-ui.md +58 -0
  171. package/docs/src/plugins/aos.md +347 -0
  172. package/docs/src/plugins/dialog.md +411 -0
  173. package/docs/src/plugins/toast.md +349 -0
  174. package/docs/src/plugins/wait.md +109 -0
  175. package/package.json +84 -0
@@ -0,0 +1,1149 @@
1
+ ---
2
+ title: useFormValidator
3
+ description: useFormValidator and useFormField are two Vue 3 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 3 applications.
4
+ ---
5
+
6
+ # {{ $frontmatter.title }}
7
+
8
+ `useFormValidator` and `useFormField` are two Vue 3 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 3 applications.
9
+
10
+ ## Prerequisites
11
+
12
+ To use this composable, you have to install the [`Valibot`](https://valibot.dev/) dependency
13
+
14
+ <NpmBadge package="valibot" />
15
+
16
+ ```bash
17
+ npm install valibot
18
+ ```
19
+
20
+ ## Introduction
21
+
22
+ ::: details Best Practices
23
+
24
+ 1. Use typed Valibot schemas to ensure type consistency.
25
+ 2. Choose the appropriate validation mode based on your form's needs.
26
+ 3. Use `useFormField` for fine-grained management of each form field.
27
+ 4. Use the `handleSubmit` returned by `useFormValidator` to handle form submission securely.
28
+ 5. Leverage computed values like `isValid`, `hasError`, `errorMessage`, and others to control your user interface state.
29
+
30
+ :::
31
+
32
+ ::: details Validation modes details
33
+
34
+ - `lazy`: (default) Validates only on value changes
35
+ - `aggressive`: Validates all fields immediately and on every change
36
+ - `eager`: (recommended) Validates on initial blur (if not empty), then on every change **(requires `useFormField` to add validation events)**
37
+ - `blur`: Validates only on focus loss **(requires `useFormField` to add validation events)**
38
+ - `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)**
39
+
40
+ :::
41
+
42
+ ::: details How to get TypeScript type safety?
43
+
44
+ The model is typed automatically from the schema.
45
+
46
+ ```ts{11,16}
47
+ import { pipe, string, nonEmpty, number, minValue, maxValue, minLength } from 'valibot'
48
+ import { useFormValidator, useFormField } from 'maz-ui/composables'
49
+
50
+ const schema = {
51
+ name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
52
+ age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
53
+ country: pipe(string('Country is required'), nonEmpty('Country is required')),
54
+ }
55
+
56
+ // Automatic type inference from schema
57
+ const { model } = useFormValidator({
58
+ schema,
59
+ })
60
+
61
+ // For useFormField, specify both schema and field name for precise typing
62
+ const { value: name } = useFormField<string>('name', { formIdentifier: 'form' })
63
+ ```
64
+
65
+ :::
66
+
67
+ ::: details How to bind validation events with useFormField for eager, blur, or progressive modes?
68
+
69
+ To use the `eager`, `blur`, or `progressive` validation modes, you must use the `useFormField` composable to add the necessary validation events.
70
+
71
+ 2 ways to bind validation events:
72
+
73
+ #### 1. Use the `ref` attribute on the component to get the reference
74
+
75
+ You can use the `ref` attribute on the component and pass the reference to the `useFormField` composable.
76
+
77
+ 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.
78
+
79
+ ```vue{3,10,17}
80
+ <template>
81
+ <MazInput
82
+ ref="inputRef"
83
+ v-model="value"
84
+ :hint="errorMessage"
85
+ :error="hasError"
86
+ :success="isValid"
87
+ />
88
+ <!-- Work with HTML input -->
89
+ <input ref="inputRef" v-model="value" />
90
+ </template>
91
+
92
+ <script setup lang="ts">
93
+ import { useFormField } from 'maz-ui/composables'
94
+ import { useTemplateRef } from 'vue'
95
+
96
+ const { value, errorMessage, isValid, hasError } = useFormField<string>('name', {
97
+ ref: useTemplateRef<HTMLInputElement>('inputRef'),
98
+ })
99
+ </script>
100
+ ```
101
+
102
+ #### 2. Use the `v-bind` directive to bind the validation events
103
+
104
+ You can use the `v-bind` directive to bind the validation events to the component or HTML element.
105
+
106
+ 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.
107
+
108
+ ```vue{7,16}
109
+ <template>
110
+ <MazInput
111
+ v-model="value"
112
+ :hint="errorMessage"
113
+ :error="hasError"
114
+ :success="isValid"
115
+ v-bind="validationEvents"
116
+ />
117
+ <!-- or -->
118
+ <input v-model="value" v-bind="validationEvents" />
119
+ </template>
120
+
121
+ <script setup lang="ts">
122
+ import { useFormField } from 'maz-ui/composables'
123
+
124
+ const { value, errorMessage, isValid, hasError, validationEvents } = useFormField<string>('name')
125
+ </script>
126
+ ```
127
+
128
+ :::
129
+
130
+ ## Basic Usage with lazy mode
131
+
132
+ 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.
133
+
134
+ ::: tip
135
+ Submit the form to show the validation errors
136
+ :::
137
+
138
+ <ComponentDemo>
139
+ <b>Form State</b>
140
+
141
+ <div class="maz-text-xs maz-p-2 maz-bg-surface-400 maz-rounded maz-mt-2">
142
+ <pre>{{ { isValid, isSubmitting, isDirty, isSubmitted, errorMessages } }}</pre>
143
+ </div>
144
+
145
+ <br />
146
+
147
+ <form novalidate class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmit">
148
+ <MazInput
149
+ v-model="model.name"
150
+ label="Enter your name (min 3 characters)"
151
+ :hint="errorMessages.name"
152
+ :error="!!errorMessages.name"
153
+ :success="fieldsStates.name.valid"
154
+ :class="{ 'has-error': !!errorMessages.name }"
155
+ />
156
+ <MazInput
157
+ v-model="model.age"
158
+ type="number"
159
+ label="Enter your age (18-100)"
160
+ :hint="errorMessages.age"
161
+ :error="!!errorMessages.age"
162
+ :success="fieldsStates.age.valid"
163
+ :class="{ 'has-error': !!errorMessages.age }"
164
+ />
165
+ <MazSelect
166
+ v-model="model.country"
167
+ :options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
168
+ label="Select your nationality (required)"
169
+ :hint="errorMessages.country"
170
+ :error="!!errorMessages.country"
171
+ :success="fieldsStates.country.valid"
172
+ :class="{ 'has-error': !!errorMessages.country }"
173
+ />
174
+ <MazCheckbox
175
+ v-model="model.agree"
176
+ :hint="errorMessages.agree"
177
+ :error="fieldsStates.agree.error"
178
+ :success="fieldsStates.agree.valid"
179
+ :class="{ 'has-error': !!errorMessages.agree }"
180
+ >
181
+ I agree to the terms and conditions (required)
182
+ </MazCheckbox>
183
+ <MazBtn type="submit" :loading="isSubmitting">
184
+ Submit
185
+ </MazBtn>
186
+ </form>
187
+ <template #code>
188
+
189
+ ```vue
190
+ <script lang="ts" setup>
191
+ import { sleep } from 'maz-ui'
192
+ import { useFormValidator, useToast } from 'maz-ui/composables'
193
+ import { boolean, literal, maxValue, minLength, minValue, nonEmpty, number, pipe, string } from 'valibot'
194
+
195
+ const toast = useToast()
196
+
197
+ const schema = {
198
+ name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
199
+ age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
200
+ country: pipe(string('Country is required'), nonEmpty('Country is required')),
201
+ agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
202
+ }
203
+
204
+ const { model, isSubmitting, handleSubmit, errorMessages, fieldsStates } = useFormValidator<typeof schema>({
205
+ schema,
206
+ defaultValues: { name: 'John Doe', age: 10 },
207
+ options: { mode: 'lazy', scrollToError: '.has-error' },
208
+ })
209
+
210
+ const onSubmit = handleSubmit(async (formData) => {
211
+ // Form submission logic
212
+ console.log(formData)
213
+ await sleep(2000)
214
+ toast.success('Form submitted', { position: 'top' })
215
+ })
216
+ </script>
217
+
218
+ <template>
219
+ <form @submit="onSubmit">
220
+ <MazInput
221
+ v-model="model.name"
222
+ label="Enter your name"
223
+ :hint="errorMessages.name"
224
+ :error="!!errorMessages.name"
225
+ :success="fieldsStates.name.valid"
226
+ :class="{ 'has-error': !!errorMessages.name }"
227
+ />
228
+ <MazInput
229
+ v-model="model.age"
230
+ type="number"
231
+ label="Enter your age"
232
+ :hint="errorMessages.age"
233
+ :error="!!errorMessages.age"
234
+ :success="fieldsStates.age.valid"
235
+ :class="{ 'has-error': !!errorMessages.age }"
236
+ />
237
+ <MazSelect
238
+ v-model="model.country"
239
+ :options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
240
+ label="Select your nationality"
241
+ :hint="errorMessages.country"
242
+ :error="!!errorMessages.country"
243
+ :success="fieldsStates.country.valid"
244
+ :class="{ 'has-error': !!errorMessages.country }"
245
+ />
246
+ <MazCheckbox
247
+ v-model="model.agree"
248
+ :hint="errorMessages.agree"
249
+ :error="fieldsStates.agree.error"
250
+ :success="fieldsStates.agree.valid"
251
+ :class="{ 'has-error': !!errorMessages.agree }"
252
+ >
253
+ I agree to the terms and conditions
254
+ </MazCheckbox>
255
+ <MazBtn type="submit" :loading="isSubmitting">
256
+ Submit
257
+ </MazBtn>
258
+ </form>
259
+ </template>
260
+ ```
261
+
262
+ </template>
263
+ </ComponentDemo>
264
+
265
+ ## Usage with useFormField
266
+
267
+ In this example, we will use the `useFormField` composable to handle the validation of each field individually.
268
+
269
+ ### Eager mode
270
+
271
+ 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.
272
+
273
+ <ComponentDemo>
274
+ <form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitEager">
275
+ <MazInput
276
+ v-model="name"
277
+ ref="nameRef"
278
+ label="Enter your name"
279
+ :hint="nameErrorMessage"
280
+ :error="hasErrorName"
281
+ :class="{ 'has-error-form2': hasErrorName }"
282
+ />
283
+ <MazInput
284
+ v-model="age"
285
+ ref="ageRef"
286
+ type="number"
287
+ label="Enter your age"
288
+ :hint="ageErrorMessage"
289
+ :error="hasErrorAge"
290
+ :class="{ 'has-error-form2': hasErrorAge }"
291
+ />
292
+ <MazSelect
293
+ v-model="country"
294
+ ref="countryRef"
295
+ :options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
296
+ label="Select your nationality"
297
+ :hint="countryErrorMessage"
298
+ :error="hasErrorCountry"
299
+ :class="{ 'has-error-form2': hasErrorCountry }"
300
+ />
301
+ <MazCheckbox
302
+ v-model="agree"
303
+ ref="agreeRef"
304
+ :hint="agreeErrorMessage"
305
+ :error="hasErrorAgree"
306
+ :class="{ 'has-error': hasErrorAgree }"
307
+ >
308
+ I agree to the terms and conditions
309
+ </MazCheckbox>
310
+ <MazBtn type="submit" :loading="isSubmittingEager">
311
+ Submit
312
+ </MazBtn>
313
+ </form>
314
+
315
+ <template #code>
316
+
317
+ ```vue
318
+ <script setup lang="ts">
319
+ import { sleep } from 'maz-ui'
320
+ import { useFormField, useFormValidator, useToast } from 'maz-ui/composables'
321
+ import { boolean, literal, maxValue, minLength, minValue, nonEmpty, number, pipe, string } from 'valibot'
322
+ import { useTemplateRef } from 'vue'
323
+
324
+ const schema = {
325
+ name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
326
+ age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
327
+ country: pipe(string('Country is required'), nonEmpty('Country is required')),
328
+ agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
329
+ }
330
+
331
+ const { isSubmitting, handleSubmit } = useFormValidator<typeof schema>({
332
+ schema,
333
+ options: { mode: 'eager', scrollToError: '.has-error-form2', identifier: 'form-eager' },
334
+ })
335
+
336
+ const { value: name, hasError: hasErrorName, errorMessage: nameErrorMessage } = useFormField<string>('name', {
337
+ ref: useTemplateRef('nameRef'),
338
+ formIdentifier: 'form-eager',
339
+ })
340
+ const { value: age, hasError: hasErrorAge, errorMessage: ageErrorMessage } = useFormField<number>('age', {
341
+ ref: useTemplateRef('ageRef'),
342
+ formIdentifier: 'form-eager',
343
+ })
344
+ const { value: agree, hasError: hasErrorAgree, errorMessage: agreeErrorMessage } = useFormField<boolean>('agree', {
345
+ ref: useTemplateRef('agreeRef'),
346
+ formIdentifier: 'form-eager'
347
+ })
348
+ const { value: country, hasError: hasErrorCountry, errorMessage: countryErrorMessage, validationEvents } = useFormField<string>('country', {
349
+ mode: 'lazy',
350
+ formIdentifier: 'form-eager'
351
+ })
352
+
353
+ const onSubmit = handleSubmit(async (formData) => {
354
+ // Form submission logic
355
+ console.log(formData)
356
+ await sleep(2000)
357
+ toast.success('Form submitted', { position: 'top' })
358
+ })
359
+ </script>
360
+
361
+ <template>
362
+ <form @submit="onSubmit">
363
+ <MazInput
364
+ ref="nameRef"
365
+ v-model="name"
366
+ label="Enter your name"
367
+ :hint="nameErrorMessage"
368
+ :error="hasErrorName"
369
+ :class="{ 'has-error-form2': hasErrorName }"
370
+ />
371
+ <MazInput
372
+ ref="ageRef"
373
+ v-model="age"
374
+ type="number"
375
+ label="Enter your age"
376
+ :hint="ageErrorMessage"
377
+ :error="hasErrorAge"
378
+ :class="{ 'has-error-form2': hasErrorAge }"
379
+ />
380
+ <MazSelect
381
+ ref="countryRef"
382
+ v-model="country"
383
+ :options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
384
+ label="Select your nationality"
385
+ :hint="countryErrorMessage"
386
+ :error="hasErrorCountry"
387
+ :class="{ 'has-error-form2': hasErrorCountry }"
388
+ />
389
+ <MazCheckbox
390
+ ref="agreeRef"
391
+ v-model="agree"
392
+ :hint="agreeErrorMessage"
393
+ :error="hasErrorAgree"
394
+ :class="{ 'has-error': hasErrorAgree }"
395
+ >
396
+ I agree to the terms and conditions
397
+ </MazCheckbox>
398
+ <MazBtn type="submit" :loading="isSubmitting">
399
+ Submit
400
+ </MazBtn>
401
+ </form>
402
+ </template>
403
+ ```
404
+
405
+ </template>
406
+ </ComponentDemo>
407
+
408
+ ### Progressive mode
409
+
410
+ 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.
411
+
412
+ <ComponentDemo>
413
+ <form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitProgressive">
414
+ <MazInput
415
+ v-model="nameProgressive"
416
+ ref="nameProgressiveRef"
417
+ label="Enter your name"
418
+ :hint="nameMessageProgressive"
419
+ :error="!!nameMessageProgressive"
420
+ :success="nameValidProgressive"
421
+ :class="{ 'has-error-progressive': !!nameMessageProgressive }"
422
+ />
423
+ <MazInput
424
+ v-model="ageProgressive"
425
+ ref="ageProgressiveRef"
426
+ type="number"
427
+ label="Enter your age"
428
+ :hint="ageMessageProgressive"
429
+ :error="!!ageMessageProgressive"
430
+ :success="ageValidProgressive"
431
+ :class="{ 'has-error-progressive': !!ageMessageProgressive }"
432
+ />
433
+ <MazSelect
434
+ v-model="countryProgressive"
435
+ ref="countryProgressiveRef"
436
+ :options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
437
+ label="Select your nationality"
438
+ :hint="countryMessageProgressive"
439
+ :error="!!countryMessageProgressive"
440
+ :success="countryValidProgressive"
441
+ :class="{ 'has-error-progressive': !!countryErrorProgressive }"
442
+ />
443
+ <MazCheckbox
444
+ v-model="agreeProgressive"
445
+ ref="agreeProgressiveRef"
446
+ :hint="agreeMessageProgressive"
447
+ :error="!!agreeMessageProgressive"
448
+ :class="{ 'has-error-progressive': !!agreeMessageProgressive }"
449
+ >
450
+ I agree to the terms and conditions
451
+ </MazCheckbox>
452
+ <MazBtn type="submit" :loading="isSubmittingProgressive">
453
+ Submit
454
+ </MazBtn>
455
+ </form>
456
+
457
+ <template #code>
458
+
459
+ ```vue
460
+ <script setup lang="ts">
461
+ import { sleep } from 'maz-ui'
462
+ import { useFormField, useFormValidator, useToast } from 'maz-ui/composables'
463
+ import { boolean, literal, maxValue, minLength, minValue, nonEmpty, number, pipe, string } from 'valibot'
464
+ import { useTemplateRef } from 'vue'
465
+
466
+ const schema = {
467
+ name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
468
+ age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
469
+ country: pipe(string('Country is required'), nonEmpty('Country is required')),
470
+ agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
471
+ }
472
+
473
+ const { isSubmitting, handleSubmit } = useFormValidator<typeof schema>({
474
+ schema,
475
+ options: { mode: 'progressive', scrollToError: '.has-error-progressive', identifier: 'form-progressive' },
476
+ })
477
+
478
+ const { value: name, hasError: nameHasError, errorMessage: nameErrorMessage } = useFormField<string>('name', {
479
+ ref: useTemplateRef('nameRef'),
480
+ formIdentifier: 'form-progressive',
481
+ })
482
+ const { value: age, hasError: ageHasError, errorMessage: ageErrorMessage } = useFormField<number>('age', {
483
+ ref: useTemplateRef('ageRef'),
484
+ formIdentifier: 'form-progressive',
485
+ })
486
+ const { value: country, hasError: countryHasError, errorMessage: countryErrorMessage, validationEvents } = useFormField<string>('country', {
487
+ mode: 'lazy',
488
+ formIdentifier: 'form-progressive',
489
+ })
490
+ const { value: agree, hasError: agreeHasError, errorMessage: agreeErrorMessage } = useFormField<boolean>('agree', {
491
+ ref: useTemplateRef('agreeRef'),
492
+ formIdentifier: 'form-progressive',
493
+ })
494
+
495
+ const onSubmit = handleSubmit(async (formData) => {
496
+ // Form submission logic
497
+ console.log(formData)
498
+ await sleep(2000)
499
+ toast.success('Form submitted', { position: 'top' })
500
+ })
501
+ </script>
502
+
503
+ <template>
504
+ <form @submit="onSubmit">
505
+ <MazInput
506
+ ref="nameRef"
507
+ v-model="name"
508
+ label="Enter your name"
509
+ :hint="nameErrorMessage"
510
+ :error="nameHasError"
511
+ :class="{ 'has-error-progressive': nameHasError }"
512
+ />
513
+ <MazInput
514
+ ref="ageRef"
515
+ v-model="age"
516
+ type="number"
517
+ label="Enter your age"
518
+ :hint="ageErrorMessage"
519
+ :error="ageHasError"
520
+ :class="{ 'has-error-progressive': ageHasError }"
521
+ />
522
+ <MazSelect
523
+ v-model="country"
524
+ v-bind="validationEvents"
525
+ :options="[{ label: 'France', value: 'FR' }, { label: 'United States', value: 'US' }]"
526
+ label="Select your nationality"
527
+ :hint="countryErrorMessage"
528
+ :error="countryHasError"
529
+ :class="{ 'has-error-progressive': countryHasError }"
530
+ />
531
+ <MazCheckbox
532
+ ref="agreeRef"
533
+ v-model="agree"
534
+ :hint="agreeErrorMessage"
535
+ :error="agreeHasError"
536
+ :class="{ 'has-error-progressive': agreeHasError }"
537
+ >
538
+ I agree to the terms and conditions
539
+ </MazCheckbox>
540
+ <MazBtn type="submit" :loading="isSubmitting">
541
+ Submit
542
+ </MazBtn>
543
+ </form>
544
+ </template>
545
+ ```
546
+
547
+ </template>
548
+ </ComponentDemo>
549
+
550
+ ## Throlling and Debouncing
551
+
552
+ You can use the `throttledFields` and `debouncedFields` options to throttle or debounce the validation of specific fields.
553
+
554
+ The fields are validated with throttling or debouncing to avoid spamming the server or to wait for the user to finish typing before validating.
555
+
556
+ You can set the throttle or debounce time in milliseconds or use `true` for the default throttle time (1000ms) or debounce time (300ms).
557
+
558
+ <ComponentDemo>
559
+ <form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmitDebounced">
560
+ <MazInput
561
+ v-model="modelDebounced.name"
562
+ label="Enter your name"
563
+ :hint="errorMessagesDebounced.name"
564
+ :error="fieldsStatesDebounced.name.error"
565
+ :success="fieldsStatesDebounced.name.valid"
566
+ :class="{ 'has-error-debounced': fieldsStatesDebounced.name.error }"
567
+ />
568
+ <MazInput
569
+ v-model="modelDebounced.age"
570
+ type="number"
571
+ label="Enter your age"
572
+ :hint="errorMessagesDebounced.age"
573
+ :error="fieldsStatesDebounced.age.error"
574
+ :success="fieldsStatesDebounced.age.valid"
575
+ :class="{ 'has-error-debounced': fieldsStatesDebounced.age.error }"
576
+ />
577
+ <MazBtn type="submit" :loading="isSubmittingDebounced">
578
+ Submit
579
+ </MazBtn>
580
+ </form>
581
+
582
+ <template #code>
583
+
584
+ ```vue{37,38}
585
+ <template>
586
+ <form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmit">
587
+ <MazInput
588
+ v-model="model.name"
589
+ label="Enter your name"
590
+ :hint="errorMessages.name"
591
+ :error="fieldsStates.name.error"
592
+ :success="fieldsStates.name.valid"
593
+ :class="{ 'has-error-debounced': fieldsStates.name.error }"
594
+ />
595
+ <MazInput
596
+ v-model="model.age"
597
+ type="number"
598
+ label="Enter your age"
599
+ :hint="errorMessages.age"
600
+ :error="fieldsStates.age.error"
601
+ :success="fieldsStates.age.valid"
602
+ :class="{ 'has-error-debounced': fieldsStates.age.error }"
603
+ />
604
+ <MazBtn type="submit" :loading="isSubmitting">
605
+ Submit
606
+ </MazBtn>
607
+ </form>
608
+ </template>
609
+
610
+ <script setup lang="ts">
611
+ import { sleep } from 'maz-ui'
612
+ import { useFormValidator, useToast, InferFormValidatorSchema } from 'maz-ui/composables'
613
+ import { string, nonEmpty, pipe, number, minValue, minLength } from 'valibot'
614
+
615
+ const { model, fieldsStates, isValid, isSubmitting, errorMessages, handleSubmit } = useFormValidator({
616
+ schema: {
617
+ name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
618
+ age: pipe(number('Age is required'), nonEmpty('Age is required'), minValue(18, 'Age must be greater than 18')),
619
+ },
620
+ options: {
621
+ debouncedFields: { name: 500 },
622
+ throttledFields: { age: true },
623
+ scrollToError: '.has-error-debounced',
624
+ },
625
+ })
626
+
627
+ const onSubmit = handleSubmit(async (formData) => {
628
+ // Form submission logic
629
+ console.log(formData)
630
+ await sleep(2000)
631
+ toast.success('Form submitted', { position: 'top' })
632
+ })
633
+ </script>
634
+ ```
635
+
636
+ </template>
637
+ </ComponentDemo>
638
+
639
+ ## Validation with async function
640
+
641
+ You can use async functions in the validation schema.
642
+
643
+ <ComponentDemo>
644
+ <form class="maz-flex maz-gap-4" @submit="onSubmitAsync">
645
+ <MazInput
646
+ v-model="modelAsync.name"
647
+ label="Enter your name"
648
+ ref="nameAsyncRef"
649
+ v-bind="validationEventsAsync"
650
+ :hint="errorMessagesAsync.name"
651
+ :error="fieldsStatesAsync.name.error"
652
+ :success="fieldsStatesAsync.name.valid"
653
+ :loading="fieldsStatesAsync.name.validating"
654
+ :class="{ 'has-error-async': fieldsStatesAsync.name.error }"
655
+ />
656
+ <MazBtn type="submit" :loading="isSubmittingAsync">
657
+ Submit
658
+ </MazBtn>
659
+ </form>
660
+
661
+ <template #code>
662
+
663
+ ```vue
664
+ <template>
665
+ <form class="maz-flex maz-flex-col maz-gap-4" @submit="onSubmit">
666
+ <MazInput
667
+ v-model="model.name"
668
+ label="Enter your name"
669
+ ref="nameRef"
670
+ v-bind="validationEvents"
671
+ :hint="errorMessages.name"
672
+ :error="fieldsStates.name.error"
673
+ :success="fieldsStates.name.valid"
674
+ :loading="fieldsStates.name.validating"
675
+ :class="{ 'has-error-debounced': fieldsStates.name.error }"
676
+ />
677
+ <MazBtn type="submit" :loading="isSubmitting">
678
+ Submit
679
+ </MazBtn>
680
+ </form>
681
+ </template>
682
+
683
+ <script setup lang="ts">
684
+ import { sleep } from 'maz-ui'
685
+ import { useFormValidator, useToast, InferFormValidatorSchema } from 'maz-ui/composables'
686
+ import { string, nonEmpty, pipe, number, minValue, minLength, pipeAsync, checkAsync } from 'valibot'
687
+
688
+ const {
689
+ model,
690
+ fieldsStates,
691
+ isValid,
692
+ isSubmitting,
693
+ errorMessages,
694
+ handleSubmit,
695
+ } = useFormValidator({
696
+ schema: {
697
+ name: pipeAsync(
698
+ string('Name is required'),
699
+ nonEmpty('Name is required'),
700
+ minLength(3, 'Name must be at least 3 characters'),
701
+ checkAsync(
702
+ async (name) => {
703
+ console.log('name', name)
704
+ await sleep(2000)
705
+ return false
706
+ },
707
+ 'Name is already taken',
708
+ )),
709
+ },
710
+ options: { mode: 'eager', scrollToError: '.has-error-async', identifier: 'form-async' },
711
+ })
712
+
713
+ const onSubmit = handleSubmit((formData) => {
714
+ // Form submission logic
715
+ console.log(formData)
716
+ toast.success('Form submitted', { position: 'top' })
717
+ })
718
+ </script>
719
+ ```
720
+
721
+ </template>
722
+
723
+ </ComponentDemo>
724
+
725
+ ## useFormValidator
726
+
727
+ `useFormValidator` is the main composable for initializing form validation.
728
+
729
+ 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.
730
+
731
+ ### Parameters
732
+
733
+ `useFormValidator<TSchema>` accepts an object with the following properties:
734
+
735
+ - `schema`: `TSchema` - The Valibot validation schema for the form.
736
+ - `model`: `Ref<Model>` (optional) - A reference to the form's data model.
737
+ - `defaultValues`: `DeepPartial<Model>` (optional) - Default values for the form fields.
738
+ - `options`: `FormValidatorOptions` (optional) - Configuration options for the form validation behavior.
739
+ - `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)
740
+ - `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).
741
+ - `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).
742
+ - `scrollToError`: `string | false` (optional) - Disable or provide a CSS selector for scrolling to errors (default '.has-field-error')
743
+ - `identifier`: `string | symbol` (optional) - Identifier for the form (useful when you have multiple forms on the same component)
744
+
745
+ ### Return
746
+
747
+ `useFormValidator` returns an object containing:
748
+
749
+ - `isDirty`: `ComputedRef<boolean>` - Indicates if the form has been modified.
750
+ - `isSubmitting`: `Ref<boolean>` - Indicates if the form is currently being submitted.
751
+ - `isSubmitted`: `Ref<boolean>` - Indicates if the form has been submitted.
752
+ - `isValid`: `ComputedRef<boolean>` - Indicates if the form is valid.
753
+ - `errors`: `ComputedRef<Record<ModelKey, ValidationIssues>>` - Validation errors for each field.
754
+ - `errorsMessages`: `ComputedRef<Record<string, string>>` - The first validation error message for each field.
755
+ - `model`: `Ref<Model>` - The form's data model.
756
+ - `fieldsStates`: `FieldsStates` - The validation state of each field.
757
+ - `validateForm`: `(setErrors?: boolean) => Promise<boolean>` - Function to validate the entire form.
758
+ - `scrollToError`: `(selector?: string, options?: { offset?: number }) => void` - Function to scroll to the first field with an error.
759
+ - `handleSubmit`: `successCallback: (model: Model) => Promise<unknown> | unknown, scrollToError?: false | string` - 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').
760
+
761
+ ## useFormField
762
+
763
+ ::: warning
764
+ Before using `useFormField`, make sure you have initialized the form with `useFormValidator`.
765
+ :::
766
+
767
+ `useFormField` is a composable for handling validation at the individual form field level.
768
+
769
+ Useful for fine-grained control over form fields, `useFormField` provides computed properties for validation state, error messages, and more.
770
+ Can be very useful when you are using fields in child components of form.
771
+
772
+ To use the modes `eager`, `progressive` or `blur`, you must use this `useFormField` composable to add the [necessary validation events](#introduction).
773
+
774
+ ### Parameters
775
+
776
+ `useFormField<T>` takes the following parameters:
777
+
778
+ - `name`: `string` - The name of the field in the validation schema (must be a key from the schema).
779
+ - `options`: `FormFieldOptions<T>` (optional) - Field-specific options.
780
+ - `defaultValue`: `T` (optional) - The default value of the field.
781
+ - `mode`: `'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'` (optional) - The validation mode for the field - [see validation modes](#introduction)
782
+ - `ref`: `Ref<HTMLElement | ComponentInstance>` (optional) - Vue ref to the component/element for automatic event binding - use `useTemplateRef()` for type safety
783
+ - `formIdentifier`: `string | symbol` (optional) - Identifier for the form (must match the one used in `useFormValidator`)
784
+
785
+ ### Return
786
+
787
+ `useFormField` returns an object containing:
788
+
789
+ - `errors`: `ComputedRef<ValidationIssues>` - Validation errors for this field.
790
+ - `errorMessage`: `ComputedRef<string>` - The first validation error message.
791
+ - `isValid`: `ComputedRef<boolean>` - Indicates if the field is valid.
792
+ - `isDirty`: `ComputedRef<boolean>` - Indicates if the field has been modified.
793
+ - `isBlurred`: `ComputedRef<boolean>` - Indicates if the field has lost focus.
794
+ - `hasError`: `ComputedRef<boolean>` - Indicates if the field has errors.
795
+ - `isValidated`: `ComputedRef<boolean>` - Indicates if the field has been validated.
796
+ - `isValidating`: `ComputedRef<boolean>` - Indicates if the field is currently being validated.
797
+ - `mode`: `ComputedRef<StrictOptions['mode']>` - The validation mode for the field.
798
+ - `value`: `WritableComputedRef<T>` - The reactive value of the field with proper TypeScript typing.
799
+ - `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.
800
+
801
+ ## Recent Improvements (v4.0.0)
802
+
803
+ ### 🚀 Enhanced Type Safety
804
+
805
+ - **Automatic schema inference**: Use `typeof schema` for precise TypeScript types
806
+ - **Field-level type safety**: `useFormField<T>` provides exact field types
807
+ - **Improved reactivity**: Optimized watchers with better performance and memory management
808
+
809
+ ### 🎯 Better Interactive Element Detection
810
+
811
+ The `ref` option in `useFormField` now automatically detects and binds events to:
812
+ - Standard form elements: `input`, `select`, `textarea`, `button`
813
+ - Focusable elements: links with `href`, elements with `tabindex`
814
+ - ARIA interactive elements: `role="button"`, `role="textbox"`, etc.
815
+ - Custom interactive elements: `data-interactive`, `data-clickable`, `.interactive`
816
+
817
+ ### 🔧 Improved Memory Management
818
+
819
+ - Automatic cleanup of event listeners to prevent memory leaks
820
+ - WeakMap-based tracking for better garbage collection
821
+ - Race condition protection in async validation
822
+
823
+ ### 📝 Better Development Experience
824
+
825
+ - More informative warning messages
826
+ - Improved error handling and validation states
827
+ - Enhanced debugging capabilities
828
+
829
+ ## Performance & Best Practices
830
+
831
+ ### 🚀 Performance Tips
832
+
833
+ - **Use `throttledFields` or `debouncedFields`** for expensive validations or network requests
834
+ - **Prefer `eager` or `progressive` modes** for better UX instead of `aggressive`
835
+ - **Use `lazy` mode** for simple forms with minimal validation
836
+ - **Leverage TypeScript**: Always use `typeof schema` for automatic type inference
837
+
838
+ ### 💡 Common Patterns
839
+
840
+ #### Multiple Forms on Same Page
841
+
842
+ ```ts
843
+ const form1 = useFormValidator<typeof schema1>({
844
+ schema: schema1,
845
+ options: { identifier: 'form-1' }
846
+ })
847
+
848
+ const form2 = useFormValidator<typeof schema2>({
849
+ schema: schema2,
850
+ options: { identifier: 'form-2' }
851
+ })
852
+
853
+ // Use matching identifiers in useFormField
854
+ const { value } = useFormField<string>('name', {
855
+ formIdentifier: 'form-1'
856
+ })
857
+ ```
858
+
859
+ #### Custom Interactive Elements
860
+
861
+ ```vue
862
+ <template>
863
+ <!-- Add data-interactive for custom components -->
864
+ <div data-interactive class="custom-input" tabindex="0">
865
+ Custom Input
866
+ </div>
867
+ </template>
868
+ ```
869
+
870
+ ### ⚠️ Common Pitfalls
871
+
872
+ - **Mismatched form identifiers**: Ensure `useFormField` uses the same `formIdentifier` as `useFormValidator`
873
+ - **Missing refs for interactive modes**: `eager`, `blur`, and `progressive` modes require either `ref` or `validationEvents`
874
+ - **Incorrect TypeScript generics**: Always specify both schema and field name: `useFormField<T>`
875
+
876
+ ## Troubleshooting
877
+
878
+ ### Type Errors
879
+
880
+ **Problem**: `WritableComputedRef<string | number | boolean | undefined>`
881
+
882
+ ```ts
883
+ // ❌ Wrong - loses type precision
884
+ const { value } = useFormField('name')
885
+
886
+ // ✅ Correct - precise typing
887
+ const { value } = useFormField<string>('name')
888
+ ```
889
+
890
+ ### Using `useTemplateRef` with `useFormField` cause TypeScript errors
891
+
892
+ **Cause:** `useTemplateRef` can create TypeScript circular references when the destructured variable name resembles the template ref name.
893
+
894
+ If you encounter TypeScript errors when using `useFormField` with `useTemplateRef`, use classic `ref()` instead:
895
+
896
+ ```typescript
897
+ // ❌ May cause TypeScript errors
898
+ const { value: email } = useFormField<string>('email', {
899
+ ref: useTemplateRef('emailRef'),
900
+ })
901
+
902
+ // ✅ Correct - precise typing
903
+ const { value: email } = useFormField<string>('email', {
904
+ ref: useTemplateRef<string>('emailRef'),
905
+ })
906
+
907
+ // ✅ Use classic `ref()` instead
908
+ const emailRef = ref<HTMLInputElement>()
909
+ const { value: email } = useFormField<string>('email', {
910
+ ref: emailRef,
911
+ })
912
+ ```
913
+
914
+ ### Validation Not Triggering
915
+
916
+ **Problem**: Field validation doesn't work with `eager`/`blur`/`progressive` modes
917
+
918
+ ```ts
919
+ // ❌ Missing ref or validation events
920
+ const { value } = useFormField<string>('name')
921
+
922
+ // ✅ Use ref for automatic detection
923
+ const { value } = useFormField<string>('name', {
924
+ ref: useTemplateRef('inputRef')
925
+ })
926
+
927
+ // ✅ Or use validation events manually
928
+ const { value, validationEvents } = useFormField<string>('name')
929
+ // Then: v-bind="validationEvents" on your component
930
+ ```
931
+
932
+ ### Element Not Found Warning
933
+
934
+ **Problem**: `No element found for ref in field 'name'`
935
+
936
+ **Solutions**:
937
+ 1. Ensure the ref is properly bound to an HTML element or Vue component
938
+ 2. Make sure the component has a `$el` property if it's a Vue component
939
+ 3. Use `data-interactive` attribute for custom interactive elements
940
+
941
+ ## Types
942
+
943
+ ### FormValidatorOptions
944
+
945
+ ```ts
946
+ interface FormValidatorOptions {
947
+ /**
948
+ * Validation mode
949
+ * - lazy: validate on input value change
950
+ * - aggressive: validate all fields immediately on form creation and on input value change
951
+ * - blur: validate on blur
952
+ * - eager: validate on blur at first (only if the field is not empty) and then on input value change
953
+ * - 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.
954
+ * @default 'lazy'
955
+ */
956
+ mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
957
+ /**
958
+ * Fields to validate with throttling
959
+ * Useful for fields that require a network request to avoid spamming the server
960
+ * @example { name: 1000 } or { name: true } for the default throttle time (1000ms)
961
+ */
962
+ throttledFields?: Partial<Record<ModelKey, number | true>>
963
+ /**
964
+ * Fields to validate with debouncing
965
+ * Useful to wait for the user to finish typing before validating
966
+ * Useful for fields that require a network request to avoid spamming the server
967
+ * @example { name: 300 } or { name: true } for the default debounce time (300ms)
968
+ */
969
+ debouncedFields?: Partial<Record<ModelKey, number | true>>
970
+ /**
971
+ * Scroll to the first error found
972
+ * @default '.has-field-error'
973
+ */
974
+ scrollToError?: string | false
975
+ /**
976
+ * Identifier to use for the form
977
+ * Useful to have multiple forms on the same page
978
+ * @default `main-form-validator`
979
+ */
980
+ identifier?: string | symbol
981
+ }
982
+ ```
983
+
984
+ ### FormFieldOptions
985
+
986
+ ```ts
987
+ interface FormFieldOptions<T> {
988
+ /**
989
+ * Default value of the field
990
+ * @default undefined
991
+ */
992
+ defaultValue?: T
993
+ /**
994
+ * Validation mode
995
+ * To override the form validation mode
996
+ */
997
+ mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
998
+ /**
999
+ * Vue ref to the component or HTML element for automatic event binding
1000
+ * Use useTemplateRef() for type safety
1001
+ * Automatically detects interactive elements (input, select, textarea, button, ARIA elements, etc.)
1002
+ * Necessary for 'eager', 'progressive' and 'blur' validation modes
1003
+ */
1004
+ ref?: Ref<HTMLElement | ComponentInstance>
1005
+ /**
1006
+ * Identifier for the form
1007
+ * Useful when you have multiple forms on the same component
1008
+ * Should be the same as the one used in `useFormValidator`
1009
+ */
1010
+ formIdentifier?: string | symbol
1011
+ }
1012
+ ```
1013
+
1014
+ <script lang="ts" setup>
1015
+ import { ref, useTemplateRef } from 'vue'
1016
+ import { useFormValidator } from 'maz-ui/src/composables/useFormValidator'
1017
+ import { useFormField } from 'maz-ui/src/composables/useFormField'
1018
+ import { useToast } from 'maz-ui/src/composables/useToast'
1019
+ import { sleep } from 'maz-ui'
1020
+ import { string, nonEmpty, pipe, number, minValue, maxValue, boolean, literal, minLength, pipeAsync, checkAsync } from 'valibot'
1021
+
1022
+ const toast = useToast()
1023
+
1024
+ const schema = ref({
1025
+ name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
1026
+ age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
1027
+ country: pipe(string('Country is required'), nonEmpty('Country is required')),
1028
+ agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
1029
+ })
1030
+
1031
+ const { model, isValid, isSubmitting, isDirty, isSubmitted, handleSubmit, errorMessages, fieldsStates } = useFormValidator<typeof schema>({
1032
+ schema,
1033
+ defaultValues: { name: 'John Doe', age: 10 },
1034
+ options: { mode: 'lazy', scrollToError: '.has-error' },
1035
+ })
1036
+
1037
+ const onSubmit = handleSubmit(async (formData) => {
1038
+ // Form submission logic
1039
+ console.log(formData)
1040
+ await sleep(2000)
1041
+ toast.success('Form submitted', { position: 'top' })
1042
+ })
1043
+
1044
+ const eagerSchema = {
1045
+ name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
1046
+ age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
1047
+ country: pipe(string('Country is required'), nonEmpty('Country is required')),
1048
+ agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
1049
+ }
1050
+
1051
+ const { isValid: isValidEager, isSubmitting: isSubmittingEager, handleSubmit: handleSubmitEager } = useFormValidator<typeof eagerSchema>({
1052
+ schema: eagerSchema,
1053
+ options: { mode: 'eager', scrollToError: '.has-error-form2', identifier: 'form-eager' },
1054
+ })
1055
+
1056
+ const { value: name, hasError: hasErrorName, errorMessage: nameErrorMessage } = useFormField<string>('name', { ref: useTemplateRef('nameRef'), formIdentifier: 'form-eager' })
1057
+ const { value: age, hasError: hasErrorAge, errorMessage: ageErrorMessage } = useFormField<number>('age', { ref: useTemplateRef('ageRef'), formIdentifier: 'form-eager' })
1058
+ const { value: country, hasError: hasErrorCountry, errorMessage: countryErrorMessage, validationEvents } = useFormField<string>('country', { mode: 'lazy', formIdentifier: 'form-eager' })
1059
+ const { value: agree, hasError: hasErrorAgree, errorMessage: agreeErrorMessage } = useFormField<boolean>('agree', { ref: useTemplateRef('agreeRef'), formIdentifier: 'form-eager' })
1060
+
1061
+ const onSubmitEager = handleSubmitEager(async (formData) => {
1062
+ // Form submission logic
1063
+ console.log(formData)
1064
+ await sleep(2000)
1065
+ toast.success('Form submitted', { position: 'top' })
1066
+ })
1067
+
1068
+ const { isValid: isValidProgressive, isSubmitting: isSubmittingProgressive, handleSubmit: handleSubmitProgressive } = useFormValidator<Model>({
1069
+ schema: {
1070
+ name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
1071
+ age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
1072
+ country: pipe(string('Country is required'), nonEmpty('Country is required')),
1073
+ agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
1074
+ },
1075
+ options: { mode: 'progressive', scrollToError: '.has-error-progressive', identifier: 'form-progressive' },
1076
+ })
1077
+
1078
+ const progressiveSchema = {
1079
+ name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
1080
+ age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
1081
+ country: pipe(string('Country is required'), nonEmpty('Country is required')),
1082
+ agree: pipe(boolean('You must agree to the terms and conditions'), literal(true, 'You must agree to the terms and conditions')),
1083
+ }
1084
+
1085
+ const { value: nameProgressive, isValid: nameValidProgressive, hasError: nameErrorProgressive, errorMessage: nameMessageProgressive } = useFormField<string>('name', { ref: useTemplateRef('nameProgressiveRef'), formIdentifier: 'form-progressive' })
1086
+ const { value: ageProgressive, isValid: ageValidProgressive, hasError: ageErrorProgressive, errorMessage: ageMessageProgressive } = useFormField<number>('age', { ref: useTemplateRef('ageProgressiveRef'), formIdentifier: 'form-progressive' })
1087
+ const { value: countryProgressive, isValid: countryValidProgressive, hasError: countryErrorProgressive, errorMessage: countryMessageProgressive, validationEventsProgressive } = useFormField<string>('country', { ref: useTemplateRef('countryProgressiveRef'), formIdentifier: 'form-progressive' })
1088
+ const { value: agreeProgressive, isValid: agreeValidProgressive, hasError: agreeErrorProgressive, errorMessage: agreeMessageProgressive } = useFormField<boolean>('agree', { ref: useTemplateRef('agreeProgressiveRef'), formIdentifier: 'form-progressive' })
1089
+
1090
+ const onSubmitProgressive = handleSubmitProgressive(async (formData) => {
1091
+ // Form submission logic
1092
+ console.log(formData)
1093
+ await sleep(2000)
1094
+ toast.success('Form submitted', { position: 'top' })
1095
+ })
1096
+
1097
+ const { model: modelDebounced, fieldsStates: fieldsStatesDebounced, isValid: isValidDebounced, isSubmitting: isSubmittingDebounced, errorMessages: errorMessagesDebounced, handleSubmit: handleSubmitDebounced } = useFormValidator({
1098
+ schema: {
1099
+ name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
1100
+ age: pipe(number('Age is required'), nonEmpty('Age is required'), minValue(18, 'Age must be greater than 18')),
1101
+ },
1102
+ options: {
1103
+ debouncedFields: { name: 500 },
1104
+ throttledFields: { age: true },
1105
+ scrollToError: '.has-error-debounced',
1106
+ },
1107
+ })
1108
+
1109
+ const onSubmitDebounced = handleSubmitDebounced(async (formData) => {
1110
+ // Form submission logic
1111
+ console.log(formData)
1112
+ await sleep(2000)
1113
+ toast.success(`Form submitted with ${JSON.stringify(formData)}`, { position: 'top' })
1114
+ })
1115
+
1116
+ const { model: modelAsync, fieldsStates: fieldsStatesAsync, isValid: isValidAsync, isSubmitting: isSubmittingAsync, errorMessages: errorMessagesAsync, handleSubmit: handleSubmitAsync } = useFormValidator({
1117
+ schema: {
1118
+ name: pipeAsync(
1119
+ string('Name is required'),
1120
+ nonEmpty('Name is required'),
1121
+ minLength(3, 'Name must be at least 3 characters'),
1122
+ checkAsync(
1123
+ async (name) => {
1124
+ console.log('name', name)
1125
+ await sleep(2000)
1126
+ return false
1127
+ },
1128
+ 'Name is already taken',
1129
+ )),
1130
+ },
1131
+ options: { mode: 'eager', scrollToError: '.has-error-async', identifier: 'form-async' },
1132
+ })
1133
+
1134
+ const {
1135
+ value: nameAsync,
1136
+ hasError: hasErrorNameAsync,
1137
+ errorMessage: nameErrorMessageAsync,
1138
+ validationEvents: validationEventsAsync,
1139
+ } = useFormField<string>('name', {
1140
+ ref: useTemplateRef('nameAsyncRef'),
1141
+ formIdentifier: 'form-async',
1142
+ })
1143
+
1144
+ const onSubmitAsync = handleSubmitAsync((formData) => {
1145
+ // Form submission logic
1146
+ console.log(formData)
1147
+ toast.success('Form submitted', { position: 'top' })
1148
+ })
1149
+ </script>