react-luna-form 0.0.84 → 0.0.86
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/type.d.ts +31 -1
- package/dist/types/luna-core/util/build.d.ts +3 -1
- package/dist/types/luna-core/util/extract.d.ts +1 -0
- package/dist/types/luna-core/util/is-input.d.ts +13 -0
- package/dist/types/luna-core/util/schema.d.ts +5 -5
- package/dist/types/luna-react/src/client/component/field/field.d.ts +19 -0
- package/dist/types/luna-react/src/client/context/failed-submit-context.d.ts +3 -0
- package/dist/types/luna-react/src/client/context/reveal-context.d.ts +1 -0
- package/dist/types/luna-react/src/client/hook/use-disclosure.d.ts +1 -0
- package/dist/types/luna-react/src/client/hook/use-form-action.d.ts +2 -1
- package/dist/types/luna-react/src/client/hook/use-host-entry.d.ts +17 -0
- package/dist/types/luna-react/src/client/hook/use-input-core.d.ts +4 -6
- package/dist/types/luna-react/src/client/hook/use-input.d.ts +1 -1
- package/dist/types/luna-react/src/client/hook/use-schema.d.ts +4 -2
- package/dist/types/luna-react/src/client/hook/use-value.d.ts +2 -2
- package/dist/types/luna-react/src/client/lib/compose-list-value.d.ts +29 -2
- package/dist/types/luna-react/src/client/lib/error-store.d.ts +3 -0
- package/dist/types/luna-react/src/client/lib/host-value-store.d.ts +30 -0
- package/dist/types/luna-react/src/client/lib/list-store.d.ts +21 -0
- package/dist/types/luna-react/src/client/lib/source-store.d.ts +3 -0
- package/dist/types/luna-react/src/client/lib/state-store.d.ts +3 -0
- package/dist/types/luna-react/src/client/lib/store-helper.d.ts +8 -0
- package/dist/types/luna-react/src/client/lib/value-store.d.ts +3 -0
- package/dist/types/luna-react/src/component/collapsible.d.ts +2 -1
- package/dist/types/luna-react/src/component/form.d.ts +2 -0
- package/dist/types/luna-react/src/component/input/input-base.d.ts +1 -0
- package/dist/types/luna-react/src/type.d.ts +1 -0
- package/docs/events/change.md +47 -1
- package/docs/fields/custom-inputs.md +4 -0
- package/docs/fields/input.md +9 -2
- package/docs/fields/list.md +2 -0
- package/docs/fields/specialized-selectors.md +4 -0
- package/docs/forms/submit.md +7 -2
- package/docs/structure/sections.md +3 -1
- package/docs/validation/overview.md +10 -1
- package/package.json +5 -5
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type Nullable } from '../../../../luna-core/index';
|
|
1
2
|
import type { MountedList } from './list-store';
|
|
2
|
-
|
|
3
|
+
/** A list's rows as a consumer is given them, in order. */
|
|
4
|
+
export type ListRows = Array<Record<string, unknown>>;
|
|
3
5
|
export declare function itemKey(listName: string, stableId: number, leafName: string): string;
|
|
4
6
|
/**
|
|
5
7
|
* The three places a leaf can be found, declared in the order they are tried.
|
|
@@ -38,4 +40,29 @@ export type ValueSources = {
|
|
|
38
40
|
* @param listName - The list's name, already carrying its ancestors' stable
|
|
39
41
|
* ids (`rules.0.rule`), which is what makes the keys unambiguous.
|
|
40
42
|
*/
|
|
41
|
-
export declare function composeListValue(listName: string, { items, leafNames }: MountedList, sources: ValueSources):
|
|
43
|
+
export declare function composeListValue(listName: string, { items, leafNames }: MountedList, sources: ValueSources): ListRows;
|
|
44
|
+
/**
|
|
45
|
+
* Every positional name a change to a list's rows can have left out of date,
|
|
46
|
+
* each with what it holds now.
|
|
47
|
+
*
|
|
48
|
+
* A consumer that keeps each report in one record -- the way the docs suggest
|
|
49
|
+
* keeping values -- holds a row's leaves there twice: inside the list's array,
|
|
50
|
+
* and under their own positional names, `items.0.value`, from the reports the
|
|
51
|
+
* leaves made as they were typed in. Removing a row moves every row after it
|
|
52
|
+
* up one position; assigning rows rewrites what every position holds. The
|
|
53
|
+
* array is reported again either way, but the names would go on holding what
|
|
54
|
+
* the rows used to hold, and `resolveEntry` tries a flat name before the path
|
|
55
|
+
* into the array: the next row to read one of them gets a row that is gone.
|
|
56
|
+
*
|
|
57
|
+
* So every name from `from` to the end of the longer of the two lists comes
|
|
58
|
+
* back, with what its position holds now, or `undefined` where no row is left
|
|
59
|
+
* -- whether or not that differs from before, because what the consumer holds
|
|
60
|
+
* is not necessarily what the list last said.
|
|
61
|
+
*
|
|
62
|
+
* @param base - The list's positional name, as the consumer addresses it.
|
|
63
|
+
* @param from - The first position the change can have touched.
|
|
64
|
+
*/
|
|
65
|
+
export declare function positionalEntries(base: string, from: number, previous: ListRows, next: ListRows): Array<{
|
|
66
|
+
name: string;
|
|
67
|
+
value: unknown;
|
|
68
|
+
}>;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export declare const clearInputErrorAtom: import("jotai").WritableAtom<null, [names: string[]], void> & {
|
|
2
2
|
init: null;
|
|
3
3
|
};
|
|
4
|
+
export declare const releaseInputErrorAtom: import("jotai").WritableAtom<null, [names: string[]], void> & {
|
|
5
|
+
init: null;
|
|
6
|
+
};
|
|
4
7
|
export declare const reportErrorAtom: import("jotai").WritableAtom<null, [newValue: Record<string, string[]>], void> & {
|
|
5
8
|
init: null;
|
|
6
9
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Nullable } from '../../../../luna-core/index';
|
|
2
|
+
export declare const hostValueAtom: import("jotai").PrimitiveAtom<Nullable<Record<string, unknown>>> & {
|
|
3
|
+
init: Nullable<Record<string, unknown>>;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* One field's entry in that record.
|
|
7
|
+
*
|
|
8
|
+
* Keyed by the name the record actually uses -- the POSITIONAL one. A leaf
|
|
9
|
+
* inside a list is named after its row's stable id, and the two diverge the
|
|
10
|
+
* moment a non-tail row is removed, so the caller translates before it
|
|
11
|
+
* subscribes. That translation lives in a React context, which an atom cannot
|
|
12
|
+
* read; doing it at the call site is what keeps this correct where a memo
|
|
13
|
+
* comparator could not be. See `ListPathContext`.
|
|
14
|
+
*
|
|
15
|
+
* Not released the way the per-name families in `store-helper` are, and it does
|
|
16
|
+
* not need to be for the same reason it is keyed this way: a position is reused
|
|
17
|
+
* where a stable id only ever climbs, so the keys here are bounded by the
|
|
18
|
+
* widest the form has ever been rather than by every row it has ever held.
|
|
19
|
+
* Measured going 1 -> 20 -> 14 -> 20 rows: the released families fall back to
|
|
20
|
+
* 23 and this one stays at 29, its high-water mark. Worth knowing before
|
|
21
|
+
* someone reads the absence of a release as an oversight.
|
|
22
|
+
*/
|
|
23
|
+
/** What the host says about one field, with the sentinel already read. */
|
|
24
|
+
export type HostEntry = {
|
|
25
|
+
/** The host names this field. A field it does not name reads `undefined`. */
|
|
26
|
+
found: boolean;
|
|
27
|
+
value: unknown;
|
|
28
|
+
};
|
|
29
|
+
export declare function readHostEntry(entry: unknown): HostEntry;
|
|
30
|
+
export declare const hostEntryAtom: import("jotai-family").AtomFamily<string, import("jotai").Atom<unknown>>;
|
|
@@ -12,6 +12,27 @@ export declare const reportPendingListRowsAtom: import("jotai-family").AtomFamil
|
|
|
12
12
|
export type MountedList = {
|
|
13
13
|
items: readonly number[];
|
|
14
14
|
leafNames: readonly string[];
|
|
15
|
+
/**
|
|
16
|
+
* The list's own `hidden`, as its definition declares it.
|
|
17
|
+
*
|
|
18
|
+
* Here rather than looked up, because there is nowhere to look it up: the
|
|
19
|
+
* schema registry `getField` answers from is filled by `onRegister` on the
|
|
20
|
+
* input path, and a container never goes through it. That gap is what let a
|
|
21
|
+
* list keep its values through a hide -- the branch that clears a target
|
|
22
|
+
* reverting to its static hidden asked only that registry, found nothing,
|
|
23
|
+
* and skipped the list, while an explicit `hidden: true` cleared it. Same
|
|
24
|
+
* declaration, two outcomes. See `tests/e2e/hidden-clear.spec.ts`.
|
|
25
|
+
*
|
|
26
|
+
* A list that is not on screen has no entry, and that is the right answer
|
|
27
|
+
* rather than a gap: the only target worth clearing is one that was showing.
|
|
28
|
+
*/
|
|
29
|
+
hidden?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The list's own `advanced.keepValue`, published for the same reason as
|
|
32
|
+
* `hidden` and flattened on the way in: this registry answers about a list
|
|
33
|
+
* that is on screen, not about the JSON that declared it.
|
|
34
|
+
*/
|
|
35
|
+
keepValue?: boolean;
|
|
15
36
|
};
|
|
16
37
|
export declare const mountedListsAtom: import("jotai").PrimitiveAtom<Record<string, MountedList>> & {
|
|
17
38
|
init: Record<string, MountedList>;
|
|
@@ -3,3 +3,6 @@ export declare const reportSourceAtom: import("jotai-family").AtomFamily<string,
|
|
|
3
3
|
export declare const clearInputSourceAtom: import("jotai").WritableAtom<null, [contributorNames: string[]], void> & {
|
|
4
4
|
init: null;
|
|
5
5
|
};
|
|
6
|
+
export declare const releaseInputSourceAtom: import("jotai").WritableAtom<null, [names: string[]], void> & {
|
|
7
|
+
init: null;
|
|
8
|
+
};
|
|
@@ -2,4 +2,7 @@ import type { FieldState } from '../../../../luna-core/index';
|
|
|
2
2
|
export declare const fieldStateAtom: import("jotai").PrimitiveAtom<Record<string, FieldState>> & {
|
|
3
3
|
init: Record<string, FieldState>;
|
|
4
4
|
};
|
|
5
|
+
export declare const releaseFieldStateAtom: import("jotai").WritableAtom<null, [names: string[]], void> & {
|
|
6
|
+
init: null;
|
|
7
|
+
};
|
|
5
8
|
export declare const reportFieldStateAtom: import("jotai-family").AtomFamily<string, import("jotai").WritableAtom<FieldState | undefined, [newValue: FieldState | undefined], void>>;
|
|
@@ -5,6 +5,11 @@ type NestedAtomFamilyOptions<TInner> = {
|
|
|
5
5
|
};
|
|
6
6
|
export declare function omitKey<T extends Record<string, unknown>>(obj: T, key: string): T;
|
|
7
7
|
export declare function createNestedRecordAtomFamily<TInner, TRecord extends Record<string, Record<string, TInner>> = Record<string, Record<string, TInner>>>(baseAtom: PrimitiveAtom<TRecord>, options?: NestedAtomFamilyOptions<TInner>): import("jotai-family").AtomFamily<string, import("jotai").WritableAtom<TInner | undefined, [target: string, value: TInner | undefined], void>>;
|
|
8
|
+
export declare function createNestedReleaseAtom(family: {
|
|
9
|
+
remove: (name: string) => void;
|
|
10
|
+
}): import("jotai").WritableAtom<null, [names: string[]], void> & {
|
|
11
|
+
init: null;
|
|
12
|
+
};
|
|
8
13
|
export declare function createNestedClearAtom<TInner>(baseAtom: PrimitiveAtom<Record<string, Record<string, TInner>>>): import("jotai").WritableAtom<null, [contributorNames: string[]], void> & {
|
|
9
14
|
init: null;
|
|
10
15
|
};
|
|
@@ -21,6 +26,9 @@ export declare function createAtomStore<T>(initialValue?: Record<string, T>): {
|
|
|
21
26
|
bulkReport: import("jotai").WritableAtom<null, [newValue: Record<string, T>], void> & {
|
|
22
27
|
init: null;
|
|
23
28
|
};
|
|
29
|
+
release: import("jotai").WritableAtom<null, [names: string[]], void> & {
|
|
30
|
+
init: null;
|
|
31
|
+
};
|
|
24
32
|
report: import("jotai-family").AtomFamily<string, import("jotai").WritableAtom<NonNullable<T> | undefined, [newValue: T | undefined], void>>;
|
|
25
33
|
};
|
|
26
34
|
export {};
|
|
@@ -7,6 +7,9 @@ export declare const clearAllValueAtom: import("jotai").WritableAtom<null, [], v
|
|
|
7
7
|
export declare const clearInputValueAtom: import("jotai").WritableAtom<null, [names: string[]], void> & {
|
|
8
8
|
init: null;
|
|
9
9
|
};
|
|
10
|
+
export declare const releaseInputValueAtom: import("jotai").WritableAtom<null, [names: string[]], void> & {
|
|
11
|
+
init: null;
|
|
12
|
+
};
|
|
10
13
|
export declare const reportValueAtom: import("jotai-family").AtomFamily<string, import("jotai").WritableAtom<{} | undefined, [newValue: unknown], void>>;
|
|
11
14
|
export declare const appliedAutoFillAtom: import("jotai").PrimitiveAtom<Record<string, unknown>> & {
|
|
12
15
|
init: Record<string, unknown>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Definition, type Sections } from '../../../luna-core/index';
|
|
2
2
|
import type { Config, Control, Slot } from '../type';
|
|
3
|
+
import type { FormEvent } from 'react';
|
|
3
4
|
export declare function Form(props: Readonly<{
|
|
4
5
|
advanced?: {
|
|
5
6
|
step?: boolean;
|
|
@@ -11,6 +12,7 @@ export declare function Form(props: Readonly<{
|
|
|
11
12
|
definition?: Definition;
|
|
12
13
|
isPending?: boolean;
|
|
13
14
|
noValidate?: boolean;
|
|
15
|
+
onSubmit?: (event: FormEvent<HTMLFormElement>) => void;
|
|
14
16
|
readOnly?: boolean;
|
|
15
17
|
sections: Sections;
|
|
16
18
|
translations?: Record<string, string>;
|
package/docs/events/change.md
CHANGED
|
@@ -159,7 +159,7 @@ A row's key can name a field that is itself a `list`, in which case its value is
|
|
|
159
159
|
Every rule above holds one level down as well: an inner list assigns rather than adds, honours its own `advanced.length`, and keeps the identity of the rows that survive. Two more only come up here:
|
|
160
160
|
|
|
161
161
|
- **A row that names an inner list without giving it rows empties it.** `"checks": []`, a `checks` key holding something that is not rows, and no `checks` key at all all mean the same thing — none — rather than leaving behind what the row in that position had.
|
|
162
|
-
- **The consumer is told
|
|
162
|
+
- **The consumer is told the whole tree at once**, under the outer list's name, with the inner rows already in it, and then every positional name the assignment rewrote (`groups.0.label`, `groups.1.checks.0.v`) with what that position holds now, or nothing where no row is left. A consumer that keeps each report in one record would otherwise go on holding the values of the rows that went away under those names. The inner lists then report their own paths (`groups.0.checks`) as they take their rows, the same way they do when a row is added or removed by hand.
|
|
163
163
|
|
|
164
164
|
This is what the relative syntax cannot do. `"groups/checks"` reaches the inner list of the row the sender is sitting in, and `"groups.0.checks"` reaches the first one; neither can say "every group", so a control that appends a group has to assign the outer list and carry the inner rows along with it.
|
|
165
165
|
|
|
@@ -232,6 +232,52 @@ A target that holds nothing is not announced — there is nothing to clear —
|
|
|
232
232
|
and a target that keeps its own values under compound names (a `group`, a
|
|
233
233
|
`list`) has each of those cleared and reported in its own right.
|
|
234
234
|
|
|
235
|
+
It does not matter how the form phrases the hiding. A rule whose state is
|
|
236
|
+
`hidden: true`, and a rule that reveals with `hidden: false` and stops matching
|
|
237
|
+
so the field falls back to its own `hidden`, are the same act and clear the
|
|
238
|
+
same values -- containers included.
|
|
239
|
+
|
|
240
|
+
What the cleared target decided goes with it. A target with `state` rules of its
|
|
241
|
+
own asserted them on the strength of its value, and that value is gone, so its
|
|
242
|
+
rules are undone as if it held nothing: a field it revealed is hidden again --
|
|
243
|
+
and cleared, if it too falls back to `hidden` -- and so on down the chain.
|
|
244
|
+
Showing the first target again brings it back empty, and nothing it had revealed
|
|
245
|
+
comes back with it. A target that keeps its value through the hide keeps what
|
|
246
|
+
its rules decided as well; see below.
|
|
247
|
+
|
|
248
|
+
#### Keeping a value through a hide
|
|
249
|
+
|
|
250
|
+
A field that declares `advanced.keepValue: true` is put away rather than
|
|
251
|
+
dropped: hiding it leaves what it held, showing it again gives it back, and the
|
|
252
|
+
consumer is told nothing because nothing was taken.
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"label": "Raw text",
|
|
257
|
+
"name": "raw",
|
|
258
|
+
"type": "textarea",
|
|
259
|
+
"hidden": true,
|
|
260
|
+
"advanced": { "keepValue": true }
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
It sits in `advanced` beside [`transient`](#transient-controls), which is the
|
|
265
|
+
other key that decides what ends up stored, rather than beside `hidden`: the
|
|
266
|
+
two read as a pair, but `hidden` is what the form does to the field and this is
|
|
267
|
+
what the field is.
|
|
268
|
+
|
|
269
|
+
It is declared on the FIELD and not on the rule, for the same reason the clear
|
|
270
|
+
does not care how the hiding was phrased: a field either survives being put
|
|
271
|
+
away or it does not, and which of several rules happened to hide it this time
|
|
272
|
+
says nothing about that. A
|
|
273
|
+
[`list`](../fields/list.md#advanced-configuration-advanced-object) may declare
|
|
274
|
+
it too, and then it covers every value its rows hold.
|
|
275
|
+
|
|
276
|
+
Reach for it where two fields are two views of the same thing and the form
|
|
277
|
+
swaps between them -- there, hiding one is a change of scenery, not a change of
|
|
278
|
+
mind. Leave it off everywhere else: a value the user cannot see and cannot
|
|
279
|
+
correct is one that still gets submitted.
|
|
280
|
+
|
|
235
281
|
#### What `when` compares against for a `chips` field
|
|
236
282
|
|
|
237
283
|
A `chips` field [holds an array](../fields/select.md#a-chips-value-is-always-an-array),
|
|
@@ -60,6 +60,10 @@ its own handlers must call the ones it received, or the form never learns the
|
|
|
60
60
|
value changed: validation will not run, and neither will any `change` event the
|
|
61
61
|
field declares.
|
|
62
62
|
|
|
63
|
+
A `readonly` field arrives `disabled` and without a `name`. Its value is still
|
|
64
|
+
submitted, but by the form, in hidden inputs of its own: nothing your component
|
|
65
|
+
renders for it is sent, so it is sent once, whatever your component renders.
|
|
66
|
+
|
|
63
67
|
## Forwarding `id` is what gives the field its name
|
|
64
68
|
|
|
65
69
|
The label is rendered by this library, outside your component, as
|
package/docs/fields/input.md
CHANGED
|
@@ -28,8 +28,8 @@ Luna Form's input fields inherit the standard logical properties available to al
|
|
|
28
28
|
- Or an object payload: `{ title: string, message: string, collapsed?: boolean }`.
|
|
29
29
|
- **`placeholder`** _(string, optional)_: The temporary placeholder text shown when the input is empty.
|
|
30
30
|
- **`required`** _(boolean, optional)_: If `true`, standard browser/HTML5 validation makes the field mandatory.
|
|
31
|
-
- **`disabled`** _(boolean, optional)_: If `true`, the field is non-interactive and blocked from events.
|
|
32
|
-
- **`readonly`** _(boolean, optional)_: If `true`, the field's value is locked and cannot be modified by the user.
|
|
31
|
+
- **`disabled`** _(boolean, optional)_: If `true`, the field is non-interactive and blocked from events, and its value is not submitted.
|
|
32
|
+
- **`readonly`** _(boolean, optional)_: If `true`, the field's value is locked and cannot be modified by the user, but it is still submitted. The control renders disabled, the one lock every input component understands, and the form sends the value itself. A field that is both `readonly` and `disabled` is not submitted.
|
|
33
33
|
- **`hidden`** _(boolean, optional)_: If `true`, the field is hidden from the user interface.
|
|
34
34
|
- **`order`** _(number, optional)_: A numeric order determining the field's position relative to adjacent fields.
|
|
35
35
|
|
|
@@ -47,6 +47,7 @@ The `advanced` property dictates finer HTML details, interactive structures, and
|
|
|
47
47
|
- **`reverse`** _(boolean)_: `checkbox` only, and it exists to turn a default **off**. A checkbox renders its box first and its label after it; `reverse: false` swaps them, `true` changes nothing, and on any other field type the key is not read. See [Layout](../structure/layout.md#advancedreverse-checkbox-only).
|
|
48
48
|
- **`aria`** _(object)_: Map of specific ARIA attributes for screen readers (e.g., `{ "aria-label": "Custom label" }`).
|
|
49
49
|
- **`data`** _(object)_: Map of specific HTML data attributes (e.g., `{ "data-test-id": "my-field" }`).
|
|
50
|
+
- **`keepValue`** _(boolean)_: If `true`, hiding the field leaves its value behind instead of taking it — the field is put away rather than dropped. See [Keeping a value through a hide](../events/change.md#keeping-a-value-through-a-hide).
|
|
50
51
|
- **`transient`** _(boolean)_: If `true`, the field fires its change events and keeps nothing of its own — it is neither stored nor reported to the consumer. For a control whose whole job is to write to _another_ field. See [Transient controls](../events/change.md#transient-controls).
|
|
51
52
|
|
|
52
53
|
### Text & Numeric Options (`input/text`, `input/email`, `input/tel`, `input/password`, `input/number`)
|
|
@@ -54,12 +55,17 @@ The `advanced` property dictates finer HTML details, interactive structures, and
|
|
|
54
55
|
These basic field types support extra manipulation properties inside the `advanced` block:
|
|
55
56
|
|
|
56
57
|
- **`length`** _({ min?: number, max?: number })_: Applies HTML structural limits (`minlength` / `maxlength`, or `min` / `max` depending on the input type).
|
|
58
|
+
- **`step`** _(number)_ (`input/number` only): A number is whole unless it declares a step, which is the browser's own default of 1. The step is rendered on the input, so its arrows move by it, and validation accepts only values on it, counted from `length.min` when there is one, as the browser does: `0.01` for a price, `0.5` for halves, `0.001` for three decimals. A step has to be a number above 0. The browser ignores 0 and below, and so does the form; text such as `"any"` is no step either, and it is never rendered. Either way the number stays whole.
|
|
57
59
|
- **`transform`** _(string | string[])_: Safely intercepts user inputs and manipulates content dynamically. Options include:
|
|
58
60
|
- `"lowercase"`
|
|
59
61
|
- `"uppercase"`
|
|
60
62
|
- `"remove-space"`
|
|
61
63
|
- `"remove-accent"`
|
|
62
64
|
|
|
65
|
+
### Empty and required numbers
|
|
66
|
+
|
|
67
|
+
A required `input/number` accepts `0` and negative numbers: required means a value is present, not that it is at least 1. An optional one left empty is not submitted at all, rather than submitted as `0`, and its `length` bounds only apply to a value that is there. `select/year` and `select/month` read an empty selection the same way.
|
|
68
|
+
|
|
63
69
|
### Temporal Options (`input/date`, `input/time`)
|
|
64
70
|
|
|
65
71
|
Date and time inputs omit transformations and instead expose a `format` property directly under `advanced` for displaying values properly:
|
|
@@ -76,6 +82,7 @@ The `validation` object resolves form errors overriding generic defaults, mappin
|
|
|
76
82
|
- **`required`** _(string)_: Specifies the error message exposed when the element is marked exactly as `required: true` and the field is empty.
|
|
77
83
|
- **`email`** _(string)_: Error message specifically asserting an invalid email format.
|
|
78
84
|
- **`length`** _({ min?: string, max?: string })_: Specific string messages shown when input lengths are breached.
|
|
85
|
+
- **`step`** _(string)_: The message shown when an `input/number` is off its step: a decimal on a number that declares no step, or a value off the `advanced.step` it declares. The form's dictionary translates it, as it does every other message.
|
|
79
86
|
- **`custom`** _(CustomValidation | CustomValidation[])_: Powerful conditional-based logic blocks. An array specifying:
|
|
80
87
|
- `field`: Optional target reference string.
|
|
81
88
|
- `operator`: Logical evaluation operations (e.g., `eq`, `neq`, `gt`, `lt`).
|
package/docs/fields/list.md
CHANGED
|
@@ -26,6 +26,8 @@ The `advanced` property customizes structural interaction rules and layout rende
|
|
|
26
26
|
- **`length`** _({ min?: number, max?: number })_: Configures restrictions on how many items can be generated. Enforces boundary conditions where minimum instances ensure permanent default items and maximum instances automatically disable insertion toggles. The matching error messages are declared under the list's `validation.length` — see the [Validation reference](../validation/overview.md#list-length).
|
|
27
27
|
Both bounds apply to the rows a list opens with, not only to the ones added by hand: an initial `value` shorter than `min` leaves the remainder standing and empty, and one longer than `max` stops at `max`. A [`value` event](../events/change.md#targeting-a-list) assigning rows is clamped the same way.
|
|
28
28
|
- **`collapsed`** _(boolean)_: When enabled, list items default to a visually collapsed representation upon mounting or adding.
|
|
29
|
+
Collapsing only hides a row: its fields keep their values, still validate and are still submitted, and a submit that fails on one of them opens the row so the error can be seen.
|
|
30
|
+
- **`keepValue`** _(boolean)_: If `true`, hiding the list leaves the values behind instead of taking them. A list holds nothing under its own name, so declaring it here is a statement about the rows: it covers every value they hold. See [Keeping a value through a hide](../events/change.md#keeping-a-value-through-a-hide).
|
|
29
31
|
|
|
30
32
|
### Collapsed Previews (`preview` object)
|
|
31
33
|
|
|
@@ -17,6 +17,8 @@ Renders a dropdown with the 12 months of the year.
|
|
|
17
17
|
}
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
An optional month 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.
|
|
21
|
+
|
|
20
22
|
### 2. Year Selector (`select/year`)
|
|
21
23
|
|
|
22
24
|
Renders a dropdown with a range of years.
|
|
@@ -38,6 +40,8 @@ Renders a dropdown with a range of years.
|
|
|
38
40
|
}
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
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
|
+
|
|
41
45
|
### 3. Day Selector (`select/day`)
|
|
42
46
|
|
|
43
47
|
Renders a dropdown with days 1 through 31.
|
package/docs/forms/submit.md
CHANGED
|
@@ -103,8 +103,13 @@ If the values have to survive, hold them outside the form: mirror
|
|
|
103
103
|
people, which is that a key you stop sending is not the same as a key you send
|
|
104
104
|
empty.
|
|
105
105
|
|
|
106
|
-
A **failed**
|
|
107
|
-
|
|
106
|
+
A **failed** submit is the opposite: nothing is cleared or reset, so what the
|
|
107
|
+
user entered stays on screen next to the errors — typed text, ticked boxes and
|
|
108
|
+
picked options alike. An error inside a collapsed
|
|
109
|
+
[section](../structure/sections.md#advancedcollapsible) or list row is brought
|
|
110
|
+
on screen too: the section or row holding it opens. The form does not take over
|
|
111
|
+
`value` either. What you pass is still what the fields read, so you can change
|
|
112
|
+
it, or put it back, while the errors are showing.
|
|
108
113
|
|
|
109
114
|
## Turning submit validation off
|
|
110
115
|
|
|
@@ -95,7 +95,7 @@ Renders the section closed, with its title as the toggle:
|
|
|
95
95
|
}
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
Four things to know before using it:
|
|
99
99
|
|
|
100
100
|
- **It always starts closed.** There is no key that opens it, and reopening the
|
|
101
101
|
form does not remember that the user had expanded it.
|
|
@@ -103,6 +103,8 @@ Three things to know before using it:
|
|
|
103
103
|
them. They keep their values, they still validate, and they are still
|
|
104
104
|
submitted. That is the opposite of what a [`state` action](../events/change.md#what-clearing-a-hidden-target-means)
|
|
105
105
|
does when it hides a field, which unmounts it and clears the value.
|
|
106
|
+
- **A failed submit opens it.** When a field inside fails validation, the
|
|
107
|
+
section opens so the error can be seen, and it stays open.
|
|
106
108
|
- **The description moves inside.** It renders with the fields rather than under
|
|
107
109
|
the title, so it is not visible while the section is closed.
|
|
108
110
|
|
|
@@ -18,6 +18,7 @@ This page documents every key of the `validation` object plus the shared operato
|
|
|
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
|
- **`length`** _({ min?: string, max?: string })_: Messages when the value's length breaches `advanced.length`.
|
|
21
|
+
- **`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).
|
|
21
22
|
- **`custom`** _(CustomValidation | CustomValidation[])_: Cross-field comparison. See [Custom (cross-field)](#custom-cross-field).
|
|
22
23
|
- **`requiredWhen`** _(WhenRule | WhenRule[])_: Conditionally required. See [requiredWhen](#requiredwhen).
|
|
23
24
|
- **`pattern`** _(PatternRule)_: Regex/format check. See [pattern](#pattern).
|
|
@@ -110,6 +111,14 @@ Validates a string value against a regular expression.
|
|
|
110
111
|
|
|
111
112
|
`pattern` never fires on an empty value — pair it with `required`/`requiredWhen` when the field is mandatory.
|
|
112
113
|
|
|
114
|
+
A `regex` or `flags` that does not compile is a mistake in the definition, not in the value. The rule then holds back every value it checks with its `message`, as it does a value that does not match, and the form keeps working. The same goes for a `pattern` among [`rules`](#rules).
|
|
115
|
+
|
|
116
|
+
A `regex` that compiles runs as written, on what the user types: in the browser when the form is submitted, and on the server when it validates with `buildFormSchema`. Nothing inspects it first, so it is part of the code the form ships with. Accept one only from someone you would let write that code.
|
|
117
|
+
|
|
118
|
+
The shape to avoid is a pattern that can split the same text between its repetitions in more than one way, such as `^(a+)+$`, `^(\w+\s?)+$` or `^(a|a)+$`. On a value that almost matches, it backtracks exponentially, and about thirty characters can hold the page, or the server, for seconds. A repetition that opens with a separator the one before cannot close with is not ambiguous and runs in linear time, as in `^[a-z]+(-[a-z]+)*$` or `^\d+(,\d+)*$`.
|
|
119
|
+
|
|
120
|
+
If definitions reach you from other people, check their patterns with a ReDoS analyzer before you accept them.
|
|
121
|
+
|
|
113
122
|
---
|
|
114
123
|
|
|
115
124
|
## `rules`
|
|
@@ -258,7 +267,7 @@ if (!result.success) {
|
|
|
258
267
|
|
|
259
268
|
| Rule | Rendered `<Form>` submit | `buildFormSchema` (headless) |
|
|
260
269
|
| --------------------------------------------- | ------------------------ | ---------------------------- |
|
|
261
|
-
| `required`, `email`, `length` (field)
|
|
270
|
+
| `required`, `email`, `length`, `step` (field) | ✅ | ✅ |
|
|
262
271
|
| `custom` | ✅ | ✅ |
|
|
263
272
|
| `requiredWhen` (top-level) | ✅ | ✅ |
|
|
264
273
|
| `pattern` | ✅ | ✅ |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-luna-form",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.86",
|
|
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",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"zod": "^4.0.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@
|
|
34
|
-
"@types/react
|
|
33
|
+
"@luna-form/core": "0.0.86",
|
|
34
|
+
"@types/react": "19.3.0",
|
|
35
|
+
"@types/react-dom": "19.3.0",
|
|
35
36
|
"eslint-plugin-react": "7.37.5",
|
|
36
37
|
"eslint-plugin-react-hooks": "7.1.1",
|
|
37
|
-
"eslint-plugin-react-refresh": "0.5.
|
|
38
|
-
"@luna-form/core": "0.0.84"
|
|
38
|
+
"eslint-plugin-react-refresh": "0.5.7"
|
|
39
39
|
},
|
|
40
40
|
"license": "Apache-2.0",
|
|
41
41
|
"repository": {
|