@whatsaaaa/form-builder-schema 0.2.0 → 0.4.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.
- package/README.md +103 -8
- package/dist/index.d.ts +497 -18
- package/dist/index.js +745 -68
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Build [Valibot](https://valibot.dev) schemas from JSON form definitions, ready t
|
|
|
8
8
|
pnpm add @whatsaaaa/form-builder-schema valibot
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Valibot 1.3 or later is required.
|
|
12
|
+
|
|
11
13
|
## Usage
|
|
12
14
|
|
|
13
15
|
```ts
|
|
@@ -30,9 +32,90 @@ const { build, validate } = createSchemaBuilder()
|
|
|
30
32
|
const schema = build(json) // v.ObjectSchema, ready for Formisch or v.parse
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
Empty optional fields output `undefined`. Rules never run on empty values, and a required empty field reports only the required message.
|
|
36
|
+
|
|
37
|
+
## Field types
|
|
38
|
+
|
|
39
|
+
There is a type for every native control, plus three display elements. Anything more specific is a `ui.widget` variant of one of them, or your own type: a star rating is a `number` with `ui: { widget: 'rating' }`, and a colour picker is a `text` with a `hexColor` rule.
|
|
40
|
+
|
|
41
|
+
| Type | Kind | Value | Props | Checks |
|
|
42
|
+
| ------------------------------------- | -------------------------------------- | ----------------------------------- | ------------------------ | -------------------------------- |
|
|
43
|
+
| `text`, `textarea`, `password`, `tel` | string | `string` | | |
|
|
44
|
+
| `email`, `url` | string | `string` | | the `email` or `url` rule |
|
|
45
|
+
| `number` | number | `number` | | |
|
|
46
|
+
| `checkbox` | boolean | `boolean` | | required means ticked |
|
|
47
|
+
| `date` | date | `'2026-10-28'` | `output?` | ISO date |
|
|
48
|
+
| `time` | date | `'14:30'` | | ISO time |
|
|
49
|
+
| `datetime` | date | `'2026-10-28T14:30'` | `output?` | ISO date and time |
|
|
50
|
+
| `select` | from the options | `string \| number` | `options?`, `valueType?` | one of the options |
|
|
51
|
+
| `radio` | from the options | `string \| number \| boolean` | `options` | one of the options |
|
|
52
|
+
| `multiselect` | array, items from the options | `(string \| number)[]` | `options` | every item is one of the options |
|
|
53
|
+
| `file` | file, or array of file with `multiple` | `File`, or `File[]` with `multiple` | `multiple?`, `accept?` | MIME type from `accept` |
|
|
54
|
+
|
|
55
|
+
- **Options** are `{ value, label }[]`: at least one, every value different and of the same type. A select with number options has kind `number`, so it rejects `'1'` and takes rules like `minValue`.
|
|
56
|
+
- **A select without `options`**, because they load from an API, takes any value of its `valueType` (`'string'`, the default, or `'number'`). With `options`, `valueType` is ignored.
|
|
57
|
+
- **Dates stay ISO strings** by default. `output: 'date'` turns the value into a `Date` after every rule, so comparison rules still compare ISO strings. A `datetime` has no time zone, so its `Date` is in local time.
|
|
58
|
+
- **A required radio** accepts any option, `false` included. **A required multiselect** needs at least one item; add `minLength: 2` for "pick at least two". Rules for the options' kind, like `maxValue`, run on each item.
|
|
59
|
+
- **A checkbox group** is a `multiselect` with `ui: { widget: 'checkboxes' }`.
|
|
60
|
+
- **Files.** `accept` takes MIME types separated by commas, such as `image/png,image/jpeg`. A `File` only carries its MIME type, so extensions (`.pdf`) and wildcards (`image/*`) are `invalid_props`. With `multiple`, file rules like `maxSize` check each file and `maxLength` limits the count. The schema reads the `File` global only while parsing, so the package loads in SSR.
|
|
61
|
+
|
|
62
|
+
### Display elements
|
|
63
|
+
|
|
64
|
+
| Type | Props |
|
|
65
|
+
| ----------- | ---------------------------------------- |
|
|
66
|
+
| `heading` | `text`, `level?` from 1 to 6 (default 2) |
|
|
67
|
+
| `paragraph` | `text`, plain text |
|
|
68
|
+
| `divider` | |
|
|
69
|
+
|
|
70
|
+
They need no `name`, add no output key, and can't have `rules` or `required`. Their `text` can be an i18n key: renderers resolve it with the same function you pass as `resolveMessage`. Whether a paragraph renders markdown is up to the renderer.
|
|
71
|
+
|
|
72
|
+
## Rules
|
|
73
|
+
|
|
74
|
+
Every Valibot action whose arguments fit in JSON is a rule with the same name. Params go next to `rule`, and a rule without a `message` uses Valibot's message, so [`@valibot/i18n`](https://valibot.dev/guides/internationalization/) works.
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{ "rule": "minLength", "value": 3, "message": "At least 3 characters" }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| Family | Rules | Kinds | Params |
|
|
81
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------ | -------------------------------------- |
|
|
82
|
+
| `lengthRules` | `length` `minLength` `maxLength` `notLength` | string, array | `value` |
|
|
83
|
+
| `textUnitRules` | `graphemes` `minGraphemes` `maxGraphemes` `notGraphemes`, `words` `minWords` `maxWords` `notWords`, `bytes` `minBytes` `maxBytes` `notBytes` | string | `value`; words also `locale` |
|
|
84
|
+
| `contentRules` | `regex` `startsWith` `endsWith` `includes` `excludes` | string; `includes` and `excludes` also array | `pattern` and `flags`, or `value` |
|
|
85
|
+
| `formatRules` | `email` `rfcEmail` `url` `domain` `slug` `uuid` `ulid` `cuid2` `nanoid` `ip` `ipv4` `ipv6` `mac` `mac48` `mac64` `hexColor` `hexadecimal` `octal` `decimal` `digits` `base64` `bic` `creditCard` `imei` `isbn` `isrc` `emoji` `hash` | string | none; `hash` takes `types` |
|
|
86
|
+
| `dateRules` | `isoDate` `isoDateTime` `isoTime` `isoTimeSecond` `isoTimestamp` `isoWeek` | string, date | none |
|
|
87
|
+
| `comparisonRules` | `minValue` `maxValue` `gtValue` `ltValue` `value` `notValue` `values` `notValues` | number, date; `value` and `notValue` also string and boolean | `value` or `values`, typed by the kind |
|
|
88
|
+
| `numberRules` | `integer` `safeInteger` `finite` `multipleOf` | number | `multipleOf` takes `value` |
|
|
89
|
+
| `fileRules` | `minSize` `maxSize` `size` `notSize` `mimeType` | file | `value` in bytes, or `types` |
|
|
90
|
+
| `normalizeRules` | `trim` `trimStart` `trimEnd` `toLowerCase` `toUpperCase` `normalize`, `toMinValue` `toMaxValue` | string; `toMinValue` and `toMaxValue` on number | none; `form`, or `value` |
|
|
91
|
+
|
|
92
|
+
- **Normalize rules** change the value instead of checking it, and run before the required check. With `trim`, `' '` counts as empty.
|
|
93
|
+
- **`regex`** takes `pattern` (without slashes) and optional `flags`. A pattern `RegExp` rejects is an `invalid_params` issue. `g` and `y` are accepted but ignored, because they make a regex skip ahead on the next value. If end users write patterns, beware of [ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS): a pattern like `(a+)+$` can freeze the page.
|
|
94
|
+
- **Comparing dates.** On a `date` field, `value` must be an ISO date, date-time or time string. Valibot compares them as text, so use the same format as the field: `'2026-01-01'` for a date, `'2026-01-01T09:00'` for a date-time.
|
|
95
|
+
- **`words`** splits text by language. Without `locale`, the runtime's default locale is used.
|
|
96
|
+
- Function-based actions (`check`, `transform`, …) can't be written in JSON. Register them as your own rules with `defineRule`.
|
|
97
|
+
|
|
98
|
+
### Registering only some rules
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { createSchemaBuilder, formatRules, lengthRules } from '@whatsaaaa/form-builder-schema'
|
|
102
|
+
|
|
103
|
+
const { build } = createSchemaBuilder({
|
|
104
|
+
includeDefaultRules: false,
|
|
105
|
+
rules: { ...formatRules, ...lengthRules },
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The built-in `email` and `url` types use the `email` and `url` rules, so keep `formatRules` (or replace those types), or `createSchemaBuilder` throws a `TypeError`.
|
|
110
|
+
|
|
111
|
+
### Listing rules
|
|
112
|
+
|
|
113
|
+
Every built-in rule has `meta` with a `label`, a `category` and sometimes a `description`, for a rule picker. `rulesForKind(kind)` returns the `[name, def]` pairs that fit a kind:
|
|
34
114
|
|
|
35
|
-
|
|
115
|
+
```ts
|
|
116
|
+
const { rulesForKind } = createSchemaBuilder()
|
|
117
|
+
rulesForKind('file').map(([name, def]) => def.meta?.label) // ['Minimum size', …]
|
|
118
|
+
```
|
|
36
119
|
|
|
37
120
|
## Checking definitions
|
|
38
121
|
|
|
@@ -54,10 +137,11 @@ Keys on a field must be reserved keys (`RESERVED_FIELD_KEYS`) or props of its ty
|
|
|
54
137
|
`createSchemaBuilder({ kinds, fieldTypes, rules })` merges your entries over the defaults by key. The merged registries are returned as `kinds`, `fieldTypes` and `rules`.
|
|
55
138
|
|
|
56
139
|
```ts
|
|
57
|
-
const
|
|
58
|
-
kind: '
|
|
59
|
-
|
|
60
|
-
|
|
140
|
+
const tags = defineFieldType({
|
|
141
|
+
kind: 'array',
|
|
142
|
+
itemKind: 'string',
|
|
143
|
+
props: v.object({ max: v.optional(v.number()) }),
|
|
144
|
+
schema: (field) => v.pipe(v.array(v.string()), v.maxLength(field.max ?? 10)), // field.max is typed
|
|
61
145
|
})
|
|
62
146
|
|
|
63
147
|
const even = defineRule({
|
|
@@ -65,11 +149,22 @@ const even = defineRule({
|
|
|
65
149
|
create: (_, message) => v.check((n: number) => n % 2 === 0, message),
|
|
66
150
|
})
|
|
67
151
|
|
|
68
|
-
const { build } = createSchemaBuilder({ fieldTypes: {
|
|
152
|
+
const { build } = createSchemaBuilder({ fieldTypes: { tags }, rules: { even } })
|
|
69
153
|
```
|
|
70
154
|
|
|
71
155
|
- A **kind** says what "empty" means for a value (`isEmpty`) and adds an optional extra `required` check. Built-in kinds are `string`, `number`, `boolean`, `date`, `array`, `file`, `object` and `none`. A type with kind `none` has no value, like a heading.
|
|
72
|
-
- A **
|
|
156
|
+
- A **field type** names the kind of its value. `kind` can also be a function of the field with its props parsed (`KindOf`), for a kind that depends on them, like a select's options. `itemKind` names the kind of each item of an array value: a rule for that kind but not for `array` then runs on each item, and reports its issues on the field. Two optional hooks fine-tune the pipeline: `required(message)` replaces the kind's required check (return `null` for none), and `finalize(field)` returns an action that runs after every rule, like `v.toDate()`.
|
|
157
|
+
- A **rule** with `stage: 'normalize'` runs before the required check. Its `params` schema lists the only keys the rule accepts. `params` can also be a function of the field's kind, for params whose type depends on it:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const atLeast = defineRule({
|
|
161
|
+
kinds: ['number', 'date'],
|
|
162
|
+
params: (kind) => v.object({ value: kind === 'date' ? v.string() : v.number() }),
|
|
163
|
+
meta: { label: 'At least', category: 'comparison' },
|
|
164
|
+
create: (params, message) => v.minValue(params.value, message),
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
73
168
|
- Mistakes in the registry, such as an unknown kind or a reserved props key, throw a `TypeError` from `createSchemaBuilder`.
|
|
74
169
|
|
|
75
170
|
## Messages
|