@velkymx/vibeui 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +48 -0
- package/dist/vibeui.css +1 -1
- package/dist/vibeui.es.js +149 -148
- package/dist/vibeui.umd.js +1 -1
- package/docs/README.md +153 -0
- package/docs/components/advanced/popover.md +150 -0
- package/docs/components/advanced/scrollspy.md +64 -0
- package/docs/components/advanced/tooltip.md +111 -0
- package/docs/components/card/card.md +215 -0
- package/docs/components/core/alert.md +72 -0
- package/docs/components/core/badge.md +81 -0
- package/docs/components/core/button-group.md +105 -0
- package/docs/components/core/button.md +127 -0
- package/docs/components/core/close-button.md +82 -0
- package/docs/components/core/link.md +36 -0
- package/docs/components/core/placeholder.md +135 -0
- package/docs/components/core/spinner.md +109 -0
- package/docs/components/data/datatable.md +416 -0
- package/docs/components/interactive/accordion.md +92 -0
- package/docs/components/interactive/carousel.md +97 -0
- package/docs/components/interactive/collapse.md +105 -0
- package/docs/components/interactive/dropdown.md +93 -0
- package/docs/components/interactive/modal.md +148 -0
- package/docs/components/interactive/offcanvas.md +89 -0
- package/docs/components/interactive/toast.md +114 -0
- package/docs/components/layout/col.md +123 -0
- package/docs/components/layout/container.md +59 -0
- package/docs/components/layout/row.md +113 -0
- package/docs/components/list/list-group.md +221 -0
- package/docs/components/navigation/breadcrumb.md +116 -0
- package/docs/components/navigation/nav.md +88 -0
- package/docs/components/navigation/navbar.md +106 -0
- package/docs/components/navigation/pagination.md +146 -0
- package/docs/components/progress/progress.md +182 -0
- package/docs/composables/back-button.md +28 -0
- package/docs/composables/breakpoints.md +54 -0
- package/docs/composables/color-mode.md +141 -0
- package/docs/forms/README.md +88 -0
- package/docs/forms/form-checkbox.md +50 -0
- package/docs/forms/form-datepicker.md +50 -0
- package/docs/forms/form-group.md +80 -0
- package/docs/forms/form-input.md +55 -0
- package/docs/forms/form-radio.md +58 -0
- package/docs/forms/form-select.md +54 -0
- package/docs/forms/form-spinbutton.md +55 -0
- package/docs/forms/form-switch.md +47 -0
- package/docs/forms/form-textarea.md +51 -0
- package/docs/forms/form-wysiwyg.md +64 -0
- package/docs/forms/input-group.md +51 -0
- package/docs/forms/validation.md +599 -0
- package/llms.txt +422 -0
- package/package.json +5 -2
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
# Form Validation
|
|
2
|
+
|
|
3
|
+
VibeUI provides a comprehensive validation system for all form components with support for both synchronous and asynchronous validation.
|
|
4
|
+
|
|
5
|
+
## Built-in Validators
|
|
6
|
+
|
|
7
|
+
### Import
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { validators } from '@velkymx/vibeui'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Available Validators
|
|
14
|
+
|
|
15
|
+
#### required()
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
validators.required(message?: string)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Ensures the field has a value.
|
|
22
|
+
|
|
23
|
+
```vue
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import { validators } from '@velkymx/vibeui'
|
|
26
|
+
|
|
27
|
+
const rules = [
|
|
28
|
+
validators.required('This field is required')
|
|
29
|
+
]
|
|
30
|
+
</script>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
#### email()
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
validators.email(message?: string)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Validates email format.
|
|
40
|
+
|
|
41
|
+
```vue
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
const rules = [
|
|
44
|
+
validators.email('Please enter a valid email address')
|
|
45
|
+
]
|
|
46
|
+
</script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### minLength()
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
validators.minLength(min: number, message?: string)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Validates minimum string length.
|
|
56
|
+
|
|
57
|
+
```vue
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
const rules = [
|
|
60
|
+
validators.minLength(8, 'Password must be at least 8 characters')
|
|
61
|
+
]
|
|
62
|
+
</script>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### maxLength()
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
validators.maxLength(max: number, message?: string)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Validates maximum string length.
|
|
72
|
+
|
|
73
|
+
```vue
|
|
74
|
+
<script setup lang="ts">
|
|
75
|
+
const rules = [
|
|
76
|
+
validators.maxLength(50, 'Username must not exceed 50 characters')
|
|
77
|
+
]
|
|
78
|
+
</script>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### min()
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
validators.min(min: number, message?: string)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Validates minimum numeric value.
|
|
88
|
+
|
|
89
|
+
```vue
|
|
90
|
+
<script setup lang="ts">
|
|
91
|
+
const rules = [
|
|
92
|
+
validators.min(18, 'You must be at least 18 years old')
|
|
93
|
+
]
|
|
94
|
+
</script>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### max()
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
validators.max(max: number, message?: string)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Validates maximum numeric value.
|
|
104
|
+
|
|
105
|
+
```vue
|
|
106
|
+
<script setup lang="ts">
|
|
107
|
+
const rules = [
|
|
108
|
+
validators.max(120, 'Please enter a valid age')
|
|
109
|
+
]
|
|
110
|
+
</script>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### pattern()
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
validators.pattern(regex: RegExp, message?: string)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Validates against a custom regex pattern.
|
|
120
|
+
|
|
121
|
+
```vue
|
|
122
|
+
<script setup lang="ts">
|
|
123
|
+
const rules = [
|
|
124
|
+
validators.pattern(/^\d{5}$/, 'ZIP code must be 5 digits'),
|
|
125
|
+
validators.pattern(/^[A-Z]/, 'Must start with uppercase letter')
|
|
126
|
+
]
|
|
127
|
+
</script>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### url()
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
validators.url(message?: string)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Validates URL format.
|
|
137
|
+
|
|
138
|
+
```vue
|
|
139
|
+
<script setup lang="ts">
|
|
140
|
+
const rules = [
|
|
141
|
+
validators.url('Please enter a valid URL')
|
|
142
|
+
]
|
|
143
|
+
</script>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### async()
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
validators.async(validatorFn: (value: any) => Promise<boolean | string>)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Creates a custom async validator for API validation.
|
|
153
|
+
|
|
154
|
+
```vue
|
|
155
|
+
<script setup lang="ts">
|
|
156
|
+
const checkUsername = validators.async(async (value) => {
|
|
157
|
+
const response = await fetch(`/api/check-username?username=${value}`)
|
|
158
|
+
const data = await response.json()
|
|
159
|
+
return data.available || 'Username is already taken'
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const rules = [
|
|
163
|
+
validators.required(),
|
|
164
|
+
checkUsername
|
|
165
|
+
]
|
|
166
|
+
</script>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Custom Validators
|
|
170
|
+
|
|
171
|
+
### Simple Custom Validator
|
|
172
|
+
|
|
173
|
+
```vue
|
|
174
|
+
<script setup lang="ts">
|
|
175
|
+
const customValidator = {
|
|
176
|
+
validator: (value) => {
|
|
177
|
+
return value === 'correct' || 'Value must be "correct"'
|
|
178
|
+
},
|
|
179
|
+
message: 'Value must be "correct"'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const rules = [customValidator]
|
|
183
|
+
</script>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Complex Custom Validator
|
|
187
|
+
|
|
188
|
+
```vue
|
|
189
|
+
<script setup lang="ts">
|
|
190
|
+
const passwordStrengthValidator = {
|
|
191
|
+
validator: (value) => {
|
|
192
|
+
if (!value) return true
|
|
193
|
+
|
|
194
|
+
const hasUpperCase = /[A-Z]/.test(value)
|
|
195
|
+
const hasLowerCase = /[a-z]/.test(value)
|
|
196
|
+
const hasNumber = /\d/.test(value)
|
|
197
|
+
const hasSpecialChar = /[!@#$%^&*]/.test(value)
|
|
198
|
+
|
|
199
|
+
if (!hasUpperCase) return 'Password must contain an uppercase letter'
|
|
200
|
+
if (!hasLowerCase) return 'Password must contain a lowercase letter'
|
|
201
|
+
if (!hasNumber) return 'Password must contain a number'
|
|
202
|
+
if (!hasSpecialChar) return 'Password must contain a special character'
|
|
203
|
+
|
|
204
|
+
return true
|
|
205
|
+
},
|
|
206
|
+
message: 'Password does not meet requirements'
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const rules = [
|
|
210
|
+
validators.required(),
|
|
211
|
+
validators.minLength(8),
|
|
212
|
+
passwordStrengthValidator
|
|
213
|
+
]
|
|
214
|
+
</script>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Using the Validation Composable
|
|
218
|
+
|
|
219
|
+
### Basic Usage
|
|
220
|
+
|
|
221
|
+
```vue
|
|
222
|
+
<script setup lang="ts">
|
|
223
|
+
import { useFormValidation, validators } from '@velkymx/vibeui'
|
|
224
|
+
|
|
225
|
+
const email = useFormValidation('')
|
|
226
|
+
|
|
227
|
+
const validateEmail = () => {
|
|
228
|
+
email.validate([
|
|
229
|
+
validators.required('Email is required'),
|
|
230
|
+
validators.email('Please enter a valid email')
|
|
231
|
+
])
|
|
232
|
+
}
|
|
233
|
+
</script>
|
|
234
|
+
|
|
235
|
+
<template>
|
|
236
|
+
<VibeFormInput
|
|
237
|
+
v-model="email.value"
|
|
238
|
+
id="email"
|
|
239
|
+
type="email"
|
|
240
|
+
label="Email"
|
|
241
|
+
:validation-state="email.validationState"
|
|
242
|
+
:validation-message="email.validationMessage"
|
|
243
|
+
@validate="validateEmail"
|
|
244
|
+
/>
|
|
245
|
+
</template>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Composable API
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
const field = useFormValidation(initialValue)
|
|
252
|
+
|
|
253
|
+
// Properties
|
|
254
|
+
field.value // The field value
|
|
255
|
+
field.validationState // 'valid' | 'invalid' | null
|
|
256
|
+
field.validationMessage // Validation message
|
|
257
|
+
field.isDirty // Has the value changed
|
|
258
|
+
field.isTouched // Has the field been focused
|
|
259
|
+
field.isValidating // Is validation in progress
|
|
260
|
+
field.isValid // Computed: validationState === 'valid'
|
|
261
|
+
field.isInvalid // Computed: validationState === 'invalid'
|
|
262
|
+
|
|
263
|
+
// Methods
|
|
264
|
+
await field.validate(rules) // Run validation
|
|
265
|
+
field.reset() // Reset to initial value
|
|
266
|
+
field.markAsTouched() // Mark as touched
|
|
267
|
+
field.markAsDirty() // Mark as dirty
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Validation Patterns
|
|
271
|
+
|
|
272
|
+
### Validate on Blur
|
|
273
|
+
|
|
274
|
+
```vue
|
|
275
|
+
<template>
|
|
276
|
+
<VibeFormInput
|
|
277
|
+
v-model="email"
|
|
278
|
+
id="email"
|
|
279
|
+
@validate="validateEmail"
|
|
280
|
+
validate-on="blur"
|
|
281
|
+
/>
|
|
282
|
+
</template>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Validate on Change
|
|
286
|
+
|
|
287
|
+
```vue
|
|
288
|
+
<template>
|
|
289
|
+
<VibeFormInput
|
|
290
|
+
v-model="password"
|
|
291
|
+
id="password"
|
|
292
|
+
@validate="validatePassword"
|
|
293
|
+
validate-on="change"
|
|
294
|
+
/>
|
|
295
|
+
</template>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Validate on Input
|
|
299
|
+
|
|
300
|
+
```vue
|
|
301
|
+
<template>
|
|
302
|
+
<VibeFormInput
|
|
303
|
+
v-model="username"
|
|
304
|
+
id="username"
|
|
305
|
+
@validate="validateUsername"
|
|
306
|
+
validate-on="input"
|
|
307
|
+
/>
|
|
308
|
+
</template>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Manual Validation
|
|
312
|
+
|
|
313
|
+
```vue
|
|
314
|
+
<script setup lang="ts">
|
|
315
|
+
const email = ref('')
|
|
316
|
+
const validationState = ref(null)
|
|
317
|
+
const validationMessage = ref('')
|
|
318
|
+
|
|
319
|
+
const validateEmail = async () => {
|
|
320
|
+
const rules = [validators.required(), validators.email()]
|
|
321
|
+
|
|
322
|
+
for (const rule of rules) {
|
|
323
|
+
const result = await rule.validator(email.value)
|
|
324
|
+
if (result !== true) {
|
|
325
|
+
validationState.value = 'invalid'
|
|
326
|
+
validationMessage.value = typeof result === 'string' ? result : rule.message
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
validationState.value = 'valid'
|
|
332
|
+
validationMessage.value = ''
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const handleSubmit = async () => {
|
|
336
|
+
await validateEmail()
|
|
337
|
+
|
|
338
|
+
if (validationState.value === 'valid') {
|
|
339
|
+
// Submit form
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
</script>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## API Validation
|
|
346
|
+
|
|
347
|
+
### Check Username Availability
|
|
348
|
+
|
|
349
|
+
```vue
|
|
350
|
+
<script setup lang="ts">
|
|
351
|
+
import { ref } from 'vue'
|
|
352
|
+
import { validators } from '@velkymx/vibeui'
|
|
353
|
+
|
|
354
|
+
const username = ref('')
|
|
355
|
+
const validationState = ref(null)
|
|
356
|
+
const validationMessage = ref('')
|
|
357
|
+
const isChecking = ref(false)
|
|
358
|
+
|
|
359
|
+
const checkUsernameAvailability = validators.async(async (value) => {
|
|
360
|
+
isChecking.value = true
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const response = await fetch(`/api/users/check-username`, {
|
|
364
|
+
method: 'POST',
|
|
365
|
+
headers: { 'Content-Type': 'application/json' },
|
|
366
|
+
body: JSON.stringify({ username: value })
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
const data = await response.json()
|
|
370
|
+
|
|
371
|
+
if (!data.available) {
|
|
372
|
+
return data.message || 'Username is already taken'
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return true
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return 'Error checking username availability'
|
|
378
|
+
} finally {
|
|
379
|
+
isChecking.value = false
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
const validateUsername = async () => {
|
|
384
|
+
const rules = [
|
|
385
|
+
validators.required('Username is required'),
|
|
386
|
+
validators.minLength(3, 'Username must be at least 3 characters'),
|
|
387
|
+
validators.maxLength(20, 'Username must not exceed 20 characters'),
|
|
388
|
+
validators.pattern(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),
|
|
389
|
+
checkUsernameAvailability
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
for (const rule of rules) {
|
|
393
|
+
const result = await rule.validator(username.value)
|
|
394
|
+
if (result !== true) {
|
|
395
|
+
validationState.value = 'invalid'
|
|
396
|
+
validationMessage.value = typeof result === 'string' ? result : rule.message
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
validationState.value = 'valid'
|
|
402
|
+
validationMessage.value = 'Username is available!'
|
|
403
|
+
}
|
|
404
|
+
</script>
|
|
405
|
+
|
|
406
|
+
<template>
|
|
407
|
+
<VibeFormInput
|
|
408
|
+
v-model="username"
|
|
409
|
+
id="username"
|
|
410
|
+
label="Username"
|
|
411
|
+
:validation-state="validationState"
|
|
412
|
+
:validation-message="validationMessage"
|
|
413
|
+
@validate="validateUsername"
|
|
414
|
+
validate-on="blur"
|
|
415
|
+
:disabled="isChecking"
|
|
416
|
+
>
|
|
417
|
+
<template v-if="isChecking">
|
|
418
|
+
<VibeSpinner size="sm" class="position-absolute end-0 top-50 translate-middle-y me-3" />
|
|
419
|
+
</template>
|
|
420
|
+
</VibeFormInput>
|
|
421
|
+
</template>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Debounced API Validation
|
|
425
|
+
|
|
426
|
+
```vue
|
|
427
|
+
<script setup lang="ts">
|
|
428
|
+
import { ref } from 'vue'
|
|
429
|
+
import { validators } from '@velkymx/vibeui'
|
|
430
|
+
|
|
431
|
+
// Debounce helper
|
|
432
|
+
function debounce(fn, delay) {
|
|
433
|
+
let timeout
|
|
434
|
+
return (...args) => {
|
|
435
|
+
clearTimeout(timeout)
|
|
436
|
+
return new Promise((resolve) => {
|
|
437
|
+
timeout = setTimeout(() => resolve(fn(...args)), delay)
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const email = ref('')
|
|
443
|
+
const validationState = ref(null)
|
|
444
|
+
const validationMessage = ref('')
|
|
445
|
+
|
|
446
|
+
const checkEmailDebounced = debounce(async (value) => {
|
|
447
|
+
const response = await fetch(`/api/check-email?email=${value}`)
|
|
448
|
+
const data = await response.json()
|
|
449
|
+
return data.available || 'Email is already registered'
|
|
450
|
+
}, 500)
|
|
451
|
+
|
|
452
|
+
const checkEmail = validators.async(checkEmailDebounced)
|
|
453
|
+
|
|
454
|
+
const validateEmail = async () => {
|
|
455
|
+
const rules = [
|
|
456
|
+
validators.required(),
|
|
457
|
+
validators.email(),
|
|
458
|
+
checkEmail
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
for (const rule of rules) {
|
|
462
|
+
const result = await rule.validator(email.value)
|
|
463
|
+
if (result !== true) {
|
|
464
|
+
validationState.value = 'invalid'
|
|
465
|
+
validationMessage.value = typeof result === 'string' ? result : rule.message
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
validationState.value = 'valid'
|
|
471
|
+
validationMessage.value = 'Email is available!'
|
|
472
|
+
}
|
|
473
|
+
</script>
|
|
474
|
+
|
|
475
|
+
<template>
|
|
476
|
+
<VibeFormInput
|
|
477
|
+
v-model="email"
|
|
478
|
+
id="email"
|
|
479
|
+
type="email"
|
|
480
|
+
label="Email"
|
|
481
|
+
:validation-state="validationState"
|
|
482
|
+
:validation-message="validationMessage"
|
|
483
|
+
@validate="validateEmail"
|
|
484
|
+
validate-on="input"
|
|
485
|
+
/>
|
|
486
|
+
</template>
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Form-Level Validation
|
|
490
|
+
|
|
491
|
+
```vue
|
|
492
|
+
<script setup lang="ts">
|
|
493
|
+
import { ref, reactive } from 'vue'
|
|
494
|
+
import { validators } from '@velkymx/vibeui'
|
|
495
|
+
|
|
496
|
+
const form = reactive({
|
|
497
|
+
username: '',
|
|
498
|
+
email: '',
|
|
499
|
+
password: '',
|
|
500
|
+
confirmPassword: ''
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
const validation = reactive({
|
|
504
|
+
username: { state: null, message: '' },
|
|
505
|
+
email: { state: null, message: '' },
|
|
506
|
+
password: { state: null, message: '' },
|
|
507
|
+
confirmPassword: { state: null, message: '' }
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
const isFormValid = ref(false)
|
|
511
|
+
|
|
512
|
+
const validateField = async (field, rules) => {
|
|
513
|
+
for (const rule of rules) {
|
|
514
|
+
const result = await rule.validator(form[field])
|
|
515
|
+
if (result !== true) {
|
|
516
|
+
validation[field].state = 'invalid'
|
|
517
|
+
validation[field].message = typeof result === 'string' ? result : rule.message
|
|
518
|
+
return false
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
validation[field].state = 'valid'
|
|
523
|
+
validation[field].message = ''
|
|
524
|
+
return true
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const validateForm = async () => {
|
|
528
|
+
const results = await Promise.all([
|
|
529
|
+
validateField('username', [validators.required(), validators.minLength(3)]),
|
|
530
|
+
validateField('email', [validators.required(), validators.email()]),
|
|
531
|
+
validateField('password', [validators.required(), validators.minLength(8)]),
|
|
532
|
+
validateField('confirmPassword', [
|
|
533
|
+
validators.required(),
|
|
534
|
+
{
|
|
535
|
+
validator: (value) => value === form.password || 'Passwords do not match',
|
|
536
|
+
message: 'Passwords do not match'
|
|
537
|
+
}
|
|
538
|
+
])
|
|
539
|
+
])
|
|
540
|
+
|
|
541
|
+
isFormValid.value = results.every(result => result === true)
|
|
542
|
+
return isFormValid.value
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const handleSubmit = async () => {
|
|
546
|
+
if (await validateForm()) {
|
|
547
|
+
console.log('Form is valid, submitting...', form)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
</script>
|
|
551
|
+
|
|
552
|
+
<template>
|
|
553
|
+
<form @submit.prevent="handleSubmit">
|
|
554
|
+
<VibeFormInput
|
|
555
|
+
v-model="form.username"
|
|
556
|
+
id="username"
|
|
557
|
+
label="Username"
|
|
558
|
+
:validation-state="validation.username.state"
|
|
559
|
+
:validation-message="validation.username.message"
|
|
560
|
+
@validate="validateField('username', [validators.required(), validators.minLength(3)])"
|
|
561
|
+
/>
|
|
562
|
+
|
|
563
|
+
<VibeFormInput
|
|
564
|
+
v-model="form.email"
|
|
565
|
+
id="email"
|
|
566
|
+
type="email"
|
|
567
|
+
label="Email"
|
|
568
|
+
:validation-state="validation.email.state"
|
|
569
|
+
:validation-message="validation.email.message"
|
|
570
|
+
@validate="validateField('email', [validators.required(), validators.email()])"
|
|
571
|
+
/>
|
|
572
|
+
|
|
573
|
+
<VibeFormInput
|
|
574
|
+
v-model="form.password"
|
|
575
|
+
id="password"
|
|
576
|
+
type="password"
|
|
577
|
+
label="Password"
|
|
578
|
+
:validation-state="validation.password.state"
|
|
579
|
+
:validation-message="validation.password.message"
|
|
580
|
+
@validate="validateField('password', [validators.required(), validators.minLength(8)])"
|
|
581
|
+
/>
|
|
582
|
+
|
|
583
|
+
<VibeFormInput
|
|
584
|
+
v-model="form.confirmPassword"
|
|
585
|
+
id="confirmPassword"
|
|
586
|
+
type="password"
|
|
587
|
+
label="Confirm Password"
|
|
588
|
+
:validation-state="validation.confirmPassword.state"
|
|
589
|
+
:validation-message="validation.confirmPassword.message"
|
|
590
|
+
@validate="validateField('confirmPassword', [
|
|
591
|
+
validators.required(),
|
|
592
|
+
{ validator: (v) => v === form.password || 'Passwords do not match', message: 'Passwords do not match' }
|
|
593
|
+
])"
|
|
594
|
+
/>
|
|
595
|
+
|
|
596
|
+
<VibeButton type="submit" variant="primary">Register</VibeButton>
|
|
597
|
+
</form>
|
|
598
|
+
</template>
|
|
599
|
+
```
|