react-luna-form 0.0.81 → 0.0.83
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/dist/client/cjs/index.js +1 -1
- package/dist/client/esm/index.js +1 -1
- package/dist/config/cjs/index.js +1 -1
- package/dist/config/esm/index.js +1 -1
- package/dist/schema/cjs/index.js +1 -1
- package/dist/schema/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/helper/input.d.ts +1 -9
- package/dist/types/luna-core/type.d.ts +5 -9
- package/dist/types/luna-core/util/attributes.d.ts +0 -1
- package/dist/types/luna-core/util/constant.d.ts +0 -6
- package/dist/types/luna-core/util/extract.d.ts +4 -2
- package/dist/types/luna-core/util/is-type.d.ts +2 -1
- package/dist/types/luna-core/util/schema.d.ts +0 -4
- package/dist/types/luna-react/src/client/component/input-selectable.d.ts +2 -2
- package/dist/types/luna-react/src/client/component/input-strategies.d.ts +19 -4
- package/dist/types/luna-react/src/client/hook/use-input-core.d.ts +3 -3
- package/docs/events/change.md +415 -0
- package/docs/fields/custom-inputs.md +128 -0
- package/docs/fields/input.md +175 -0
- package/docs/fields/list.md +230 -0
- package/docs/fields/select.md +194 -0
- package/docs/fields/specialized-selectors.md +175 -0
- package/docs/index.md +53 -0
- package/docs/interpolation/format-filters.md +128 -0
- package/docs/setup.md +165 -0
- package/docs/validation/overview.md +318 -0
- package/package.json +4 -3
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Specialized Selectors
|
|
2
|
+
|
|
3
|
+
Specialized selectors in Luna Form come with pre-defined data sources, such as months, years, or timezones. These allow you to quickly implement common form fields without manually defining a `source` array.
|
|
4
|
+
|
|
5
|
+
## Date & Time Selectors
|
|
6
|
+
|
|
7
|
+
### 1. Month Selector (`select/month`)
|
|
8
|
+
|
|
9
|
+
Renders a dropdown with the 12 months of the year.
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"name": "expiry_month",
|
|
14
|
+
"type": "select/month",
|
|
15
|
+
"label": "Expiration Month",
|
|
16
|
+
"required": true
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 2. Year Selector (`select/year`)
|
|
21
|
+
|
|
22
|
+
Renders a dropdown with a range of years.
|
|
23
|
+
|
|
24
|
+
- **Default behavior**: Shows the current year plus the next 5 years.
|
|
25
|
+
- **Customization**: Use `advanced.length.min` and `advanced.length.max` to define the start and end years relative to the current year.
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"name": "expiry_year",
|
|
30
|
+
"type": "select/year",
|
|
31
|
+
"label": "Expiration Year",
|
|
32
|
+
"advanced": {
|
|
33
|
+
"length": {
|
|
34
|
+
"min": 0,
|
|
35
|
+
"max": 10
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Day Selector (`select/day`)
|
|
42
|
+
|
|
43
|
+
Renders a dropdown with days 1 through 31.
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"name": "birth_day",
|
|
48
|
+
"type": "select/day",
|
|
49
|
+
"label": "Day"
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 4. Timezone Selector (`select/timezone`)
|
|
54
|
+
|
|
55
|
+
Provides a comprehensive list of global timezones.
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"name": "user_timezone",
|
|
60
|
+
"type": "select/timezone",
|
|
61
|
+
"label": "Your Timezone",
|
|
62
|
+
"defaultValue": "America/New_York"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Utility Selectors
|
|
69
|
+
|
|
70
|
+
### 5. Active/Binary Selector (`select/active`)
|
|
71
|
+
|
|
72
|
+
A simple binary toggle, typically rendered as "Yes" and "No".
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"name": "is_active",
|
|
77
|
+
"type": "select/active",
|
|
78
|
+
"label": "Account Active?",
|
|
79
|
+
"defaultValue": "true"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## specialized Chips
|
|
86
|
+
|
|
87
|
+
These types render as buttons (tags) instead of dropdowns.
|
|
88
|
+
|
|
89
|
+
### 6. Day Chips (`chips/day`)
|
|
90
|
+
|
|
91
|
+
Renders chips for the 7 days of the week.
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"name": "available_days",
|
|
96
|
+
"type": "chips/day",
|
|
97
|
+
"label": "Select Days",
|
|
98
|
+
"advanced": {
|
|
99
|
+
"multiple": true
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 7. Month Chips (`chips/month`)
|
|
105
|
+
|
|
106
|
+
Renders chips for the 12 months of the year.
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"name": "subscription_months",
|
|
111
|
+
"type": "chips/month",
|
|
112
|
+
"label": "Subscription Months"
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Localization
|
|
119
|
+
|
|
120
|
+
The option labels of these selectors are produced by the framework, not written in the schema, so most of them are resolved through the form's `lang` rather than against the `translations` dictionary the way an inline [array source](select.md#translating-options) is.
|
|
121
|
+
|
|
122
|
+
- `select/month`, `select/day`, `chips/day` and `chips/month` take their names from the form's `lang`, resolved through `Intl`. Without `lang` they fall back to the runtime locale, and a malformed tag (`es_MX`, `español`) falls back too rather than failing.
|
|
123
|
+
- `select/year` renders plain numbers.
|
|
124
|
+
- `select/timezone` renders its region groups and zone names in English.
|
|
125
|
+
- `select/active` is the exception: `Yes` and `No` are authored copy rather than locale data, so they **are** resolved against the dictionary, using their English text as the key — the same way the built-in `(Optional)` suffix is. The library ships the Spanish for both, so `lang: "es"` alone is enough; see [built-in translations](list.md#built-in-translations).
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"lang": "es",
|
|
130
|
+
"sections": [
|
|
131
|
+
{
|
|
132
|
+
"fields": [{ "name": "month", "type": "select/month" }]
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The dropdown lists `enero` through `diciembre`, while the submitted value stays the month number. Setting `lang` also keeps the server and client renders on the same locale; without it each side resolves its own default, which can differ.
|
|
139
|
+
|
|
140
|
+
`select/active` instead reads its two labels from the dictionary, and the library already ships the Spanish for them:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"lang": "es",
|
|
145
|
+
"sections": [
|
|
146
|
+
{
|
|
147
|
+
"fields": [{ "name": "active", "type": "select/active" }]
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The dropdown lists `Sí` and `No`, while the submitted value stays the boolean. A `lang` with no built-in dictionary renders them in English, and a form that wants different wording declares the keys itself:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"lang": "es",
|
|
158
|
+
"translations": { "es": { "Yes": "Activo", "No": "Inactivo" } }
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
To control the wording further — or to localize any of the locale-driven selectors — declare an explicit `source`. It overrides the built-in options and, being an array, its labels go through the dictionary:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"name": "active",
|
|
167
|
+
"type": "select/active",
|
|
168
|
+
"source": [
|
|
169
|
+
{ "value": "true", "label": "active_yes" },
|
|
170
|
+
{ "value": "false", "label": "active_no" }
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
For `select/timezone` the override must keep the group shape (`{ "label": "...", "items": [...] }`) that the grouped selector expects.
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# react-luna-form
|
|
2
|
+
|
|
3
|
+
Reference for the JSON that drives a luna form. These files ship inside the
|
|
4
|
+
published package, so what you are reading always describes the version you have
|
|
5
|
+
installed.
|
|
6
|
+
|
|
7
|
+
Forms are declared as data, not composed as components: a `sections` array of
|
|
8
|
+
field descriptors is the single source for what renders **and** for the Zod
|
|
9
|
+
schema that validates it. Most mistakes come from assuming otherwise.
|
|
10
|
+
|
|
11
|
+
Start at [setup.md](setup.md) if you are wiring the form up for the first
|
|
12
|
+
time: entry points, `defineConfig`, and the difference between the client and
|
|
13
|
+
server `Form`.
|
|
14
|
+
|
|
15
|
+
## Where to look
|
|
16
|
+
|
|
17
|
+
| You are | Read |
|
|
18
|
+
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
19
|
+
| Declaring a text, number, date or password field | [fields/input.md](fields/input.md) |
|
|
20
|
+
| Declaring a `select`, `radio` or `chips` field, or filling one from a source | [fields/select.md](fields/select.md) |
|
|
21
|
+
| Declaring a repeatable group of fields | [fields/list.md](fields/list.md) |
|
|
22
|
+
| Looking for a date range, a color or another ready-made picker | [fields/specialized-selectors.md](fields/specialized-selectors.md) |
|
|
23
|
+
| Implementing a field type this library does not ship | [fields/custom-inputs.md](fields/custom-inputs.md) |
|
|
24
|
+
| Making a field required, or validating against another field | [validation/overview.md](validation/overview.md) |
|
|
25
|
+
| Showing, hiding, clearing or filling a field when another one changes | [events/change.md](events/change.md) |
|
|
26
|
+
| Formatting an interpolated value inside a label or a message | [interpolation/format-filters.md](interpolation/format-filters.md) |
|
|
27
|
+
|
|
28
|
+
## Three things that surprise people
|
|
29
|
+
|
|
30
|
+
Read the relevant page before writing the field, not after debugging it.
|
|
31
|
+
|
|
32
|
+
- **A field type nothing implements renders nothing at all.** `config.inputs`
|
|
33
|
+
is an exact map, with no prefix resolution: a `type` that was never
|
|
34
|
+
registered resolves to `undefined` and the field is skipped, on the server
|
|
35
|
+
and on the client alike. No error, no placeholder — the field is simply
|
|
36
|
+
absent from the form.
|
|
37
|
+
- **`defineConfig({ validation })` replaces the defaults, it does not merge.**
|
|
38
|
+
Passing `{ change: false }` also turns off `blur`, `submit` and
|
|
39
|
+
`showError` — the form loses its error summary and nothing says why. Pass
|
|
40
|
+
every key you still want.
|
|
41
|
+
- **A `change` event also runs at mount.** A field that arrives with a value or
|
|
42
|
+
a `defaultValue` fires its `change` events once, so anything they reveal,
|
|
43
|
+
clear or fetch happens before the user touches the form. See
|
|
44
|
+
[events/change.md](events/change.md).
|
|
45
|
+
|
|
46
|
+
## Entry points
|
|
47
|
+
|
|
48
|
+
| Import | Use |
|
|
49
|
+
| ------------------------ | ------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `react-luna-form` | The `Form` component and client hooks |
|
|
51
|
+
| `react-luna-form/config` | `defineConfig`, `defineInput`, `defineCustomInput` and the rest of the registration helpers |
|
|
52
|
+
| `react-luna-form/schema` | Building and inspecting the Zod schema derived from `sections` |
|
|
53
|
+
| `react-luna-form/server` | Server-safe helpers, usable under the `react-server` condition |
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Format Filters
|
|
2
|
+
|
|
3
|
+
Format filters transform interpolated values into locale-aware strings using a pipe syntax inspired by Vue and Angular.
|
|
4
|
+
|
|
5
|
+
## Where filters can be used
|
|
6
|
+
|
|
7
|
+
Filters work in any string passed through Luna Form's interpolation engine, including:
|
|
8
|
+
|
|
9
|
+
- Field `description` (static or applied via a `state` change action)
|
|
10
|
+
- Field `label`
|
|
11
|
+
- `value` event payloads
|
|
12
|
+
- `source.url` and `source.body`
|
|
13
|
+
|
|
14
|
+
## Syntax
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
{key | filter:arg1:arg2}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `key` is the value to look up in the interpolation context. Supports dot notation (`{user.amount}`).
|
|
21
|
+
- After `|` comes one or more filters separated by additional `|` pipes.
|
|
22
|
+
- Each filter accepts optional positional arguments separated by `:`.
|
|
23
|
+
|
|
24
|
+
Multiple filters can be chained; the output of the previous filter becomes the input of the next.
|
|
25
|
+
|
|
26
|
+
## Locale resolution
|
|
27
|
+
|
|
28
|
+
Locale-aware filters read the locale from `config.env.locale`. If not set, filters fall back to the runtime default (typically the browser's locale).
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const config = {
|
|
32
|
+
env: {
|
|
33
|
+
locale: 'es-ES',
|
|
34
|
+
},
|
|
35
|
+
// ...
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Fallback rules
|
|
40
|
+
|
|
41
|
+
- **Unknown filter name**: the entire placeholder is preserved literally (`{value | foo}` stays as `{value | foo}`).
|
|
42
|
+
- **Invalid input** (NaN for numeric filters, unparseable date for date filters): the filter returns `String(value)` and the surrounding template still renders.
|
|
43
|
+
- **Missing key**: the placeholder is preserved literally.
|
|
44
|
+
|
|
45
|
+
## Reference
|
|
46
|
+
|
|
47
|
+
### `currency`
|
|
48
|
+
|
|
49
|
+
Formats a number as currency.
|
|
50
|
+
|
|
51
|
+
- **Syntax**: `{value | currency:CODE}`
|
|
52
|
+
- **Args**:
|
|
53
|
+
- `CODE` (string, default `"USD"`) — ISO 4217 currency code.
|
|
54
|
+
- **Backend**: `Intl.NumberFormat(locale, { style: 'currency', currency: CODE })`.
|
|
55
|
+
- **Examples**:
|
|
56
|
+
- `1234.56` with `currency:USD` and `locale: en-US` → `$1,234.56`.
|
|
57
|
+
- `1234.56` with `currency:EUR` and `locale: es-ES` → `1234,56 €`.
|
|
58
|
+
|
|
59
|
+
### `percent`
|
|
60
|
+
|
|
61
|
+
Formats a fractional number as a percentage.
|
|
62
|
+
|
|
63
|
+
- **Syntax**: `{value | percent}`
|
|
64
|
+
- **Backend**: `Intl.NumberFormat(locale, { style: 'percent' })`.
|
|
65
|
+
- **Examples**:
|
|
66
|
+
- `0.25` with `locale: en-US` → `25%`.
|
|
67
|
+
- **Note**: input is interpreted as a fraction (`1` is `100%`).
|
|
68
|
+
|
|
69
|
+
### `number`
|
|
70
|
+
|
|
71
|
+
Formats a number with locale-aware thousands separators.
|
|
72
|
+
|
|
73
|
+
- **Syntax**: `{value | number}`
|
|
74
|
+
- **Backend**: `Intl.NumberFormat(locale)`.
|
|
75
|
+
- **Examples**:
|
|
76
|
+
- `1234567` with `locale: en-US` → `1,234,567`.
|
|
77
|
+
- `1234567` with `locale: es-ES` → `1.234.567`.
|
|
78
|
+
|
|
79
|
+
### `date`
|
|
80
|
+
|
|
81
|
+
Formats a date or ISO string with a named style.
|
|
82
|
+
|
|
83
|
+
- **Syntax**: `{value | date:STYLE}`
|
|
84
|
+
- **Args**:
|
|
85
|
+
- `STYLE` (string, default `"short"`) — one of `short`, `medium`, `long`, `full`, `relative`. Any other value is passed directly to `date-fns`'s `format` as a pattern.
|
|
86
|
+
- **Backend**: `date-fns` (`format`, `formatDistanceToNow` for `relative`).
|
|
87
|
+
- **Accepts**: `Date`, ISO string, or numeric timestamp (ms).
|
|
88
|
+
- **Examples**:
|
|
89
|
+
- `"2026-05-10"` with `date:long` and `locale: en-US` → `May 10, 2026`.
|
|
90
|
+
- A future date with `date:relative` and `locale: en-US` → `in 7 days`.
|
|
91
|
+
|
|
92
|
+
### `duration`
|
|
93
|
+
|
|
94
|
+
Produces a localized human-readable duration in years, months, days, hours, minutes, and seconds.
|
|
95
|
+
|
|
96
|
+
- **Syntax**: `{value | duration}` or `{value | duration:UNIT}`
|
|
97
|
+
- **Args**:
|
|
98
|
+
- `UNIT` (string, default `"ms"`) — input unit when the value is a number. One of `ms`, `s`, `min`, `h`, `d`.
|
|
99
|
+
- **Backend**: `date-fns` (`formatDistanceToNow`, `formatDuration`, `intervalToDuration`).
|
|
100
|
+
- **Behavior depends on input type**:
|
|
101
|
+
- **Number** (or numeric string): treated as a duration in `UNIT` units, converted to milliseconds, then formatted as a breakdown (`"1 day 2 hours"`).
|
|
102
|
+
- **`Date` or non-numeric ISO string**: distance between the date and "now" with a suffix (`"3 months ago"`, `"in 2 days"`).
|
|
103
|
+
- **Multipliers to milliseconds**:
|
|
104
|
+
|
|
105
|
+
| Unit | Multiplier |
|
|
106
|
+
| ----- | ---------- |
|
|
107
|
+
| `ms` | 1 |
|
|
108
|
+
| `s` | 1,000 |
|
|
109
|
+
| `min` | 60,000 |
|
|
110
|
+
| `h` | 3,600,000 |
|
|
111
|
+
| `d` | 86,400,000 |
|
|
112
|
+
|
|
113
|
+
- **Examples**:
|
|
114
|
+
- `{age | duration:s}` with `3600` → `1 hour`.
|
|
115
|
+
- `{wait | duration:min}` with `90` → `1 hour 30 minutes`.
|
|
116
|
+
- `{ms | duration}` with `93_600_000` → `1 day 2 hours`.
|
|
117
|
+
- `{createdAt | duration}` with an ISO date in the past → `3 months ago`.
|
|
118
|
+
|
|
119
|
+
## Supported locales
|
|
120
|
+
|
|
121
|
+
Out of the box, the following locales map to a `date-fns/locale` bundle for `date` (relative and pattern styles) and `duration`:
|
|
122
|
+
|
|
123
|
+
- `en`, `en-US` → `enUS`
|
|
124
|
+
- `es`, `es-ES`, `es-MX` → `es`
|
|
125
|
+
|
|
126
|
+
Other locale codes match by language prefix (e.g. `es-AR` → `es`). If no match is found, `enUS` is used as fallback.
|
|
127
|
+
|
|
128
|
+
`Intl`-backed filters (`currency`, `percent`, `number`) accept any well-formed BCP 47 locale tag supported by the runtime.
|
package/docs/setup.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Setup
|
|
2
|
+
|
|
3
|
+
How a form is configured and rendered. The pages under `fields/`,
|
|
4
|
+
`validation/` and `events/` describe the JSON; this one describes the code
|
|
5
|
+
around it.
|
|
6
|
+
|
|
7
|
+
## Entry points
|
|
8
|
+
|
|
9
|
+
| Import | Contains |
|
|
10
|
+
| ------------------------ | ----------------------------------------------------------------------------------------- |
|
|
11
|
+
| `react-luna-form` | `Form` (client), `withDeclaredFields`, and the `Sections`, `Source` and `ZodSchema` types |
|
|
12
|
+
| `react-luna-form/server` | `Form` (server) and `Fallback` |
|
|
13
|
+
| `react-luna-form/config` | `defineConfig` and the `define*` registration helpers |
|
|
14
|
+
| `react-luna-form/schema` | `buildFormSchema` and `collectIssues` |
|
|
15
|
+
|
|
16
|
+
## Configuring
|
|
17
|
+
|
|
18
|
+
`defineConfig` is called once, usually in a `luna.config.ts` at the root of the
|
|
19
|
+
application, and its result is passed to every `Form`.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { defineConfig, defineInput, defineSelect } from 'react-luna-form/config'
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
inputs: [defineInput(Input), defineSelect(Select)],
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### `inputs` (required)
|
|
30
|
+
|
|
31
|
+
The components that render each field type. Registration is by exact type
|
|
32
|
+
string; a type nothing registers renders nothing at all. The helpers cover the
|
|
33
|
+
types this library ships:
|
|
34
|
+
|
|
35
|
+
| Helper | Registers |
|
|
36
|
+
| ------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
37
|
+
| `defineInput` | `input`, `input/date`, `input/email`, `input/number`, `input/password`, `input/tel`, `input/text`, `input/time` |
|
|
38
|
+
| `defineSelect` | `select`, `select/active`, `select/day`, `select/month`, `select/timezone`, `select/year` |
|
|
39
|
+
| `defineChips` | `chips`, `chips/day`, `chips/month` |
|
|
40
|
+
| `defineRadio` | `radio` |
|
|
41
|
+
| `defineCheckbox` | `checkbox` |
|
|
42
|
+
| `defineTextArea` | `textarea` |
|
|
43
|
+
| `defineCustomInput` | whatever names you give it — see [fields/custom-inputs.md](fields/custom-inputs.md) |
|
|
44
|
+
|
|
45
|
+
### `validation`
|
|
46
|
+
|
|
47
|
+
When to validate and whether to summarise errors. All four default to `true`.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
validation: { blur: true, change: true, showError: true, submit: true }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**This object replaces the defaults, it does not merge into them.** Passing
|
|
54
|
+
`{ change: false }` also turns off `blur`, `submit` and `showError`, so the form
|
|
55
|
+
stops validating on blur and loses its error summary with no indication why.
|
|
56
|
+
Pass every key you still want.
|
|
57
|
+
|
|
58
|
+
### `fetcher.remotePatterns`
|
|
59
|
+
|
|
60
|
+
Which hosts a field's remote `source` may call. The rules:
|
|
61
|
+
|
|
62
|
+
| Value | Effect |
|
|
63
|
+
| ------- | ------------------------------------ |
|
|
64
|
+
| omitted | **every host is allowed** |
|
|
65
|
+
| `[]` | every external URL is blocked |
|
|
66
|
+
| a list | an external URL must match one entry |
|
|
67
|
+
|
|
68
|
+
A pattern may constrain `protocol`, `hostname` and `port`; every part is
|
|
69
|
+
optional and an omitted part matches anything. Relative URLs count as internal
|
|
70
|
+
and are always allowed.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
fetcher: {
|
|
74
|
+
remotePatterns: [{ protocol: 'https', hostname: 'api.example.com' }]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A blocked URL is not an error. The library logs
|
|
79
|
+
`URL blocked by remotePatterns: <url>` and the field renders with no options at
|
|
80
|
+
all, which on screen looks like an empty select rather than a rejected request.
|
|
81
|
+
Check the console when a remote source comes back empty.
|
|
82
|
+
|
|
83
|
+
### `style`, `alert`, `env`
|
|
84
|
+
|
|
85
|
+
- `style` takes `compact`, `horizontal` and `showOptionalLabel`.
|
|
86
|
+
- `alert` is the component that renders the error summary, when `showError`
|
|
87
|
+
is on.
|
|
88
|
+
- `env` is a flat record of values available to interpolation, in labels and in
|
|
89
|
+
descriptions. Its `locale` key is also what those two read to format values.
|
|
90
|
+
|
|
91
|
+
## Rendering
|
|
92
|
+
|
|
93
|
+
### `Form` from `react-luna-form`
|
|
94
|
+
|
|
95
|
+
The client form. It owns its own state, so it is the one to use for anything
|
|
96
|
+
interactive.
|
|
97
|
+
|
|
98
|
+
| Prop | Purpose |
|
|
99
|
+
| ----------------------------------------- | ---------------------------------------------------------------------- |
|
|
100
|
+
| `sections` | the field descriptors — the form itself |
|
|
101
|
+
| `config` | the result of `defineConfig` |
|
|
102
|
+
| `value` | the current values |
|
|
103
|
+
| `action` | server action invoked on submit, receives the form data and the schema |
|
|
104
|
+
| `onSuccess` | called with the parsed data after `action` succeeds |
|
|
105
|
+
| `onValueChange` | called with `{ name, value }` on every change |
|
|
106
|
+
| `readOnly` | renders every field disabled |
|
|
107
|
+
| `translations` | per-locale strings for labels, options and messages |
|
|
108
|
+
| `lang` | the locale to resolve `translations` against |
|
|
109
|
+
| `context` | arbitrary values available to interpolation |
|
|
110
|
+
| `definition`, `advanced.step`, `children` | multi-step and custom controls |
|
|
111
|
+
|
|
112
|
+
### `Form` from `react-luna-form/server`
|
|
113
|
+
|
|
114
|
+
The same descriptors rendered without client state. It accepts the same props
|
|
115
|
+
minus the interactive ones: there is no `action`, `onSuccess` or
|
|
116
|
+
`onValueChange`. Use it for a form that only needs to be displayed.
|
|
117
|
+
|
|
118
|
+
### `Fallback`
|
|
119
|
+
|
|
120
|
+
A `Suspense` boundary whose fallback is the form itself in `readOnly` mode:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
<Fallback config={config} sections={sections}>
|
|
124
|
+
<SomethingAsync />
|
|
125
|
+
</Fallback>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The placeholder is the real layout rather than a generic skeleton, so nothing
|
|
129
|
+
shifts when the content arrives.
|
|
130
|
+
|
|
131
|
+
## `withDeclaredFields`
|
|
132
|
+
|
|
133
|
+
A form treats a name missing from its `value` object as a field the caller did
|
|
134
|
+
not mention, and leaves that input showing whatever it was showing. That is
|
|
135
|
+
correct while values are only ever being added, and wrong the moment one is
|
|
136
|
+
lost: reverting to an earlier state does not empty a key, it removes it, and the
|
|
137
|
+
input goes on displaying text that exists nowhere else.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { withDeclaredFields } from 'react-luna-form'
|
|
141
|
+
|
|
142
|
+
;<Form
|
|
143
|
+
sections={sections}
|
|
144
|
+
config={config}
|
|
145
|
+
value={withDeclaredFields(sections, value)}
|
|
146
|
+
/>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Naming the field is the difference between saying nothing about it and saying it
|
|
150
|
+
holds nothing, and only the second empties the input. Wrapping the value is the
|
|
151
|
+
opt-in: pass the result and the object becomes the form's state.
|
|
152
|
+
|
|
153
|
+
Absence is resolved with the same lookup the form uses, so a name that is a path
|
|
154
|
+
(`basicAuth.username` against a nested object) is handled correctly rather than
|
|
155
|
+
shadowed by a flat key.
|
|
156
|
+
|
|
157
|
+
## `react-luna-form/schema`
|
|
158
|
+
|
|
159
|
+
`buildFormSchema(sections)` derives the Zod schema from the same descriptors
|
|
160
|
+
that render the form, which is what keeps the rendered fields and the validated
|
|
161
|
+
shape from drifting apart. `collectIssues(error)` flattens a Zod error into
|
|
162
|
+
`{ path, message }` entries.
|
|
163
|
+
|
|
164
|
+
Use them to validate on the server against the same rules the form applies in
|
|
165
|
+
the browser.
|