@macrulez/vue-form-schema 0.1.0 → 0.1.5
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/README.md +707 -410
- package/dist/MaskEngine-BqJQYybS.js +233 -0
- package/dist/MaskEngine-BwAs2Zb0.cjs +1 -0
- package/dist/__tests__/phase3.test.d.ts +2 -0
- package/dist/__tests__/phase3.test.d.ts.map +1 -0
- package/dist/__tests__/phase4.test.d.ts +2 -0
- package/dist/__tests__/phase4.test.d.ts.map +1 -0
- package/dist/__tests__/useFieldArray.test.d.ts +2 -0
- package/dist/__tests__/useFieldArray.test.d.ts.map +1 -0
- package/dist/__tests__/useMultiStepForm.test.d.ts +2 -0
- package/dist/__tests__/useMultiStepForm.test.d.ts.map +1 -0
- package/dist/core/ConditionEvaluator.d.ts +1 -1
- package/dist/core/ConditionEvaluator.d.ts.map +1 -1
- package/dist/core/ValidationEngine.d.ts +3 -1
- package/dist/core/ValidationEngine.d.ts.map +1 -1
- package/dist/core/inferTypes.d.ts +39 -0
- package/dist/core/inferTypes.d.ts.map +1 -0
- package/dist/core/registry.d.ts +17 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/schemaUtils.d.ts +20 -0
- package/dist/core/schemaUtils.d.ts.map +1 -0
- package/dist/core/types.d.ts +43 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/useFieldArray.d.ts +21 -0
- package/dist/core/useFieldArray.d.ts.map +1 -0
- package/dist/core/useForm.d.ts.map +1 -1
- package/dist/core/useFormDebug.d.ts +29 -0
- package/dist/core/useFormDebug.d.ts.map +1 -0
- package/dist/core/useFormField.d.ts +18 -0
- package/dist/core/useFormField.d.ts.map +1 -0
- package/dist/core/useMultiStepForm.d.ts +30 -0
- package/dist/core/useMultiStepForm.d.ts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +386 -196
- package/dist/parsers/valibot.d.ts +20 -0
- package/dist/parsers/valibot.d.ts.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui.cjs +1 -1
- package/dist/ui.d.ts +114 -18
- package/dist/ui.js +360 -193
- package/dist/valibot.cjs +1 -0
- package/dist/valibot.d.ts +84 -0
- package/dist/valibot.js +43 -0
- package/dist/yup.d.ts +26 -2
- package/dist/zod.d.ts +26 -2
- package/package.json +39 -4
- package/dist/MaskEngine-D22m29OM.js +0 -157
- package/dist/MaskEngine-hd5xHed7.cjs +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# vue-form-schema
|
|
2
2
|
|
|
3
|
-
Reactive forms from a declarative schema (JSON, Zod, or
|
|
3
|
+
Reactive forms from a declarative schema (JSON, Zod, Yup, or Valibot) for Vue 3. A headless, SSR-compatible alternative to VeeValidate / FormKit for forms that are generated dynamically or driven from the server.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -17,13 +17,27 @@ Reactive forms from a declarative schema (JSON, Zod, or Yup) for Vue 3. A headle
|
|
|
17
17
|
- [JSON schema](#json-schema)
|
|
18
18
|
- [Zod](#zod)
|
|
19
19
|
- [Yup](#yup)
|
|
20
|
+
- [Valibot](#valibot)
|
|
20
21
|
- [Built-in validators](#built-in-validators)
|
|
21
22
|
- [Custom validators](#custom-validators)
|
|
23
|
+
- [Cross-field validation](#cross-field-validation)
|
|
22
24
|
- [Conditional fields](#conditional-fields)
|
|
25
|
+
- [Dynamic options](#dynamic-options)
|
|
26
|
+
- [Dynamic array fields](#dynamic-array-fields)
|
|
27
|
+
- [Multi-step forms](#multi-step-forms)
|
|
28
|
+
- [Field transform & parse](#field-transform--parse)
|
|
29
|
+
- [File upload field](#file-upload-field)
|
|
30
|
+
- [Custom field components](#custom-field-components)
|
|
31
|
+
- [Component registry](#component-registry)
|
|
23
32
|
- [Input masking](#input-masking)
|
|
33
|
+
- [Schema composition](#schema-composition)
|
|
34
|
+
- [TypeScript inference](#typescript-inference)
|
|
35
|
+
- [Persisted forms](#persisted-forms)
|
|
36
|
+
- [Debug mode](#debug-mode)
|
|
24
37
|
- [FormRenderer UI component](#formrenderer-ui-component)
|
|
38
|
+
- [Tailwind UI theme](#tailwind-ui-theme)
|
|
39
|
+
- [Accessibility](#accessibility)
|
|
25
40
|
- [SSR compatibility](#ssr-compatibility)
|
|
26
|
-
- [TypeScript generics](#typescript-generics)
|
|
27
41
|
- [Architecture](#architecture)
|
|
28
42
|
- [Bundle size & peer dependencies](#bundle-size--peer-dependencies)
|
|
29
43
|
|
|
@@ -31,52 +45,69 @@ Reactive forms from a declarative schema (JSON, Zod, or Yup) for Vue 3. A headle
|
|
|
31
45
|
|
|
32
46
|
## Features
|
|
33
47
|
|
|
34
|
-
- **Any schema source** —
|
|
48
|
+
- **Any schema source** — `FieldDefinition[]`, JSON array, Zod, Yup, or Valibot
|
|
35
49
|
- **Headless by default** — zero UI dependencies in the core; bring your own components
|
|
36
|
-
- **Reactive conditions** — `visible`, `disabled
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
50
|
+
- **Reactive conditions** — `visible`, `disabled` accept a boolean, function, or string expression
|
|
51
|
+
- **Dynamic options** — sync and async `options` functions with dependency tracking (`optionsDeps`)
|
|
52
|
+
- **Dynamic array fields** — `type: 'array'` with `useFieldArray` composable (append / remove / move / swap)
|
|
53
|
+
- **Multi-step wizard** — `useMultiStepForm` with per-step validation and `MultiStepFormRenderer`
|
|
54
|
+
- **Validation** — sync + async validators, `validateMode: 'first' | 'all'`, `validateOn: 'eager'`
|
|
55
|
+
- **Cross-field** — `sameAs` validator; validators receive all current values as second argument
|
|
56
|
+
- **Transform & parse** — `transform` runs on every `setField`; `parse` runs at submit time
|
|
57
|
+
- **File upload** — `type: 'file'` with `fileType`, `fileSize`, `fileCount` validators; drag-and-drop UI
|
|
58
|
+
- **Custom components** — `field.component` + per-app and per-subtree component registry
|
|
59
|
+
- **Input masking** — phone (RU/EU), date, IBAN, INN, custom `#`/`A` patterns; no external deps
|
|
60
|
+
- **Schema composition** — `mergeSchemas`, `omitFields`, `pickFields`, `extendField`
|
|
61
|
+
- **TypeScript inference** — `InferValues<T>` maps schema literals to typed values
|
|
62
|
+
- **Persisted forms** — `persist: 'local' | 'session'` with SSR-safe storage
|
|
63
|
+
- **Debug mode** — `debug: true` logs state changes; `useFormDebug` returns a reactive snapshot
|
|
64
|
+
- **Tailwind UI theme** — `vue-form-schema/ui/tailwind` subentry with utility-class components
|
|
65
|
+
- **Accessibility** — `aria-required`, `aria-invalid`, `aria-describedby`, `fieldset`/`legend` for radio
|
|
66
|
+
- **SSR-safe** — no direct browser APIs in the core
|
|
67
|
+
- **Tree-shakeable** — Zod/Yup/Valibot adapters and UI are separate entry points
|
|
42
68
|
|
|
43
69
|
---
|
|
44
70
|
|
|
45
71
|
## Demo
|
|
46
72
|
|
|
47
|
-
A local interactive demo covers all major features of the library. Clone the repo and run:
|
|
48
|
-
|
|
49
73
|
```bash
|
|
50
74
|
npm install
|
|
51
75
|
npm run demo
|
|
52
76
|
```
|
|
53
77
|
|
|
54
|
-
Opens at **http://localhost:5174
|
|
78
|
+
Opens at **http://localhost:5174**:
|
|
55
79
|
|
|
56
80
|
| Page | What it shows |
|
|
57
81
|
|---|---|
|
|
58
|
-
| **Basic form** | `FieldDefinition[]` with built-in validators
|
|
59
|
-
| **JSON schema** | Server-driven schema with rule-based validators
|
|
60
|
-
| **Zod schema** | `
|
|
61
|
-
| **Yup schema** | `
|
|
62
|
-
| **Conditional fields** | `visible` as
|
|
63
|
-
| **Input masking** | All presets + custom
|
|
64
|
-
| **FormRenderer** |
|
|
82
|
+
| **Basic form** | `FieldDefinition[]` with built-in validators |
|
|
83
|
+
| **JSON schema** | Server-driven schema with rule-based validators |
|
|
84
|
+
| **Zod schema** | `parseZod()` with type inference |
|
|
85
|
+
| **Yup schema** | `parseYup()` with Yup constraints |
|
|
86
|
+
| **Conditional fields** | `visible` / `disabled` as function and string expression |
|
|
87
|
+
| **Input masking** | All presets + custom patterns |
|
|
88
|
+
| **FormRenderer** | Slot overrides, custom component map |
|
|
89
|
+
| **Array fields** | `type: 'array'` + `useFieldArray` API |
|
|
90
|
+
| **Multi-step wizard** | `useMultiStepForm` + step progress |
|
|
91
|
+
| **Dependent fields** | Sync/async function options, `defaultValue` as function |
|
|
92
|
+
| **Custom registry** | `provideRegistry` replaces built-in checkbox with PillToggle |
|
|
93
|
+
| **File upload** | Drag-and-drop, `fileType`/`fileSize`/`fileCount` validators |
|
|
94
|
+
| **Tailwind theme** | Default `FormRenderer` vs `TailwindFormRenderer` side by side |
|
|
95
|
+
| **Accessibility** | `aria-*` attributes, `fieldset`/`legend` for radio groups |
|
|
65
96
|
|
|
66
97
|
---
|
|
67
98
|
|
|
68
99
|
## Installation
|
|
69
100
|
|
|
70
101
|
```bash
|
|
71
|
-
npm install vue-form-schema
|
|
102
|
+
npm install @macrulez/vue-form-schema
|
|
72
103
|
```
|
|
73
104
|
|
|
74
|
-
|
|
105
|
+
Optional peer dependencies:
|
|
75
106
|
|
|
76
107
|
```bash
|
|
77
|
-
npm install
|
|
78
|
-
npm install
|
|
79
|
-
npm install
|
|
108
|
+
npm install zod # Zod adapter
|
|
109
|
+
npm install yup # Yup adapter
|
|
110
|
+
npm install valibot # Valibot adapter
|
|
80
111
|
```
|
|
81
112
|
|
|
82
113
|
---
|
|
@@ -85,8 +116,8 @@ npm install yup # optional — only if you use the Yup adapter
|
|
|
85
116
|
|
|
86
117
|
```vue
|
|
87
118
|
<script setup lang="ts">
|
|
88
|
-
import { useForm } from 'vue-form-schema'
|
|
89
|
-
import type { FieldDefinition } from 'vue-form-schema'
|
|
119
|
+
import { useForm } from '@macrulez/vue-form-schema'
|
|
120
|
+
import type { FieldDefinition } from '@macrulez/vue-form-schema'
|
|
90
121
|
|
|
91
122
|
const schema: FieldDefinition[] = [
|
|
92
123
|
{ type: 'text', name: 'name', label: 'Full name', required: true },
|
|
@@ -130,19 +161,32 @@ const { values, errors, touched, isValid, isSubmitting, submit, setField } = use
|
|
|
130
161
|
</template>
|
|
131
162
|
```
|
|
132
163
|
|
|
164
|
+
Or use `FormRenderer` for zero-markup rendering:
|
|
165
|
+
|
|
166
|
+
```vue
|
|
167
|
+
<script setup lang="ts">
|
|
168
|
+
import { useForm } from '@macrulez/vue-form-schema'
|
|
169
|
+
import { FormRenderer } from '@macrulez/vue-form-schema/ui'
|
|
170
|
+
|
|
171
|
+
const form = useForm({ schema, onSubmit })
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<template>
|
|
175
|
+
<FormRenderer :form="form" submit-label="Save" />
|
|
176
|
+
</template>
|
|
177
|
+
```
|
|
178
|
+
|
|
133
179
|
---
|
|
134
180
|
|
|
135
181
|
## FieldDefinition reference
|
|
136
182
|
|
|
137
|
-
Every field in your schema is described by a `FieldDefinition` object.
|
|
138
|
-
|
|
139
183
|
```ts
|
|
140
184
|
interface FieldDefinition {
|
|
141
185
|
// ─── Required ─────────────────────────────────────────────────────────────
|
|
142
186
|
type: 'text' | 'number' | 'email' | 'select' | 'checkbox'
|
|
143
|
-
| 'radio' | 'textarea' | 'date' | 'array' | 'group'
|
|
187
|
+
| 'radio' | 'textarea' | 'date' | 'array' | 'group' | 'file'
|
|
144
188
|
|
|
145
|
-
/** Flat dot-path key
|
|
189
|
+
/** Flat dot-path key in the values object, e.g. "address.city" */
|
|
146
190
|
name: string
|
|
147
191
|
|
|
148
192
|
// ─── Display ──────────────────────────────────────────────────────────────
|
|
@@ -150,20 +194,14 @@ interface FieldDefinition {
|
|
|
150
194
|
placeholder?: string
|
|
151
195
|
|
|
152
196
|
// ─── Initial value ────────────────────────────────────────────────────────
|
|
153
|
-
|
|
197
|
+
/** Static value or a function called at init with already-resolved partial values */
|
|
198
|
+
defaultValue?: unknown | ((values: Record<string, unknown>) => unknown)
|
|
154
199
|
|
|
155
200
|
// ─── Constraints ──────────────────────────────────────────────────────────
|
|
156
201
|
required?: boolean
|
|
157
|
-
|
|
158
|
-
/** Static boolean, or a function receiving all current values */
|
|
159
202
|
disabled?: boolean | ((values: Record<string, unknown>) => boolean)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
* Static boolean, a function, or a string expression.
|
|
163
|
-
* String expressions have access to the `values` variable and support
|
|
164
|
-
* arithmetic, comparison, logical operators. Unsafe calls are blocked.
|
|
165
|
-
*/
|
|
166
|
-
visible?: boolean | string | ((values: Record<string, unknown>) => boolean)
|
|
203
|
+
/** Boolean, function, or string expression evaluated against live values */
|
|
204
|
+
visible?: boolean | string | ((values: Record<string, unknown>) => boolean)
|
|
167
205
|
|
|
168
206
|
// ─── Validation ───────────────────────────────────────────────────────────
|
|
169
207
|
validators?: ValidatorFn[]
|
|
@@ -172,41 +210,41 @@ interface FieldDefinition {
|
|
|
172
210
|
// ─── Masking ──────────────────────────────────────────────────────────────
|
|
173
211
|
mask?: string | MaskConfig
|
|
174
212
|
|
|
175
|
-
// ─── select / radio
|
|
176
|
-
|
|
213
|
+
// ─── select / radio options ───────────────────────────────────────────────
|
|
214
|
+
/** Static array, sync function, or async function */
|
|
215
|
+
options?: FieldOption[]
|
|
216
|
+
| ((values: Record<string, unknown>) => FieldOption[])
|
|
217
|
+
| ((values: Record<string, unknown>) => Promise<FieldOption[]>)
|
|
218
|
+
/** Field names that trigger async options re-fetch when their values change */
|
|
219
|
+
optionsDeps?: string[]
|
|
177
220
|
|
|
178
221
|
// ─── group / array ────────────────────────────────────────────────────────
|
|
179
222
|
fields?: FieldDefinition[]
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### Nested fields
|
|
184
|
-
|
|
185
|
-
Use `type: 'group'` to create a named group of fields. Child field names must use dot-path notation so values are properly scoped.
|
|
186
223
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
224
|
+
// ─── transform / parse ────────────────────────────────────────────────────
|
|
225
|
+
/** Applied on every setField call — use for trim, coercion, formatting */
|
|
226
|
+
transform?: (value: unknown, values: Record<string, unknown>) => unknown
|
|
227
|
+
/** Applied at submit time to produce the final payload value */
|
|
228
|
+
parse?: (raw: unknown) => unknown
|
|
229
|
+
|
|
230
|
+
// ─── Custom component ─────────────────────────────────────────────────────
|
|
231
|
+
/** Vue component or registered name; receives FormFieldProps */
|
|
232
|
+
component?: Component | string
|
|
233
|
+
|
|
234
|
+
// ─── File field options ───────────────────────────────────────────────────
|
|
235
|
+
accept?: string // passed to <input accept>
|
|
236
|
+
multiple?: boolean
|
|
237
|
+
maxSize?: number // bytes (informational; use fileSize validator to enforce)
|
|
238
|
+
maxFiles?: number // informational; use fileCount validator to enforce
|
|
239
|
+
}
|
|
199
240
|
```
|
|
200
241
|
|
|
201
|
-
The resulting `values` object will be `{ 'address.city': '...', 'address.country': '...' }`.
|
|
202
|
-
|
|
203
242
|
---
|
|
204
243
|
|
|
205
244
|
## useForm composable
|
|
206
245
|
|
|
207
246
|
```ts
|
|
208
|
-
import { useForm } from 'vue-form-schema'
|
|
209
|
-
|
|
247
|
+
import { useForm } from '@macrulez/vue-form-schema'
|
|
210
248
|
const form = useForm(config)
|
|
211
249
|
```
|
|
212
250
|
|
|
@@ -216,43 +254,34 @@ const form = useForm(config)
|
|
|
216
254
|
|---|---|---|---|
|
|
217
255
|
| `schema` | `FieldDefinition[] \| JSONSchema` | — | Field definitions |
|
|
218
256
|
| `initialValues` | `Partial<T>` | `{}` | Seed values (override field defaults) |
|
|
219
|
-
| `validateOn` | `'input' \| 'blur' \| 'submit'` | `'blur'` | When
|
|
220
|
-
| `
|
|
257
|
+
| `validateOn` | `'input' \| 'blur' \| 'submit' \| 'eager'` | `'blur'` | When validation fires |
|
|
258
|
+
| `validateMode` | `'first' \| 'all'` | `'first'` | Return first error only, or all errors |
|
|
259
|
+
| `clearOnHide` | `boolean` | `false` | Reset field value when it becomes hidden |
|
|
221
260
|
| `onSubmit` | `(values: T) => void \| Promise<void>` | — | Called after successful validation |
|
|
261
|
+
| `persist` | `false \| 'session' \| 'local'` | `false` | Persist values to sessionStorage / localStorage |
|
|
262
|
+
| `persistKey` | `string` | auto | Storage key prefix |
|
|
263
|
+
| `debug` | `boolean` | `false` | Log state changes to `console.group` |
|
|
222
264
|
|
|
223
265
|
### Return value
|
|
224
266
|
|
|
225
267
|
| Property | Type | Description |
|
|
226
268
|
|---|---|---|
|
|
227
|
-
| `fields` | `ComputedRef<FieldDefinition[]>` |
|
|
228
|
-
| `values` | `Ref<T>` | Current form values
|
|
269
|
+
| `fields` | `ComputedRef<FieldDefinition[]>` | Fields after conditions are evaluated |
|
|
270
|
+
| `values` | `Ref<T>` | Current form values |
|
|
229
271
|
| `errors` | `Ref<Record<string, string[]>>` | Validation errors keyed by field name |
|
|
230
|
-
| `touched` | `Ref<Record<string, boolean>>` |
|
|
231
|
-
| `
|
|
272
|
+
| `touched` | `Ref<Record<string, boolean>>` | Fields that have been blurred |
|
|
273
|
+
| `optionsLoading` | `Ref<Record<string, boolean>>` | Async options loading state per field |
|
|
274
|
+
| `isDirty` | `ComputedRef<boolean>` | `true` when values differ from initial state |
|
|
232
275
|
| `isValid` | `ComputedRef<boolean>` | `true` when all visible fields pass validation |
|
|
233
276
|
| `isSubmitting` | `Ref<boolean>` | `true` while `onSubmit` is running |
|
|
234
277
|
| `submit()` | `() => Promise<void>` | Touch all fields, validate, call `onSubmit` |
|
|
235
|
-
| `reset(values?)` |
|
|
236
|
-
| `setField(path, value)` |
|
|
237
|
-
| `getField(path)` |
|
|
238
|
-
|
|
239
|
-
### Example — programmatic control
|
|
240
|
-
|
|
241
|
-
```ts
|
|
242
|
-
const { setField, getField, reset } = useForm({ schema })
|
|
278
|
+
| `reset(values?)` | — | Restore initial state or supply new values |
|
|
279
|
+
| `setField(path, value)` | — | Set a value by dot-path |
|
|
280
|
+
| `getField(path)` | — | Read a value by dot-path |
|
|
243
281
|
|
|
244
|
-
|
|
245
|
-
setField('address.city', 'Berlin')
|
|
282
|
+
### `validateOn: 'eager'`
|
|
246
283
|
|
|
247
|
-
|
|
248
|
-
console.log(getField('address.city')) // 'Berlin'
|
|
249
|
-
|
|
250
|
-
// Reset to schema defaults
|
|
251
|
-
reset()
|
|
252
|
-
|
|
253
|
-
// Reset to specific values
|
|
254
|
-
reset({ name: 'Alice', email: 'alice@example.com' })
|
|
255
|
-
```
|
|
284
|
+
With `'eager'`, validation runs on input — but only after the field has been blurred at least once. This avoids showing errors while the user is still typing for the first time.
|
|
256
285
|
|
|
257
286
|
---
|
|
258
287
|
|
|
@@ -260,30 +289,24 @@ reset({ name: 'Alice', email: 'alice@example.com' })
|
|
|
260
289
|
|
|
261
290
|
### FieldDefinition array
|
|
262
291
|
|
|
263
|
-
The native format — an array of `FieldDefinition` objects, usually created from the parser functions or manually:
|
|
264
|
-
|
|
265
292
|
```ts
|
|
266
|
-
import type { FieldDefinition } from 'vue-form-schema'
|
|
293
|
+
import type { FieldDefinition } from '@macrulez/vue-form-schema'
|
|
267
294
|
|
|
268
295
|
const schema: FieldDefinition[] = [
|
|
269
296
|
{ type: 'text', name: 'username', required: true },
|
|
270
297
|
]
|
|
271
|
-
|
|
272
298
|
useForm({ schema })
|
|
273
299
|
```
|
|
274
300
|
|
|
275
301
|
### JSON schema
|
|
276
302
|
|
|
277
|
-
A serialisable
|
|
303
|
+
A serialisable format for server-driven schemas. Pass directly to `useForm` (auto-detected) or call `parseJSON` explicitly.
|
|
278
304
|
|
|
279
305
|
```ts
|
|
280
|
-
import { parseJSON } from 'vue-form-schema'
|
|
281
|
-
|
|
282
306
|
const raw = [
|
|
283
307
|
{
|
|
284
308
|
type: 'text',
|
|
285
309
|
name: 'username',
|
|
286
|
-
label: 'Username',
|
|
287
310
|
default: '',
|
|
288
311
|
required: true,
|
|
289
312
|
validators: [
|
|
@@ -291,44 +314,21 @@ const raw = [
|
|
|
291
314
|
{ rule: 'maxLength', value: 20 },
|
|
292
315
|
],
|
|
293
316
|
},
|
|
294
|
-
{
|
|
295
|
-
type: 'email',
|
|
296
|
-
name: 'email',
|
|
297
|
-
validators: [{ rule: 'email' }],
|
|
298
|
-
},
|
|
299
317
|
]
|
|
300
318
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// Option B — parse explicitly
|
|
319
|
+
useForm({ schema: raw }) // auto-detected
|
|
320
|
+
// or
|
|
321
|
+
import { parseJSON } from '@macrulez/vue-form-schema'
|
|
305
322
|
const fields = parseJSON(raw)
|
|
306
|
-
useForm({ schema: fields })
|
|
307
323
|
```
|
|
308
324
|
|
|
309
|
-
**Supported JSON validator rules:**
|
|
310
|
-
|
|
311
|
-
| Rule | Parameter | Example |
|
|
312
|
-
|---|---|---|
|
|
313
|
-
| `required` | — | `{ rule: 'required' }` |
|
|
314
|
-
| `minLength` | `value: number` | `{ rule: 'minLength', value: 2 }` |
|
|
315
|
-
| `maxLength` | `value: number` | `{ rule: 'maxLength', value: 100 }` |
|
|
316
|
-
| `min` | `value: number` | `{ rule: 'min', value: 0 }` |
|
|
317
|
-
| `max` | `value: number` | `{ rule: 'max', value: 9999 }` |
|
|
318
|
-
| `pattern` | `value: string` (regex) | `{ rule: 'pattern', value: '^[a-z]+$' }` |
|
|
319
|
-
| `email` | — | `{ rule: 'email' }` |
|
|
320
|
-
| `url` | — | `{ rule: 'url' }` |
|
|
321
|
-
|
|
322
|
-
All rules accept an optional `message` string to override the default error text.
|
|
325
|
+
**Supported JSON validator rules:** `required`, `minLength`, `maxLength`, `min`, `max`, `pattern`, `email`, `url`. All accept an optional `message` override.
|
|
323
326
|
|
|
324
327
|
### Zod
|
|
325
328
|
|
|
326
|
-
Install Zod and import the adapter from `vue-form-schema/zod`:
|
|
327
|
-
|
|
328
329
|
```ts
|
|
329
330
|
import { z } from 'zod'
|
|
330
|
-
import { parseZod } from 'vue-form-schema/zod'
|
|
331
|
-
import { useForm } from 'vue-form-schema'
|
|
331
|
+
import { parseZod } from '@macrulez/vue-form-schema/zod'
|
|
332
332
|
|
|
333
333
|
const schema = z.object({
|
|
334
334
|
name: z.string().min(2).describe('Full name'),
|
|
@@ -338,40 +338,16 @@ const schema = z.object({
|
|
|
338
338
|
})
|
|
339
339
|
|
|
340
340
|
const fields = parseZod(schema)
|
|
341
|
-
|
|
342
|
-
const { values, submit } = useForm({ schema: fields })
|
|
341
|
+
const { values } = useForm({ schema: fields })
|
|
343
342
|
```
|
|
344
343
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
```ts
|
|
348
|
-
type FormData = z.infer<typeof schema>
|
|
349
|
-
|
|
350
|
-
const { values } = useForm<FormData>({ schema: fields })
|
|
351
|
-
// values.value.name is string
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
**Zod type mapping:**
|
|
355
|
-
|
|
356
|
-
| Zod type | Field type |
|
|
357
|
-
|---|---|
|
|
358
|
-
| `z.string()` | `text` |
|
|
359
|
-
| `z.number()` | `number` |
|
|
360
|
-
| `z.boolean()` | `checkbox` |
|
|
361
|
-
| `z.enum(...)` | `select` |
|
|
362
|
-
| `z.array(...)` | `array` |
|
|
363
|
-
| `z.object(...)` | `group` |
|
|
364
|
-
|
|
365
|
-
Use `.describe('label text')` on any field to set the `label`.
|
|
344
|
+
**Zod → field type mapping:** `z.string()` → `text`, `z.number()` → `number`, `z.boolean()` → `checkbox`, `z.enum()` → `select`, `z.array()` → `array`, `z.object()` → `group`. Use `.describe('label')` to set the field label.
|
|
366
345
|
|
|
367
346
|
### Yup
|
|
368
347
|
|
|
369
|
-
Install Yup and import the adapter from `vue-form-schema/yup`:
|
|
370
|
-
|
|
371
348
|
```ts
|
|
372
349
|
import { object, string, number } from 'yup'
|
|
373
|
-
import { parseYup } from 'vue-form-schema/yup'
|
|
374
|
-
import { useForm } from 'vue-form-schema'
|
|
350
|
+
import { parseYup } from '@macrulez/vue-form-schema/yup'
|
|
375
351
|
|
|
376
352
|
const schema = object({
|
|
377
353
|
name: string().required().label('Full name'),
|
|
@@ -380,275 +356,634 @@ const schema = object({
|
|
|
380
356
|
})
|
|
381
357
|
|
|
382
358
|
const fields = parseYup(schema)
|
|
359
|
+
const { values } = useForm({ schema: fields })
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Valibot
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
import * as v from 'valibot'
|
|
366
|
+
import { parseValibot } from '@macrulez/vue-form-schema/valibot'
|
|
367
|
+
|
|
368
|
+
const schema = v.object({
|
|
369
|
+
name: v.pipe(v.string(), v.minLength(2)),
|
|
370
|
+
email: v.pipe(v.string(), v.email()),
|
|
371
|
+
age: v.optional(v.number()),
|
|
372
|
+
role: v.picklist(['admin', 'user']),
|
|
373
|
+
})
|
|
383
374
|
|
|
384
|
-
const
|
|
375
|
+
const fields = parseValibot(schema)
|
|
376
|
+
const { values } = useForm({ schema: fields })
|
|
385
377
|
```
|
|
386
378
|
|
|
387
|
-
**
|
|
379
|
+
**Valibot → field type mapping:** `v.string()` → `text`, `v.number()` → `number`, `v.boolean()` → `checkbox`, `v.picklist()` / `v.enum()` → `select`, `v.array()` → `array`, `v.object()` → `group`. `v.pipe(v.string(), v.email())` → `type: 'email'`. `v.optional()` / `v.nullable()` → `required: false`.
|
|
388
380
|
|
|
389
|
-
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Built-in validators
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
import {
|
|
387
|
+
required, minLength, maxLength, min, max, pattern, email, url,
|
|
388
|
+
sameAs,
|
|
389
|
+
fileType, fileSize, fileCount,
|
|
390
|
+
} from '@macrulez/vue-form-schema'
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
| Function | Description |
|
|
390
394
|
|---|---|
|
|
391
|
-
| `
|
|
392
|
-
| `
|
|
393
|
-
| `
|
|
394
|
-
| `
|
|
395
|
-
| `
|
|
395
|
+
| `required` | Fails for `null`, `undefined`, `''`, or empty array |
|
|
396
|
+
| `minLength(n, msg?)` | Min length for string or array |
|
|
397
|
+
| `maxLength(n, msg?)` | Max length for string or array |
|
|
398
|
+
| `min(n, msg?)` | Numeric minimum |
|
|
399
|
+
| `max(n, msg?)` | Numeric maximum |
|
|
400
|
+
| `pattern(re, msg?)` | Regex match |
|
|
401
|
+
| `email` | Basic email format |
|
|
402
|
+
| `url` | Valid URL (`new URL()`) |
|
|
403
|
+
| `sameAs(field, msg?)` | Value must equal another field |
|
|
404
|
+
| `fileType(types[], msg?)` | File MIME type or extension whitelist |
|
|
405
|
+
| `fileSize(bytes, msg?)` | Max file size |
|
|
406
|
+
| `fileCount(n, msg?)` | Max number of files |
|
|
396
407
|
|
|
397
408
|
---
|
|
398
409
|
|
|
399
|
-
##
|
|
410
|
+
## Custom validators
|
|
400
411
|
|
|
401
|
-
|
|
412
|
+
### Sync
|
|
402
413
|
|
|
403
414
|
```ts
|
|
404
|
-
import {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
} from 'vue-form-schema'
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
| Function | Signature | Description |
|
|
417
|
-
|---|---|---|
|
|
418
|
-
| `required` | `ValidatorFn` | Fails for `null`, `undefined`, `''`, or empty array |
|
|
419
|
-
| `minLength(n, msg?)` | `(n: number) => ValidatorFn` | Min length for string or array |
|
|
420
|
-
| `maxLength(n, msg?)` | `(n: number) => ValidatorFn` | Max length for string or array |
|
|
421
|
-
| `min(n, msg?)` | `(n: number) => ValidatorFn` | Numeric minimum |
|
|
422
|
-
| `max(n, msg?)` | `(n: number) => ValidatorFn` | Numeric maximum |
|
|
423
|
-
| `pattern(re, msg?)` | `(re: RegExp) => ValidatorFn` | Regex match |
|
|
424
|
-
| `email` | `ValidatorFn` | Basic email format check |
|
|
425
|
-
| `url` | `ValidatorFn` | Valid URL (uses `new URL()`) |
|
|
415
|
+
import type { ValidatorFn } from '@macrulez/vue-form-schema'
|
|
416
|
+
|
|
417
|
+
const noSpaces: ValidatorFn = (value) =>
|
|
418
|
+
typeof value === 'string' && value.includes(' ') ? 'No spaces allowed' : null
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Async
|
|
422
|
+
|
|
423
|
+
Async validators are debounced (300 ms). Errors are merged into `errors` after resolution.
|
|
426
424
|
|
|
427
425
|
```ts
|
|
428
|
-
import {
|
|
426
|
+
import type { AsyncValidatorFn } from '@macrulez/vue-form-schema'
|
|
427
|
+
|
|
428
|
+
const uniqueUsername: AsyncValidatorFn = async (value) => {
|
|
429
|
+
const { taken } = await fetch(`/api/check?q=${value}`).then((r) => r.json())
|
|
430
|
+
return taken ? 'Username is taken' : null
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Multiple errors per field (`validateMode`)
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
useForm({
|
|
438
|
+
schema,
|
|
439
|
+
validateMode: 'all', // collect all errors per field (default: 'first')
|
|
440
|
+
})
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Cross-field validation
|
|
446
|
+
|
|
447
|
+
Use `sameAs` for password confirmation or write a custom validator — all validators receive `allValues` as the second argument.
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
import { sameAs } from '@macrulez/vue-form-schema'
|
|
429
451
|
|
|
430
452
|
const schema: FieldDefinition[] = [
|
|
453
|
+
{ type: 'text', name: 'password', label: 'Password', required: true },
|
|
431
454
|
{
|
|
432
455
|
type: 'text',
|
|
433
|
-
name: '
|
|
456
|
+
name: 'confirm',
|
|
457
|
+
label: 'Confirm password',
|
|
458
|
+
validators: [sameAs('password', 'Passwords must match')],
|
|
459
|
+
},
|
|
460
|
+
]
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Conditional fields
|
|
466
|
+
|
|
467
|
+
`visible` and `disabled` can be a boolean, a reactive function, or a safe string expression.
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
const schema: FieldDefinition[] = [
|
|
471
|
+
{ type: 'checkbox', name: 'hasCompany', label: 'I represent a company' },
|
|
472
|
+
{
|
|
473
|
+
type: 'text',
|
|
474
|
+
name: 'companyName',
|
|
475
|
+
label: 'Company name',
|
|
476
|
+
visible: (values) => values['hasCompany'] === true,
|
|
434
477
|
required: true,
|
|
435
|
-
validators: [
|
|
436
|
-
minLength(3, 'At least 3 characters'),
|
|
437
|
-
maxLength(20),
|
|
438
|
-
],
|
|
439
478
|
},
|
|
479
|
+
// string expression — has access to the `values` variable
|
|
440
480
|
{
|
|
441
|
-
type: '
|
|
442
|
-
name: '
|
|
443
|
-
|
|
481
|
+
type: 'select',
|
|
482
|
+
name: 'drink',
|
|
483
|
+
label: 'Drink',
|
|
484
|
+
visible: 'values.age >= 18',
|
|
485
|
+
options: [{ label: 'Beer', value: 'beer' }, { label: 'Water', value: 'water' }],
|
|
444
486
|
},
|
|
445
487
|
]
|
|
446
488
|
```
|
|
447
489
|
|
|
448
|
-
|
|
490
|
+
Set `clearOnHide: true` in `useForm` to automatically reset a hidden field's value.
|
|
449
491
|
|
|
450
|
-
|
|
492
|
+
---
|
|
451
493
|
|
|
452
|
-
|
|
494
|
+
## Dynamic options
|
|
453
495
|
|
|
454
|
-
|
|
496
|
+
`options` can be a static array, a **sync function**, or an **async function**.
|
|
455
497
|
|
|
456
498
|
```ts
|
|
457
|
-
|
|
499
|
+
// Sync — re-evaluated on every values change
|
|
500
|
+
{
|
|
501
|
+
type: 'select',
|
|
502
|
+
name: 'city',
|
|
503
|
+
options: (values) => citiesByCountry[values['country'] as string] ?? [],
|
|
504
|
+
}
|
|
458
505
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
506
|
+
// Async — fetched on mount and re-fetched when optionsDeps change
|
|
507
|
+
{
|
|
508
|
+
type: 'select',
|
|
509
|
+
name: 'framework',
|
|
510
|
+
optionsDeps: ['language'],
|
|
511
|
+
options: async (values) => {
|
|
512
|
+
const res = await fetch(`/api/frameworks?lang=${values['language']}`)
|
|
513
|
+
return res.json()
|
|
514
|
+
},
|
|
462
515
|
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
While loading, `optionsLoading.value['framework']` is `true` and the select is disabled in `FormRenderer`. Access the loading state directly via `form.optionsLoading`.
|
|
519
|
+
|
|
520
|
+
### Computed `defaultValue`
|
|
463
521
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
522
|
+
`defaultValue` can also be a function evaluated at form initialisation with already-resolved partial values as context:
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
{
|
|
526
|
+
type: 'text',
|
|
527
|
+
name: 'displayName',
|
|
528
|
+
defaultValue: (values) => `${values.firstName} ${values.lastName}`,
|
|
468
529
|
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## Dynamic array fields
|
|
469
535
|
|
|
536
|
+
### Schema definition
|
|
537
|
+
|
|
538
|
+
```ts
|
|
470
539
|
const schema: FieldDefinition[] = [
|
|
471
|
-
{ type: 'text', name: 'password', label: 'Password' },
|
|
472
540
|
{
|
|
473
|
-
type: '
|
|
474
|
-
name: '
|
|
475
|
-
label: '
|
|
476
|
-
|
|
541
|
+
type: 'array',
|
|
542
|
+
name: 'members',
|
|
543
|
+
label: 'Team members',
|
|
544
|
+
fields: [
|
|
545
|
+
{ type: 'text', name: 'members.name', label: 'Name', required: true },
|
|
546
|
+
{ type: 'email', name: 'members.email', label: 'Email', required: true },
|
|
547
|
+
],
|
|
477
548
|
},
|
|
478
549
|
]
|
|
479
550
|
```
|
|
480
551
|
|
|
481
|
-
|
|
552
|
+
`FormRenderer` renders an `ArrayField` automatically with Add / Remove buttons.
|
|
482
553
|
|
|
483
|
-
|
|
554
|
+
### `useFieldArray` composable
|
|
484
555
|
|
|
485
556
|
```ts
|
|
486
|
-
import
|
|
557
|
+
import { useFieldArray } from '@macrulez/vue-form-schema'
|
|
487
558
|
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
559
|
+
const { rows, count, append, prepend, remove, move, swap, replace } =
|
|
560
|
+
useFieldArray(form, 'members')
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
| Method | Description |
|
|
564
|
+
|---|---|
|
|
565
|
+
| `append(defaults?)` | Add a row at the end |
|
|
566
|
+
| `prepend(defaults?)` | Add a row at the beginning |
|
|
567
|
+
| `remove(index)` | Remove a row |
|
|
568
|
+
| `move(from, to)` | Move a row |
|
|
569
|
+
| `swap(a, b)` | Swap two rows |
|
|
570
|
+
| `replace(index, defaults?)` | Replace a row with fresh defaults |
|
|
493
571
|
|
|
572
|
+
`rows` is a `ComputedRef<FieldArrayRow[]>`. Each row exposes `index`, `key`, and `fields` — the nested `FieldDefinition[]` with prefixed paths for that row.
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Multi-step forms
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
import { useMultiStepForm } from '@macrulez/vue-form-schema'
|
|
580
|
+
|
|
581
|
+
const wizard = useMultiStepForm(
|
|
582
|
+
[
|
|
583
|
+
{ title: 'Account', schema: accountFields },
|
|
584
|
+
{ title: 'Profile', schema: profileFields },
|
|
585
|
+
{ title: 'Confirm', schema: confirmFields },
|
|
586
|
+
],
|
|
587
|
+
async (allValues) => {
|
|
588
|
+
await api.register(allValues)
|
|
589
|
+
},
|
|
590
|
+
)
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
| Property / Method | Description |
|
|
594
|
+
|---|---|
|
|
595
|
+
| `currentStep` | `Ref<number>` — 0-based index |
|
|
596
|
+
| `totalSteps` | Number of steps |
|
|
597
|
+
| `isFirstStep` / `isLastStep` | `ComputedRef<boolean>` |
|
|
598
|
+
| `form` | `UseFormReturn` for the current step |
|
|
599
|
+
| `values` | All values across all steps merged |
|
|
600
|
+
| `next()` | Validate current step then advance (returns `false` if invalid) |
|
|
601
|
+
| `back()` | Go to previous step |
|
|
602
|
+
| `goTo(n)` | Jump to step `n` |
|
|
603
|
+
| `submit()` | Validate all steps then call `onSubmit` |
|
|
604
|
+
|
|
605
|
+
### `MultiStepFormRenderer`
|
|
606
|
+
|
|
607
|
+
```vue
|
|
608
|
+
import { MultiStepFormRenderer } from '@macrulez/vue-form-schema/ui'
|
|
609
|
+
|
|
610
|
+
<MultiStepFormRenderer :wizard="wizard" />
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Renders the current step's fields and Back / Next / Submit navigation buttons.
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Field transform & parse
|
|
618
|
+
|
|
619
|
+
```ts
|
|
494
620
|
const schema: FieldDefinition[] = [
|
|
495
621
|
{
|
|
496
622
|
type: 'text',
|
|
497
623
|
name: 'username',
|
|
498
|
-
|
|
499
|
-
|
|
624
|
+
// trim on every keystroke
|
|
625
|
+
transform: (value) => (typeof value === 'string' ? value.trim() : value),
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
type: 'text',
|
|
629
|
+
name: 'tags',
|
|
630
|
+
defaultValue: 'vue,react',
|
|
631
|
+
// split into array at submit time — values.tags is still a string
|
|
632
|
+
parse: (raw) => String(raw).split(',').map((s) => s.trim()),
|
|
500
633
|
},
|
|
501
634
|
]
|
|
502
635
|
```
|
|
503
636
|
|
|
504
|
-
|
|
637
|
+
`transform` mutates `values` immediately. `parse` runs only at submit time and does not mutate `values`.
|
|
505
638
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
`visible` and `disabled` can be a static boolean, a reactive function, or a safe string expression.
|
|
639
|
+
---
|
|
509
640
|
|
|
510
|
-
|
|
641
|
+
## File upload field
|
|
511
642
|
|
|
512
643
|
```ts
|
|
644
|
+
import { fileType, fileSize, fileCount } from '@macrulez/vue-form-schema'
|
|
645
|
+
|
|
513
646
|
const schema: FieldDefinition[] = [
|
|
514
647
|
{
|
|
515
|
-
type: '
|
|
516
|
-
name: '
|
|
517
|
-
label: '
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
required: true,
|
|
524
|
-
// shown only when hasCompany is checked
|
|
525
|
-
visible: (values) => values['hasCompany'] === true,
|
|
648
|
+
type: 'file',
|
|
649
|
+
name: 'avatar',
|
|
650
|
+
label: 'Profile photo',
|
|
651
|
+
accept: 'image/*',
|
|
652
|
+
validators: [
|
|
653
|
+
fileType(['image/'], 'Only images are accepted'),
|
|
654
|
+
fileSize(2 * 1024 * 1024, 'Max 2 MB'),
|
|
655
|
+
],
|
|
526
656
|
},
|
|
527
657
|
{
|
|
528
|
-
type: '
|
|
529
|
-
name: '
|
|
530
|
-
label: '
|
|
531
|
-
|
|
532
|
-
|
|
658
|
+
type: 'file',
|
|
659
|
+
name: 'attachments',
|
|
660
|
+
label: 'Attachments',
|
|
661
|
+
multiple: true,
|
|
662
|
+
validators: [fileCount(5, 'Up to 5 files')],
|
|
533
663
|
},
|
|
534
664
|
]
|
|
535
665
|
```
|
|
536
666
|
|
|
537
|
-
|
|
667
|
+
`values['avatar']` is `File | null`; `values['attachments']` is `File[] | null`.
|
|
668
|
+
|
|
669
|
+
`FormRenderer` automatically renders `FileField` with a drag-and-drop zone and a file list with remove buttons.
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## Custom field components
|
|
674
|
+
|
|
675
|
+
A custom component is a pure **presentation layer** — it receives pre-computed validation state as props and signals changes back to the form. No validation logic lives inside the component itself.
|
|
676
|
+
|
|
677
|
+
### How validation flows
|
|
678
|
+
|
|
679
|
+
```
|
|
680
|
+
useForm
|
|
681
|
+
├─ validators / asyncValidators / required ← defined in the schema
|
|
682
|
+
├─ errors.value['fieldName'] = ['Too short'] ← computed internally
|
|
683
|
+
└─ passes to your component via props:
|
|
684
|
+
error: string[] — list of error messages
|
|
685
|
+
touched: boolean — whether the field has been blurred
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
Your component's only job:
|
|
689
|
+
|
|
690
|
+
| What | How |
|
|
691
|
+
|---|---|
|
|
692
|
+
| Report a value change | `emit('update:modelValue', newValue)` |
|
|
693
|
+
| Trigger validation | `emit('blur')` — fires validation when `validateOn` is `'blur'` or `'eager'` |
|
|
694
|
+
| Show errors | Read `props.error` / `props.touched` (or use `useFormField`) |
|
|
695
|
+
|
|
696
|
+
### The `FormFieldProps` contract
|
|
697
|
+
|
|
698
|
+
Every component that plugs into the library must declare these props and two emits:
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
import type { FormFieldProps } from '@macrulez/vue-form-schema'
|
|
702
|
+
|
|
703
|
+
// props
|
|
704
|
+
const props = defineProps<FormFieldProps>()
|
|
705
|
+
// {
|
|
706
|
+
// field: FieldDefinition — the full field config (validators, label, …)
|
|
707
|
+
// modelValue: unknown — current value from form state
|
|
708
|
+
// error: string[] — validation errors (empty when valid)
|
|
709
|
+
// touched: boolean — true after first blur
|
|
710
|
+
// }
|
|
711
|
+
|
|
712
|
+
// emits
|
|
713
|
+
const emit = defineEmits<{
|
|
714
|
+
'update:modelValue': [value: unknown]
|
|
715
|
+
blur: []
|
|
716
|
+
}>()
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Complete example — custom phone input
|
|
720
|
+
|
|
721
|
+
```vue
|
|
722
|
+
<!-- MyPhoneInput.vue -->
|
|
723
|
+
<script setup lang="ts">
|
|
724
|
+
import { computed } from 'vue'
|
|
725
|
+
import { useFormField } from '@macrulez/vue-form-schema'
|
|
726
|
+
import type { FormFieldProps } from '@macrulez/vue-form-schema'
|
|
727
|
+
|
|
728
|
+
const props = defineProps<FormFieldProps>()
|
|
729
|
+
const emit = defineEmits<{
|
|
730
|
+
'update:modelValue': [value: string]
|
|
731
|
+
blur: []
|
|
732
|
+
}>()
|
|
733
|
+
|
|
734
|
+
const { hasError, errorMessage, isRequired } = useFormField(props)
|
|
735
|
+
|
|
736
|
+
// strip non-digits for storage, display formatted
|
|
737
|
+
const display = computed(() =>
|
|
738
|
+
String(props.modelValue ?? '').replace(/\D/g, '').replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3'),
|
|
739
|
+
)
|
|
740
|
+
</script>
|
|
741
|
+
|
|
742
|
+
<template>
|
|
743
|
+
<div class="field">
|
|
744
|
+
<label :for="field.name">
|
|
745
|
+
{{ field.label }}
|
|
746
|
+
<span v-if="isRequired" aria-hidden="true">*</span>
|
|
747
|
+
</label>
|
|
748
|
+
|
|
749
|
+
<input
|
|
750
|
+
:id="field.name"
|
|
751
|
+
type="tel"
|
|
752
|
+
:value="display"
|
|
753
|
+
:aria-invalid="hasError ? 'true' : 'false'"
|
|
754
|
+
:aria-describedby="hasError ? `${field.name}-error` : undefined"
|
|
755
|
+
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value.replace(/\D/g, ''))"
|
|
756
|
+
@blur="emit('blur')"
|
|
757
|
+
/>
|
|
758
|
+
|
|
759
|
+
<p v-if="hasError" :id="`${field.name}-error`" role="alert">
|
|
760
|
+
{{ errorMessage }}
|
|
761
|
+
</p>
|
|
762
|
+
</div>
|
|
763
|
+
</template>
|
|
764
|
+
```
|
|
538
765
|
|
|
539
|
-
|
|
766
|
+
### Attach to a field via `field.component`
|
|
540
767
|
|
|
541
768
|
```ts
|
|
769
|
+
import MyPhoneInput from './MyPhoneInput.vue'
|
|
770
|
+
import { minLength, pattern } from '@macrulez/vue-form-schema'
|
|
771
|
+
|
|
542
772
|
const schema: FieldDefinition[] = [
|
|
543
|
-
{ type: 'number', name: 'age', label: 'Age' },
|
|
544
773
|
{
|
|
545
|
-
type: '
|
|
546
|
-
name: '
|
|
547
|
-
label: '
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
{
|
|
774
|
+
type: 'text',
|
|
775
|
+
name: 'phone',
|
|
776
|
+
label: 'Phone number',
|
|
777
|
+
component: MyPhoneInput, // ← your component renders instead of TextField
|
|
778
|
+
required: true,
|
|
779
|
+
validators: [
|
|
780
|
+
minLength(10, 'Enter a full phone number'),
|
|
781
|
+
pattern(/^\d{10}$/, 'Digits only, 10 characters'),
|
|
553
782
|
],
|
|
554
783
|
},
|
|
555
784
|
]
|
|
556
785
|
```
|
|
557
786
|
|
|
558
|
-
###
|
|
787
|
+
### Using without `FormRenderer` (manual wiring)
|
|
559
788
|
|
|
560
|
-
|
|
789
|
+
If you render fields yourself — without `FormRenderer` — wire errors and the touch handler directly:
|
|
790
|
+
|
|
791
|
+
```vue
|
|
792
|
+
<script setup lang="ts">
|
|
793
|
+
import { useForm } from '@macrulez/vue-form-schema'
|
|
794
|
+
import MyPhoneInput from './MyPhoneInput.vue'
|
|
795
|
+
|
|
796
|
+
const form = useForm({ schema, validateOn: 'blur' })
|
|
797
|
+
const touchField = (form as any).touchField // exposed internally
|
|
798
|
+
</script>
|
|
799
|
+
|
|
800
|
+
<template>
|
|
801
|
+
<form @submit.prevent="form.submit()">
|
|
802
|
+
<MyPhoneInput
|
|
803
|
+
:field="form.fields.value[0]"
|
|
804
|
+
:model-value="form.values.value.phone"
|
|
805
|
+
:error="form.errors.value.phone ?? []"
|
|
806
|
+
:touched="form.touched.value.phone ?? false"
|
|
807
|
+
@update:model-value="form.setField('phone', $event)"
|
|
808
|
+
@blur="touchField('phone')"
|
|
809
|
+
/>
|
|
810
|
+
<button type="submit">Save</button>
|
|
811
|
+
</form>
|
|
812
|
+
</template>
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
### `useFormField` helper — computed shortcuts
|
|
561
816
|
|
|
562
817
|
```ts
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
818
|
+
import { useFormField } from '@macrulez/vue-form-schema'
|
|
819
|
+
|
|
820
|
+
const props = defineProps<FormFieldProps>()
|
|
821
|
+
const {
|
|
822
|
+
hasError, // ComputedRef<boolean> — touched && error.length > 0
|
|
823
|
+
errorMessage, // ComputedRef<string | null> — first error, or null
|
|
824
|
+
allErrors, // ComputedRef<string[]> — all errors when touched, else []
|
|
825
|
+
isRequired, // ComputedRef<boolean>
|
|
826
|
+
isDisabled, // ComputedRef<boolean>
|
|
827
|
+
} = useFormField(props)
|
|
567
828
|
```
|
|
568
829
|
|
|
569
830
|
---
|
|
570
831
|
|
|
832
|
+
## Component registry
|
|
833
|
+
|
|
834
|
+
Replace all instances of a field type across a subtree — useful for integrating UI libraries.
|
|
835
|
+
|
|
836
|
+
### App-level (Vue plugin)
|
|
837
|
+
|
|
838
|
+
```ts
|
|
839
|
+
import { createApp } from 'vue'
|
|
840
|
+
import { createFormRegistry } from '@macrulez/vue-form-schema'
|
|
841
|
+
import { ElInput, ElSelect } from 'element-plus'
|
|
842
|
+
|
|
843
|
+
createApp(App)
|
|
844
|
+
.use(createFormRegistry({ text: ElInput, select: ElSelect }))
|
|
845
|
+
.mount('#app')
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### Subtree-level
|
|
849
|
+
|
|
850
|
+
```ts
|
|
851
|
+
import { provideRegistry } from '@macrulez/vue-form-schema'
|
|
852
|
+
|
|
853
|
+
// Inside a component's setup()
|
|
854
|
+
provideRegistry({ checkbox: MyToggle })
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
Component priority: `field.component` > `FormRenderer :components` prop > registry > built-in defaults.
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
571
861
|
## Input masking
|
|
572
862
|
|
|
573
|
-
Masks format user input in real time.
|
|
863
|
+
Masks format user input in real time. Applied automatically in `FormRenderer`; also usable standalone.
|
|
574
864
|
|
|
575
|
-
###
|
|
865
|
+
### Presets
|
|
576
866
|
|
|
577
|
-
| Preset |
|
|
578
|
-
|
|
579
|
-
| `phone-ru` | `+7 (
|
|
580
|
-
| `phone-eu` |
|
|
581
|
-
| `date` |
|
|
582
|
-
| `inn` |
|
|
583
|
-
| `iban` | `
|
|
867
|
+
| Preset | Example output |
|
|
868
|
+
|---|---|
|
|
869
|
+
| `phone-ru` | `+7 (916) 123-45-67` |
|
|
870
|
+
| `phone-eu` | `+49 (30) 123-45-67` |
|
|
871
|
+
| `date` | `01.01.2024` |
|
|
872
|
+
| `inn` | `123456789012` |
|
|
873
|
+
| `iban` | `GB29 NWBK 6016 1331 9268 19` |
|
|
584
874
|
|
|
585
875
|
```ts
|
|
586
|
-
|
|
587
|
-
const field: FieldDefinition = {
|
|
588
|
-
type: 'text',
|
|
589
|
-
name: 'phone',
|
|
590
|
-
label: 'Phone',
|
|
591
|
-
mask: { preset: 'phone-ru' },
|
|
592
|
-
}
|
|
876
|
+
{ type: 'text', name: 'phone', mask: { preset: 'phone-ru' } }
|
|
593
877
|
```
|
|
594
878
|
|
|
595
879
|
### Custom patterns
|
|
596
880
|
|
|
597
|
-
|
|
598
|
-
- `A` — matches a single letter (a–z, A–Z); output is uppercased
|
|
599
|
-
- Any other character — treated as a fixed literal
|
|
881
|
+
`#` = digit, `A` = letter (uppercased), anything else = literal.
|
|
600
882
|
|
|
601
883
|
```ts
|
|
602
|
-
|
|
603
|
-
const field: FieldDefinition = {
|
|
604
|
-
type: 'text',
|
|
605
|
-
name: 'postcode',
|
|
606
|
-
label: 'Postcode',
|
|
607
|
-
mask: { pattern: 'AA####' }, // e.g. AB1234
|
|
608
|
-
}
|
|
884
|
+
{ type: 'text', name: 'postcode', mask: { pattern: 'AA####' } } // AB1234
|
|
609
885
|
```
|
|
610
886
|
|
|
611
|
-
### Standalone
|
|
887
|
+
### Standalone API
|
|
612
888
|
|
|
613
889
|
```ts
|
|
614
|
-
import { applyMask, removeMask, bindMask } from 'vue-form-schema'
|
|
890
|
+
import { applyMask, removeMask, bindMask } from '@macrulez/vue-form-schema'
|
|
615
891
|
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
// → '+7 (916) 123-45-67'
|
|
892
|
+
applyMask('9161234567', { preset: 'phone-ru' }) // '+7 (916) 123-45-67'
|
|
893
|
+
removeMask('+7 (916) 123-45-67', { preset: 'phone-ru' }) // '9161234567'
|
|
619
894
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
895
|
+
const cleanup = bindMask(inputEl, { preset: 'date' })
|
|
896
|
+
onUnmounted(cleanup)
|
|
897
|
+
```
|
|
623
898
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
899
|
+
---
|
|
900
|
+
|
|
901
|
+
## Schema composition
|
|
902
|
+
|
|
903
|
+
```ts
|
|
904
|
+
import { mergeSchemas, omitFields, pickFields, extendField } from '@macrulez/vue-form-schema'
|
|
905
|
+
|
|
906
|
+
const base = [
|
|
907
|
+
{ type: 'text' as const, name: 'firstName' },
|
|
908
|
+
{ type: 'text' as const, name: 'lastName' },
|
|
909
|
+
{ type: 'email' as const, name: 'email' },
|
|
910
|
+
]
|
|
911
|
+
|
|
912
|
+
// Combine — later schemas win on name collision
|
|
913
|
+
const extended = mergeSchemas(base, [{ type: 'text' as const, name: 'phone' }])
|
|
914
|
+
|
|
915
|
+
// Remove fields
|
|
916
|
+
const noEmail = omitFields(base, ['email'])
|
|
917
|
+
|
|
918
|
+
// Keep only specific fields
|
|
919
|
+
const nameOnly = pickFields(base, ['firstName', 'lastName'])
|
|
920
|
+
|
|
921
|
+
// Non-mutating patch
|
|
922
|
+
const required = extendField(base, 'email', { required: true, label: 'Email address' })
|
|
627
923
|
```
|
|
628
924
|
|
|
629
925
|
---
|
|
630
926
|
|
|
631
|
-
##
|
|
927
|
+
## TypeScript inference
|
|
632
928
|
|
|
633
|
-
`
|
|
929
|
+
`InferValues<T>` maps a `readonly FieldDefinition[]` literal to a typed values object.
|
|
634
930
|
|
|
635
931
|
```ts
|
|
636
|
-
import {
|
|
932
|
+
import { defineSchema } from '@macrulez/vue-form-schema'
|
|
933
|
+
import type { InferValues } from '@macrulez/vue-form-schema'
|
|
934
|
+
|
|
935
|
+
const schema = defineSchema([
|
|
936
|
+
{ type: 'text' as const, name: 'username' as const },
|
|
937
|
+
{ type: 'number' as const, name: 'age' as const },
|
|
938
|
+
{ type: 'checkbox' as const, name: 'agreed' as const },
|
|
939
|
+
] as const)
|
|
940
|
+
|
|
941
|
+
type Values = InferValues<typeof schema>
|
|
942
|
+
// { username: string; age: number; agreed: boolean }
|
|
943
|
+
|
|
944
|
+
const { values } = useForm<Values>({ schema })
|
|
945
|
+
// values.value.username is string ✓
|
|
637
946
|
```
|
|
638
947
|
|
|
639
|
-
|
|
948
|
+
**Type mapping:** `checkbox` → `boolean`, `number` → `number`, `array` → `unknown[]`, everything else → `string`.
|
|
640
949
|
|
|
641
|
-
|
|
642
|
-
<script setup lang="ts">
|
|
643
|
-
import { useForm } from 'vue-form-schema'
|
|
644
|
-
import { FormRenderer } from 'vue-form-schema/ui'
|
|
950
|
+
---
|
|
645
951
|
|
|
646
|
-
|
|
647
|
-
</script>
|
|
952
|
+
## Persisted forms
|
|
648
953
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
954
|
+
```ts
|
|
955
|
+
useForm({
|
|
956
|
+
schema,
|
|
957
|
+
persist: 'local', // or 'session'
|
|
958
|
+
persistKey: 'checkout', // optional — defaults to a hash of field names
|
|
959
|
+
})
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
Values are restored from storage on `onMounted`. `reset()` clears the stored value. SSR-safe: the storage read is guarded by `typeof window !== 'undefined'`.
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
966
|
+
## Debug mode
|
|
967
|
+
|
|
968
|
+
```ts
|
|
969
|
+
// Log every values change to console.group
|
|
970
|
+
useForm({ schema, debug: true })
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
```ts
|
|
974
|
+
// Reactive snapshot of all form state
|
|
975
|
+
import { useFormDebug } from '@macrulez/vue-form-schema'
|
|
976
|
+
|
|
977
|
+
const { snapshot } = useFormDebug(form)
|
|
978
|
+
// snapshot.value = { values, errors, touched, isDirty, isValid, isSubmitting }
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
---
|
|
982
|
+
|
|
983
|
+
## FormRenderer UI component
|
|
984
|
+
|
|
985
|
+
```ts
|
|
986
|
+
import { FormRenderer } from '@macrulez/vue-form-schema/ui'
|
|
652
987
|
```
|
|
653
988
|
|
|
654
989
|
### Props
|
|
@@ -663,50 +998,16 @@ const form = useForm({ schema, onSubmit })
|
|
|
663
998
|
|
|
664
999
|
| Slot | Scope | Description |
|
|
665
1000
|
|---|---|---|
|
|
666
|
-
| `#field-{name}` | `{ field, value, error, touched, setValue, touch }` | Replace
|
|
667
|
-
| `#label-{name}` | `{ field }` | Replace
|
|
668
|
-
| `#error-{name}` | `{ field, error }` | Replace
|
|
1001
|
+
| `#field-{name}` | `{ field, value, error, touched, setValue, touch }` | Replace an entire field |
|
|
1002
|
+
| `#label-{name}` | `{ field }` | Replace a label |
|
|
1003
|
+
| `#error-{name}` | `{ field, error }` | Replace error display |
|
|
669
1004
|
| `#submit` | `{ isSubmitting, isValid }` | Replace the submit button |
|
|
670
1005
|
|
|
671
|
-
### Custom field renderers
|
|
672
|
-
|
|
673
|
-
Pass a `components` map to swap built-in inputs with your own (e.g. Element Plus, Naive UI):
|
|
674
|
-
|
|
675
|
-
```vue
|
|
676
|
-
<script setup lang="ts">
|
|
677
|
-
import { ElInput, ElSelect } from 'element-plus'
|
|
678
|
-
|
|
679
|
-
const myComponents = { text: ElInput, select: ElSelect }
|
|
680
|
-
</script>
|
|
681
|
-
|
|
682
|
-
<template>
|
|
683
|
-
<FormRenderer :form="form" :components="myComponents" />
|
|
684
|
-
</template>
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
### Per-field slot override
|
|
688
|
-
|
|
689
|
-
```vue
|
|
690
|
-
<template>
|
|
691
|
-
<FormRenderer :form="form">
|
|
692
|
-
<!-- replace the "avatar" field entirely -->
|
|
693
|
-
<template #field-avatar="{ value, setValue }">
|
|
694
|
-
<AvatarUploader :model-value="value" @update:model-value="setValue" />
|
|
695
|
-
</template>
|
|
696
|
-
|
|
697
|
-
<!-- custom submit -->
|
|
698
|
-
<template #submit="{ isSubmitting }">
|
|
699
|
-
<MyButton type="submit" :loading="isSubmitting">Save profile</MyButton>
|
|
700
|
-
</template>
|
|
701
|
-
</FormRenderer>
|
|
702
|
-
</template>
|
|
703
|
-
```
|
|
704
|
-
|
|
705
1006
|
### Built-in field components
|
|
706
1007
|
|
|
707
|
-
|
|
1008
|
+
All exported individually from `vue-form-schema/ui`:
|
|
708
1009
|
|
|
709
|
-
| Component |
|
|
1010
|
+
| Component | Field types |
|
|
710
1011
|
|---|---|
|
|
711
1012
|
| `TextField` | `text`, `email` |
|
|
712
1013
|
| `NumberField` | `number` |
|
|
@@ -715,56 +1016,55 @@ The following components are exported from `vue-form-schema/ui` and can be used
|
|
|
715
1016
|
| `CheckboxField` | `checkbox` |
|
|
716
1017
|
| `RadioField` | `radio` |
|
|
717
1018
|
| `DateField` | `date` |
|
|
1019
|
+
| `ArrayField` | `array` |
|
|
1020
|
+
| `FileField` | `file` |
|
|
718
1021
|
|
|
719
1022
|
---
|
|
720
1023
|
|
|
721
|
-
##
|
|
1024
|
+
## Tailwind UI theme
|
|
722
1025
|
|
|
723
|
-
|
|
1026
|
+
A drop-in replacement for `FormRenderer` using Tailwind utility classes — no custom CSS needed in your app. Requires Tailwind CSS installed and configured to scan library source files.
|
|
724
1027
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1028
|
+
```ts
|
|
1029
|
+
import { TailwindFormRenderer } from '@macrulez/vue-form-schema/ui/tailwind'
|
|
1030
|
+
```
|
|
728
1031
|
|
|
729
|
-
|
|
1032
|
+
```vue
|
|
1033
|
+
<!-- Same form, same schema — just swap the renderer -->
|
|
1034
|
+
<TailwindFormRenderer :form="form" submit-label="Save" />
|
|
1035
|
+
```
|
|
730
1036
|
|
|
731
|
-
|
|
1037
|
+
All field components are also exported individually:
|
|
732
1038
|
|
|
733
1039
|
```ts
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1040
|
+
import {
|
|
1041
|
+
TwTextField, TwSelectField, TwCheckboxField,
|
|
1042
|
+
TwRadioField, TwFileField, TwArrayField,
|
|
1043
|
+
// …
|
|
1044
|
+
} from '@macrulez/vue-form-schema/ui/tailwind'
|
|
1045
|
+
```
|
|
740
1046
|
|
|
741
|
-
|
|
742
|
-
schema,
|
|
743
|
-
onSubmit: (data) => {
|
|
744
|
-
// data is typed as UserForm
|
|
745
|
-
console.log(data.name)
|
|
746
|
-
},
|
|
747
|
-
})
|
|
1047
|
+
---
|
|
748
1048
|
|
|
749
|
-
|
|
750
|
-
```
|
|
1049
|
+
## Accessibility
|
|
751
1050
|
|
|
752
|
-
|
|
1051
|
+
All built-in field components include full a11y attributes:
|
|
753
1052
|
|
|
754
|
-
|
|
755
|
-
|
|
1053
|
+
| Feature | How |
|
|
1054
|
+
|---|---|
|
|
1055
|
+
| `aria-required` | Set to `"true"` on required inputs, selects, textareas, fieldsets |
|
|
1056
|
+
| `aria-invalid` | Set to `"true"` when the field is touched and has errors |
|
|
1057
|
+
| `aria-describedby` | Points to `"{name}-error"` when errors are present |
|
|
1058
|
+
| `role="alert"` + `aria-live="polite"` | Error lists are announced by screen readers on appearance |
|
|
1059
|
+
| `label[for]` + `input[id]` | All inputs have matching label and id |
|
|
1060
|
+
| `fieldset` + `legend` | Radio groups use semantic grouping |
|
|
1061
|
+
| `aria-checked` | Checkboxes reflect boolean state explicitly |
|
|
756
1062
|
|
|
757
|
-
|
|
758
|
-
const myValidator: ValidatorFn = (value, allValues) => {
|
|
759
|
-
return String(value).length > 0 ? null : 'Required'
|
|
760
|
-
}
|
|
1063
|
+
---
|
|
761
1064
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
return ok ? null : 'Not available'
|
|
766
|
-
}
|
|
767
|
-
```
|
|
1065
|
+
## SSR compatibility
|
|
1066
|
+
|
|
1067
|
+
The core (`useForm`, validators, parsers, `ConditionEvaluator`) does not use browser APIs. `bindMask` and `FormRenderer` use DOM APIs — wrap them in `onMounted` or `<ClientOnly>` when needed.
|
|
768
1068
|
|
|
769
1069
|
---
|
|
770
1070
|
|
|
@@ -773,40 +1073,25 @@ const myAsyncValidator: AsyncValidatorFn = async (value) => {
|
|
|
773
1073
|
```
|
|
774
1074
|
useForm
|
|
775
1075
|
│
|
|
776
|
-
├──
|
|
1076
|
+
├── Schema normalisation (json / zod / yup / valibot)
|
|
777
1077
|
│ └── FieldDefinition[]
|
|
778
1078
|
│
|
|
779
1079
|
├── ConditionEvaluator
|
|
780
|
-
│
|
|
1080
|
+
│ watchEffect → resolves visible / disabled / options per field
|
|
781
1081
|
│
|
|
782
1082
|
├── ValidationEngine
|
|
783
1083
|
│ sync validators → errors Record<path, string[]>
|
|
784
|
-
│ async validators → debounced
|
|
1084
|
+
│ async validators → debounced 300ms
|
|
785
1085
|
│
|
|
786
1086
|
├── MaskEngine (standalone)
|
|
787
1087
|
│ applyMask / removeMask / bindMask
|
|
788
1088
|
│
|
|
789
|
-
└── (optional)
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
```
|
|
797
|
-
index.ts
|
|
798
|
-
├── core/types.ts
|
|
799
|
-
├── core/useForm.ts
|
|
800
|
-
│ ├── core/ValidationEngine.ts
|
|
801
|
-
│ ├── core/ConditionEvaluator.ts
|
|
802
|
-
│ └── parsers/json.ts
|
|
803
|
-
├── core/MaskEngine.ts
|
|
804
|
-
├── parsers/zod.ts ← vue-form-schema/zod
|
|
805
|
-
└── parsers/yup.ts ← vue-form-schema/yup
|
|
806
|
-
|
|
807
|
-
ui/index.ts ← vue-form-schema/ui
|
|
808
|
-
├── ui/FormRenderer.vue
|
|
809
|
-
└── ui/fields/*.vue
|
|
1089
|
+
└── (optional) UI subpackages
|
|
1090
|
+
FormRenderer [/ui]
|
|
1091
|
+
TailwindFormRenderer [/ui/tailwind]
|
|
1092
|
+
useFieldArray [core]
|
|
1093
|
+
useMultiStepForm [core]
|
|
1094
|
+
useFormDebug [core]
|
|
810
1095
|
```
|
|
811
1096
|
|
|
812
1097
|
---
|
|
@@ -815,10 +1100,12 @@ ui/index.ts ← vue-form-schema/ui
|
|
|
815
1100
|
|
|
816
1101
|
| Entry point | Peer deps | Notes |
|
|
817
1102
|
|---|---|---|
|
|
818
|
-
| `vue-form-schema` | `vue ^3.3` | Core
|
|
819
|
-
| `vue-form-schema/zod` | `zod ^3` |
|
|
820
|
-
| `vue-form-schema/yup` | `yup ^1` |
|
|
821
|
-
| `vue-form-schema/
|
|
1103
|
+
| `vue-form-schema` | `vue ^3.3` | Core — headless, no UI |
|
|
1104
|
+
| `vue-form-schema/zod` | `zod ^3` | Optional adapter |
|
|
1105
|
+
| `vue-form-schema/yup` | `yup ^1` | Optional adapter |
|
|
1106
|
+
| `vue-form-schema/valibot` | `valibot ^1` | Optional adapter |
|
|
1107
|
+
| `vue-form-schema/ui` | `vue ^3.3` | BEM-styled built-in components |
|
|
1108
|
+
| `vue-form-schema/ui/tailwind` | `vue ^3.3`, Tailwind CSS | Tailwind utility-class components |
|
|
822
1109
|
|
|
823
1110
|
All entry points are tree-shakeable ESM + CJS dual builds.
|
|
824
1111
|
|
|
@@ -827,3 +1114,13 @@ All entry points are tree-shakeable ESM + CJS dual builds.
|
|
|
827
1114
|
## License
|
|
828
1115
|
|
|
829
1116
|
MIT
|
|
1117
|
+
|
|
1118
|
+
---
|
|
1119
|
+
|
|
1120
|
+
## Author
|
|
1121
|
+
|
|
1122
|
+
Danil Lisin Vladimirovich aka Macrulez
|
|
1123
|
+
|
|
1124
|
+
GitHub: [macrulezru](https://github.com/macrulezru) · Website: [macrulez.ru/en](https://macrulez.ru/en)
|
|
1125
|
+
|
|
1126
|
+
Questions and bugs — [issues](https://github.com/macrulezru/vue-form-schema/issues)
|