@whatsaaaa/form-builder-affects 0.0.0 → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Whatsaaaa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # @whatsaaaa/form-builder-affects
2
+
3
+ Conditions for JSON form definitions of [`@whatsaaaa/form-builder-schema`](https://github.com/whatsaaaa/form-builder/tree/main/packages/schema): show, hide and clear fields, sections and steps by the values of other fields. A hidden field isn't validated, in every Formisch package and in a plain `v.parse`.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add @whatsaaaa/form-builder-affects @whatsaaaa/form-builder-schema valibot
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Give the schema builder the extension, and write `affects` on the fields:
14
+
15
+ ```ts
16
+ import { affects } from '@whatsaaaa/form-builder-affects'
17
+ import { createSchemaBuilder, type JsonForm } from '@whatsaaaa/form-builder-schema'
18
+
19
+ const builder = createSchemaBuilder({ extensions: [affects()] })
20
+
21
+ const json: JsonForm = {
22
+ version: '1',
23
+ fields: [
24
+ {
25
+ name: 'accountType',
26
+ type: 'radio',
27
+ required: true,
28
+ options: [
29
+ { value: 'personal', label: 'Personal' },
30
+ { value: 'business', label: 'Business' },
31
+ ],
32
+ },
33
+ {
34
+ name: 'vatId',
35
+ type: 'text',
36
+ required: true,
37
+ affects: [{ effect: 'hideAndClear', when: { field: 'accountType', notEquals: 'business' } }],
38
+ },
39
+ ],
40
+ }
41
+
42
+ const schema = builder.build(json) // vatId is required only for a business account
43
+ ```
44
+
45
+ With the [Vue](https://github.com/whatsaaaa/form-builder/tree/main/frameworks/vue) or [React](https://github.com/whatsaaaa/form-builder/tree/main/frameworks/react) adapter, pass the builder, and the hidden fields, sections and steps leave the form:
46
+
47
+ ```ts
48
+ const jsonForm = useJsonForm(json, { builder, components })
49
+ ```
50
+
51
+ Without the extension, `affects` is an unknown key: `validate()` reports it, and `build()` throws. A definition never runs without its conditions by mistake.
52
+
53
+ ## Affects
54
+
55
+ `affects` is a list of effects, each with the condition that applies it:
56
+
57
+ | Effect | While its condition is met |
58
+ | -------------- | ------------------------------------------------------- |
59
+ | `show` | the field shows. Otherwise it is hidden, like `hide` |
60
+ | `hide` | the field is hidden, and keeps its value |
61
+ | `hideAndClear` | the field is hidden, and its value cleared: it is blank |
62
+
63
+ - **Hide always wins.** A field is hidden when its `show` condition isn't met, or its `hide` or `hideAndClear` condition is. It is cleared only when its `hideAndClear` condition is met.
64
+ - **Each effect goes once on a field.** Join conditions with `all` or `any`. A field can have `show` with `hide` or `hideAndClear`.
65
+ - **Where:** any field in `fields`, at any depth, display elements, groups and arrays included, and the layout's steps, sections and display elements. Not on a row, nor on the `of` field of a list, which is the item itself: put them on the list.
66
+
67
+ ## Conditions
68
+
69
+ ```text
70
+ Condition = { "field": Path, Operator: Value }
71
+ | { "all": [Condition, ...] }
72
+ | { "any": [Condition, ...] }
73
+ ```
74
+
75
+ A test reads one field, by its dot path, with exactly one operator. `all` and `any` take at least one condition, and nest to any depth.
76
+
77
+ | Operator | Value | True when the field |
78
+ | ----------- | --------------------------- | --------------------------------------------------------- |
79
+ | `equals` | a string, number or boolean | holds the value |
80
+ | `notEquals` | a string, number or boolean | doesn't hold the value, or is empty |
81
+ | `in` | a list of at least one | holds one of the values |
82
+ | `includes` | a string, number or boolean | is a multiselect or a list of values, and holds the value |
83
+ | `empty` | `true` | is empty |
84
+ | `notEmpty` | `true` | isn't empty |
85
+
86
+ Paths are the paths of form rules: `isBusiness`, `billing.zip`, or `lines.*.kind` for the field of the **same item** of `lines`, from a field, section or display element inside its items. A path with an index, like `lines.0.kind`, or to an item's field from outside the items, is an issue. Nested lists bind a `*` to each item the field is in: `orders.*.lines.*.sku`.
87
+
88
+ ### Operators by kind
89
+
90
+ | Kind | `equals`, `notEquals`, `in` | `includes` | `empty`, `notEmpty` |
91
+ | ---------------------------------------------------------------------- | --------------------------- | ---------- | ------------------- |
92
+ | string, number, boolean, date (text, select, radio, checkbox, date...) | yes | no | yes |
93
+ | array: a multiselect, or a list of values with `of` | no | yes | yes |
94
+ | array: a list of objects with `fields`, or of files | no | no | yes |
95
+ | file | no | no | yes |
96
+ | object: a group, which is never empty | no | no | no |
97
+ | your own kinds | no | no | yes |
98
+
99
+ A value must be one the field can hold: its schema, rules included, takes it, like a `default`. So `{ "field": "country", "equals": "Germny" }` on a select is an issue, not a condition that is never met. A date is the ISO string the field takes: `"2026-11-09"`, `"14:30"`, `"2026-11-09T14:30"`.
100
+
101
+ - **Conditions read the input**, what the form holds now, turned the way a renderer sets it: `"5"` is `5` for a number. Normalize rules like `trim` aren't applied.
102
+ - **Empty** is the kind's empty, the one `required` uses: `undefined`, `''`, `[]`. An empty field never `equals` anything, and `notEquals` is true for it.
103
+ - **A checkbox nobody touched holds nothing**, not `false`. To show something while it isn't ticked, write `{ "notEquals": true }`, or give the checkbox `"default": false`. A required checkbox only takes `true`, so `{ "equals": false }` on one is an issue.
104
+
105
+ ## What a hidden field does
106
+
107
+ - **It isn't validated:** no `required`, no rules, and no form rule that reads it, or is on it. A hidden list skips its own `required`, `minLength` and `maxLength` too. Formisch shows no error on it, and an old one goes at the next validation.
108
+ - **It counts as empty** in every other condition, so a chain follows: when `companyType` hides with the business account, a `vatId` shown for `companyType: "ltd"` hides too, though `companyType` still holds `"ltd"`.
109
+ - **A field `hideAndClear` hides is blank**: it submits `undefined`, a list `[]`, and each field of a group is blank. The adapters set it blank in the form too, with Formisch's `setInput`, as soon as it is cleared, so it is blank when it shows again. A field that starts hidden this way starts blank, without its `default`.
110
+ - **A field hidden otherwise keeps its value**: by `hide`, by a `show` that isn't met, or by a group, list, section or step that `hide` hides. It submits its output when its value is valid, and nothing when it isn't, so its errors, which nobody can see, never block the form.
111
+
112
+ So **plain hiding submits old values**. A product line that was a service line still submits its hours, and a hidden "Repeat the shipping ZIP" still submits what it held. Use `hideAndClear` for data that only makes sense while the field shows, and `hide` or `show` to keep what the user typed for when the field comes back. To clear a field a `show` hides, give it `hideAndClear` with the opposite condition.
113
+
114
+ Typing the form, mark every field that can hide as optional in the output type: a required field submits nothing while it is hidden.
115
+
116
+ ## Groups, lists, sections and steps
117
+
118
+ - **A hidden group, list, section or step hides everything in it**, and clears it when `hideAndClear` hides it. A field inside is hidden even when its own `show` is met.
119
+ - **A group whose fields are all hidden hides too.** The fields that count are those with a value, nested groups and lists included, and the display elements with `affects` of their own: a heading without conditions doesn't keep a group on show, but a paragraph shown by a condition does. A list never hides this way: it has a value of its own, and its add button.
120
+ - **A section, a row or a step of the layout whose fields are all hidden hides**, by the same rule. A field hidden in a row drops out of it, and the row's other cells keep their spans: no field moves to another row. Next and Back skip a hidden step, and the adapters leave it out of `steps.list`, unless the user is on it.
121
+ - **A step or a section with `affects`** hides and clears the fields placed in it. Where the layout splits a group, only the fields of the group placed in the section go with it.
122
+
123
+ ## Loops
124
+
125
+ A condition can't depend on itself. These are all `cycle` issues, which name the loop:
126
+
127
+ - `a` hides with `b`, and `b` with `a`;
128
+ - a field that reads itself;
129
+ - a group that reads a field inside it, like "show `company` when `company.name` is filled": it would have two answers;
130
+ - a step or a section that reads a field placed in it.
131
+
132
+ The fields of a list's items are checked once, by their `*` paths.
133
+
134
+ ## Checks
135
+
136
+ `validate()` checks every `affects` along with the rest of the definition:
137
+
138
+ | Problem | Code |
139
+ | ------------------------------------------------------------------------------------------------------------- | --------------- |
140
+ | `affects` isn't a list, an affect isn't an object, an unknown `effect`, no `when`, an effect twice | `invalid_shape` |
141
+ | A test with no operator, or with two; `all` or `any` with `field`; an empty `all`, `any` or `in`; a bad value | `invalid_shape` |
142
+ | `affects` on the `of` field of a list | `invalid_shape` |
143
+ | A key no affect or condition takes, like a misspelled operator `equal` | `unknown_key` |
144
+ | A path to no field with a value: a typo, a display element, an index, an item's field from outside its item | `unknown_field` |
145
+ | An operator the field's kind doesn't take, like `equals` on a file | `kind_mismatch` |
146
+ | A value the field can't hold, or an empty one | `invalid_value` |
147
+ | Conditions that depend on each other | `cycle` |
148
+
149
+ Each issue has its full path, such as `['fields', 3, 'affects', 0, 'when', 'all', 1, 'field']`. A field a condition reads that has issues of its own adds none.
150
+
151
+ ## Rendering
152
+
153
+ The schema skips hidden fields and gives their output either way, so validation and submitting need nothing more. To render, `builder.getVisibility(json)` tells what to hide, in any framework:
154
+
155
+ ```ts
156
+ const visibility = builder.getVisibility(json)
157
+ const state = visibility.of(getInput(form)) // the same object until what is hidden changes
158
+
159
+ state.isHidden(fieldNode, indexes) // a field of getFieldTree or getLayout
160
+ state.isStepHidden(step)
161
+ state.shownNodes(nodes, indexes) // the layout nodes to render
162
+ for (const { path, input } of state.toClear) setInput(form, { path, input }) // in an effect
163
+ ```
164
+
165
+ The Vue and React adapters do all of this. [Formisch in each framework](https://github.com/whatsaaaa/form-builder/blob/main/packages/schema/docs/frameworks.md) has a renderer for Preact, Solid, Svelte, Qwik and Angular that does it too. Without the clearing effect, the output is still right, but a cleared field shows its old value when it comes back.
166
+
167
+ ## JSON Schema
168
+
169
+ The package ships `definition.schema.json`, the JSON Schema of definitions for a builder with `affects()`: every key of the schema package's own, with `affects` on fields, steps and sections. Point a definition at it with `$schema`, and editors suggest effects and operators:
170
+
171
+ ```json
172
+ {
173
+ "$schema": "./node_modules/@whatsaaaa/form-builder-affects/definition.schema.json",
174
+ "version": "1",
175
+ "fields": []
176
+ }
177
+ ```
178
+
179
+ It can't check paths, kinds, values or loops: `validate()` does. For a builder with your own registries too, make it with the schema package's `toJsonSchema(builder)`, which takes the extension's keys.
180
+
181
+ ## TypeScript
182
+
183
+ `builder.defineForm()` types `affects` on fields, steps, sections and display elements, with the types `Affect`, `Condition`, `Effect` and `Scalar` this package exports.
184
+
185
+ ## License
186
+
187
+ MIT