react-luna-form 0.0.87 → 0.0.89
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 +1 -1
- 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/handle/source-event.d.ts +2 -1
- package/dist/types/luna-core/handle/value-event.d.ts +2 -1
- package/dist/types/luna-core/helper/input.d.ts +16 -7
- package/dist/types/luna-core/type.d.ts +15 -4
- package/dist/types/luna-core/util/build.d.ts +20 -1
- package/dist/types/luna-core/util/constant.d.ts +4 -0
- package/dist/types/luna-core/util/date.d.ts +147 -6
- package/dist/types/luna-core/util/format.d.ts +11 -0
- package/dist/types/luna-core/util/is-input.d.ts +3 -2
- package/dist/types/luna-core/util/list.d.ts +3 -1
- package/dist/types/luna-core/util/pattern.d.ts +28 -0
- package/dist/types/luna-core/util/prepare.d.ts +3 -2
- package/dist/types/luna-core/util/schema.d.ts +4 -3
- package/dist/types/luna-core/util/string.d.ts +7 -0
- package/dist/types/luna-react/src/client/component/field/field-list-preview-item.d.ts +3 -3
- package/dist/types/luna-react/src/client/component/field/field-preview-item.d.ts +4 -2
- package/dist/types/luna-react/src/client/component/field/field-preview-value.d.ts +4 -1
- package/dist/types/luna-react/src/client/component/field/field-preview.d.ts +2 -2
- package/dist/types/luna-react/src/client/component/formatted-description.d.ts +2 -2
- package/dist/types/luna-react/src/client/component/guard/list-guard.d.ts +1 -1
- package/dist/types/luna-react/src/client/component/input-strategies.d.ts +9 -1
- package/dist/types/luna-react/src/client/component/slot/slot.d.ts +1 -2
- package/dist/types/luna-react/src/client/hook/use-input-core.d.ts +2 -2
- package/dist/types/luna-react/src/component/column.d.ts +2 -2
- package/dist/types/luna-react/src/component/description.d.ts +2 -2
- package/dist/types/luna-react/src/component/field/field-list.d.ts +2 -3
- package/dist/types/luna-react/src/component/field/field.d.ts +3 -3
- package/dist/types/luna-react/src/component/field-description.d.ts +2 -2
- package/dist/types/luna-react/src/component/form.d.ts +1 -0
- package/dist/types/luna-react/src/component/input/input-base.d.ts +3 -3
- package/dist/types/luna-react/src/component/input-group.d.ts +2 -2
- package/dist/types/luna-react/src/component/input-label.d.ts +2 -2
- package/dist/types/luna-react/src/component/slot/list-slot.d.ts +1 -1
- package/dist/types/luna-react/src/component/slot/slot-base.d.ts +2 -3
- package/dist/types/luna-react/src/component/slot/slot-create.d.ts +2 -3
- package/dist/types/luna-react/src/component/slot/slot-list.d.ts +2 -3
- package/dist/types/luna-react/src/component/slot/slot.d.ts +1 -2
- package/dist/types/luna-react/src/config/index.d.ts +1 -0
- package/dist/types/luna-react/src/server/component/input.d.ts +2 -2
- package/dist/types/luna-react/src/server/component/slot.d.ts +1 -2
- package/dist/types/luna-react/src/type.d.ts +2 -1
- package/docs/events/change.md +4 -1
- package/docs/fields/custom-inputs.md +153 -4
- package/docs/fields/data-source.md +5 -3
- package/docs/fields/input.md +195 -4
- package/docs/fields/list.md +2 -1
- package/docs/fields/specialized-selectors.md +51 -12
- package/docs/forms/submit.md +6 -8
- package/docs/index.md +2 -1
- package/docs/interpolation/format-filters.md +28 -29
- package/docs/interpolation/overview.md +44 -14
- package/docs/setup.md +22 -17
- package/docs/structure/definition.md +45 -6
- package/docs/validation/overview.md +14 -11
- package/package.json +3 -3
- package/dist/types/luna-react/src/client/hook/use-format.d.ts +0 -5
|
@@ -21,10 +21,18 @@ An optional month nobody picked is not submitted at all, and a required one asks
|
|
|
21
21
|
|
|
22
22
|
### 2. Year Selector (`select/year`)
|
|
23
23
|
|
|
24
|
-
Renders a dropdown with
|
|
24
|
+
Renders a dropdown with the years from `advanced.length.min` to `advanced.length.max`, both included, written as whole numbers:
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"name": "graduation_year",
|
|
29
|
+
"type": "select/year",
|
|
30
|
+
"label": "Graduation Year",
|
|
31
|
+
"advanced": { "length": { "min": 1980, "max": 2030 } }
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The form keeps no clock, so which year it is now is the host's to say, the way it says [which day is today](input.md#the-first-and-the-last-day) to a date. Pass the years in [`context`](../structure/definition.md#what-the-host-knows-context) and point the bounds at them:
|
|
28
36
|
|
|
29
37
|
```json
|
|
30
38
|
{
|
|
@@ -33,14 +41,30 @@ Renders a dropdown with a range of years.
|
|
|
33
41
|
"label": "Expiration Year",
|
|
34
42
|
"advanced": {
|
|
35
43
|
"length": {
|
|
36
|
-
"min":
|
|
37
|
-
"max":
|
|
44
|
+
"min": { "$ref": "#/context/years.current" },
|
|
45
|
+
"max": { "$ref": "#/context/years.inTen" }
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
```
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
```ts
|
|
52
|
+
// In the application, counted where the business is, not where the server runs.
|
|
53
|
+
const year = Number(
|
|
54
|
+
new Intl.DateTimeFormat('en', {
|
|
55
|
+
timeZone: 'America/Bogota',
|
|
56
|
+
year: 'numeric',
|
|
57
|
+
}).format(new Date())
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const context = { years: { current: year, inTen: year + 10 } }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
At 03:00 UTC on 1 January the year is already the next one in UTC and still the old one in Bogota, so the host decides where it is counted. The server renders and validates with the years it is given, the same ones the browser gets.
|
|
64
|
+
|
|
65
|
+
A year select needs both bounds. Without one, or with one that is no whole number, such as `"2026"` written as text or a `$ref` the context does not hold, it offers no year, and a development build names it in the console. A minimum after the maximum is named the same way.
|
|
66
|
+
|
|
67
|
+
The schema checks the same bounds, so a year outside them, from a host value or a submit made by hand, is held back with `validation.length.min` or `validation.length.max` as the message, or a default such as `Too small: expected number to be >=2026`. A field that offers no year takes none: any year is held back with `This year is not available`. An optional year nobody picked is not submitted at all, and a required one asks for a value, the way an [`input/number`](input.md#empty-and-required-numbers) does.
|
|
44
68
|
|
|
45
69
|
### 3. Day Selector (`select/day`)
|
|
46
70
|
|
|
@@ -56,17 +80,32 @@ Renders a dropdown with days 1 through 31.
|
|
|
56
80
|
|
|
57
81
|
### 4. Timezone Selector (`select/timezone`)
|
|
58
82
|
|
|
59
|
-
Provides
|
|
83
|
+
Provides every time zone the runtime knows, from `Intl.supportedValuesOf('timeZone')`, grouped by region. A field submits the zone's IANA name, such as `America/Bogota`.
|
|
60
84
|
|
|
61
85
|
```json
|
|
62
86
|
{
|
|
63
|
-
"name": "
|
|
87
|
+
"name": "meeting_zone",
|
|
64
88
|
"type": "select/timezone",
|
|
65
|
-
"label": "
|
|
66
|
-
"
|
|
89
|
+
"label": "Time zone",
|
|
90
|
+
"advanced": { "suggested": { "$ref": "#/context/user.timeZone" } }
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// Worked out once per request, on the server.
|
|
96
|
+
const context = {
|
|
97
|
+
now: new Date().toISOString(),
|
|
98
|
+
user: { timeZone: session.timeZone },
|
|
67
99
|
}
|
|
100
|
+
|
|
101
|
+
<Form sections={sections} context={context} config={config} />
|
|
68
102
|
```
|
|
69
103
|
|
|
104
|
+
- **`advanced.suggested`** is the zone offered first, alone in a "Suggested" group. The host knows it, from a profile, a cookie or a header, so it usually arrives through [`context`](../structure/definition.md#what-the-host-knows-context), and the server and the browser suggest the same one. Without it there is no "Suggested" group. A zone written another way is suggested, and submitted, by the name the runtime gives it, so it is listed once: `america/bogota` as `America/Bogota`, `US/Eastern` as `America/New_York`. A value that is no zone suggests nothing, an offset such as `+05:00` included, and a development build names it in the console.
|
|
105
|
+
- **[`context.now`](../interpolation/overview.md#the-instant-relative-dates-use)**, an ISO date, time and offset, is the instant the zones are labelled for. A zone's offset depends on it, `Madrid - Central European (UTC+01:00)` in January and `(UTC+02:00)` in July, and so does its name, which some zones have changed over the years. Without it each zone is labelled by its city alone, `Madrid`.
|
|
106
|
+
|
|
107
|
+
The list is the runtime's own, so two engines can list different zones: Node and Chromium list `Asia/Calcutta` and leave out `UTC`, which can still be suggested.
|
|
108
|
+
|
|
70
109
|
---
|
|
71
110
|
|
|
72
111
|
## Utility Selectors
|
|
@@ -123,7 +162,7 @@ Renders chips for the 12 months of the year.
|
|
|
123
162
|
|
|
124
163
|
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.
|
|
125
164
|
|
|
126
|
-
- `select/month`, `select/day`, `chips/day` and `chips/month` take their names from the form's `lang`, resolved through `Intl`. Without `lang`
|
|
165
|
+
- `select/month`, `select/day`, `chips/day` and `chips/month` take their names from the form's `lang`, resolved through `Intl`. Without `lang`, with a malformed tag (`es_MX`, `español`), or with one the runtime has no language for (`sp`), they are in English, whatever language the machine rendering them speaks.
|
|
127
166
|
- `select/year` renders plain numbers.
|
|
128
167
|
- `select/timezone` renders its region groups and zone names in English.
|
|
129
168
|
- `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).
|
|
@@ -139,7 +178,7 @@ The option labels of these selectors are produced by the framework, not written
|
|
|
139
178
|
}
|
|
140
179
|
```
|
|
141
180
|
|
|
142
|
-
The dropdown lists `enero` through `diciembre`, while the submitted value stays the month number.
|
|
181
|
+
The dropdown lists `enero` through `diciembre`, while the submitted value stays the month number. The server and the client render the same names, with `lang` or without it.
|
|
143
182
|
|
|
144
183
|
`select/active` instead reads its two labels from the dictionary, and the library already ships the Spanish for them:
|
|
145
184
|
|
package/docs/forms/submit.md
CHANGED
|
@@ -28,18 +28,16 @@ On submit, in this order:
|
|
|
28
28
|
|
|
29
29
|
1. **The form data becomes an object.** A name that appears more than once
|
|
30
30
|
arrives as an array; everything else as a single value.
|
|
31
|
-
2. **
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
2. **It is validated** against the schema built from the fields that are
|
|
32
|
+
currently mounted. A date is read in either shape the form accepts and comes
|
|
33
|
+
out of the schema as `yyyy-MM-dd`, so your action sees one shape whatever
|
|
34
34
|
`advanced.format` says.
|
|
35
|
-
3. **
|
|
36
|
-
currently mounted.
|
|
37
|
-
4. **Dotted names are un-flattened.** `basicAuth.username` becomes
|
|
35
|
+
3. **Dotted names are un-flattened.** `basicAuth.username` becomes
|
|
38
36
|
`{ basicAuth: { username } }`, and a numeric segment becomes an array index,
|
|
39
37
|
so `rules.0.value` rebuilds the list — with the gaps of removed rows closed
|
|
40
38
|
up.
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
4. **Your `action` is awaited** with that object and the schema.
|
|
40
|
+
5. **`onSuccess` is called** with what the action returned, and the form clears
|
|
43
41
|
itself.
|
|
44
42
|
|
|
45
43
|
## Only mounted fields are validated
|
package/docs/index.md
CHANGED
|
@@ -22,7 +22,8 @@ between the client and server `Form`.
|
|
|
22
22
|
| Declaring a text, number, date or password field | [fields/input.md](fields/input.md) |
|
|
23
23
|
| Declaring a `select`, `radio` or `chips` field, or filling one from a source | [fields/select.md](fields/select.md) |
|
|
24
24
|
| Declaring a repeatable group of fields | [fields/list.md](fields/list.md) |
|
|
25
|
-
|
|
|
25
|
+
| Picking a range of days, bounding a date or blocking days | [fields/input.md](fields/input.md#a-range-of-days) |
|
|
26
|
+
| Picking a day of the month, a month, a year, weekdays or a time zone | [fields/specialized-selectors.md](fields/specialized-selectors.md) |
|
|
26
27
|
| Implementing a field type this library does not ship | [fields/custom-inputs.md](fields/custom-inputs.md) |
|
|
27
28
|
| Filling a `select` from an API | [fields/data-source.md](fields/data-source.md) |
|
|
28
29
|
| Making a field required, or validating against another field | [validation/overview.md](validation/overview.md) |
|
|
@@ -25,21 +25,21 @@ Filters work in any string passed through Luna Form's interpolation engine, incl
|
|
|
25
25
|
|
|
26
26
|
Multiple filters can be chained; the output of the previous filter becomes the input of the next.
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## Language
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Filters format in the form's `lang`, the tag that also picks its translations
|
|
31
|
+
and names its months:
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
const config = {
|
|
36
|
-
env: {
|
|
37
|
-
locale: 'es-ES',
|
|
38
|
-
},
|
|
39
|
-
// ...
|
|
40
|
-
}
|
|
33
|
+
```tsx
|
|
34
|
+
<Form sections={sections} config={config} lang="es-ES" />
|
|
41
35
|
```
|
|
42
36
|
|
|
37
|
+
It applies wherever a filter is written — a label, a description, a `value` or
|
|
38
|
+
`source` payload — so one setting keeps them consistent. Without `lang`, or with
|
|
39
|
+
one that is no language tag or names no language the runtime has, filters write
|
|
40
|
+
English, on every machine. See
|
|
41
|
+
[Interpolation](overview.md#the-language-filters-use).
|
|
42
|
+
|
|
43
43
|
## Fallback rules
|
|
44
44
|
|
|
45
45
|
- **Unknown filter name**: the entire placeholder is preserved literally (`{value | foo}` stays as `{value | foo}`).
|
|
@@ -57,8 +57,8 @@ Formats a number as currency.
|
|
|
57
57
|
- `CODE` (string, default `"USD"`) — ISO 4217 currency code.
|
|
58
58
|
- **Backend**: `Intl.NumberFormat(locale, { style: 'currency', currency: CODE })`.
|
|
59
59
|
- **Examples**:
|
|
60
|
-
- `1234.56` with `currency:USD` and `
|
|
61
|
-
- `1234.56` with `currency:EUR` and `
|
|
60
|
+
- `1234.56` with `currency:USD` and `lang: en-US` → `$1,234.56`.
|
|
61
|
+
- `1234.56` with `currency:EUR` and `lang: es-ES` → `1234,56 €`.
|
|
62
62
|
|
|
63
63
|
### `percent`
|
|
64
64
|
|
|
@@ -67,7 +67,7 @@ Formats a fractional number as a percentage.
|
|
|
67
67
|
- **Syntax**: `{value | percent}`
|
|
68
68
|
- **Backend**: `Intl.NumberFormat(locale, { style: 'percent' })`.
|
|
69
69
|
- **Examples**:
|
|
70
|
-
- `0.25` with `
|
|
70
|
+
- `0.25` with `lang: en-US` → `25%`.
|
|
71
71
|
- **Note**: input is interpreted as a fraction (`1` is `100%`).
|
|
72
72
|
|
|
73
73
|
### `number`
|
|
@@ -77,8 +77,8 @@ Formats a number with locale-aware thousands separators.
|
|
|
77
77
|
- **Syntax**: `{value | number}`
|
|
78
78
|
- **Backend**: `Intl.NumberFormat(locale)`.
|
|
79
79
|
- **Examples**:
|
|
80
|
-
- `1234567` with `
|
|
81
|
-
- `1234567` with `
|
|
80
|
+
- `1234567` with `lang: en-US` → `1,234,567`.
|
|
81
|
+
- `1234567` with `lang: es-ES` → `1.234.567`.
|
|
82
82
|
|
|
83
83
|
### `date`
|
|
84
84
|
|
|
@@ -87,11 +87,13 @@ Formats a date or ISO string with a named style.
|
|
|
87
87
|
- **Syntax**: `{value | date:STYLE}`
|
|
88
88
|
- **Args**:
|
|
89
89
|
- `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.
|
|
90
|
-
- **Backend**: `
|
|
90
|
+
- **Backend**: `Intl.DateTimeFormat` with `dateStyle` for the named styles, `Intl.RelativeTimeFormat` for `relative`, and `date-fns`'s `format` for a pattern.
|
|
91
91
|
- **Accepts**: `Date`, ISO string, or numeric timestamp (ms).
|
|
92
|
+
- **`relative`** is measured from [`context.now`](overview.md#the-instant-relative-dates-use), the instant the host passes, never from the clock of the machine that renders it. Without it the date is shown in the `medium` style.
|
|
92
93
|
- **Examples**:
|
|
93
|
-
- `"2026-05-10"` with `date:long` and `
|
|
94
|
-
-
|
|
94
|
+
- `"2026-05-10"` with `date:long` and `lang: en-US` → `May 10, 2026`.
|
|
95
|
+
- `"2026-05-10"` with `date:long` and `lang: es-ES` → `10 de mayo de 2026`.
|
|
96
|
+
- `"2026-10-12T12:00:00Z"` with `date:relative`, `context.now` at `"2026-10-05T12:00:00Z"` and `lang: en-US` → `in 7 days`; with no `context.now` → `Oct 12, 2026`.
|
|
95
97
|
|
|
96
98
|
### `duration`
|
|
97
99
|
|
|
@@ -100,10 +102,10 @@ Produces a localized human-readable duration in years, months, days, hours, minu
|
|
|
100
102
|
- **Syntax**: `{value | duration}` or `{value | duration:UNIT}`
|
|
101
103
|
- **Args**:
|
|
102
104
|
- `UNIT` (string, default `"ms"`) — input unit when the value is a number. One of `ms`, `s`, `min`, `h`, `d`.
|
|
103
|
-
- **Backend**: `date-fns` (`
|
|
105
|
+
- **Backend**: `Intl.RelativeTimeFormat` for a date, and `date-fns` (`formatDuration`, `intervalToDuration`) for a number.
|
|
104
106
|
- **Behavior depends on input type**:
|
|
105
107
|
- **Number** (or numeric string): treated as a duration in `UNIT` units, converted to milliseconds, then formatted as a breakdown (`"1 day 2 hours"`).
|
|
106
|
-
- **`Date` or non-numeric ISO string**: distance between the date and
|
|
108
|
+
- **`Date` or non-numeric ISO string**: distance between the date and [`context.now`](overview.md#the-instant-relative-dates-use) with a suffix (`"3 months ago"`, `"in 2 days"`). Without `context.now` the date is shown in the `medium` style.
|
|
107
109
|
- **Multipliers to milliseconds**:
|
|
108
110
|
|
|
109
111
|
| Unit | Multiplier |
|
|
@@ -118,15 +120,12 @@ Produces a localized human-readable duration in years, months, days, hours, minu
|
|
|
118
120
|
- `{age | duration:s}` with `3600` → `1 hour`.
|
|
119
121
|
- `{wait | duration:min}` with `90` → `1 hour 30 minutes`.
|
|
120
122
|
- `{ms | duration}` with `93_600_000` → `1 day 2 hours`.
|
|
121
|
-
- `{createdAt | duration}` with
|
|
122
|
-
|
|
123
|
-
## Supported locales
|
|
123
|
+
- `{createdAt | duration}` with `"2026-07-07T12:00:00Z"` and `context.now` at `"2026-10-05T12:00:00Z"` → `3 months ago`.
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
## Supported languages
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
- `es`, `es-ES`, `es-MX` → `es`
|
|
127
|
+
The named date styles, `relative`, `currency`, `percent` and `number` are written by `Intl`, in any well-formed BCP 47 tag the runtime supports. In English the named styles read as they always did: `10/2/26`, `Oct 2, 2026`, `October 2, 2026`, `Friday, October 2, 2026`.
|
|
129
128
|
|
|
130
|
-
|
|
129
|
+
`relative` writes the distance in the largest unit that holds it once rounded: seconds, minutes, hours, days up to a month, months up to a year, and then years. `in 40 seconds`, `in 3 hours`, `in 1 month`; 59 minutes and 40 seconds is `1 hour ago`, and no distance at all is `now` (`ahora` in Spanish).
|
|
131
130
|
|
|
132
|
-
|
|
131
|
+
A date pattern (`date:dd/MM/yyyy`) and a `duration` of a number are written by `date-fns`, which the library ships in English and Spanish. A tag matches by its base language (`es-AR` writes Spanish), and any other language gets English names and units.
|
|
@@ -33,13 +33,18 @@ about interpolation is one being read as the other.
|
|
|
33
33
|
dot notation for anything nested:
|
|
34
34
|
|
|
35
35
|
```tsx
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
const context = { user: { firstName: 'Jane' }, plan: 'pro' }
|
|
37
|
+
|
|
38
|
+
export function Page() {
|
|
39
|
+
return <Form sections={sections} config={config} context={context} />
|
|
40
|
+
}
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
Keep `context` the same object while nothing in it changes, as here, or with
|
|
44
|
+
`useMemo` when it comes from state: every field renders again when it is a new
|
|
45
|
+
one. See
|
|
46
|
+
[What the host knows](../structure/definition.md#what-the-host-knows-context).
|
|
47
|
+
|
|
43
48
|
```json
|
|
44
49
|
{
|
|
45
50
|
"name": "greeting",
|
|
@@ -148,19 +153,44 @@ until somebody fills it.
|
|
|
148
153
|
- **Spaces are trimmed.** `{ value }` and `{value}` are the same placeholder, as
|
|
149
154
|
are `{total|currency:USD}` and `{total | currency:USD}`.
|
|
150
155
|
|
|
151
|
-
## The
|
|
156
|
+
## The language filters use
|
|
157
|
+
|
|
158
|
+
Filters format in the form's language, its `lang`: the same tag that picks its
|
|
159
|
+
[translations](../fields/list.md#built-in-translations) and names its months.
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
<Form sections={sections} config={config} lang="es-CO" />
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
It reaches every filter the form runs: a label, a description and the payload
|
|
166
|
+
of a `value` or `source` action format the same value the same way. Without
|
|
167
|
+
`lang`, with one that is no language tag (`es_MX`), or with one the runtime has
|
|
168
|
+
no language for (`sp`), the form speaks English, on the server and in the
|
|
169
|
+
browser alike, whatever language their machines speak. Its translations follow
|
|
170
|
+
the same tag, so a form never writes in two languages.
|
|
171
|
+
`config.env.locale` is not read; a development build names one left in the
|
|
172
|
+
config.
|
|
152
173
|
|
|
153
|
-
|
|
174
|
+
## The instant relative dates use
|
|
154
175
|
|
|
155
|
-
|
|
156
|
-
|
|
176
|
+
The form keeps no clock. A [`date:relative`](format-filters.md#date) or a [`duration`](format-filters.md#duration) of a date is measured from `context.now`, an ISO date, time and offset the host passes, such as `2026-10-05T19:30:00-05:00` or what `toISOString()` gives:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// Worked out once per request, on the server.
|
|
180
|
+
const context = { now: new Date().toISOString(), renewal: plan.renewsAt }
|
|
181
|
+
|
|
182
|
+
<Form sections={sections} context={context} config={config} />
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{ "label": "Renews {context.renewal | date:relative}" }
|
|
157
187
|
```
|
|
158
188
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
189
|
+
With `now` at `2026-10-05T12:00:00Z` and the renewal at `2026-10-10T12:00:00Z`, the label reads `Renews in 5 days` on the server and in the browser alike, whatever their clocks say. Without `context.now`, or with one that is no ISO instant, the date is shown in the `medium` style: `Renews Oct 10, 2026`.
|
|
190
|
+
|
|
191
|
+
The offset is what makes it an instant. Without one, `2026-10-05T12:00:00` or `2026-10-05` would be read in the zone of the machine that renders it, so the form takes it for no instant. So is a `Date` or a timestamp: a development build names each once for the `context` that carries it. The date being measured is read the way the [`date`](format-filters.md#date) filter reads any date, so give it with its offset too: a day alone is midnight where the code runs, and the distance to it changes with the machine.
|
|
192
|
+
|
|
193
|
+
`now` is the one key of `context` the form reads on its own, wherever the text it formats can read `context`: a label and a description. A [`value` or `source` payload](#event-payloads) resolves against the selected option and nothing else, so a relative date written there shows the date. A [`select/timezone`](../fields/specialized-selectors.md#4-timezone-selector-selecttimezone) labels its zones for the same instant.
|
|
164
194
|
|
|
165
195
|
## An unresolved placeholder cancels a request
|
|
166
196
|
|
package/docs/setup.md
CHANGED
|
@@ -16,7 +16,7 @@ dependency, so your application resolves one copy of React, Zod and the rest.
|
|
|
16
16
|
| Peer | Version | Used for |
|
|
17
17
|
| -------------------- | --------- | ------------------------------------------- |
|
|
18
18
|
| `react`, `react-dom` | `^19.0.0` | rendering, and `useActionState` on submit |
|
|
19
|
-
| `zod` | `^4.0.
|
|
19
|
+
| `zod` | `^4.0.6` | the schema derived from `sections` |
|
|
20
20
|
| `jotai` | `^2.0.0` | the value, error and source stores |
|
|
21
21
|
| `swr` | `^2.0.0` | fetching a field's remote `source` |
|
|
22
22
|
| `date-fns` | `^4.0.0` | date and time formatting, `duration` filter |
|
|
@@ -29,6 +29,10 @@ them in for you; anywhere else, install them alongside the library. A missing
|
|
|
29
29
|
peer surfaces as a module resolution error at import time, not as a form that
|
|
30
30
|
renders badly.
|
|
31
31
|
|
|
32
|
+
Zod has to be 4.0.6 or later. An earlier 4.0 reports a value that an optional
|
|
33
|
+
text or email field holds back as `Invalid input`, instead of with the field's
|
|
34
|
+
own message.
|
|
35
|
+
|
|
32
36
|
## Styling
|
|
33
37
|
|
|
34
38
|
The components render Tailwind utility classes and **the build ships no CSS**.
|
|
@@ -141,7 +145,8 @@ Check the console when a remote source comes back empty — see
|
|
|
141
145
|
- `alert` is the component that renders the error summary, when `showError`
|
|
142
146
|
is on.
|
|
143
147
|
- `env` is a flat record of values available to interpolation, in labels and in
|
|
144
|
-
descriptions.
|
|
148
|
+
descriptions. It holds the application's values: the language is the form's
|
|
149
|
+
`lang`, and a development build names an `env.locale` nothing reads.
|
|
145
150
|
|
|
146
151
|
## Warnings
|
|
147
152
|
|
|
@@ -164,21 +169,21 @@ production build does not. Where nothing defines it at all, they are shown.
|
|
|
164
169
|
The client form. It owns its own state, so it is the one to use for anything
|
|
165
170
|
interactive.
|
|
166
171
|
|
|
167
|
-
| Prop | Purpose
|
|
168
|
-
| --------------- |
|
|
169
|
-
| `sections` | the field descriptors — the form itself
|
|
170
|
-
| `config` | the result of `defineConfig`
|
|
171
|
-
| `value` | the current values
|
|
172
|
-
| `action` | server action invoked on submit, receives the form data and the schema — see [forms/submit.md](forms/submit.md)
|
|
173
|
-
| `onSuccess` | called with the parsed data after `action` succeeds
|
|
174
|
-
| `onValueChange` | called with `{ name, value }` on every change
|
|
175
|
-
| `readOnly` | renders every field disabled
|
|
176
|
-
| `translations` | per-
|
|
177
|
-
| `lang` | the
|
|
178
|
-
| `context` | arbitrary values available to interpolation
|
|
179
|
-
| `definition` | the JSON that [`$ref`](structure/definition.md) entries resolve against
|
|
180
|
-
| `advanced.step` | numbers the [sections](structure/sections.md#advancedstep-numbers-the-sections)
|
|
181
|
-
| `children` | the form's control area — see [forms/submit.md](forms/submit.md#nothing-submits-without-a-control)
|
|
172
|
+
| Prop | Purpose |
|
|
173
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
174
|
+
| `sections` | the field descriptors — the form itself |
|
|
175
|
+
| `config` | the result of `defineConfig` |
|
|
176
|
+
| `value` | the current values |
|
|
177
|
+
| `action` | server action invoked on submit, receives the form data and the schema — see [forms/submit.md](forms/submit.md) |
|
|
178
|
+
| `onSuccess` | called with the parsed data after `action` succeeds |
|
|
179
|
+
| `onValueChange` | called with `{ name, value }` on every change |
|
|
180
|
+
| `readOnly` | renders every field disabled |
|
|
181
|
+
| `translations` | per-language strings for labels, options and messages |
|
|
182
|
+
| `lang` | the form's language: its translations, its month and day names, its filters and the dates it shows; English without one |
|
|
183
|
+
| `context` | arbitrary values available to interpolation, and to a `$ref` under `#/context/` |
|
|
184
|
+
| `definition` | the JSON that [`$ref`](structure/definition.md) entries resolve against |
|
|
185
|
+
| `advanced.step` | numbers the [sections](structure/sections.md#advancedstep-numbers-the-sections) |
|
|
186
|
+
| `children` | the form's control area — see [forms/submit.md](forms/submit.md#nothing-submits-without-a-control) |
|
|
182
187
|
|
|
183
188
|
### `Form` from `react-luna-form/server`
|
|
184
189
|
|
|
@@ -67,6 +67,42 @@ section:
|
|
|
67
67
|
References inside a definition are resolved too, so one entry can be built out
|
|
68
68
|
of others.
|
|
69
69
|
|
|
70
|
+
## What the host knows: `#/context/`
|
|
71
|
+
|
|
72
|
+
A definition is JSON the form reuses. What the host only knows when it renders,
|
|
73
|
+
such as the options a user may pick or the nights already booked, is passed as
|
|
74
|
+
the form's `context` prop instead and read with the same syntax, under
|
|
75
|
+
`#/context/`:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"name": "room",
|
|
80
|
+
"type": "select",
|
|
81
|
+
"source": { "$ref": "#/context/rooms" }
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
const context = useMemo(() => ({ rooms }), [rooms])
|
|
87
|
+
|
|
88
|
+
<Form sections={sections} context={context} config={config} />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Both roots resolve the same way and follow the rules below. A path with no root
|
|
92
|
+
reads the definition, as it always has, and an entry of the definition can
|
|
93
|
+
point into `#/context/`.
|
|
94
|
+
|
|
95
|
+
One key of `context` is read by the form on its own: `now`, the instant
|
|
96
|
+
[relative dates](../interpolation/overview.md#the-instant-relative-dates-use)
|
|
97
|
+
are measured from and a time zone select labels its zones for. The form keeps
|
|
98
|
+
no clock, so without it there is no "now".
|
|
99
|
+
|
|
100
|
+
Keep `context` the same object while nothing in it changes, and hand over a new
|
|
101
|
+
one when something does. Every field renders again when `context` is a new
|
|
102
|
+
object, since a label or a description may interpolate it, and a field whose
|
|
103
|
+
JSON reads `#/context/` is resolved again as well. A value changed in place, in
|
|
104
|
+
the same object, is not something the form can notice.
|
|
105
|
+
|
|
70
106
|
## The rules
|
|
71
107
|
|
|
72
108
|
- **Resolution happens once, before anything else.** The tree is resolved first,
|
|
@@ -76,24 +112,27 @@ of others.
|
|
|
76
112
|
stays as the literal `{ "$ref": ... }` object. What that looks like on screen
|
|
77
113
|
depends on where it was — a field whose `source` is still a reference renders
|
|
78
114
|
with no options at all, which is the usual way a typo in the path is noticed.
|
|
79
|
-
|
|
80
|
-
|
|
115
|
+
A path that holds nothing is unresolved too: a key that is missing, and one
|
|
116
|
+
the host left `undefined` or `null`.
|
|
117
|
+
- **An empty or missing root resolves nothing under it.** `definition={{}}` is
|
|
118
|
+
treated as absent, and so is `context={{}}`: every `$ref` into a root that is
|
|
119
|
+
absent is left as written.
|
|
81
120
|
- **A cycle stops instead of hanging.** Two entries pointing at each other
|
|
82
121
|
resolve until the loop closes and leave the reference that would repeat
|
|
83
122
|
unresolved.
|
|
84
123
|
|
|
85
124
|
## Pass it everywhere the sections go
|
|
86
125
|
|
|
87
|
-
`definition`
|
|
88
|
-
|
|
126
|
+
`definition` and `context` are props of both the client and the server `Form`,
|
|
127
|
+
and the third and fourth arguments of the headless builder:
|
|
89
128
|
|
|
90
129
|
```ts
|
|
91
130
|
import { buildFormSchema } from 'react-luna-form/schema'
|
|
92
131
|
|
|
93
|
-
const schema = buildFormSchema(sections, translations, definition)
|
|
132
|
+
const schema = buildFormSchema(sections, translations, definition, context)
|
|
94
133
|
```
|
|
95
134
|
|
|
96
|
-
A schema built without
|
|
135
|
+
A schema built without them sees `$ref` objects where the form sees
|
|
97
136
|
fields — the rendered form and the validated shape drift apart, which is the one
|
|
98
137
|
thing [the schema being derived from the same descriptors](../validation/overview.md#headless-validation)
|
|
99
138
|
is meant to prevent.
|
|
@@ -17,7 +17,10 @@ This page documents every key of the `validation` object plus the shared operato
|
|
|
17
17
|
|
|
18
18
|
- **`required`** _(string)_: Message shown when a field marked `required: true` is empty.
|
|
19
19
|
- **`email`** _(string)_: Message for an invalid email format (`input/email`).
|
|
20
|
-
- **`
|
|
20
|
+
- **`date`** _(string)_: Message when an `input/date` holds text that is no day. See [Input](../fields/input.md#what-a-date-holds).
|
|
21
|
+
- **`length`** _({ min?: string, max?: string })_: Messages when the value breaches `advanced.length`: its length, a number's range, or a date's first and last day. See [Input](../fields/input.md#the-first-and-the-last-day).
|
|
22
|
+
- **`reserved`** _(string)_: Message when an `input/date` holds a day its `advanced.reserved` lists. See [Input](../fields/input.md#days-nobody-can-pick).
|
|
23
|
+
- **`range`** _(string)_: Message when an `input/date` range has an end missing, or its last day before its first. See [Input](../fields/input.md#a-range-of-days).
|
|
21
24
|
- **`step`** _(string)_: Message when an `input/number` is off its step: a decimal on a whole number, or a value off `advanced.step`. See [Input](../fields/input.md).
|
|
22
25
|
- **`custom`** _(CustomValidation | CustomValidation[])_: Cross-field comparison. See [Custom (cross-field)](#custom-cross-field).
|
|
23
26
|
- **`requiredWhen`** _(WhenRule | WhenRule[])_: Conditionally required. See [requiredWhen](#requiredwhen).
|
|
@@ -260,20 +263,20 @@ if (!result.success) {
|
|
|
260
263
|
}
|
|
261
264
|
```
|
|
262
265
|
|
|
263
|
-
- **`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`
|
|
266
|
+
- **`buildFormSchema(sections, translations?, definition?, context?)`** — 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` and `context` resolve [`$ref` entries](../structure/definition.md), and have to be the same ones the form renders with. A date comes out of it as `yyyy-MM-dd`, as it does from the form's submit.
|
|
264
267
|
- **`collectIssues(error)`** — flattens a `ZodError` into `{ path, message }` entries with **dotted paths** (e.g. `rules.0.value`), including nested list issues.
|
|
265
268
|
|
|
266
269
|
### What each path validates
|
|
267
270
|
|
|
268
|
-
| Rule
|
|
269
|
-
|
|
|
270
|
-
| `required`, `email`, `length`, `step` (field) | ✅ | ✅ |
|
|
271
|
-
| `custom`
|
|
272
|
-
| `requiredWhen` (top-level)
|
|
273
|
-
| `pattern`
|
|
274
|
-
| `rules` (top-level)
|
|
275
|
-
| `requiredWhen` **inside a list** (item scope)
|
|
276
|
-
| List `length`
|
|
271
|
+
| Rule | Rendered `<Form>` submit | `buildFormSchema` (headless) |
|
|
272
|
+
| -------------------------------------------------------------------------- | ------------------------ | ---------------------------- |
|
|
273
|
+
| `required`, `email`, `length`, `step`, `date`, `reserved`, `range` (field) | ✅ | ✅ |
|
|
274
|
+
| `custom` | ✅ | ✅ |
|
|
275
|
+
| `requiredWhen` (top-level) | ✅ | ✅ |
|
|
276
|
+
| `pattern` | ✅ | ✅ |
|
|
277
|
+
| `rules` (top-level) | ✅ | ✅ |
|
|
278
|
+
| `requiredWhen` **inside a list** (item scope) | — | ✅ |
|
|
279
|
+
| List `length` | — | ✅ |
|
|
277
280
|
|
|
278
281
|
The headless builder is the superset: item-scoped conditions and list length require the nested tree it produces.
|
|
279
282
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-luna-form",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.89",
|
|
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",
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"react-dom": "^19.0.0",
|
|
27
27
|
"swr": "^2.0.0",
|
|
28
28
|
"tailwind-merge": "^3.7.0",
|
|
29
|
-
"zod": "^4.0.
|
|
29
|
+
"zod": "^4.0.6"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@luna-form/core": "0.0.
|
|
32
|
+
"@luna-form/core": "0.0.89",
|
|
33
33
|
"@types/react": "19.3.0",
|
|
34
34
|
"@types/react-dom": "19.3.0",
|
|
35
35
|
"eslint-plugin-react": "7.37.5",
|