react-luna-form 0.0.81 → 0.0.82
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 +6 -10
- 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/schema.d.ts +0 -4
- package/docs/events/change.md +391 -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,391 @@
|
|
|
1
|
+
# Change Event Documentation
|
|
2
|
+
|
|
3
|
+
The `change` event in Luna Form allows you to trigger a sequence of actions whenever the value of a specific field is modified. A `change` event is configured as an array of objects, where each object defines a distinct action.
|
|
4
|
+
|
|
5
|
+
Luna Form evaluates the array sequentially, applying the declared updates or conditions.
|
|
6
|
+
|
|
7
|
+
## Value Interpolation
|
|
8
|
+
|
|
9
|
+
In many actions (like `value`, `source`, and `state`), you may want to use the value of the field that triggered the change. You can use the special string `{value}` to capture and inject this value dynamically into strings or objects.
|
|
10
|
+
|
|
11
|
+
Placeholders also support format filters with pipe syntax (`{value | filter:arg}`) for locale-aware formatting of currency, dates, percentages, and durations. See [Format Filters](../interpolation/format-filters.md) for the full reference.
|
|
12
|
+
|
|
13
|
+
### Passing a structure rather than text
|
|
14
|
+
|
|
15
|
+
A placeholder is normally replaced _inside_ a string, so what comes out is text. When the template is **nothing but one placeholder** and the value behind it is an object or an array, the `value` action hands that value over untouched instead:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"action": "value",
|
|
20
|
+
"value": { "rows": "{preset_rows}" }
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If the triggering field's data carries `preset_rows` as an array, `rows` receives the array itself — not `"[object Object]"`, and not the literal `"{preset_rows}"`.
|
|
25
|
+
|
|
26
|
+
Three cases do **not** qualify and stay text, exactly as before:
|
|
27
|
+
|
|
28
|
+
- text around the placeholder — `"rows: {preset_rows}"`;
|
|
29
|
+
- a format filter — `"{preset_rows | currency:USD}"`, which formats into text by definition;
|
|
30
|
+
- a scalar behind the placeholder — `{value}` over the number `25` is still `"25"`.
|
|
31
|
+
|
|
32
|
+
This is what makes an array of rows expressible in a definition at all, and it is how a `list` gets filled. See [Targeting a list](#targeting-a-list).
|
|
33
|
+
|
|
34
|
+
## Target Resolution in Lists
|
|
35
|
+
|
|
36
|
+
When working with fields inside a `list`, targets are resolved globally (from the form root) by default. To target another field within the **same list item**, use the following syntax:
|
|
37
|
+
|
|
38
|
+
`"listName/fieldName"`
|
|
39
|
+
|
|
40
|
+
This tells Luna Form to find the current iteration of `listName` and update the `fieldName` within that specific row.
|
|
41
|
+
|
|
42
|
+
**Example:**
|
|
43
|
+
If you have a list named `users` and you want one field to update another field called `nickname` in same row:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"name": "fullname",
|
|
48
|
+
"event": {
|
|
49
|
+
"change": [
|
|
50
|
+
{
|
|
51
|
+
"action": "value",
|
|
52
|
+
"value": {
|
|
53
|
+
"users/nickname": "{value}"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If you specify `"nickname"` (without the list prefix and slash), it will attempt to update a field named `nickname` at the root of the form.
|
|
62
|
+
|
|
63
|
+
## Supported Actions
|
|
64
|
+
|
|
65
|
+
The `change` event array supports three action types, indicated by the `action` property: `value`, `state`, and `source`.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### 1. Value Action (`ValueEvent`)
|
|
70
|
+
|
|
71
|
+
The `value` action is used to explicitly set or update the value of one or more fields elsewhere in the form when the current field changes.
|
|
72
|
+
|
|
73
|
+
**Properties:**
|
|
74
|
+
|
|
75
|
+
- `action` (string): Must be set to `"value"`.
|
|
76
|
+
- `value` (object): A key-value mapping where each key represents a target field name, and the value is the new data to apply to that field. You can use string interpolation like `{value}` to pass the changed value.
|
|
77
|
+
- `onlyIfTargetEmpty` (boolean, optional): If `true`, the target field's value will only be updated if it is currently empty. Useful for applying default or propagated values without overwriting user input. Ignored when the target is a `list` — a list has no single value to call empty.
|
|
78
|
+
|
|
79
|
+
**Example:**
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"event": {
|
|
84
|
+
"change": [
|
|
85
|
+
{
|
|
86
|
+
"action": "value",
|
|
87
|
+
"onlyIfTargetEmpty": true,
|
|
88
|
+
"value": {
|
|
89
|
+
"billingAddress": "{value}",
|
|
90
|
+
"hasProvidedAddress": true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### Targeting a list
|
|
99
|
+
|
|
100
|
+
A `list` is a valid target. Give it an array of objects and it becomes exactly those rows — one object per row, each key matching a field name inside the list:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"label": "Preset",
|
|
105
|
+
"name": "preset",
|
|
106
|
+
"type": "select",
|
|
107
|
+
"advanced": { "transient": true },
|
|
108
|
+
"source": [
|
|
109
|
+
{ "label": "Empty", "value": "empty", "preset_rows": [] },
|
|
110
|
+
{
|
|
111
|
+
"label": "Two rows",
|
|
112
|
+
"value": "two",
|
|
113
|
+
"preset_rows": [
|
|
114
|
+
{ "key": "alpha", "amount": "1" },
|
|
115
|
+
{ "key": "beta", "amount": "2" }
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
"event": {
|
|
120
|
+
"change": [{ "action": "value", "value": { "rows": "{preset_rows}" } }]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Points worth knowing:
|
|
126
|
+
|
|
127
|
+
- **It assigns, it does not add.** The list ends up with the rows you gave it and nothing else; the values of the rows that went away are discarded. To append, send the current array plus the new element — the sender is the one that knows what is already there.
|
|
128
|
+
- **`advanced.length` still applies.** A shorter array leaves the remainder standing and empty up to `min` and anything past `max` is dropped — the same clamp an initial `value` gets at mount, so a list holds the same rows however they arrived.
|
|
129
|
+
- **Rows that survive keep their identity.** They are matched by position, so growing a list does not remount the rows already on screen, and a caret sitting in one of them stays put.
|
|
130
|
+
- **A row that only carries some of the keys leaves the rest empty**, rather than keeping what the previous row in that position had.
|
|
131
|
+
- **Assigning what is already there does nothing.** No rebuild, and no report to the consumer. Deduplication is still the sender's job, though: this only recognises an assignment that matches the list as a whole, and by the time it does the event has already been raised.
|
|
132
|
+
|
|
133
|
+
A **nested** list works the same way, reached with the relative syntax described in [Target Resolution in Lists](#target-resolution-in-lists). A field inside a row of `group` fills the `items` list in its own row with `"group/items"`; the plain name would not reach it, because once rendered a nested list is called by its path (`group.0.items`).
|
|
134
|
+
|
|
135
|
+
#### Rows that hold rows
|
|
136
|
+
|
|
137
|
+
A row's key can name a field that is itself a `list`, in which case its value is that list's rows rather than a value. The whole tree is assigned in one event, as deep as the lists go:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"action": "value",
|
|
142
|
+
"value": {
|
|
143
|
+
"groups": [
|
|
144
|
+
{
|
|
145
|
+
"name": "First",
|
|
146
|
+
"checks": [
|
|
147
|
+
{ "key": "alpha", "amount": "1" },
|
|
148
|
+
{ "key": "beta", "amount": "2" }
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
{ "name": "Second", "checks": [{ "key": "gamma", "amount": "3" }] }
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
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:
|
|
158
|
+
|
|
159
|
+
- **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.
|
|
160
|
+
- **The consumer is told once for the whole tree**, under the outer list's name, with the inner rows already in it. 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.
|
|
161
|
+
|
|
162
|
+
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.
|
|
163
|
+
|
|
164
|
+
The picker above is marked [`transient`](#transient-controls) because its job is to fill the list, not to hold a value of its own.
|
|
165
|
+
|
|
166
|
+
#### Transient controls
|
|
167
|
+
|
|
168
|
+
A field whose entire job is to write somewhere else can declare `"advanced": { "transient": true }`. It fires its change events and keeps nothing: it is not stored, and the consumer is never told about it.
|
|
169
|
+
|
|
170
|
+
Without it, the control also saves whatever it emitted, and a value the user can neither see nor edit ends up in the submitted form and in whatever the consumer persists.
|
|
171
|
+
|
|
172
|
+
Two consequences to expect:
|
|
173
|
+
|
|
174
|
+
- **It shows nothing back**, because there is no value to show. On a visible text input, typing will not stick.
|
|
175
|
+
- **Choosing the same value twice in a row acts twice**, where an ordinary field treats the repeat as a no-op. For a command that is the point — pressing a button again is meant to do the thing again.
|
|
176
|
+
|
|
177
|
+
It does not go with `required` or `validation`, which would demand a value that by definition never arrives and block the submit with nothing on screen to explain why.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### 2. State Action (`StateEvent`)
|
|
182
|
+
|
|
183
|
+
The `state` action modifies the interactive state or visibility of other fields based on the changed value.
|
|
184
|
+
|
|
185
|
+
**Properties:**
|
|
186
|
+
|
|
187
|
+
- `action` (string): Must be set to `"state"`.
|
|
188
|
+
- `target` (string | array of strings): The field name, or an array of field names, that the state modifications should be applied to.
|
|
189
|
+
- `state` (object): An object containing the new state to apply. Properties are independent of field names — the form may safely contain a field whose `name` is `"description"`, `"disabled"` or `"hidden"`; `target` identifies the field, the keys inside `state` configure it.
|
|
190
|
+
- `disabled` (boolean, optional): Whether the field should be disabled.
|
|
191
|
+
- `hidden` (boolean, optional): Whether the field should be hidden from view. **Note:** When a field becomes hidden (`hidden: true`), its current value is automatically cleared from the form state to ensure no non-applicable data is submitted.
|
|
192
|
+
- `description` (string | object, optional): A new description to render for the target field. Supports interpolation, format filters, and **Markdown** (specifically links). e.g. `"Check our [terms](https://example.com) for details."`.
|
|
193
|
+
- `when` (string | array of strings | [Condition object](#the-condition-object), optional): A condition that specifies when this state should be applied.
|
|
194
|
+
- If a **string** is provided, it matches exactly against the field's new value.
|
|
195
|
+
- If an **array of strings** is provided, it performs an **OR match** (the state is applied if the field's new value matches _any_ string in the array).
|
|
196
|
+
- If a **Condition object** is provided, it evaluates complex logic.
|
|
197
|
+
|
|
198
|
+
**Example:**
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"event": {
|
|
203
|
+
"change": [
|
|
204
|
+
{
|
|
205
|
+
"action": "state",
|
|
206
|
+
"target": ["creditCardNumber", "cvvCode"],
|
|
207
|
+
"state": {
|
|
208
|
+
"disabled": false,
|
|
209
|
+
"hidden": false
|
|
210
|
+
},
|
|
211
|
+
"when": "credit_card"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"action": "state",
|
|
215
|
+
"target": "paypalEmail",
|
|
216
|
+
"state": {
|
|
217
|
+
"hidden": false
|
|
218
|
+
},
|
|
219
|
+
"when": {
|
|
220
|
+
"operator": "eq",
|
|
221
|
+
"value": "paypal"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Updating description with interpolation and filters:**
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"label": "Price",
|
|
234
|
+
"name": "price",
|
|
235
|
+
"type": "input/text",
|
|
236
|
+
"event": {
|
|
237
|
+
"change": [
|
|
238
|
+
{
|
|
239
|
+
"action": "state",
|
|
240
|
+
"target": "price",
|
|
241
|
+
"state": {
|
|
242
|
+
"description": "Total to pay: {value | currency:USD}"
|
|
243
|
+
},
|
|
244
|
+
"when": {
|
|
245
|
+
"operator": "neq",
|
|
246
|
+
"value": ""
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### 3. Source Action (`SourceEvent`)
|
|
257
|
+
|
|
258
|
+
The `source` action dispatches a request to a remote data source to fetch and populate data into a target field. This is commonly used for cascading selections (e.g., selecting a Country fetches its related States).
|
|
259
|
+
|
|
260
|
+
**Properties:**
|
|
261
|
+
|
|
262
|
+
- `action` (string): Must be set to `"source"`.
|
|
263
|
+
- `target` (string): The target field that will receive the data resolved from the source request (like a "select" field for States).
|
|
264
|
+
- `source` (DataSource object): The configuration object for fetching remote data (typically includes `url`, mapping keys, namespace, etc.).
|
|
265
|
+
|
|
266
|
+
**Example:**
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"event": {
|
|
271
|
+
"change": [
|
|
272
|
+
{
|
|
273
|
+
"action": "source",
|
|
274
|
+
"target": "stateField",
|
|
275
|
+
"source": {
|
|
276
|
+
"url": "/api/states?countryId={value}"
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## When a change event runs at mount
|
|
287
|
+
|
|
288
|
+
A `change` event does not only fire when the user edits the field. A field that
|
|
289
|
+
mounts already holding something — either a `value` passed to the form or its
|
|
290
|
+
own `defaultValue` — runs its `change` events **once**, at mount.
|
|
291
|
+
|
|
292
|
+
This is usually what you want: a form reopened on saved data arrives with the
|
|
293
|
+
same fields revealed, the same targets cleared and the same sources fetched as
|
|
294
|
+
when it was filled in. It is worth knowing about because it means the actions
|
|
295
|
+
below run before the user has touched anything.
|
|
296
|
+
|
|
297
|
+
Two details:
|
|
298
|
+
|
|
299
|
+
- **It happens once.** Re-rendering does not fire it again.
|
|
300
|
+
- **A select waits for its options.** For a field whose options come from a
|
|
301
|
+
`source`, the initial run is held until the data has loaded, so an action
|
|
302
|
+
keyed on the selected entity sees the entity rather than a bare value. Text
|
|
303
|
+
and date fields run as soon as the value is valid.
|
|
304
|
+
|
|
305
|
+
A field that mounts empty and has no `defaultValue` does not run anything until
|
|
306
|
+
it is edited.
|
|
307
|
+
|
|
308
|
+
## Sequencing Actions
|
|
309
|
+
|
|
310
|
+
Because `change` takes an array, multiple different events can be sequenced together to build complex logic flows.
|
|
311
|
+
|
|
312
|
+
**Chained Example:**
|
|
313
|
+
|
|
314
|
+
```json
|
|
315
|
+
{
|
|
316
|
+
"name": "paymentMethod",
|
|
317
|
+
"type": "radio",
|
|
318
|
+
"event": {
|
|
319
|
+
"change": [
|
|
320
|
+
{
|
|
321
|
+
"action": "value",
|
|
322
|
+
"value": {
|
|
323
|
+
"transactionFee": "0.00"
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"action": "state",
|
|
328
|
+
"target": "creditCardGroup",
|
|
329
|
+
"state": {
|
|
330
|
+
"hidden": true
|
|
331
|
+
},
|
|
332
|
+
"when": ["paypal", "bank_transfer"]
|
|
333
|
+
}
|
|
334
|
+
]
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## The `Condition` Object
|
|
342
|
+
|
|
343
|
+
In the `state` action (and potentially other areas), the `when` property can be defined as a `Condition` object for more complex logical evaluations, rather than a simple equality check.
|
|
344
|
+
|
|
345
|
+
### Properties:
|
|
346
|
+
|
|
347
|
+
- `operator` (string, optional): The logical operator to apply. Defaults to `"eq"` (equals). Supported operators are:
|
|
348
|
+
- `"eq"`: Equals
|
|
349
|
+
- `"neq"`: Not equals
|
|
350
|
+
- `"in"`: In array (value is within the specified list)
|
|
351
|
+
- `"nin"`: Not in array
|
|
352
|
+
- `"gt"`: Greater than
|
|
353
|
+
- `"gte"`: Greater than or equal
|
|
354
|
+
- `"lt"`: Less than
|
|
355
|
+
- `"lte"`: Less than or equal
|
|
356
|
+
- `value` (string | number | array): The target value to compare against the field's current value.
|
|
357
|
+
- `field` (string, optional): If specified, the condition evaluates the value of _another_ field instead of the field that triggered the change event.
|
|
358
|
+
|
|
359
|
+
### Examples of Conditions
|
|
360
|
+
|
|
361
|
+
**1. Greater Than Operator (`gt`):**
|
|
362
|
+
Enable a "Discount Code" field only if the "Total Amount" is greater than 100.
|
|
363
|
+
|
|
364
|
+
```json
|
|
365
|
+
{
|
|
366
|
+
"action": "state",
|
|
367
|
+
"target": "discountCode",
|
|
368
|
+
"state": { "disabled": false },
|
|
369
|
+
"when": {
|
|
370
|
+
"operator": "gt",
|
|
371
|
+
"value": 100
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**2. Not In Array Operator (`nin`):**
|
|
377
|
+
Hide a "State/Province" field if the selected "Country" is not in a specific list.
|
|
378
|
+
|
|
379
|
+
```json
|
|
380
|
+
{
|
|
381
|
+
"action": "state",
|
|
382
|
+
"target": "stateProvince",
|
|
383
|
+
"state": { "hidden": true },
|
|
384
|
+
"when": {
|
|
385
|
+
"operator": "nin",
|
|
386
|
+
"value": ["US", "CA", "MX"]
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Custom Inputs
|
|
2
|
+
|
|
3
|
+
A field `type` that this library does not ship can be implemented by the host
|
|
4
|
+
application and registered under its own name. This is how a form gets a people
|
|
5
|
+
picker, a credential selector, or any control that only makes sense inside one
|
|
6
|
+
product.
|
|
7
|
+
|
|
8
|
+
## Registering
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import {
|
|
12
|
+
defineConfig,
|
|
13
|
+
defineCustomInput,
|
|
14
|
+
defineInput,
|
|
15
|
+
} from 'react-luna-form/config'
|
|
16
|
+
import { PeoplePicker } from './components/people-picker'
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
inputs: [
|
|
20
|
+
defineInput(Input),
|
|
21
|
+
defineCustomInput('people/picker', PeoplePicker),
|
|
22
|
+
],
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`defineCustomInput(types, component)` takes one name or an array of names. Every
|
|
27
|
+
name becomes a key in `config.inputs`.
|
|
28
|
+
|
|
29
|
+
**The lookup is an exact string match.** There is no prefix resolution: naming a
|
|
30
|
+
field `people/picker` does not fall back to `people`, and naming it
|
|
31
|
+
`select/mine` does not fall back to `select`. `defineConfig` writes one entry per
|
|
32
|
+
registered string, and rendering reads `config.inputs[field.type]` directly.
|
|
33
|
+
|
|
34
|
+
**A type nothing registers renders nothing at all.** The lookup returns
|
|
35
|
+
`undefined`, the field is skipped, and the form renders without it — on the
|
|
36
|
+
server and on the client alike. There is no error, no warning and no
|
|
37
|
+
placeholder, so a typo in a `type` looks exactly like a field somebody chose not
|
|
38
|
+
to include. Check the spelling against your `defineConfig` when a field does not
|
|
39
|
+
appear.
|
|
40
|
+
|
|
41
|
+
## What your component receives
|
|
42
|
+
|
|
43
|
+
Props are applied in this order:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
<Component
|
|
47
|
+
{...commonProps} // id, name, placeholder, disabled, required
|
|
48
|
+
{...ariaAttributes} // aria-invalid, aria-errormessage
|
|
49
|
+
{...dataAttributes} // data-invalid, data-readonly
|
|
50
|
+
{...extraProps} // whatever the field family adds
|
|
51
|
+
{...inputProps} // the prepared value or defaultValue
|
|
52
|
+
onBlur={onBlur}
|
|
53
|
+
onChange={onChange}
|
|
54
|
+
/>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`onChange` and `onBlur` are applied **last**, after every spread. A component
|
|
58
|
+
that forwards its props unchanged gets them for free. A component that declares
|
|
59
|
+
its own handlers must call the ones it received, or the form never learns the
|
|
60
|
+
value changed: validation will not run, and neither will any `change` event the
|
|
61
|
+
field declares.
|
|
62
|
+
|
|
63
|
+
## Forwarding `id` is what gives the field its name
|
|
64
|
+
|
|
65
|
+
The label is rendered by this library, outside your component, as
|
|
66
|
+
`<label htmlFor={field.name}>`. The matching `id` arrives in your props.
|
|
67
|
+
|
|
68
|
+
If your component does not put that `id` on the element the user actually
|
|
69
|
+
focuses, the label points at nothing. The field still works with a mouse, so
|
|
70
|
+
nothing looks broken — but it has no accessible name, screen readers announce it
|
|
71
|
+
as unlabelled, and a test that reaches fields by their label cannot find it.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
// Correct: the id reaches the real input.
|
|
75
|
+
function PeoplePicker({ ...props }: React.ComponentProps<'input'>) {
|
|
76
|
+
return <input {...props} />
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Wrong: the id lands on a wrapper the label does not point to.
|
|
80
|
+
function PeoplePicker({ id, ...props }: React.ComponentProps<'input'>) {
|
|
81
|
+
return (
|
|
82
|
+
<div id={id}>
|
|
83
|
+
<input {...props} />
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The same applies when you build on a component library: whatever primitive
|
|
90
|
+
renders the focusable element is where the `id` belongs.
|
|
91
|
+
|
|
92
|
+
## Clearing a value on a select-family type
|
|
93
|
+
|
|
94
|
+
If you register a custom input under a `select` variant, be aware that an
|
|
95
|
+
`onChange('')` is **discarded**, not applied. Radix and similar components
|
|
96
|
+
re-emit `onChange("")` when a trigger remounts, and an empty string is never a
|
|
97
|
+
real selection, so the library drops it rather than wipe a value the user
|
|
98
|
+
already chose.
|
|
99
|
+
|
|
100
|
+
The consequence for a custom input: emitting an empty string to clear the field
|
|
101
|
+
does nothing. Emit a sentinel value your own code recognises, and translate it
|
|
102
|
+
back where you read the form.
|
|
103
|
+
|
|
104
|
+
This applies only to select-family fields. Text fields pass an empty string
|
|
105
|
+
through unchanged.
|
|
106
|
+
|
|
107
|
+
## Change events are debounced on text fields
|
|
108
|
+
|
|
109
|
+
For `input/*` and `textarea`, a declared `change` event fires 300 ms after the
|
|
110
|
+
last keystroke, not on every one. If you are testing a custom text input that
|
|
111
|
+
triggers a change event, wait for that window rather than asserting immediately.
|
|
112
|
+
|
|
113
|
+
Select-family fields dispatch straight away.
|
|
114
|
+
|
|
115
|
+
## Server and client
|
|
116
|
+
|
|
117
|
+
The same registered component is used by both render paths. If your custom
|
|
118
|
+
input needs browser APIs, state or effects, mark it `'use client'` as you would
|
|
119
|
+
any other component — the library does not do it for you.
|
|
120
|
+
|
|
121
|
+
## Checklist
|
|
122
|
+
|
|
123
|
+
Before shipping a custom input:
|
|
124
|
+
|
|
125
|
+
- Its `type` string matches `defineCustomInput` exactly, character for character
|
|
126
|
+
- The `id` it receives reaches the focusable element
|
|
127
|
+
- It forwards or calls the `onChange` and `onBlur` it receives
|
|
128
|
+
- If it is a select variant, it does not rely on `''` to clear
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Input Fields (`input/*`, `textarea`, `checkbox`)
|
|
2
|
+
|
|
3
|
+
The `input/*` field types in Luna Form map to standard HTML `<input>` elements. These fields are used for capturing text, numbers, dates, times, and other generic data values from the user.
|
|
4
|
+
|
|
5
|
+
## Supported Types
|
|
6
|
+
|
|
7
|
+
- `input`: Same as `input/text`. The rendered `type` is taken from the part after the last `/`, and a bare `input` falls back to `text`.
|
|
8
|
+
- `input/text`: Standard text input (`<input type="text">`).
|
|
9
|
+
- `input/email`: Email address input with built-in format validation (`<input type="email">`).
|
|
10
|
+
- `input/password`: Password input with text masked (`<input type="password">`).
|
|
11
|
+
- `input/number`: Numeric input (`<input type="number">`).
|
|
12
|
+
- `input/tel`: Telephone number input (`<input type="tel">`).
|
|
13
|
+
- `input/date`: Date picker component.
|
|
14
|
+
- `input/time`: Time picker component.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Properties
|
|
19
|
+
|
|
20
|
+
Luna Form's input fields inherit the standard logical properties available to all fields:
|
|
21
|
+
|
|
22
|
+
- **`name`** _(string, required)_: The unique identifier for the field in the form output.
|
|
23
|
+
- **`type`** _(string, required)_: Evaluates the type of input to render (e.g., `"input/text"`, `"input/password"`).
|
|
24
|
+
- **`label`** _(string, optional)_: The human-readable label shown to the user.
|
|
25
|
+
- **`defaultValue`** _(any, optional)_: The initial value of the field when the form is mounted and no external value is provided.
|
|
26
|
+
- **`description`** _(string | object, optional)_: Help text or extra context for the form field. Supports **Markdown** (specifically links).
|
|
27
|
+
- Can be a direct string.
|
|
28
|
+
- Or an object payload: `{ title: string, message: string, collapsed?: boolean }`.
|
|
29
|
+
- **`placeholder`** _(string, optional)_: The temporary placeholder text shown when the input is empty.
|
|
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.
|
|
33
|
+
- **`hidden`** _(boolean, optional)_: If `true`, the field is hidden from the user interface.
|
|
34
|
+
- **`order`** _(number, optional)_: A numeric order determining the field's position relative to adjacent fields.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Advanced Configuration (`advanced` object)
|
|
39
|
+
|
|
40
|
+
The `advanced` property dictates finer HTML details, interactive structures, and native HTML attributes.
|
|
41
|
+
|
|
42
|
+
### Common Options
|
|
43
|
+
|
|
44
|
+
- **`autocomplete`** _(string)_: Standard HTML autocomplete attribute (e.g., `"username"`, `"off"`).
|
|
45
|
+
- **`cols`** _(number)_: Layout column span for CSS grid alignment.
|
|
46
|
+
- **`horizontal`** _(boolean)_: Overrides configuration layout to render label and input horizontally.
|
|
47
|
+
- **`reverse`** _(boolean)_: If `true`, reverses the flex direction.
|
|
48
|
+
- **`aria`** _(object)_: Map of specific ARIA attributes for screen readers (e.g., `{ "aria-label": "Custom label" }`).
|
|
49
|
+
- **`data`** _(object)_: Map of specific HTML data attributes (e.g., `{ "data-test-id": "my-field" }`).
|
|
50
|
+
- **`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
|
+
### Text & Numeric Options (`input/text`, `input/email`, `input/tel`, `input/password`, `input/number`)
|
|
53
|
+
|
|
54
|
+
These basic field types support extra manipulation properties inside the `advanced` block:
|
|
55
|
+
|
|
56
|
+
- **`length`** _({ min?: number, max?: number })_: Applies HTML structural limits (`minlength` / `maxlength`, or `min` / `max` depending on the input type).
|
|
57
|
+
- **`transform`** _(string | string[])_: Safely intercepts user inputs and manipulates content dynamically. Options include:
|
|
58
|
+
- `"lowercase"`
|
|
59
|
+
- `"uppercase"`
|
|
60
|
+
- `"remove-space"`
|
|
61
|
+
- `"remove-accent"`
|
|
62
|
+
|
|
63
|
+
### Temporal Options (`input/date`, `input/time`)
|
|
64
|
+
|
|
65
|
+
Date and time inputs omit transformations and instead expose a `format` property directly under `advanced` for displaying values properly:
|
|
66
|
+
|
|
67
|
+
- **Date format** _(used with `input/date`)_: `"yyyy-MM-dd"`, `"MM/dd/yyyy"`, `"dd/MM/yyyy"`, or `"MMMM d, yyyy"`.
|
|
68
|
+
- **Time format** _(used with `input/time`)_: `"HH:mm"`, `"HH:mm:ss"`, `"hh:mm a"`, or `"hh:mm:ss a"`.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Validation (`validation` object)
|
|
73
|
+
|
|
74
|
+
The `validation` object resolves form errors overriding generic defaults, mapping the constraint rule name directly to the specific error message string.
|
|
75
|
+
|
|
76
|
+
- **`required`** _(string)_: Specifies the error message exposed when the element is marked exactly as `required: true` and the field is empty.
|
|
77
|
+
- **`email`** _(string)_: Error message specifically asserting an invalid email format.
|
|
78
|
+
- **`length`** _({ min?: string, max?: string })_: Specific string messages shown when input lengths are breached.
|
|
79
|
+
- **`custom`** _(CustomValidation | CustomValidation[])_: Powerful conditional-based logic blocks. An array specifying:
|
|
80
|
+
- `field`: Optional target reference string.
|
|
81
|
+
- `operator`: Logical evaluation operations (e.g., `eq`, `neq`, `gt`, `lt`).
|
|
82
|
+
- `message`: The resulting validation string.
|
|
83
|
+
- **`requiredWhen`** _(WhenRule | WhenRule[])_: Makes the field required only when a condition over a sibling field holds.
|
|
84
|
+
- **`pattern`** _(PatternRule)_: Validates the value against a regular expression (with optional interpolation bypass).
|
|
85
|
+
- **`rules`** _(AssertRule[])_: A list of gated assertions (`oneOf`, `min`/`max`, `pattern`, …).
|
|
86
|
+
|
|
87
|
+
_See the full [Validation reference](../validation/overview.md) for `requiredWhen`, `pattern`, `rules`, the operator vocabulary, and headless (server-side) validation._
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Events (`event` object)
|
|
92
|
+
|
|
93
|
+
You can attach logic-rich cascading events seamlessly directly inside the field schema.
|
|
94
|
+
|
|
95
|
+
- **`event.change`** _(ChangeEvent[])_: Triggers actions when logic runs. Common actions include emitting `value`, interacting with dependencies (`state`), or triggering data refetches (`source`). _Refer to the [Change Event Documentation](../events/change.md) for robust logic breakdowns._
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Complete Example Reference
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"name": "username",
|
|
104
|
+
"type": "input/text",
|
|
105
|
+
"label": "User Nickname",
|
|
106
|
+
"placeholder": "e.g. john_doe",
|
|
107
|
+
"required": true,
|
|
108
|
+
"description": {
|
|
109
|
+
"title": "Rule",
|
|
110
|
+
"message": "No spaces. No special characters.",
|
|
111
|
+
"collapsed": true
|
|
112
|
+
},
|
|
113
|
+
"advanced": {
|
|
114
|
+
"transform": ["lowercase", "remove-space"],
|
|
115
|
+
"length": {
|
|
116
|
+
"min": 4,
|
|
117
|
+
"max": 20
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"validation": {
|
|
121
|
+
"required": "You must provide a nickname to continue.",
|
|
122
|
+
"length": {
|
|
123
|
+
"min": "Your nickname needs at least 4 letters."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## `textarea`
|
|
132
|
+
|
|
133
|
+
Multi-line text. It takes the same core properties as an `input/*` field, and
|
|
134
|
+
two of the `advanced` options above apply to it: `length` and `autocomplete`.
|
|
135
|
+
Everything else in `advanced` is ignored for this type.
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"name": "notes",
|
|
140
|
+
"type": "textarea",
|
|
141
|
+
"label": "Notes",
|
|
142
|
+
"advanced": {
|
|
143
|
+
"length": { "max": 500 }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`length.min` and `length.max` are emitted as the `minLength` and `maxLength`
|
|
149
|
+
attributes on the rendered element.
|
|
150
|
+
|
|
151
|
+
Like the `input/*` family, a declared `change` event on a `textarea` is
|
|
152
|
+
debounced: it fires 300 ms after the last keystroke, not on every one.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## `checkbox`
|
|
157
|
+
|
|
158
|
+
A single boolean. It is the one field family whose value is not carried by
|
|
159
|
+
`value`: the rendered element receives `checked` instead.
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"name": "accepted",
|
|
164
|
+
"type": "checkbox",
|
|
165
|
+
"label": "I accept the terms",
|
|
166
|
+
"required": true
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
A checkbox reads its state by truthiness, not by presence. `defaultValue: true`
|
|
171
|
+
starts it checked; `false`, an omitted value, `null` and `0` all start it clear.
|
|
172
|
+
A value passed to the form is read the same way, and both render paths agree.
|
|
173
|
+
|
|
174
|
+
For a group of options rather than a single flag, use `chips` or `radio` — see
|
|
175
|
+
[select.md](select.md).
|