@macrulez/vue-form-schema 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +829 -0
  2. package/dist/MaskEngine-D22m29OM.js +157 -0
  3. package/dist/MaskEngine-hd5xHed7.cjs +1 -0
  4. package/dist/__tests__/ConditionEvaluator.test.d.ts +2 -0
  5. package/dist/__tests__/ConditionEvaluator.test.d.ts.map +1 -0
  6. package/dist/__tests__/MaskEngine.test.d.ts +2 -0
  7. package/dist/__tests__/MaskEngine.test.d.ts.map +1 -0
  8. package/dist/__tests__/ValidationEngine.test.d.ts +2 -0
  9. package/dist/__tests__/ValidationEngine.test.d.ts.map +1 -0
  10. package/dist/__tests__/parsers.test.d.ts +2 -0
  11. package/dist/__tests__/parsers.test.d.ts.map +1 -0
  12. package/dist/__tests__/useForm.test.d.ts +2 -0
  13. package/dist/__tests__/useForm.test.d.ts.map +1 -0
  14. package/dist/core/ConditionEvaluator.d.ts +18 -0
  15. package/dist/core/ConditionEvaluator.d.ts.map +1 -0
  16. package/dist/core/MaskEngine.d.ts +21 -0
  17. package/dist/core/MaskEngine.d.ts.map +1 -0
  18. package/dist/core/ValidationEngine.d.ts +25 -0
  19. package/dist/core/ValidationEngine.d.ts.map +1 -0
  20. package/dist/core/types.d.ts +76 -0
  21. package/dist/core/types.d.ts.map +1 -0
  22. package/dist/core/useForm.d.ts +3 -0
  23. package/dist/core/useForm.d.ts.map +1 -0
  24. package/dist/index.cjs +1 -0
  25. package/dist/index.d.ts +7 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +278 -0
  28. package/dist/parsers/json.d.ts +3 -0
  29. package/dist/parsers/json.d.ts.map +1 -0
  30. package/dist/parsers/yup.d.ts +10 -0
  31. package/dist/parsers/yup.d.ts.map +1 -0
  32. package/dist/parsers/zod.d.ts +10 -0
  33. package/dist/parsers/zod.d.ts.map +1 -0
  34. package/dist/ui/index.d.ts +9 -0
  35. package/dist/ui/index.d.ts.map +1 -0
  36. package/dist/ui.cjs +1 -0
  37. package/dist/ui.d.ts +324 -0
  38. package/dist/ui.js +438 -0
  39. package/dist/yup.cjs +1 -0
  40. package/dist/yup.d.ts +45 -0
  41. package/dist/yup.js +55 -0
  42. package/dist/zod.cjs +1 -0
  43. package/dist/zod.d.ts +45 -0
  44. package/dist/zod.js +104 -0
  45. package/package.json +69 -0
package/README.md ADDED
@@ -0,0 +1,829 @@
1
+ # vue-form-schema
2
+
3
+ Reactive forms from a declarative schema (JSON, Zod, or Yup) for Vue 3. A headless, SSR-compatible alternative to VeeValidate / FormKit for forms that are generated dynamically or driven from the server.
4
+
5
+ ---
6
+
7
+ ## Contents
8
+
9
+ - [Features](#features)
10
+ - [Demo](#demo)
11
+ - [Installation](#installation)
12
+ - [Quick start](#quick-start)
13
+ - [FieldDefinition reference](#fielddefinition-reference)
14
+ - [useForm composable](#useform-composable)
15
+ - [Schema formats](#schema-formats)
16
+ - [FieldDefinition array](#fielddefinition-array)
17
+ - [JSON schema](#json-schema)
18
+ - [Zod](#zod)
19
+ - [Yup](#yup)
20
+ - [Built-in validators](#built-in-validators)
21
+ - [Custom validators](#custom-validators)
22
+ - [Conditional fields](#conditional-fields)
23
+ - [Input masking](#input-masking)
24
+ - [FormRenderer UI component](#formrenderer-ui-component)
25
+ - [SSR compatibility](#ssr-compatibility)
26
+ - [TypeScript generics](#typescript-generics)
27
+ - [Architecture](#architecture)
28
+ - [Bundle size & peer dependencies](#bundle-size--peer-dependencies)
29
+
30
+ ---
31
+
32
+ ## Features
33
+
34
+ - **Any schema source** — define fields as a plain JSON array, a Zod object, or a Yup object
35
+ - **Headless by default** — zero UI dependencies in the core; bring your own components
36
+ - **Reactive conditions** — `visible`, `disabled`, and `required` accept a boolean, a function, or a string expression evaluated against live form values
37
+ - **Validation** — sync and async validators, built-in rules (`required`, `email`, `minLength`, …), cross-field access
38
+ - **Input masking** — phone (RU/EU), date, IBAN, INN, and custom `#`/`A` patterns; no external dependencies
39
+ - **SSR-safe** — no direct browser APIs in the core; validation and state work on the server
40
+ - **Tree-shakeable** — Zod/Yup adapters are separate entry points; the UI subpackage is optional
41
+ - **TypeScript 5+** — generic type inference flows from schema to `values`
42
+
43
+ ---
44
+
45
+ ## Demo
46
+
47
+ A local interactive demo covers all major features of the library. Clone the repo and run:
48
+
49
+ ```bash
50
+ npm install
51
+ npm run demo
52
+ ```
53
+
54
+ Opens at **http://localhost:5174** with the following examples:
55
+
56
+ | Page | What it shows |
57
+ |---|---|
58
+ | **Basic form** | `FieldDefinition[]` with built-in validators — `required`, `minLength`, `email`, `min/max` |
59
+ | **JSON schema** | Server-driven schema with rule-based validators (`{ rule: 'minLength', value: 3 }`) |
60
+ | **Zod schema** | `z.object()` → `parseZod()`, type inference, `.describe()` for labels |
61
+ | **Yup schema** | `yup.object()` → `parseYup()`, Yup constraints delegated to `validateSync` |
62
+ | **Conditional fields** | `visible` as a function and as a string expression; `clearOnHide` behaviour |
63
+ | **Input masking** | All presets + custom `#`/`A` patterns; standalone `applyMask` / `removeMask` playground |
64
+ | **FormRenderer** | Out-of-the-box rendering; slot overrides (`#field-{name}`, `#submit`); custom component map |
65
+
66
+ ---
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ npm install vue-form-schema
72
+ ```
73
+
74
+ Peer dependencies:
75
+
76
+ ```bash
77
+ npm install vue # required
78
+ npm install zod # optional — only if you use the Zod adapter
79
+ npm install yup # optional — only if you use the Yup adapter
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Quick start
85
+
86
+ ```vue
87
+ <script setup lang="ts">
88
+ import { useForm } from 'vue-form-schema'
89
+ import type { FieldDefinition } from 'vue-form-schema'
90
+
91
+ const schema: FieldDefinition[] = [
92
+ { type: 'text', name: 'name', label: 'Full name', required: true },
93
+ { type: 'email', name: 'email', label: 'Email', required: true },
94
+ {
95
+ type: 'select',
96
+ name: 'role',
97
+ label: 'Role',
98
+ options: [
99
+ { label: 'Admin', value: 'admin' },
100
+ { label: 'User', value: 'user' },
101
+ ],
102
+ },
103
+ ]
104
+
105
+ const { values, errors, touched, isValid, isSubmitting, submit, setField } = useForm({
106
+ schema,
107
+ validateOn: 'blur',
108
+ onSubmit: async (data) => {
109
+ await fetch('/api/users', { method: 'POST', body: JSON.stringify(data) })
110
+ },
111
+ })
112
+ </script>
113
+
114
+ <template>
115
+ <form @submit.prevent="submit">
116
+ <div v-for="field in schema" :key="field.name">
117
+ <label>{{ field.label }}</label>
118
+ <input
119
+ :type="field.type"
120
+ :value="values[field.name]"
121
+ @input="setField(field.name, ($event.target as HTMLInputElement).value)"
122
+ @blur="touched[field.name] = true"
123
+ />
124
+ <span v-if="touched[field.name] && errors[field.name]">
125
+ {{ errors[field.name][0] }}
126
+ </span>
127
+ </div>
128
+ <button type="submit" :disabled="!isValid || isSubmitting">Submit</button>
129
+ </form>
130
+ </template>
131
+ ```
132
+
133
+ ---
134
+
135
+ ## FieldDefinition reference
136
+
137
+ Every field in your schema is described by a `FieldDefinition` object.
138
+
139
+ ```ts
140
+ interface FieldDefinition {
141
+ // ─── Required ─────────────────────────────────────────────────────────────
142
+ type: 'text' | 'number' | 'email' | 'select' | 'checkbox'
143
+ | 'radio' | 'textarea' | 'date' | 'array' | 'group'
144
+
145
+ /** Flat dot-path key used in the values object, e.g. "address.city" */
146
+ name: string
147
+
148
+ // ─── Display ──────────────────────────────────────────────────────────────
149
+ label?: string
150
+ placeholder?: string
151
+
152
+ // ─── Initial value ────────────────────────────────────────────────────────
153
+ defaultValue?: unknown // set on mount and after reset()
154
+
155
+ // ─── Constraints ──────────────────────────────────────────────────────────
156
+ required?: boolean
157
+
158
+ /** Static boolean, or a function receiving all current values */
159
+ 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)
167
+
168
+ // ─── Validation ───────────────────────────────────────────────────────────
169
+ validators?: ValidatorFn[]
170
+ asyncValidators?: AsyncValidatorFn[]
171
+
172
+ // ─── Masking ──────────────────────────────────────────────────────────────
173
+ mask?: string | MaskConfig
174
+
175
+ // ─── select / radio ───────────────────────────────────────────────────────
176
+ options?: { label: string; value: unknown }[]
177
+
178
+ // ─── group / array ────────────────────────────────────────────────────────
179
+ 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
+
187
+ ```ts
188
+ const schema: FieldDefinition[] = [
189
+ {
190
+ type: 'group',
191
+ name: 'address',
192
+ label: 'Address',
193
+ fields: [
194
+ { type: 'text', name: 'address.city', label: 'City', required: true },
195
+ { type: 'text', name: 'address.country', label: 'Country', required: true },
196
+ ],
197
+ },
198
+ ]
199
+ ```
200
+
201
+ The resulting `values` object will be `{ 'address.city': '...', 'address.country': '...' }`.
202
+
203
+ ---
204
+
205
+ ## useForm composable
206
+
207
+ ```ts
208
+ import { useForm } from 'vue-form-schema'
209
+
210
+ const form = useForm(config)
211
+ ```
212
+
213
+ ### Config
214
+
215
+ | Property | Type | Default | Description |
216
+ |---|---|---|---|
217
+ | `schema` | `FieldDefinition[] \| JSONSchema` | — | Field definitions |
218
+ | `initialValues` | `Partial<T>` | `{}` | Seed values (override field defaults) |
219
+ | `validateOn` | `'input' \| 'blur' \| 'submit'` | `'blur'` | When sync validation is triggered |
220
+ | `clearOnHide` | `boolean` | `false` | Reset a field's value when it becomes hidden |
221
+ | `onSubmit` | `(values: T) => void \| Promise<void>` | — | Called after successful validation |
222
+
223
+ ### Return value
224
+
225
+ | Property | Type | Description |
226
+ |---|---|---|
227
+ | `fields` | `ComputedRef<FieldDefinition[]>` | Field list after conditions are evaluated |
228
+ | `values` | `Ref<T>` | Current form values (reactive) |
229
+ | `errors` | `Ref<Record<string, string[]>>` | Validation errors keyed by field name |
230
+ | `touched` | `Ref<Record<string, boolean>>` | Whether a field has been interacted with |
231
+ | `isDirty` | `ComputedRef<boolean>` | `true` when values differ from the initial state |
232
+ | `isValid` | `ComputedRef<boolean>` | `true` when all visible fields pass validation |
233
+ | `isSubmitting` | `Ref<boolean>` | `true` while `onSubmit` is running |
234
+ | `submit()` | `() => Promise<void>` | Touch all fields, validate, call `onSubmit` |
235
+ | `reset(values?)` | `(values?: Partial<T>) => void` | Restore initial state (or supply new values) |
236
+ | `setField(path, value)` | `(path: string, value: unknown) => void` | Programmatically set a value |
237
+ | `getField(path)` | `(path: string) => unknown` | Read a value by dot-path |
238
+
239
+ ### Example — programmatic control
240
+
241
+ ```ts
242
+ const { setField, getField, reset } = useForm({ schema })
243
+
244
+ // Set a nested field
245
+ setField('address.city', 'Berlin')
246
+
247
+ // Read it back
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
+ ```
256
+
257
+ ---
258
+
259
+ ## Schema formats
260
+
261
+ ### FieldDefinition array
262
+
263
+ The native format — an array of `FieldDefinition` objects, usually created from the parser functions or manually:
264
+
265
+ ```ts
266
+ import type { FieldDefinition } from 'vue-form-schema'
267
+
268
+ const schema: FieldDefinition[] = [
269
+ { type: 'text', name: 'username', required: true },
270
+ ]
271
+
272
+ useForm({ schema })
273
+ ```
274
+
275
+ ### JSON schema
276
+
277
+ A serialisable array format useful when the schema arrives from an API. Validators are expressed as named rules instead of functions. Pass directly to `useForm` or call `parseJSON` explicitly.
278
+
279
+ ```ts
280
+ import { parseJSON } from 'vue-form-schema'
281
+
282
+ const raw = [
283
+ {
284
+ type: 'text',
285
+ name: 'username',
286
+ label: 'Username',
287
+ default: '',
288
+ required: true,
289
+ validators: [
290
+ { rule: 'minLength', value: 3, message: 'At least 3 characters' },
291
+ { rule: 'maxLength', value: 20 },
292
+ ],
293
+ },
294
+ {
295
+ type: 'email',
296
+ name: 'email',
297
+ validators: [{ rule: 'email' }],
298
+ },
299
+ ]
300
+
301
+ // Option A — pass directly to useForm (auto-detected)
302
+ useForm({ schema: raw })
303
+
304
+ // Option B — parse explicitly
305
+ const fields = parseJSON(raw)
306
+ useForm({ schema: fields })
307
+ ```
308
+
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.
323
+
324
+ ### Zod
325
+
326
+ Install Zod and import the adapter from `vue-form-schema/zod`:
327
+
328
+ ```ts
329
+ import { z } from 'zod'
330
+ import { parseZod } from 'vue-form-schema/zod'
331
+ import { useForm } from 'vue-form-schema'
332
+
333
+ const schema = z.object({
334
+ name: z.string().min(2).describe('Full name'),
335
+ age: z.number().min(0).optional(),
336
+ email: z.string().email(),
337
+ role: z.enum(['admin', 'user']),
338
+ })
339
+
340
+ const fields = parseZod(schema)
341
+
342
+ const { values, submit } = useForm({ schema: fields })
343
+ ```
344
+
345
+ Type inference is preserved — `values` will be typed as the Zod schema's output type when you supply it explicitly:
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`.
366
+
367
+ ### Yup
368
+
369
+ Install Yup and import the adapter from `vue-form-schema/yup`:
370
+
371
+ ```ts
372
+ import { object, string, number } from 'yup'
373
+ import { parseYup } from 'vue-form-schema/yup'
374
+ import { useForm } from 'vue-form-schema'
375
+
376
+ const schema = object({
377
+ name: string().required().label('Full name'),
378
+ email: string().email().required(),
379
+ age: number().min(0).optional(),
380
+ })
381
+
382
+ const fields = parseYup(schema)
383
+
384
+ const { values, submit } = useForm({ schema: fields })
385
+ ```
386
+
387
+ **Yup type mapping:**
388
+
389
+ | Yup type | Field type |
390
+ |---|---|
391
+ | `string()` | `text` |
392
+ | `number()` | `number` |
393
+ | `boolean()` | `checkbox` |
394
+ | `array()` | `array` |
395
+ | `object()` | `group` |
396
+
397
+ ---
398
+
399
+ ## Built-in validators
400
+
401
+ Import standalone validator functions when building a `FieldDefinition` array manually:
402
+
403
+ ```ts
404
+ import {
405
+ required,
406
+ minLength,
407
+ maxLength,
408
+ min,
409
+ max,
410
+ pattern,
411
+ email,
412
+ url,
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()`) |
426
+
427
+ ```ts
428
+ import { minLength, maxLength, email } from 'vue-form-schema'
429
+
430
+ const schema: FieldDefinition[] = [
431
+ {
432
+ type: 'text',
433
+ name: 'username',
434
+ required: true,
435
+ validators: [
436
+ minLength(3, 'At least 3 characters'),
437
+ maxLength(20),
438
+ ],
439
+ },
440
+ {
441
+ type: 'email',
442
+ name: 'email',
443
+ validators: [email],
444
+ },
445
+ ]
446
+ ```
447
+
448
+ ---
449
+
450
+ ## Custom validators
451
+
452
+ ### Sync validator
453
+
454
+ A `ValidatorFn` receives the field's current value and all form values. Return a string on failure, or `null` on success.
455
+
456
+ ```ts
457
+ import type { ValidatorFn } from 'vue-form-schema'
458
+
459
+ const noSpaces: ValidatorFn = (value) => {
460
+ if (typeof value === 'string' && value.includes(' ')) return 'No spaces allowed'
461
+ return null
462
+ }
463
+
464
+ // Cross-field: confirm password
465
+ const matchesPassword: ValidatorFn = (value, allValues) => {
466
+ if (value !== allValues['password']) return 'Passwords do not match'
467
+ return null
468
+ }
469
+
470
+ const schema: FieldDefinition[] = [
471
+ { type: 'text', name: 'password', label: 'Password' },
472
+ {
473
+ type: 'text',
474
+ name: 'confirmPassword',
475
+ label: 'Confirm password',
476
+ validators: [matchesPassword],
477
+ },
478
+ ]
479
+ ```
480
+
481
+ ### Async validator
482
+
483
+ An `AsyncValidatorFn` returns a `Promise<string | null>`. Async validators are debounced (300 ms by default) so they don't fire on every keystroke.
484
+
485
+ ```ts
486
+ import type { AsyncValidatorFn } from 'vue-form-schema'
487
+
488
+ const uniqueUsername: AsyncValidatorFn = async (value) => {
489
+ const res = await fetch(`/api/check-username?q=${value}`)
490
+ const { taken } = await res.json()
491
+ return taken ? 'This username is already taken' : null
492
+ }
493
+
494
+ const schema: FieldDefinition[] = [
495
+ {
496
+ type: 'text',
497
+ name: 'username',
498
+ required: true,
499
+ asyncValidators: [uniqueUsername],
500
+ },
501
+ ]
502
+ ```
503
+
504
+ ---
505
+
506
+ ## Conditional fields
507
+
508
+ `visible` and `disabled` can be a static boolean, a reactive function, or a safe string expression.
509
+
510
+ ### Function condition
511
+
512
+ ```ts
513
+ const schema: FieldDefinition[] = [
514
+ {
515
+ type: 'checkbox',
516
+ name: 'hasCompany',
517
+ label: 'I represent a company',
518
+ },
519
+ {
520
+ type: 'text',
521
+ name: 'companyName',
522
+ label: 'Company name',
523
+ required: true,
524
+ // shown only when hasCompany is checked
525
+ visible: (values) => values['hasCompany'] === true,
526
+ },
527
+ {
528
+ type: 'text',
529
+ name: 'vat',
530
+ label: 'VAT number',
531
+ // disabled unless company name is entered
532
+ disabled: (values) => !values['companyName'],
533
+ },
534
+ ]
535
+ ```
536
+
537
+ ### String expression
538
+
539
+ String expressions have access to a `values` variable. Arithmetic, comparison, and logical operators are supported. Potentially unsafe calls (`fetch`, `eval`, etc.) are rejected by a whitelist check.
540
+
541
+ ```ts
542
+ const schema: FieldDefinition[] = [
543
+ { type: 'number', name: 'age', label: 'Age' },
544
+ {
545
+ type: 'select',
546
+ name: 'alcoholChoice',
547
+ label: 'Drink preference',
548
+ visible: 'values.age >= 18', // string expression
549
+ options: [
550
+ { label: 'Beer', value: 'beer' },
551
+ { label: 'Wine', value: 'wine' },
552
+ { label: 'Water', value: 'water' },
553
+ ],
554
+ },
555
+ ]
556
+ ```
557
+
558
+ ### clearOnHide
559
+
560
+ When `clearOnHide: true` is set in `useForm`, hiding a field resets its value back to `defaultValue` (or `null`).
561
+
562
+ ```ts
563
+ useForm({
564
+ schema,
565
+ clearOnHide: true,
566
+ })
567
+ ```
568
+
569
+ ---
570
+
571
+ ## Input masking
572
+
573
+ Masks format user input in real time. They are applied automatically inside `FormRenderer` and can be used standalone via the `applyMask` / `removeMask` / `bindMask` functions.
574
+
575
+ ### Mask presets
576
+
577
+ | Preset | Pattern | Example output |
578
+ |---|---|---|
579
+ | `phone-ru` | `+7 (###) ###-##-##` | `+7 (916) 123-45-67` |
580
+ | `phone-eu` | `+## (##) ###-##-##` | `+49 (30) 123-45-67` |
581
+ | `date` | `##.##.####` | `01.01.2024` |
582
+ | `inn` | `############` | `123456789012` |
583
+ | `iban` | `AA## #### #### #### #### #### ####` | `GB29 NWBK 6016 ...` |
584
+
585
+ ```ts
586
+ // Using a preset
587
+ const field: FieldDefinition = {
588
+ type: 'text',
589
+ name: 'phone',
590
+ label: 'Phone',
591
+ mask: { preset: 'phone-ru' },
592
+ }
593
+ ```
594
+
595
+ ### Custom patterns
596
+
597
+ - `#` — matches a single digit (0–9)
598
+ - `A` — matches a single letter (a–z, A–Z); output is uppercased
599
+ - Any other character — treated as a fixed literal
600
+
601
+ ```ts
602
+ // Postcode: two letters + four digits
603
+ const field: FieldDefinition = {
604
+ type: 'text',
605
+ name: 'postcode',
606
+ label: 'Postcode',
607
+ mask: { pattern: 'AA####' }, // e.g. AB1234
608
+ }
609
+ ```
610
+
611
+ ### Standalone mask API
612
+
613
+ ```ts
614
+ import { applyMask, removeMask, bindMask } from 'vue-form-schema'
615
+
616
+ // Format a raw value
617
+ applyMask('9161234567', { preset: 'phone-ru' })
618
+ // → '+7 (916) 123-45-67'
619
+
620
+ // Strip mask literals from a formatted value
621
+ removeMask('+7 (916) 123-45-67', { preset: 'phone-ru' })
622
+ // → '9161234567'
623
+
624
+ // Attach to a native <input> element (returns cleanup fn)
625
+ const cleanup = bindMask(inputElement, { preset: 'date' })
626
+ // call cleanup() in onUnmounted
627
+ ```
628
+
629
+ ---
630
+
631
+ ## FormRenderer UI component
632
+
633
+ `FormRenderer` is in the optional `vue-form-schema/ui` subpackage. It renders fields automatically from the schema and delegates display to built-in or custom components.
634
+
635
+ ```ts
636
+ import { FormRenderer } from 'vue-form-schema/ui'
637
+ ```
638
+
639
+ ### Basic usage
640
+
641
+ ```vue
642
+ <script setup lang="ts">
643
+ import { useForm } from 'vue-form-schema'
644
+ import { FormRenderer } from 'vue-form-schema/ui'
645
+
646
+ const form = useForm({ schema, onSubmit })
647
+ </script>
648
+
649
+ <template>
650
+ <FormRenderer :form="form" submit-label="Save" />
651
+ </template>
652
+ ```
653
+
654
+ ### Props
655
+
656
+ | Prop | Type | Default | Description |
657
+ |---|---|---|---|
658
+ | `form` | `UseFormReturn` | — | Return value of `useForm` |
659
+ | `components` | `Partial<Record<FieldType, Component>>` | built-ins | Override per-type renderers |
660
+ | `submitLabel` | `string` | `'Submit'` | Submit button text |
661
+
662
+ ### Slots
663
+
664
+ | Slot | Scope | Description |
665
+ |---|---|---|
666
+ | `#field-{name}` | `{ field, value, error, touched, setValue, touch }` | Replace the entire field |
667
+ | `#label-{name}` | `{ field }` | Replace the field label |
668
+ | `#error-{name}` | `{ field, error }` | Replace the error display |
669
+ | `#submit` | `{ isSubmitting, isValid }` | Replace the submit button |
670
+
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
+ ### Built-in field components
706
+
707
+ The following components are exported from `vue-form-schema/ui` and can be used independently:
708
+
709
+ | Component | Types |
710
+ |---|---|
711
+ | `TextField` | `text`, `email` |
712
+ | `NumberField` | `number` |
713
+ | `TextareaField` | `textarea` |
714
+ | `SelectField` | `select` |
715
+ | `CheckboxField` | `checkbox` |
716
+ | `RadioField` | `radio` |
717
+ | `DateField` | `date` |
718
+
719
+ ---
720
+
721
+ ## SSR compatibility
722
+
723
+ The core package (`useForm`, validators, parsers, `ConditionEvaluator`) does not use any browser-specific APIs. State management and validation work on the server.
724
+
725
+ `bindMask` and `FormRenderer` use DOM APIs (`HTMLInputElement`, event listeners) and should only be called or mounted on the client side. Gate them with `onMounted` or Vue's `<ClientOnly>` wrapper as needed.
726
+
727
+ ---
728
+
729
+ ## TypeScript generics
730
+
731
+ `useForm` is generic over the shape of the values object. Supply the type explicitly to get full inference on `values.value`:
732
+
733
+ ```ts
734
+ interface UserForm {
735
+ name: string
736
+ email: string
737
+ age: number | null
738
+ role: 'admin' | 'user'
739
+ }
740
+
741
+ const { values, submit } = useForm<UserForm>({
742
+ schema,
743
+ onSubmit: (data) => {
744
+ // data is typed as UserForm
745
+ console.log(data.name)
746
+ },
747
+ })
748
+
749
+ // values.value is Ref<UserForm>
750
+ ```
751
+
752
+ ### Validator function types
753
+
754
+ ```ts
755
+ import type { ValidatorFn, AsyncValidatorFn } from 'vue-form-schema'
756
+
757
+ // Sync: (value, allValues) => string | null
758
+ const myValidator: ValidatorFn = (value, allValues) => {
759
+ return String(value).length > 0 ? null : 'Required'
760
+ }
761
+
762
+ // Async: (value, allValues) => Promise<string | null>
763
+ const myAsyncValidator: AsyncValidatorFn = async (value) => {
764
+ const ok = await checkServer(value)
765
+ return ok ? null : 'Not available'
766
+ }
767
+ ```
768
+
769
+ ---
770
+
771
+ ## Architecture
772
+
773
+ ```
774
+ useForm
775
+
776
+ ├── SchemaParser (json / zod / yup)
777
+ │ └── FieldDefinition[]
778
+
779
+ ├── ConditionEvaluator
780
+ │ reactive watchEffect → resolves visible / disabled per field
781
+
782
+ ├── ValidationEngine
783
+ │ sync validators → errors Record<path, string[]>
784
+ │ async validators → debounced, merged into errors
785
+
786
+ ├── MaskEngine (standalone)
787
+ │ applyMask / removeMask / bindMask
788
+
789
+ └── (optional) FormRenderer [/ui]
790
+ renders FieldDefinition[] → input components
791
+ slots: #field-{name}, #label-{name}, #error-{name}, #submit
792
+ ```
793
+
794
+ **Module dependency graph:**
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
810
+ ```
811
+
812
+ ---
813
+
814
+ ## Bundle size & peer dependencies
815
+
816
+ | Entry point | Peer deps | Notes |
817
+ |---|---|---|
818
+ | `vue-form-schema` | `vue ^3.3` | Core ≤ 8 KB gzip |
819
+ | `vue-form-schema/zod` | `zod ^3` | Lazy — zero cost if unused |
820
+ | `vue-form-schema/yup` | `yup ^1` | Lazy — zero cost if unused |
821
+ | `vue-form-schema/ui` | `vue ^3.3` | Optional UI layer |
822
+
823
+ All entry points are tree-shakeable ESM + CJS dual builds.
824
+
825
+ ---
826
+
827
+ ## License
828
+
829
+ MIT