react-luna-form 0.0.83 → 0.0.85
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 +140 -0
- package/dist/client/cjs/index.js +1 -1
- package/dist/client/esm/index.js +1 -1
- package/dist/server/cjs/index.js +1 -1
- package/dist/server/esm/index.js +1 -1
- package/dist/types/luna-core/type.d.ts +17 -1
- package/dist/types/luna-core/util/build.d.ts +1 -1
- package/dist/types/luna-core/util/schema.d.ts +4 -4
- package/dist/types/luna-core/util/style.d.ts +1 -5
- package/dist/types/luna-react/src/client/lib/list-store.d.ts +17 -0
- package/docs/events/change.md +77 -2
- package/docs/fields/data-source.md +113 -0
- package/docs/fields/input.md +2 -1
- package/docs/fields/list.md +5 -2
- package/docs/fields/select.md +49 -1
- package/docs/forms/submit.md +129 -0
- package/docs/index.md +9 -3
- package/docs/interpolation/format-filters.md +4 -0
- package/docs/interpolation/overview.md +179 -0
- package/docs/setup.md +65 -14
- package/docs/structure/definition.md +99 -0
- package/docs/structure/layout.md +189 -0
- package/docs/structure/sections.md +187 -0
- package/docs/validation/overview.md +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Layout
|
|
2
|
+
|
|
3
|
+
Inside a section, fields stack one per row. Everything else — fields side by
|
|
4
|
+
side, a label beside its control — is opt in, and this page is where those keys
|
|
5
|
+
live.
|
|
6
|
+
|
|
7
|
+
## Columns (`type: "column"`)
|
|
8
|
+
|
|
9
|
+
A `column` is a grid that holds fields. It is declared among the fields of a
|
|
10
|
+
section (or of a [list](../fields/list.md) row) and takes a `fields` array of
|
|
11
|
+
its own:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"type": "column",
|
|
16
|
+
"advanced": { "cols": 2 },
|
|
17
|
+
"fields": [
|
|
18
|
+
{ "name": "first_name", "type": "input/text", "label": "First name" },
|
|
19
|
+
{ "name": "last_name", "type": "input/text", "label": "Last name" }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
A column has no `name` and holds no value. It groups its fields visually and
|
|
25
|
+
nothing more: they keep the names they declare, and the submitted object is the
|
|
26
|
+
same one you would get without it.
|
|
27
|
+
|
|
28
|
+
| Key | Type | Does |
|
|
29
|
+
| --------------- | ---------------------- | ------------------------------------------ |
|
|
30
|
+
| `type` | `"column"` | required, exactly this string |
|
|
31
|
+
| `fields` | `Array<Field \| List>` | what the grid holds |
|
|
32
|
+
| `advanced.cols` | `1 \| 2 \| 3` | how many columns the grid has, default `2` |
|
|
33
|
+
| `description` | `string \| object` | help text under the grid |
|
|
34
|
+
| `order` | `number` | position among the section's other entries |
|
|
35
|
+
|
|
36
|
+
### `cols`, and the values that are not
|
|
37
|
+
|
|
38
|
+
`cols` accepts `1`, `2` and `3`. **Anything else falls back to `2`** —
|
|
39
|
+
`"cols": 4`, `"cols": 0`, a string, or the key left out entirely all produce the
|
|
40
|
+
same two-column grid, with no error and no warning. A four-column layout is not
|
|
41
|
+
something to reach for by writing `4`; it does not exist.
|
|
42
|
+
|
|
43
|
+
The grid is one column below the `md` breakpoint and `cols` columns from there
|
|
44
|
+
up, so a column layout stacks on a phone whatever it says.
|
|
45
|
+
|
|
46
|
+
### A field's own `cols` is a span
|
|
47
|
+
|
|
48
|
+
Inside a column, a field's `advanced.cols` says how many of the grid's columns
|
|
49
|
+
that field takes:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"type": "column",
|
|
54
|
+
"advanced": { "cols": 3 },
|
|
55
|
+
"fields": [
|
|
56
|
+
{
|
|
57
|
+
"name": "card_number",
|
|
58
|
+
"type": "input/text",
|
|
59
|
+
"label": "Card number",
|
|
60
|
+
"advanced": { "cols": 2 }
|
|
61
|
+
},
|
|
62
|
+
{ "name": "cvc", "type": "input/text", "label": "CVC" }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The card number takes two of the three columns and the CVC takes the third.
|
|
68
|
+
Spans of `1`, `2` and `3` are honoured and anything else is ignored, leaving the
|
|
69
|
+
field at one cell — unlike the container's `cols`, an out-of-range span does not
|
|
70
|
+
fall back to a default.
|
|
71
|
+
|
|
72
|
+
The same key means different things in the two places, which is worth reading
|
|
73
|
+
twice: on a `column` it is **how many columns the grid has**, on a field inside
|
|
74
|
+
it, **how many of them the field covers**.
|
|
75
|
+
|
|
76
|
+
A field's span outside a column does nothing at all. The class is emitted, the
|
|
77
|
+
parent is not a grid, and the field goes on filling the width.
|
|
78
|
+
|
|
79
|
+
### Description
|
|
80
|
+
|
|
81
|
+
A column's `description` renders **under** the grid, not above it, and unlike a
|
|
82
|
+
[section's description](sections.md#title-and-description-are-translated-not-interpolated)
|
|
83
|
+
it goes through interpolation: `{context.*}`, `{env.*}` and
|
|
84
|
+
[format filters](../interpolation/format-filters.md) all resolve. It also takes
|
|
85
|
+
the `{ title, message, collapsed }` object form that a field description takes.
|
|
86
|
+
|
|
87
|
+
### What a column can hold
|
|
88
|
+
|
|
89
|
+
Fields and [lists](../fields/list.md). A column inside a list row is supported
|
|
90
|
+
and keeps its grid; the fields inside it are named by the row's path like any
|
|
91
|
+
other leaf.
|
|
92
|
+
|
|
93
|
+
A column has no visibility of its own: `hidden` on a column is not read. Hide
|
|
94
|
+
the fields it holds instead — and note that when every field in a column is
|
|
95
|
+
hidden, the column counts as hidden for the
|
|
96
|
+
[section's visibility](sections.md#order-and-what-disappears), while the grid
|
|
97
|
+
element itself stays in the layout.
|
|
98
|
+
|
|
99
|
+
### Labels truncate inside a grid
|
|
100
|
+
|
|
101
|
+
A label that is longer than its cell is truncated to one line inside a column,
|
|
102
|
+
where the same label wraps outside one. This keeps the inputs of a row aligned
|
|
103
|
+
with each other instead of one sinking below its neighbour. Write the short
|
|
104
|
+
version in the `label` and put the rest in the `description`.
|
|
105
|
+
|
|
106
|
+
## Ordering fields and columns
|
|
107
|
+
|
|
108
|
+
Every entry in a `fields` array — a field, a column, a list — sorts by `order`
|
|
109
|
+
before rendering. Entries carrying one sort ascending; entries without one hold
|
|
110
|
+
their relative position and follow the rest. Because a column sorts as a single
|
|
111
|
+
entry, `order` interleaves whole columns with the plain fields around them, not
|
|
112
|
+
the fields inside them.
|
|
113
|
+
|
|
114
|
+
## Orientation (`advanced.horizontal`)
|
|
115
|
+
|
|
116
|
+
By default a field renders its label above its control. `advanced.horizontal`
|
|
117
|
+
puts them side by side from the `md` breakpoint up:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"name": "timeout",
|
|
122
|
+
"type": "input/number",
|
|
123
|
+
"label": "Timeout",
|
|
124
|
+
"advanced": { "horizontal": true }
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A form that wants every field that way sets it once as
|
|
129
|
+
`style.horizontal` in [`defineConfig`](../setup.md#style-alert-env). A field
|
|
130
|
+
that declares nothing inherits it; one that declares `false` opts out of it.
|
|
131
|
+
|
|
132
|
+
Two consequences:
|
|
133
|
+
|
|
134
|
+
- **`radio` and `checkbox` are always horizontal**, whatever the key or the
|
|
135
|
+
form-wide style says. Their control belongs beside its label, so neither is
|
|
136
|
+
read for them.
|
|
137
|
+
- **The description moves.** A vertical field renders its description after the
|
|
138
|
+
control; a horizontal one renders it under the label, in the label's own
|
|
139
|
+
column.
|
|
140
|
+
|
|
141
|
+
The rendered field carries `data-orientation="horizontal"` or `"vertical"`,
|
|
142
|
+
which is the attribute to key CSS on rather than the class names.
|
|
143
|
+
|
|
144
|
+
## `advanced.reverse` (checkbox only)
|
|
145
|
+
|
|
146
|
+
A `checkbox` renders its box first and its label after it. `reverse: false`
|
|
147
|
+
swaps them:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"name": "accepted",
|
|
152
|
+
"type": "checkbox",
|
|
153
|
+
"label": "I accept the terms",
|
|
154
|
+
"advanced": { "reverse": false }
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The default is the reversed order, so this key exists to turn it **off**, and
|
|
159
|
+
`"reverse": true` changes nothing. It is read only for `checkbox`; on a `radio`,
|
|
160
|
+
an `input/*` or anything else it is ignored.
|
|
161
|
+
|
|
162
|
+
## Spacing
|
|
163
|
+
|
|
164
|
+
Spacing between fields is a property of the section that holds them, not of the
|
|
165
|
+
field: see [`advanced.compact`](sections.md#advancedcompact).
|
|
166
|
+
|
|
167
|
+
## Styling hooks
|
|
168
|
+
|
|
169
|
+
Every structural element carries a `data-slot`, so a design system can restyle
|
|
170
|
+
the scaffolding without patching the library. The components you register in
|
|
171
|
+
[`defineConfig`](../setup.md#inputs-required) are yours already; these are the
|
|
172
|
+
parts around them.
|
|
173
|
+
|
|
174
|
+
| `data-slot` | Wraps |
|
|
175
|
+
| ------------------- | ------------------------------------------------------- |
|
|
176
|
+
| `field-set` | one section, the rendered `<fieldset>` |
|
|
177
|
+
| `field-set-content` | the body of a collapsible section |
|
|
178
|
+
| `field-group` | the stack of entries inside a section |
|
|
179
|
+
| `field-separator` | the rule drawn by `advanced.separator` |
|
|
180
|
+
| `column` | the grid of a `column` |
|
|
181
|
+
| `field` | one field, with `data-orientation` on it |
|
|
182
|
+
| `field-label` | the `<label>` |
|
|
183
|
+
| `field-content` | the label and, when horizontal, the description with it |
|
|
184
|
+
| `field-control` | the form's own control area — where `children` renders |
|
|
185
|
+
| `list-item-card` | one row of a [list](../fields/list.md) |
|
|
186
|
+
|
|
187
|
+
A field also carries `data-invalid` while it holds an error and `data-readonly`
|
|
188
|
+
while it is disabled, both readable from the descendants with
|
|
189
|
+
`group-data-[...]`.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Sections
|
|
2
|
+
|
|
3
|
+
`sections` is the top level of a form. Every other page describes what goes
|
|
4
|
+
inside one; this page describes the section itself.
|
|
5
|
+
|
|
6
|
+
A section is a **visual grouping and nothing else**. It draws a legend, sets the
|
|
7
|
+
spacing around its fields and can collapse them out of the way, but it does not
|
|
8
|
+
scope names: every field in the form shares one flat namespace, whichever
|
|
9
|
+
section it was declared in. Two fields called `email` in two different sections
|
|
10
|
+
are one field with two renderings, and
|
|
11
|
+
[`buildFormSchema`](../validation/overview.md#headless-validation) produces one
|
|
12
|
+
key for them.
|
|
13
|
+
|
|
14
|
+
## The shape
|
|
15
|
+
|
|
16
|
+
| Key | Type | Does |
|
|
17
|
+
| ---------------------- | ------------------ | --------------------------------------------- |
|
|
18
|
+
| `fields` | `Fields` | what the section renders |
|
|
19
|
+
| `title` | `string` | the legend |
|
|
20
|
+
| `description` | `string` | help text under the legend |
|
|
21
|
+
| `id` | `string \| number` | the `id` attribute of the rendered `fieldset` |
|
|
22
|
+
| `order` | `number` | position among the other sections |
|
|
23
|
+
| `advanced.compact` | `boolean` | tighter spacing between this section's fields |
|
|
24
|
+
| `advanced.collapsible` | `boolean` | render the section closed, behind a toggle |
|
|
25
|
+
| `advanced.separator` | `boolean` | draw a rule after the section |
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"sections": [
|
|
30
|
+
{
|
|
31
|
+
"id": "payment",
|
|
32
|
+
"title": "Payment Method",
|
|
33
|
+
"description": "All transactions are secure and encrypted",
|
|
34
|
+
"advanced": { "separator": true },
|
|
35
|
+
"fields": [{ "name": "name", "type": "input/text", "label": "Name" }]
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## A section with no title and no description draws nothing
|
|
42
|
+
|
|
43
|
+
There is no empty legend and no wrapper to style around it: the section's fields
|
|
44
|
+
join the flow of the form as if they had been declared without one. This is the
|
|
45
|
+
form to reach for when you want the grouping only for `compact` spacing or for
|
|
46
|
+
`order`.
|
|
47
|
+
|
|
48
|
+
Declaring `advanced.collapsible` on such a section does nothing either — with no
|
|
49
|
+
title there is nothing to click, so the fields render open.
|
|
50
|
+
|
|
51
|
+
## Title and description are translated, not interpolated
|
|
52
|
+
|
|
53
|
+
Both are resolved against the [`translations`](../fields/list.md#localization)
|
|
54
|
+
dictionary and both render `[text](url)` markdown links. Neither is passed
|
|
55
|
+
through interpolation: a `{context.name}` in a section title stays on screen as
|
|
56
|
+
the literal text `{context.name}`.
|
|
57
|
+
|
|
58
|
+
This is the one place the rule differs. A field's `label` and `description`
|
|
59
|
+
_are_ interpolated, and so is a [column's description](layout.md#description).
|
|
60
|
+
If a section heading has to carry a value, put the value in a field description
|
|
61
|
+
or in a column instead.
|
|
62
|
+
|
|
63
|
+
## `advanced.compact`
|
|
64
|
+
|
|
65
|
+
Spacing between the fields of this section, tight instead of loose. It is
|
|
66
|
+
declared per section:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"title": "Connection",
|
|
71
|
+
"advanced": { "compact": true },
|
|
72
|
+
"fields": [{ "name": "host", "type": "input/text" }]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A section that declares nothing inherits `style.compact` from
|
|
77
|
+
[`defineConfig`](../setup.md#style-alert-env), so the form-wide default holds
|
|
78
|
+
and a section only has to speak up to differ from it — including to opt out,
|
|
79
|
+
with `"compact": false` under a compact form.
|
|
80
|
+
|
|
81
|
+
## `advanced.separator`
|
|
82
|
+
|
|
83
|
+
Draws a horizontal rule **after** the section, between it and whatever follows.
|
|
84
|
+
On the last section it draws a trailing rule, which is rarely what you want.
|
|
85
|
+
|
|
86
|
+
## `advanced.collapsible`
|
|
87
|
+
|
|
88
|
+
Renders the section closed, with its title as the toggle:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"title": "Advanced options",
|
|
93
|
+
"advanced": { "collapsible": true },
|
|
94
|
+
"fields": [{ "name": "timeout", "type": "input/number" }]
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Three things to know before using it:
|
|
99
|
+
|
|
100
|
+
- **It always starts closed.** There is no key that opens it, and reopening the
|
|
101
|
+
form does not remember that the user had expanded it.
|
|
102
|
+
- **The fields inside stay mounted.** Collapsing hides them; it does not unmount
|
|
103
|
+
them. They keep their values, they still validate, and they are still
|
|
104
|
+
submitted. That is the opposite of what a [`state` action](../events/change.md#what-clearing-a-hidden-target-means)
|
|
105
|
+
does when it hides a field, which unmounts it and clears the value.
|
|
106
|
+
- **The description moves inside.** It renders with the fields rather than under
|
|
107
|
+
the title, so it is not visible while the section is closed.
|
|
108
|
+
|
|
109
|
+
A collapsible section also renders no step number, whatever `advanced.step` says.
|
|
110
|
+
|
|
111
|
+
## `advanced.step` numbers the sections
|
|
112
|
+
|
|
113
|
+
Declared on the **form**, not on a section, it puts a numbered badge beside each
|
|
114
|
+
section's legend, counting the sections in the order they render:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<Form advanced={{ step: true }} sections={sections} config={config} />
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It is a numbering, not a wizard: every section is on screen at once, and the
|
|
121
|
+
library ships nothing that shows them one at a time. The badge appears only on
|
|
122
|
+
sections that draw a legend — a section with no title and no description has
|
|
123
|
+
nowhere to put it, and a collapsible one does not take it.
|
|
124
|
+
|
|
125
|
+
## `order`, and what disappears
|
|
126
|
+
|
|
127
|
+
`sections` renders in array order unless a section declares `order`. Sections
|
|
128
|
+
carrying one sort ascending; sections without one keep their relative order and
|
|
129
|
+
follow the rest. The same rule applies to the entries inside a section, so a
|
|
130
|
+
field and a column can be interleaved by `order` — see
|
|
131
|
+
[layout.md](layout.md#ordering-fields-and-columns).
|
|
132
|
+
|
|
133
|
+
Two kinds of section never render at all:
|
|
134
|
+
|
|
135
|
+
- **One with no fields.** `"fields": []` is dropped before rendering, legend
|
|
136
|
+
included.
|
|
137
|
+
- **One whose fields are all hidden.** Hidden by `hidden: true` in the JSON or
|
|
138
|
+
by a [`state` action](../events/change.md#2-state-action-stateevent) at run
|
|
139
|
+
time, it makes no difference: when nothing inside a section is visible the
|
|
140
|
+
section goes with it, and it comes back when one of them is shown again. A
|
|
141
|
+
column counts as hidden when every field in it is.
|
|
142
|
+
|
|
143
|
+
A section has **no visibility flag of its own**. Writing `"hidden": true` on a
|
|
144
|
+
section is accepted and ignored; hide its fields instead, or leave the section
|
|
145
|
+
out of the array.
|
|
146
|
+
|
|
147
|
+
## `id`
|
|
148
|
+
|
|
149
|
+
Lands on the rendered `<fieldset>` as its `id` attribute, for an anchor link or
|
|
150
|
+
a test selector. It is not a name and nothing resolves against it: fields are
|
|
151
|
+
addressed by their own `name` from anywhere in the form.
|
|
152
|
+
|
|
153
|
+
## Complete example
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"advanced": { "step": true },
|
|
158
|
+
"sections": [
|
|
159
|
+
{
|
|
160
|
+
"id": "contact",
|
|
161
|
+
"title": "Contact",
|
|
162
|
+
"description": "How we reach you about this order",
|
|
163
|
+
"advanced": { "separator": true },
|
|
164
|
+
"fields": [
|
|
165
|
+
{ "name": "email", "type": "input/email", "label": "Email" },
|
|
166
|
+
{ "name": "phone", "type": "input/tel", "label": "Phone" }
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"title": "Delivery",
|
|
171
|
+
"advanced": { "compact": true },
|
|
172
|
+
"fields": [
|
|
173
|
+
{ "name": "street", "type": "input/text", "label": "Street" },
|
|
174
|
+
{ "name": "city", "type": "input/text", "label": "City" }
|
|
175
|
+
]
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"title": "Advanced options",
|
|
179
|
+
"advanced": { "collapsible": true },
|
|
180
|
+
"order": 99,
|
|
181
|
+
"fields": [
|
|
182
|
+
{ "name": "notes", "type": "textarea", "label": "Delivery notes" }
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
```
|
|
@@ -251,7 +251,7 @@ if (!result.success) {
|
|
|
251
251
|
}
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
-
- **`buildFormSchema(sections, translations?, definition?)`** — walks the form tree (sections → columns → lists → fields) and returns a Zod schema. Lists become arrays of objects, so nested `requiredWhen`/`rules` resolve in **item scope**. `translations` maps messages to localized strings; `definition` resolves `$ref`
|
|
254
|
+
- **`buildFormSchema(sections, translations?, definition?)`** — walks the form tree (sections → columns → lists → fields) and returns a Zod schema. Lists become arrays of objects, so nested `requiredWhen`/`rules` resolve in **item scope**. `translations` maps messages to localized strings; `definition` resolves [`$ref` entries](../structure/definition.md), and has to be the same one the form renders with.
|
|
255
255
|
- **`collectIssues(error)`** — flattens a `ZodError` into `{ path, message }` entries with **dotted paths** (e.g. `rules.0.value`), including nested list issues.
|
|
256
256
|
|
|
257
257
|
### What each path validates
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-luna-form",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.85",
|
|
4
4
|
"description": "A React library for building forms based on JSON",
|
|
5
5
|
"main": "./dist/client/cjs/index.js",
|
|
6
6
|
"module": "./dist/client/esm/index.js",
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/react": "19.2.18",
|
|
34
|
-
"@types/react-dom": "19.2.
|
|
34
|
+
"@types/react-dom": "19.2.5",
|
|
35
35
|
"eslint-plugin-react": "7.37.5",
|
|
36
36
|
"eslint-plugin-react-hooks": "7.1.1",
|
|
37
|
-
"eslint-plugin-react-refresh": "0.5.
|
|
38
|
-
"@luna-form/core": "0.0.
|
|
37
|
+
"eslint-plugin-react-refresh": "0.5.5",
|
|
38
|
+
"@luna-form/core": "0.0.85"
|
|
39
39
|
},
|
|
40
40
|
"license": "Apache-2.0",
|
|
41
41
|
"repository": {
|