@velkymx/vibeui 0.8.2 → 1.0.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/AGENTS.md +44 -0
- package/README.md +245 -58
- package/dist/purify.es-1D-VntFD.mjs +679 -0
- package/dist/src/App.vue.d.ts +3 -0
- package/dist/src/components/HelloWorld.vue.d.ts +6 -0
- package/dist/src/components/VibeAccordion.vue.d.ts +89 -0
- package/dist/{components → src/components}/VibeAlert.vue.d.ts +24 -30
- package/dist/src/components/VibeAutocomplete.vue.d.ts +74 -0
- package/dist/{components → src/components}/VibeBadge.vue.d.ts +10 -18
- package/dist/src/components/VibeBreadcrumb.vue.d.ts +51 -0
- package/dist/{components → src/components}/VibeButton.vue.d.ts +16 -20
- package/dist/{components → src/components}/VibeButtonGroup.vue.d.ts +10 -18
- package/dist/{components → src/components}/VibeCard.vue.d.ts +51 -24
- package/dist/{components → src/components}/VibeCarousel.vue.d.ts +27 -49
- package/dist/src/components/VibeChartBar.vue.d.ts +61 -0
- package/dist/src/components/VibeChartLine.vue.d.ts +70 -0
- package/dist/src/components/VibeChartPie.vue.d.ts +34 -0
- package/dist/{components → src/components}/VibeCloseButton.vue.d.ts +9 -7
- package/dist/{components → src/components}/VibeCol.vue.d.ts +9 -17
- package/dist/src/components/VibeCollapse.vue.d.ts +76 -0
- package/dist/src/components/VibeContainer.vue.d.ts +36 -0
- package/dist/src/components/VibeDataTable.vue.d.ts +131 -0
- package/dist/src/components/VibeDatePicker.vue.d.ts +120 -0
- package/dist/src/components/VibeDraggable.vue.d.ts +77 -0
- package/dist/{components → src/components}/VibeDropdown.vue.d.ts +46 -43
- package/dist/src/components/VibeDroppable.vue.d.ts +70 -0
- package/dist/src/components/VibeFileInput.vue.d.ts +127 -0
- package/dist/{components → src/components}/VibeFormCheckbox.vue.d.ts +30 -22
- package/dist/{components → src/components}/VibeFormDatepicker.vue.d.ts +27 -30
- package/dist/{components → src/components}/VibeFormGroup.vue.d.ts +11 -14
- package/dist/{components → src/components}/VibeFormInput.vue.d.ts +22 -23
- package/dist/{components → src/components}/VibeFormRadio.vue.d.ts +23 -24
- package/dist/{components → src/components}/VibeFormSelect.vue.d.ts +28 -34
- package/dist/{components → src/components}/VibeFormSpinbutton.vue.d.ts +31 -34
- package/dist/{components → src/components}/VibeFormSwitch.vue.d.ts +22 -23
- package/dist/{components → src/components}/VibeFormTextarea.vue.d.ts +24 -25
- package/dist/{components → src/components}/VibeFormWysiwyg.vue.d.ts +24 -23
- package/dist/src/components/VibeHero.vue.d.ts +108 -0
- package/dist/{components → src/components}/VibeIcon.vue.d.ts +27 -7
- package/dist/{components → src/components}/VibeInputGroup.vue.d.ts +11 -13
- package/dist/{components → src/components}/VibeLink.vue.d.ts +14 -18
- package/dist/{components → src/components}/VibeListGroup.vue.d.ts +26 -22
- package/dist/{components → src/components}/VibeModal.vue.d.ts +29 -45
- package/dist/{components → src/components}/VibeNav.vue.d.ts +40 -20
- package/dist/{components → src/components}/VibeNavbar.vue.d.ts +12 -19
- package/dist/src/components/VibeNavbarBrand.vue.d.ts +34 -0
- package/dist/src/components/VibeNavbarNav.vue.d.ts +80 -0
- package/dist/{components → src/components}/VibeNavbarToggle.vue.d.ts +7 -5
- package/dist/{components → src/components}/VibeOffcanvas.vue.d.ts +28 -43
- package/dist/{components → src/components}/VibePagination.vue.d.ts +38 -29
- package/dist/{components → src/components}/VibePlaceholder.vue.d.ts +9 -17
- package/dist/src/components/VibePopover.vue.d.ts +72 -0
- package/dist/src/components/VibeProgress.vue.d.ts +37 -0
- package/dist/src/components/VibeResizable.vue.d.ts +140 -0
- package/dist/{components → src/components}/VibeRow.vue.d.ts +9 -17
- package/dist/src/components/VibeScrollspy.vue.d.ts +89 -0
- package/dist/src/components/VibeSkeleton.vue.d.ts +54 -0
- package/dist/src/components/VibeSlider.vue.d.ts +77 -0
- package/dist/src/components/VibeSortable.vue.d.ts +59 -0
- package/dist/{components → src/components}/VibeSpinner.vue.d.ts +4 -7
- package/dist/src/components/VibeStepper.vue.d.ts +133 -0
- package/dist/src/components/VibeTab.vue.d.ts +41 -0
- package/dist/src/components/VibeTabContent.vue.d.ts +56 -0
- package/dist/src/components/VibeTabs.vue.d.ts +78 -0
- package/dist/{components → src/components}/VibeToast.vue.d.ts +40 -42
- package/dist/src/components/VibeToastHost.vue.d.ts +16 -0
- package/dist/src/components/VibeTooltip.vue.d.ts +63 -0
- package/dist/src/components/chart/chartColors.d.ts +3 -0
- package/dist/src/components/chart/chartResize.d.ts +2 -0
- package/dist/src/components/chart/chartTooltip.d.ts +2 -0
- package/dist/src/components/chart/chartTypes.d.ts +6 -0
- package/dist/src/components/chart/drawBar.d.ts +5 -0
- package/dist/src/components/chart/drawLine.d.ts +18 -0
- package/dist/src/components/chart/drawPie.d.ts +4 -0
- package/dist/src/components/dndStore.d.ts +7 -0
- package/dist/{components → src/components}/index.d.ts +18 -1
- package/dist/{composables → src/composables}/useBreakpoints.d.ts +1 -0
- package/dist/{composables → src/composables}/useColorMode.d.ts +1 -1
- package/dist/src/composables/useForm.d.ts +22 -0
- package/dist/src/composables/useId.d.ts +2 -0
- package/dist/src/composables/usePosition.d.ts +20 -0
- package/dist/src/composables/useToast.d.ts +52 -0
- package/dist/src/directives/vTooltip.d.ts +24 -0
- package/dist/{index.d.ts → src/index.d.ts} +6 -0
- package/dist/src/injectionKeys.d.ts +28 -0
- package/dist/{types.d.ts → src/types.d.ts} +32 -6
- package/dist/src/utils/safeCss.d.ts +16 -0
- package/dist/src/utils/safeHref.d.ts +7 -0
- package/dist/src/utils/sanitizeHtml.d.ts +13 -0
- package/dist/vibeui.css +2 -1
- package/dist/vibeui.es.js +8351 -3508
- package/dist/vibeui.umd.js +3 -1
- package/docs/README.md +85 -13
- package/docs/components/advanced/popover.md +27 -52
- package/docs/components/advanced/scrollspy.md +57 -15
- package/docs/components/advanced/tooltip.md +26 -47
- package/docs/components/card/card.md +9 -1
- package/docs/components/charts/chart-bar.md +129 -0
- package/docs/components/charts/chart-line.md +136 -0
- package/docs/components/charts/chart-pie.md +102 -0
- package/docs/components/core/alert.md +27 -5
- package/docs/components/core/badge.md +0 -6
- package/docs/components/core/button-group.md +0 -6
- package/docs/components/core/button.md +1 -1
- package/docs/components/core/close-button.md +5 -5
- package/docs/components/core/link.md +60 -17
- package/docs/components/core/placeholder.md +0 -6
- package/docs/components/core/skeleton.md +40 -0
- package/docs/components/core/spinner.md +0 -6
- package/docs/components/data/datatable.md +29 -10
- package/docs/components/hero/hero.md +64 -0
- package/docs/components/interactive/accordion.md +29 -0
- package/docs/components/interactive/carousel.md +19 -4
- package/docs/components/interactive/collapse.md +3 -2
- package/docs/components/interactive/date-picker.md +78 -0
- package/docs/components/interactive/draggable.md +91 -0
- package/docs/components/interactive/dropdown.md +54 -18
- package/docs/components/interactive/modal.md +15 -20
- package/docs/components/interactive/offcanvas.md +4 -1
- package/docs/components/interactive/resizable.md +73 -0
- package/docs/components/interactive/slider.md +57 -0
- package/docs/components/interactive/sortable.md +52 -0
- package/docs/components/interactive/stepper.md +83 -0
- package/docs/components/interactive/tabs.md +66 -0
- package/docs/components/interactive/toast.md +93 -30
- package/docs/components/layout/col.md +0 -6
- package/docs/components/layout/container.md +0 -6
- package/docs/components/layout/row.md +0 -6
- package/docs/components/list/list-group.md +4 -0
- package/docs/components/navigation/breadcrumb.md +4 -0
- package/docs/components/navigation/nav.md +64 -1
- package/docs/components/navigation/navbar.md +2 -0
- package/docs/components/navigation/pagination.md +16 -0
- package/docs/components/progress/progress.md +7 -1
- package/docs/composables/color-mode.md +44 -6
- package/docs/composables/use-form.md +78 -0
- package/docs/composables/use-position.md +68 -0
- package/docs/composables/use-toast.md +91 -0
- package/docs/directives/v-tooltip.md +58 -0
- package/docs/forms/README.md +60 -61
- package/docs/forms/autocomplete.md +96 -0
- package/docs/forms/file-input.md +97 -0
- package/docs/forms/form-checkbox.md +75 -25
- package/docs/forms/form-datepicker.md +58 -28
- package/docs/forms/form-group.md +79 -44
- package/docs/forms/form-input.md +88 -31
- package/docs/forms/form-radio.md +60 -36
- package/docs/forms/form-select.md +75 -31
- package/docs/forms/form-spinbutton.md +72 -33
- package/docs/forms/form-switch.md +53 -25
- package/docs/forms/form-textarea.md +60 -29
- package/docs/forms/form-wysiwyg.md +73 -38
- package/docs/forms/input-group.md +53 -34
- package/docs/forms/validation.md +135 -521
- package/docs/getting-started/starter-template.md +136 -0
- package/docs/superpowers/plans/2026-05-12-vibe-charts.md +1999 -0
- package/docs/superpowers/plans/2026-05-29-e2e-browser-testing.md +615 -0
- package/docs/superpowers/specs/2026-05-12-charting-design.md +185 -0
- package/docs/superpowers/specs/2026-05-29-e2e-browser-testing-design.md +168 -0
- package/docs/utilities/position.md +82 -0
- package/docs/versioning.md +57 -0
- package/examples/README.md +201 -0
- package/examples/album.html +262 -0
- package/examples/blog.html +166 -0
- package/examples/carousel.html +114 -0
- package/examples/checkout.html +275 -0
- package/examples/cover.html +89 -0
- package/examples/dashboard.html +156 -0
- package/examples/index.html +129 -0
- package/examples/jumbotron.html +56 -0
- package/examples/mobile-dashboard.html +138 -0
- package/examples/pricing.html +172 -0
- package/examples/product.html +145 -0
- package/examples/sidebars.html +166 -0
- package/examples/sign-in.html +119 -0
- package/examples/starter.html +314 -0
- package/examples/sticky-footer-navbar.html +70 -0
- package/examples/sticky-footer.html +60 -0
- package/examples/test-simple.html +91 -0
- package/llms.txt +407 -47
- package/package.json +42 -13
- package/dist/App.vue.d.ts +0 -2
- package/dist/components/HelloWorld.vue.d.ts +0 -5
- package/dist/components/VibeAccordion.vue.d.ts +0 -90
- package/dist/components/VibeBreadcrumb.vue.d.ts +0 -47
- package/dist/components/VibeCollapse.vue.d.ts +0 -84
- package/dist/components/VibeContainer.vue.d.ts +0 -44
- package/dist/components/VibeDataTable.vue.d.ts +0 -76
- package/dist/components/VibeNavbarBrand.vue.d.ts +0 -42
- package/dist/components/VibeNavbarNav.vue.d.ts +0 -61
- package/dist/components/VibePopover.vue.d.ts +0 -95
- package/dist/components/VibeProgress.vue.d.ts +0 -45
- package/dist/components/VibeScrollspy.vue.d.ts +0 -77
- package/dist/components/VibeTabContent.vue.d.ts +0 -74
- package/dist/components/VibeTooltip.vue.d.ts +0 -86
- package/dist/composables/useId.d.ts +0 -5
- package/dist/types/index.d.ts +0 -6
- /package/dist/{composables → src/composables}/useBackButton.d.ts +0 -0
- /package/dist/{composables → src/composables}/useFormValidation.d.ts +0 -0
- /package/dist/{main.d.ts → src/main.d.ts} +0 -0
package/docs/forms/validation.md
CHANGED
|
@@ -1,599 +1,213 @@
|
|
|
1
1
|
# Form Validation
|
|
2
2
|
|
|
3
|
-
VibeUI
|
|
3
|
+
VibeUI ships a validation system for all form components, supporting synchronous and asynchronous rules. Use it with the `useFormValidation` composable (single field), the `useForm` composable (whole form), or by passing rule results directly.
|
|
4
4
|
|
|
5
5
|
## Built-in Validators
|
|
6
6
|
|
|
7
|
-
|
|
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">
|
|
7
|
+
```ts
|
|
25
8
|
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
9
|
```
|
|
80
10
|
|
|
81
|
-
|
|
11
|
+
Each validator returns a `ValidationRule`. A validator function returns `true` for valid, `false` to use the rule's `message`, or a non-empty `string` as the error message. Most validators skip empty values — pair them with `required()` to make a field mandatory.
|
|
82
12
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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.
|
|
13
|
+
| Validator | Signature | Description |
|
|
14
|
+
|-----------|-----------|-------------|
|
|
15
|
+
| `required` | `(message?)` | Non-empty value (also checks array length). |
|
|
16
|
+
| `email` | `(message?)` | Valid email format. |
|
|
17
|
+
| `minLength` | `(min, message?)` | String length ≥ `min`. |
|
|
18
|
+
| `maxLength` | `(max, message?)` | String length ≤ `max`. |
|
|
19
|
+
| `min` | `(min, message?)` | Numeric value ≥ `min`. |
|
|
20
|
+
| `max` | `(max, message?)` | Numeric value ≤ `max`. |
|
|
21
|
+
| `pattern` | `(regex, message?)` | Matches the regex. |
|
|
22
|
+
| `url` | `(message?)` | Parses as a valid URL. |
|
|
23
|
+
| `async` | `(fn)` | Wraps an async validator `(value) => Promise<boolean \| string>`. |
|
|
120
24
|
|
|
121
|
-
```
|
|
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">
|
|
25
|
+
```ts
|
|
140
26
|
const rules = [
|
|
141
|
-
validators.
|
|
27
|
+
validators.required('Email is required'),
|
|
28
|
+
validators.email()
|
|
142
29
|
]
|
|
143
|
-
</script>
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
#### async()
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
validators.async(validatorFn: (value: any) => Promise<boolean | string>)
|
|
150
30
|
```
|
|
151
31
|
|
|
152
|
-
|
|
32
|
+
## Custom Rules
|
|
153
33
|
|
|
154
|
-
|
|
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
|
-
})
|
|
34
|
+
A rule is `{ validator, message? }`. The validator may be sync or async.
|
|
161
35
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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"'
|
|
36
|
+
```ts
|
|
37
|
+
const passwordStrength = {
|
|
38
|
+
validator: (value: unknown) => {
|
|
39
|
+
const v = String(value ?? '')
|
|
40
|
+
if (!v) return true
|
|
41
|
+
if (!/[A-Z]/.test(v)) return 'Needs an uppercase letter'
|
|
42
|
+
if (!/\d/.test(v)) return 'Needs a number'
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
180
45
|
}
|
|
181
46
|
|
|
182
|
-
const rules = [
|
|
183
|
-
</script>
|
|
47
|
+
const rules = [validators.required(), validators.minLength(8), passwordStrength]
|
|
184
48
|
```
|
|
185
49
|
|
|
186
|
-
|
|
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'
|
|
50
|
+
## Single-field validation: `useFormValidation`
|
|
203
51
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
message: 'Password does not meet requirements'
|
|
207
|
-
}
|
|
52
|
+
```ts
|
|
53
|
+
import { useFormValidation } from '@velkymx/vibeui'
|
|
208
54
|
|
|
209
|
-
const
|
|
210
|
-
validators.required(),
|
|
211
|
-
validators.minLength(8),
|
|
212
|
-
passwordStrengthValidator
|
|
213
|
-
]
|
|
214
|
-
</script>
|
|
55
|
+
const field = useFormValidation('') // initial value
|
|
215
56
|
```
|
|
216
57
|
|
|
217
|
-
|
|
58
|
+
Returns:
|
|
218
59
|
|
|
219
|
-
|
|
60
|
+
| Member | Type | Description |
|
|
61
|
+
|--------|------|-------------|
|
|
62
|
+
| `value` | `Ref` | The field value (bind with `v-model="field.value"`). |
|
|
63
|
+
| `validationState` | `Ref<'valid' \| 'invalid' \| null>` | Current state. |
|
|
64
|
+
| `validationMessage` | `Ref<string>` | Current message. |
|
|
65
|
+
| `isDirty` | `Ref<boolean>` | Set via `markAsDirty()`. |
|
|
66
|
+
| `isTouched` | `Ref<boolean>` | Set via `markAsTouched()`. |
|
|
67
|
+
| `isValidating` | `Ref<boolean>` | True while an async rule is running. |
|
|
68
|
+
| `isValid` | `ComputedRef<boolean>` | `validationState === 'valid'`. |
|
|
69
|
+
| `isInvalid` | `ComputedRef<boolean>` | `validationState === 'invalid'`. |
|
|
70
|
+
| `validate(rules)` | `(rules) => Promise<FormValidationResult>` | Runs rules; concurrency-safe (only the latest call wins). |
|
|
71
|
+
| `reset()` | `() => void` | Reset to initial value and clear state. |
|
|
72
|
+
| `markAsTouched()` / `markAsDirty()` | `() => void` | Manual flag setters. |
|
|
220
73
|
|
|
221
74
|
```vue
|
|
222
75
|
<script setup lang="ts">
|
|
223
76
|
import { useFormValidation, validators } from '@velkymx/vibeui'
|
|
224
77
|
|
|
225
78
|
const email = useFormValidation('')
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
email.validate([
|
|
229
|
-
validators.required('Email is required'),
|
|
230
|
-
validators.email('Please enter a valid email')
|
|
231
|
-
])
|
|
232
|
-
}
|
|
79
|
+
const validateEmail = () =>
|
|
80
|
+
email.validate([validators.required(), validators.email()])
|
|
233
81
|
</script>
|
|
234
82
|
|
|
235
83
|
<template>
|
|
236
|
-
<
|
|
237
|
-
v-model="email.value"
|
|
238
|
-
id="email"
|
|
239
|
-
type="email"
|
|
84
|
+
<VibeFormGroup
|
|
240
85
|
label="Email"
|
|
241
86
|
:validation-state="email.validationState"
|
|
242
87
|
:validation-message="email.validationMessage"
|
|
243
|
-
|
|
244
|
-
|
|
88
|
+
>
|
|
89
|
+
<VibeFormInput v-model="email.value" type="email" @validate="validateEmail" />
|
|
90
|
+
</VibeFormGroup>
|
|
245
91
|
</template>
|
|
246
92
|
```
|
|
247
93
|
|
|
248
|
-
|
|
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
|
|
94
|
+
Editing an invalid field clears its `invalid` state automatically until you re-validate.
|
|
271
95
|
|
|
272
|
-
|
|
96
|
+
## Whole-form validation: `useForm`
|
|
273
97
|
|
|
274
|
-
```
|
|
275
|
-
|
|
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
|
|
98
|
+
```ts
|
|
99
|
+
import { useForm } from '@velkymx/vibeui'
|
|
286
100
|
|
|
287
|
-
|
|
288
|
-
<template>
|
|
289
|
-
<VibeFormInput
|
|
290
|
-
v-model="password"
|
|
291
|
-
id="password"
|
|
292
|
-
@validate="validatePassword"
|
|
293
|
-
validate-on="change"
|
|
294
|
-
/>
|
|
295
|
-
</template>
|
|
101
|
+
const form = useForm({ username: '', email: '', age: 0 })
|
|
296
102
|
```
|
|
297
103
|
|
|
298
|
-
|
|
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
|
-
```
|
|
104
|
+
Returns:
|
|
310
105
|
|
|
311
|
-
|
|
106
|
+
| Member | Type | Description |
|
|
107
|
+
|--------|------|-------------|
|
|
108
|
+
| `fields` | reactive `T` | Form values; bind with `v-model="form.fields.x"`. |
|
|
109
|
+
| `errors` | reactive map | Per-field error messages (`''` when valid). |
|
|
110
|
+
| `touched` | reactive map | Per-field touched flags. |
|
|
111
|
+
| `isDirty` | `ComputedRef<boolean>` | True once any field differs from its initial value. |
|
|
112
|
+
| `isValid` | `ComputedRef<boolean>` | **`false` until the first `validate()`/`validateField()` call**, then reflects whether all errors are empty. |
|
|
113
|
+
| `values` | `ComputedRef<T>` | Snapshot copy of `fields`. |
|
|
114
|
+
| `validate(rules)` | `(rules) => Promise<{ valid, errors }>` | Validate the listed fields. |
|
|
115
|
+
| `validateField(key, rule)` | `(key, rule) => Promise<string>` | Validate one field; returns its message. |
|
|
116
|
+
| `reset()` | `() => void` | Restore initial values and clear errors/touched/validated state. |
|
|
117
|
+
| `markTouched(key)` / `markAllTouched()` | — | Touched flag setters. |
|
|
118
|
+
| `setField(key, value)` | — | Programmatically set a field. |
|
|
312
119
|
|
|
313
120
|
```vue
|
|
314
121
|
<script setup lang="ts">
|
|
315
|
-
|
|
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
|
-
}
|
|
122
|
+
import { useForm, validators } from '@velkymx/vibeui'
|
|
334
123
|
|
|
335
|
-
const
|
|
336
|
-
await validateEmail()
|
|
124
|
+
const form = useForm({ username: '', email: '', password: '' })
|
|
337
125
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
126
|
+
const rules = {
|
|
127
|
+
username: [validators.required(), validators.minLength(3)],
|
|
128
|
+
email: [validators.required(), validators.email()],
|
|
129
|
+
password: [validators.required(), validators.minLength(8)]
|
|
341
130
|
}
|
|
342
|
-
</script>
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
## API Validation
|
|
346
|
-
|
|
347
|
-
### Check Username Availability
|
|
348
131
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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!'
|
|
132
|
+
const onSubmit = async () => {
|
|
133
|
+
const { valid } = await form.validate(rules)
|
|
134
|
+
if (valid) console.log('submit', form.values.value)
|
|
403
135
|
}
|
|
404
136
|
</script>
|
|
405
137
|
|
|
406
138
|
<template>
|
|
407
|
-
<
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
139
|
+
<form @submit.prevent="onSubmit">
|
|
140
|
+
<VibeFormGroup label="Username" :validation-message="form.errors.username"
|
|
141
|
+
:validation-state="form.errors.username ? 'invalid' : null">
|
|
142
|
+
<VibeFormInput v-model="form.fields.username"
|
|
143
|
+
@validate="form.validateField('username', rules.username)" />
|
|
144
|
+
</VibeFormGroup>
|
|
145
|
+
|
|
146
|
+
<VibeFormGroup label="Email" :validation-message="form.errors.email"
|
|
147
|
+
:validation-state="form.errors.email ? 'invalid' : null">
|
|
148
|
+
<VibeFormInput v-model="form.fields.email" type="email"
|
|
149
|
+
@validate="form.validateField('email', rules.email)" />
|
|
150
|
+
</VibeFormGroup>
|
|
151
|
+
|
|
152
|
+
<VibeFormGroup label="Password" :validation-message="form.errors.password"
|
|
153
|
+
:validation-state="form.errors.password ? 'invalid' : null">
|
|
154
|
+
<VibeFormInput v-model="form.fields.password" type="password"
|
|
155
|
+
@validate="form.validateField('password', rules.password)" />
|
|
156
|
+
</VibeFormGroup>
|
|
157
|
+
|
|
158
|
+
<VibeButton type="submit" variant="primary" :disabled="!form.isValid">
|
|
159
|
+
Register
|
|
160
|
+
</VibeButton>
|
|
161
|
+
</form>
|
|
421
162
|
</template>
|
|
422
163
|
```
|
|
423
164
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
```vue
|
|
427
|
-
<script setup lang="ts">
|
|
428
|
-
import { ref } from 'vue'
|
|
429
|
-
import { validators } from '@velkymx/vibeui'
|
|
165
|
+
## When validation runs: `validateOn`
|
|
430
166
|
|
|
431
|
-
|
|
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
|
-
}
|
|
167
|
+
Form controls emit `validate` based on `validateOn`:
|
|
441
168
|
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
}
|
|
169
|
+
- Text-like controls (input, textarea, datepicker, spinbutton, wysiwyg): `'input' \| 'blur' \| 'change'`, default `'blur'`.
|
|
170
|
+
- Choice controls (checkbox, radio, switch, select): `'change' \| 'blur'`, default `'change'`.
|
|
469
171
|
|
|
470
|
-
|
|
471
|
-
validationMessage.value = 'Email is available!'
|
|
472
|
-
}
|
|
473
|
-
</script>
|
|
172
|
+
Wire `@validate` to your validation call:
|
|
474
173
|
|
|
475
|
-
|
|
476
|
-
|
|
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>
|
|
174
|
+
```vue
|
|
175
|
+
<VibeFormInput v-model="field.value" validate-on="input" @validate="run" />
|
|
487
176
|
```
|
|
488
177
|
|
|
489
|
-
##
|
|
178
|
+
## Async / API Validation
|
|
179
|
+
|
|
180
|
+
`validators.async` runs an async check (e.g. uniqueness). Because `useFormValidation.validate` is concurrency-safe, only the latest call applies its result.
|
|
490
181
|
|
|
491
182
|
```vue
|
|
492
183
|
<script setup lang="ts">
|
|
493
|
-
import {
|
|
494
|
-
import { validators } from '@velkymx/vibeui'
|
|
184
|
+
import { useFormValidation, validators } from '@velkymx/vibeui'
|
|
495
185
|
|
|
496
|
-
const
|
|
497
|
-
username: '',
|
|
498
|
-
email: '',
|
|
499
|
-
password: '',
|
|
500
|
-
confirmPassword: ''
|
|
501
|
-
})
|
|
186
|
+
const username = useFormValidation('')
|
|
502
187
|
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
confirmPassword: { state: null, message: '' }
|
|
188
|
+
const isAvailable = validators.async(async (value) => {
|
|
189
|
+
const res = await fetch(`/api/check-username?u=${encodeURIComponent(String(value))}`)
|
|
190
|
+
const data = await res.json()
|
|
191
|
+
return data.available || 'Username is already taken'
|
|
508
192
|
})
|
|
509
193
|
|
|
510
|
-
const
|
|
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
|
-
}
|
|
194
|
+
const validateUsername = () =>
|
|
195
|
+
username.validate([validators.required(), validators.minLength(3), isAvailable])
|
|
550
196
|
</script>
|
|
551
197
|
|
|
552
198
|
<template>
|
|
553
|
-
<
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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>
|
|
199
|
+
<VibeFormGroup
|
|
200
|
+
label="Username"
|
|
201
|
+
:validation-state="username.validationState"
|
|
202
|
+
:validation-message="username.validationMessage"
|
|
203
|
+
>
|
|
204
|
+
<VibeFormInput v-model="username.value" @validate="validateUsername" />
|
|
205
|
+
</VibeFormGroup>
|
|
598
206
|
</template>
|
|
599
207
|
```
|
|
208
|
+
|
|
209
|
+
## Notes
|
|
210
|
+
|
|
211
|
+
- **`useForm.isValid` starts `false`** and only becomes meaningful after the first validation runs — safe to use for disabling a submit button before any input.
|
|
212
|
+
- **Editing clears `invalid`** (in `useFormValidation`) so messages don't linger while the user fixes the field.
|
|
213
|
+
- **Empty-value skipping:** most validators pass on empty values; add `required()` to enforce presence.
|