@juantroconisf/lib 11.6.0 → 11.8.0
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 +180 -437
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +247 -154
- package/dist/index.mjs +247 -154
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,437 +1,180 @@
|
|
|
1
|
-
# @juantroconisf/lib
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
</
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
###
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
###
|
|
174
|
-
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
{/* Boolean toggle */}
|
|
183
|
-
<Checkbox {...on.checkbox("agreed")}>I agree to the terms</Checkbox>
|
|
184
|
-
<Switch {...on.switch("notifications")}>Notifications</Switch>
|
|
185
|
-
|
|
186
|
-
{/* String radio selection */}
|
|
187
|
-
<RadioGroup {...on.radio("gender")} label="Gender">
|
|
188
|
-
<Radio value="male">Male</Radio>
|
|
189
|
-
<Radio value="female">Female</Radio>
|
|
190
|
-
</RadioGroup>
|
|
191
|
-
|
|
192
|
-
{/* Dropdown selection */}
|
|
193
|
-
<Select {...on.select("country")} label="Country">
|
|
194
|
-
<SelectItem key="us">United States</SelectItem>
|
|
195
|
-
<SelectItem key="ca">Canada</SelectItem>
|
|
196
|
-
</Select>
|
|
197
|
-
|
|
198
|
-
{/* Inside an array — pass item ID as 2nd arg */}
|
|
199
|
-
<Input {...on.input("users.name", user.id)} label="Name" />
|
|
200
|
-
<Switch {...on.switch("users.active", user.id)}>Active</Switch>
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
---
|
|
204
|
-
|
|
205
|
-
## Manual Updates
|
|
206
|
-
|
|
207
|
-
For updating state outside of a component (e.g. in an event handler, after an API call), use the manual functions returned by `useForm`.
|
|
208
|
-
|
|
209
|
-
| Function | Use for | Signature |
|
|
210
|
-
| ------------------------ | ------------------------------- | --------------------- |
|
|
211
|
-
| `onFieldChange` | Scalar / nested object field | `(id, value)` |
|
|
212
|
-
| `onArrayItemChange` | Object array item field | `({ at, id, value })` |
|
|
213
|
-
| `onSelectionChange` | Scalar / nested selection field | `(id, value)` |
|
|
214
|
-
| `onArraySelectionChange` | Array item selection field | `({ at, id, value })` |
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
const { onFieldChange, onArrayItemChange, onSelectionChange } = useForm(schema);
|
|
218
|
-
|
|
219
|
-
// Update a scalar field
|
|
220
|
-
onFieldChange("email", "user@example.com");
|
|
221
|
-
|
|
222
|
-
// Update a field inside an array item
|
|
223
|
-
onArrayItemChange({ at: "users.name", id: userId, value: "Alice" });
|
|
224
|
-
|
|
225
|
-
// Update a selection field inside an array item
|
|
226
|
-
onArraySelectionChange({ at: "users.role", id: userId, value: "admin" });
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
> **`at`** is the composite dot-notation path (`"array.field"`), **`id`** is the item's unique identifier, **`value`** is the new value to set. These are direct setters — call them imperatively, not as event listeners.
|
|
230
|
-
|
|
231
|
-
---
|
|
232
|
-
|
|
233
|
-
## Array Helpers
|
|
234
|
-
|
|
235
|
-
Extracted from `helpers` on the `useForm` return value. Items in object arrays are tracked by ID (default field: `id`).
|
|
236
|
-
|
|
237
|
-
| Method | Description |
|
|
238
|
-
| ------------------------------------------- | -------------------------------------------------- |
|
|
239
|
-
| `addItem(path, item, index?)` | Adds an item to the end (or at `index`) |
|
|
240
|
-
| `removeItemByIndex(path, index)` | Removes by index |
|
|
241
|
-
| `removeById(path, id)` | Removes by item ID |
|
|
242
|
-
| `updateByIndex(path, index, partial)` | Partially updates an item by index (shallow merge) |
|
|
243
|
-
| `updateById(path, id, partial)` | Partially updates an item by ID (shallow merge) |
|
|
244
|
-
| `moveItemByIndex(path, fromIndex, toIndex)` | Reorders by index |
|
|
245
|
-
| `moveById(path, fromId, toId)` | Reorders by ID |
|
|
246
|
-
| `getItemById(path, id)` | O(1) lookup by ID |
|
|
247
|
-
|
|
248
|
-
```ts
|
|
249
|
-
const { helpers } = useForm(schema);
|
|
250
|
-
|
|
251
|
-
helpers.addItem("users", { id: Date.now(), name: "", role: "guest" });
|
|
252
|
-
helpers.removeById("users", 123);
|
|
253
|
-
helpers.updateById("users", 123, { name: "Alice" }); // partial update — other fields preserved
|
|
254
|
-
helpers.moveById("users", 123, 456); // moves item 123 to where item 456 is
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### Custom Array Identifier
|
|
258
|
-
|
|
259
|
-
By default, the library tracks array items by their `id` field. If your items use a different field (e.g. `uuid`, `slug`, `code`), configure it via `arrayIdentifiers` in the options.
|
|
260
|
-
|
|
261
|
-
```ts
|
|
262
|
-
const { on, helpers, onArrayItemChange } = useForm(
|
|
263
|
-
{
|
|
264
|
-
employees: array()
|
|
265
|
-
.of(
|
|
266
|
-
object({
|
|
267
|
-
uuid: string().required(),
|
|
268
|
-
name: string().required(),
|
|
269
|
-
role: string().required(),
|
|
270
|
-
}),
|
|
271
|
-
)
|
|
272
|
-
.default([]),
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
arrayIdentifiers: {
|
|
276
|
-
employees: "uuid", // tells the library to use `uuid` instead of `id`
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
**Every ID-based function picks this up automatically:**
|
|
283
|
-
|
|
284
|
-
```ts
|
|
285
|
-
// on.* bindings — the second arg is the item identifier
|
|
286
|
-
<Input {...on.input("employees.name", employee.uuid)} label="Name" />
|
|
287
|
-
<Select {...on.select("employees.role", employee.uuid)} label="Role">...</Select>
|
|
288
|
-
|
|
289
|
-
// Manual setters — `id` is the item identifier
|
|
290
|
-
onArrayItemChange({ at: "employees.name", id: employee.uuid, value: "Alice" });
|
|
291
|
-
onArraySelectionChange({ at: "employees.role", id: employee.uuid, value: "admin" });
|
|
292
|
-
|
|
293
|
-
// Helpers — all `*ById` methods use the configured identifier
|
|
294
|
-
helpers.removeById("employees", employee.uuid);
|
|
295
|
-
helpers.updateById("employees", employee.uuid, { name: "Alice" }); // partial update
|
|
296
|
-
helpers.moveById("employees", fromUuid, toUuid);
|
|
297
|
-
helpers.getItemById("employees", employee.uuid);
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
> The identifier field you configure must be a **scalar** (string or number) present in every item. The library uses it to build an internal O(1) index map, which is why array operations are fast regardless of list size.
|
|
301
|
-
|
|
302
|
-
**Type inference:** `arrayIdentifiers` is typed so only valid scalar keys of each array element are accepted:
|
|
303
|
-
|
|
304
|
-
```ts
|
|
305
|
-
// ✅ uuid is a string field on each employee
|
|
306
|
-
{
|
|
307
|
-
arrayIdentifiers: {
|
|
308
|
-
employees: "uuid";
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ❌ TypeScript error — name is not a valid identifier (may not be unique)
|
|
313
|
-
{
|
|
314
|
-
arrayIdentifiers: {
|
|
315
|
-
employees: "name";
|
|
316
|
-
}
|
|
317
|
-
} // error if "name" is not in type
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
---
|
|
321
|
-
|
|
322
|
-
## Form Submission
|
|
323
|
-
|
|
324
|
-
### Option 1: `ControlledForm` (recommended)
|
|
325
|
-
|
|
326
|
-
`ControlledForm` is a drop-in replacement for HeroUI's `<Form>`. It automatically validates on submit and only calls `onSubmit` if the schema passes.
|
|
327
|
-
|
|
328
|
-
```tsx
|
|
329
|
-
const { ControlledForm } = useForm(schema);
|
|
330
|
-
|
|
331
|
-
return (
|
|
332
|
-
<ControlledForm onSubmit={(data, event) => api.post("/submit", data)}>
|
|
333
|
-
<Input {...on.input("email")} label='Email' />
|
|
334
|
-
<Button type='submit'>Submit</Button>
|
|
335
|
-
</ControlledForm>
|
|
336
|
-
);
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
`data` is fully typed from your schema. No manual type annotations needed.
|
|
340
|
-
|
|
341
|
-
### Option 2: `onFormSubmit`
|
|
342
|
-
|
|
343
|
-
For use with plain HTML `<form>` elements:
|
|
344
|
-
|
|
345
|
-
```tsx
|
|
346
|
-
const { onFormSubmit } = useForm(schema);
|
|
347
|
-
|
|
348
|
-
const handleSubmit = onFormSubmit((data, event) => {
|
|
349
|
-
api.post("/submit", data);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
return (
|
|
353
|
-
<form onSubmit={handleSubmit}>
|
|
354
|
-
<Input {...on.input("email")} label='Email' />
|
|
355
|
-
<button type='submit'>Submit</button>
|
|
356
|
-
</form>
|
|
357
|
-
);
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### Option 3: External handler with `FormSubmit`
|
|
361
|
-
|
|
362
|
-
When you need to define the submit handler outside the JSX, use the `FormSubmit` utility type:
|
|
363
|
-
|
|
364
|
-
```tsx
|
|
365
|
-
import { useForm, FormSubmit } from "@juantroconisf/lib";
|
|
366
|
-
|
|
367
|
-
const { ControlledForm } = useForm({
|
|
368
|
-
email: string().required().default(""),
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
const handleSubmit: FormSubmit<typeof ControlledForm> = (data, event) => {
|
|
372
|
-
// data.email is correctly typed as string — no schema re-declaration needed
|
|
373
|
-
console.log(data.email);
|
|
374
|
-
};
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
`FormSubmit<typeof ControlledForm>` is a shorthand for `React.ComponentProps<typeof ControlledForm>["onSubmit"]`.
|
|
378
|
-
|
|
379
|
-
---
|
|
380
|
-
|
|
381
|
-
## API Reference
|
|
382
|
-
|
|
383
|
-
### `useForm(schema, options?)`
|
|
384
|
-
|
|
385
|
-
| Parameter | Type | Description |
|
|
386
|
-
| --------- | ---------------------------- | -------------------------------------------------------- |
|
|
387
|
-
| `schema` | `Record<string, ConfigType>` | Yup schemas or primitive values defining your form shape |
|
|
388
|
-
| `options` | `FormOptions` | Optional configuration |
|
|
389
|
-
|
|
390
|
-
#### `FormOptions`
|
|
391
|
-
|
|
392
|
-
| Option | Type | Description |
|
|
393
|
-
| ------------------ | ------------------------ | --------------------------------------------------------- |
|
|
394
|
-
| `arrayIdentifiers` | `Record<string, string>` | Override the ID field for object arrays (default: `"id"`) |
|
|
395
|
-
| `resetOnSubmit` | `boolean` | Reset form to defaults after successful submit |
|
|
396
|
-
| `onFormSubmit` | `Function` | Default submit handler passed via options |
|
|
397
|
-
| `keepValues` | `(keyof State)[]` | Fields to preserve during reset |
|
|
398
|
-
|
|
399
|
-
### `useForm` returns
|
|
400
|
-
|
|
401
|
-
| Property | Type | Description |
|
|
402
|
-
| ------------------------ | ----------------------------- | -------------------------------------------------- |
|
|
403
|
-
| `state` | `InferState<S>` | Current form values |
|
|
404
|
-
| `setState` | `Dispatch<SetStateAction<S>>` | Direct state setter |
|
|
405
|
-
| `metadata` | `Map<string, FieldMetadata>` | Per-field `isTouched`, `isInvalid`, `errorMessage` |
|
|
406
|
-
| `on` | `OnMethods<S>` | Component binding methods |
|
|
407
|
-
| `helpers` | `HelpersFunc<S>` | Array manipulation methods |
|
|
408
|
-
| `isDirty` | `boolean` | `true` if any field has been touched |
|
|
409
|
-
| `onFieldChange` | Function | Manual scalar field update |
|
|
410
|
-
| `onArrayItemChange` | Function | Manual array item field update |
|
|
411
|
-
| `onSelectionChange` | Function | Manual scalar selection update |
|
|
412
|
-
| `onArraySelectionChange` | Function | Manual array item selection update |
|
|
413
|
-
| `onFieldBlur` | Function | Manual blur trigger |
|
|
414
|
-
| `onFormReset` | Function | Resets state and clears metadata |
|
|
415
|
-
| `onFormSubmit` | Function | Wraps a submit handler with validation |
|
|
416
|
-
| `ControlledForm` | Component | Validated `<Form>` wrapper |
|
|
417
|
-
|
|
418
|
-
---
|
|
419
|
-
|
|
420
|
-
## Localization
|
|
421
|
-
|
|
422
|
-
Validation error messages are automatically localized. The library reads a `LOCALE` cookie on initialization.
|
|
423
|
-
|
|
424
|
-
- **Supported**: `en` (English — default), `es` (Spanish)
|
|
425
|
-
- **Setting the locale**:
|
|
426
|
-
|
|
427
|
-
```ts
|
|
428
|
-
document.cookie = "LOCALE=es; path=/; max-age=31536000";
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
If no cookie is found, or the value is unrecognized, English is used.
|
|
432
|
-
|
|
433
|
-
---
|
|
434
|
-
|
|
435
|
-
## License
|
|
436
|
-
|
|
437
|
-
ISC © Juan T
|
|
1
|
+
# @juantroconisf/lib
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@juantroconisf/lib)
|
|
4
|
+
[](https://opensource.org/licenses/ISC)
|
|
5
|
+
|
|
6
|
+
A type-safe form management library for **React** and **HeroUI**. It eliminates boilerplate through a declarative `on.*` API that bridges your [Yup](https://github.com/jquense/yup) schema directly to HeroUI component props — complete with validation, error messages, dirty tracking, and ID-based array management.
|
|
7
|
+
|
|
8
|
+
## Why this library?
|
|
9
|
+
|
|
10
|
+
Managing forms in React often involves manual state syncing and verbose validation logic. This library acts as a **bridge**, automatically mapping your [Yup](https://github.com/jquense/yup) schema's constraints directly to your HeroUI components.
|
|
11
|
+
|
|
12
|
+
### Key Benefits
|
|
13
|
+
- **Zero Boilerplate:** No more manual `value`, `onChange`, and `onBlur` wiring.
|
|
14
|
+
- **Type-Safe:** Automatic inference from your schema with full Intellisense.
|
|
15
|
+
- **Auto-Validation:** Errors and `isRequired` states flow naturally from your schema.
|
|
16
|
+
- **ID-Based Arrays:** Seamless handling of dynamic lists with $O(1)$ performance.
|
|
17
|
+
|
|
18
|
+
## Table of Contents
|
|
19
|
+
|
|
20
|
+
- [Quick Start](#quick-start)
|
|
21
|
+
- [How-to Guides](#how-to-guides)
|
|
22
|
+
- [1. Working with Nested Objects](#1-working-with-nested-objects)
|
|
23
|
+
- [2. Dynamic Arrays](#2-managing-dynamic-arrays)
|
|
24
|
+
- [3. N-Level Structures](#3-n-level-deep-structures)
|
|
25
|
+
- [4. Manual Updates](#4-performing-manual-updates)
|
|
26
|
+
- [Reference](#reference)
|
|
27
|
+
- [useForm API](#useformschema-options)
|
|
28
|
+
- [on.* API](#the-on-api)
|
|
29
|
+
- [Array Helpers](#array-helpers-helpers)
|
|
30
|
+
- [Explanation](#explanation)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
Get up and running in under a minute.
|
|
37
|
+
|
|
38
|
+
### 1. Install
|
|
39
|
+
```bash
|
|
40
|
+
pnpm add @juantroconisf/lib yup
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Implementation
|
|
44
|
+
```tsx
|
|
45
|
+
import { useForm } from "@juantroconisf/lib";
|
|
46
|
+
import { string, boolean } from "yup";
|
|
47
|
+
import { Input, Switch, Button } from "@heroui/react";
|
|
48
|
+
|
|
49
|
+
const MyForm = () => {
|
|
50
|
+
const { on, ControlledForm } = useForm({
|
|
51
|
+
fullName: string().required().default(""),
|
|
52
|
+
darkMode: boolean().default(false),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<ControlledForm onSubmit={(data) => console.log(data)}>
|
|
57
|
+
{/* on.input() automatically handles value, onValueChange, isInvalid, and errorMessage */}
|
|
58
|
+
<Input {...on.input("fullName")} label='Full Name' />
|
|
59
|
+
|
|
60
|
+
<Switch {...on.switch("darkMode")}>Dark Mode</Switch>
|
|
61
|
+
|
|
62
|
+
<Button type='submit' color='primary'>
|
|
63
|
+
Submit
|
|
64
|
+
</Button>
|
|
65
|
+
</ControlledForm>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## How-to Guides
|
|
73
|
+
|
|
74
|
+
Practical recipes for common form scenarios.
|
|
75
|
+
|
|
76
|
+
### 1. Working with Nested Objects
|
|
77
|
+
Dot notation reaches arbitrarily deep. TypeScript validates that the path exists and has the correct type.
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<Input {...on.input("settings.profile.username")} label="Username" />
|
|
81
|
+
<Switch {...on.switch("settings.notifications")}>
|
|
82
|
+
Enable Notifications
|
|
83
|
+
</Switch>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Managing Dynamic Arrays
|
|
87
|
+
Arrays of objects are tracked by item `id` (or a custom identifier). This ensures inputs hold their state correctly even after re-ordering or deletions.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
{state.users.map((user) => (
|
|
91
|
+
<div key={user.id}>
|
|
92
|
+
{/* Bind to a field inside the array item using: path + itemId */}
|
|
93
|
+
<Input {...on.input("users.name", user.id)} label='Name' />
|
|
94
|
+
|
|
95
|
+
<Button onPress={() => helpers.removeById("users", user.id)}>
|
|
96
|
+
Remove
|
|
97
|
+
</Button>
|
|
98
|
+
</div>
|
|
99
|
+
))}
|
|
100
|
+
|
|
101
|
+
<Button onPress={() => helpers.addItem("users", { id: Date.now(), name: "" })}>
|
|
102
|
+
Add User
|
|
103
|
+
</Button>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3. N-Level Deep Structures
|
|
107
|
+
For arrays inside objects inside arrays, pass a variadic sequence alternating between structural paths and identifiers.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// Schema: items[].form_response.input_values[].value
|
|
111
|
+
<Input
|
|
112
|
+
{...on.input("items", itemId, "form_response.input_values", inputId, "value")}
|
|
113
|
+
label='Deep Value'
|
|
114
|
+
/>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 4. Performing Manual Updates
|
|
118
|
+
To update state outside of a component (e.g., in an API response handler), use the manual setters.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const { onFieldChange, onArrayItemChange } = useForm(schema);
|
|
122
|
+
|
|
123
|
+
// Update a top-level or nested field
|
|
124
|
+
onFieldChange("settings.theme", "dark");
|
|
125
|
+
|
|
126
|
+
// Update a field inside an array item
|
|
127
|
+
onArrayItemChange({ at: "users.name", id: userId, value: "Alice" });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Reference
|
|
133
|
+
|
|
134
|
+
### `useForm(schema, options?)`
|
|
135
|
+
|
|
136
|
+
| Option | Type | Description |
|
|
137
|
+
| :--- | :--- | :--- |
|
|
138
|
+
| `arrayIdentifiers` | `Record<string, string>` | Override the ID field (default: `"id"`) |
|
|
139
|
+
| `resetOnSubmit` | `boolean` | Reset form after success (default: `false`) |
|
|
140
|
+
| `keepValues` | `(keyof State)[]` | Fields to exclude from reset |
|
|
141
|
+
|
|
142
|
+
### The `on.*` API
|
|
143
|
+
Methods that bridge schema logic to HeroUI components.
|
|
144
|
+
|
|
145
|
+
| Method | HeroUI Component | Key Props Returned |
|
|
146
|
+
| :--- | :--- | :--- |
|
|
147
|
+
| `on.input()` | `Input`, `Textarea` | `value`, `onValueChange` |
|
|
148
|
+
| `on.select()` | `Select` | `selectedKeys`, `onSelectionChange` |
|
|
149
|
+
| `on.switch()` | `Switch` | `isSelected`, `onValueChange` |
|
|
150
|
+
| `on.checkbox()` | `Checkbox` | `isSelected`, `onValueChange` |
|
|
151
|
+
| `on.radio()` | `RadioGroup` | `value`, `onValueChange` |
|
|
152
|
+
|
|
153
|
+
### Array Helpers (`helpers.*`)
|
|
154
|
+
| Method | Description |
|
|
155
|
+
| :--- | :--- |
|
|
156
|
+
| `addItem(path, item)` | Append a new item |
|
|
157
|
+
| `removeById(path, id)` | Fast $O(1)$ removal by ID |
|
|
158
|
+
| `updateById(path, id, partial)` | Shallow merge by ID |
|
|
159
|
+
| `moveById(path, fromId, toId)` | Reorder items |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Explanation
|
|
164
|
+
|
|
165
|
+
### The Bridge Architecture
|
|
166
|
+
Traditional form libraries often require you to manually map state to component props (`value={state.foo} onChange={...}`).
|
|
167
|
+
|
|
168
|
+
This library uses a **Bridge Pattern**:
|
|
169
|
+
1. Your **Schema** defines the "Source of Truth".
|
|
170
|
+
2. The **`on.*` methods** act as the bridge, translating that truth into the specific prop contract needed by each HeroUI component.
|
|
171
|
+
3. This ensures that validation errors, required indicators, and values are always perfectly in sync without manual wiring.
|
|
172
|
+
|
|
173
|
+
### Localization
|
|
174
|
+
Validation messages are automatically localized by reading the `LOCALE` cookie (`en` or `es`).
|
|
175
|
+
```ts
|
|
176
|
+
document.cookie = "LOCALE=es; path=/;";
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
ISC © Juan T
|
package/dist/index.d.mts
CHANGED
|
@@ -238,6 +238,8 @@ interface HelpersFunc<O extends StateType> {
|
|
|
238
238
|
moveById: <K extends ArrayPaths<O>>(arrayKey: K, fromId: string | number, toId: string | number) => void;
|
|
239
239
|
/** Gets an item from an array by its unique identifier (O(1) via indexMap). */
|
|
240
240
|
getItemById: <K extends ArrayPaths<O>>(arrayKey: K, itemId: string | number) => (NestedFieldValue<O, K> extends (infer E)[] ? E : never) | undefined;
|
|
241
|
+
/** Validates a single item in an array by its unique identifier. */
|
|
242
|
+
validateItem: <K extends ArrayPaths<O>>(arrayKey: K, itemId: string | number) => Promise<boolean>;
|
|
241
243
|
}
|
|
242
244
|
/**
|
|
243
245
|
* The response object from the useForm hook.
|
package/dist/index.d.ts
CHANGED
|
@@ -238,6 +238,8 @@ interface HelpersFunc<O extends StateType> {
|
|
|
238
238
|
moveById: <K extends ArrayPaths<O>>(arrayKey: K, fromId: string | number, toId: string | number) => void;
|
|
239
239
|
/** Gets an item from an array by its unique identifier (O(1) via indexMap). */
|
|
240
240
|
getItemById: <K extends ArrayPaths<O>>(arrayKey: K, itemId: string | number) => (NestedFieldValue<O, K> extends (infer E)[] ? E : never) | undefined;
|
|
241
|
+
/** Validates a single item in an array by its unique identifier. */
|
|
242
|
+
validateItem: <K extends ArrayPaths<O>>(arrayKey: K, itemId: string | number) => Promise<boolean>;
|
|
241
243
|
}
|
|
242
244
|
/**
|
|
243
245
|
* The response object from the useForm hook.
|