@juantroconisf/lib 9.4.0 → 11.0.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 +304 -244
- package/dist/index.d.mts +75 -19
- package/dist/index.d.ts +75 -19
- package/dist/index.js +129 -16
- package/dist/index.mjs +129 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,40 +1,24 @@
|
|
|
1
1
|
# @juantroconisf/lib
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
Designed for complex applications, it provides **O(1) value updates**, stable **ID-based array management**, and deep nesting support, all seamlessly integrated with **Yup** schemas.
|
|
3
|
+
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.
|
|
6
4
|
|
|
7
5
|
---
|
|
8
6
|
|
|
9
7
|
## Table of Contents
|
|
10
8
|
|
|
11
|
-
- [Features](#features)
|
|
12
9
|
- [Installation](#installation)
|
|
13
|
-
- [Localization](#localization)
|
|
14
10
|
- [Quick Start](#quick-start)
|
|
15
|
-
- [
|
|
11
|
+
- [Core Concepts](#core-concepts)
|
|
12
|
+
- [Schema Definition](#schema-definition)
|
|
16
13
|
- [Scalar Fields](#scalar-fields)
|
|
17
14
|
- [Nested Objects](#nested-objects)
|
|
18
|
-
- [
|
|
19
|
-
|
|
15
|
+
- [Array Fields](#array-fields)
|
|
16
|
+
- [The `on` API — Component Bindings](#the-on-api--component-bindings)
|
|
17
|
+
- [Manual Updates](#manual-updates)
|
|
18
|
+
- [Array Helpers](#array-helpers)
|
|
19
|
+
- [Form Submission](#form-submission)
|
|
20
20
|
- [API Reference](#api-reference)
|
|
21
|
-
|
|
22
|
-
- [The `on` API](#the-on-api)
|
|
23
|
-
- [ControlledForm](#controlledform)
|
|
24
|
-
- [Array Helpers](#array-helpers)
|
|
25
|
-
- [Form Controls](#form-controls)
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Features
|
|
30
|
-
|
|
31
|
-
- 🎯 **Polymorphic `on` API**: A unified interface (`on.input`, `on.select`, `on.autocomplete`) saving lines of boilerplate.
|
|
32
|
-
- 🧱 **ControlledForm Component**: Zero-boilerplate validated submissions leveraging the `@heroui/react` Form component directly.
|
|
33
|
-
- 🧩 **Deep Nesting**: Dot-notation support (e.g., `settings.theme`) out of the box.
|
|
34
|
-
- 🔢 **ID-Based Arrays**: List items are tracked by unique identifiers (default: `id`), ensuring states survive mapping, reordering, and deletions.
|
|
35
|
-
- ⚡ **O(1) Performance**: Internal mapping for array updates offers incredible speed regardless of data density.
|
|
36
|
-
- 🛡️ **Complete Type Safety**: TypeScript infers all acceptable paths, data structures, and outputs natively.
|
|
37
|
-
- 🌍 **Built-in Localization**: Ships with English and Spanish validation translations via runtime cookies.
|
|
21
|
+
- [Localization](#localization)
|
|
38
22
|
|
|
39
23
|
---
|
|
40
24
|
|
|
@@ -46,314 +30,390 @@ pnpm add @juantroconisf/lib yup
|
|
|
46
30
|
|
|
47
31
|
---
|
|
48
32
|
|
|
49
|
-
## Localization
|
|
50
|
-
|
|
51
|
-
Validation strings (e.g., minimum length, required fields) are localized automatically. The library checks for a browser cookie named `LOCALE` on initialization.
|
|
52
|
-
|
|
53
|
-
- **Supported Locales**: `en` (English - default) and `es` (Spanish).
|
|
54
|
-
- **How to Set**:
|
|
55
|
-
Set the cookie in your client application:
|
|
56
|
-
```ts
|
|
57
|
-
document.cookie = "LOCALE=es; path=/; max-age=31536000"; // Sets language to Spanish
|
|
58
|
-
```
|
|
59
|
-
If no cookie is detected, or the value is unrecognized, the library gracefully falls back to English (`en`).
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
33
|
## Quick Start
|
|
64
34
|
|
|
65
|
-
Throughout this documentation, we will refer to a common, diverse schema. It includes simple scalars, deep objects, and complex arrays.
|
|
66
|
-
|
|
67
35
|
```tsx
|
|
68
36
|
import { useForm } from "@juantroconisf/lib";
|
|
69
|
-
import { string,
|
|
70
|
-
import { Input, Switch,
|
|
71
|
-
|
|
72
|
-
// 1. Define your diverse schema structure
|
|
73
|
-
const mySchema = {
|
|
74
|
-
fullName: string().required("Name is required").default(""),
|
|
75
|
-
age: number().min(18).default(18),
|
|
76
|
-
settings: object({
|
|
77
|
-
theme: string().oneOf(["light", "dark"]).default("dark"),
|
|
78
|
-
notifications: boolean().default(true),
|
|
79
|
-
}),
|
|
80
|
-
users: array()
|
|
81
|
-
.of(
|
|
82
|
-
object().shape({
|
|
83
|
-
id: number().required(),
|
|
84
|
-
role: string().required(),
|
|
85
|
-
}),
|
|
86
|
-
)
|
|
87
|
-
.default([{ id: Date.now(), role: "admin" }]),
|
|
88
|
-
};
|
|
37
|
+
import { string, boolean } from "yup";
|
|
38
|
+
import { Input, Switch, Button } from "@heroui/react";
|
|
89
39
|
|
|
90
40
|
const MyForm = () => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const handleSubmit = (data, event) => {
|
|
96
|
-
console.log("Submitted payload:", data);
|
|
97
|
-
// data.fullName -> string
|
|
98
|
-
// data.settings.theme -> "light" | "dark"
|
|
99
|
-
// data.users[0].role -> string
|
|
100
|
-
};
|
|
41
|
+
const { on, ControlledForm } = useForm({
|
|
42
|
+
fullName: string().required().default(""),
|
|
43
|
+
darkMode: boolean().default(false),
|
|
44
|
+
});
|
|
101
45
|
|
|
102
46
|
return (
|
|
103
|
-
<ControlledForm onSubmit={
|
|
104
|
-
{/* Scalar Fields */}
|
|
47
|
+
<ControlledForm onSubmit={(data) => console.log(data)}>
|
|
105
48
|
<Input {...on.input("fullName")} label='Full Name' />
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Enable Notifications
|
|
111
|
-
</Switch>
|
|
112
|
-
<Select {...on.select("settings.theme")} label='Theme'>
|
|
113
|
-
<SelectItem key='light'>Light</SelectItem>
|
|
114
|
-
<SelectItem key='dark'>Dark</SelectItem>
|
|
115
|
-
</Select>
|
|
116
|
-
|
|
117
|
-
{/* Array Fields */}
|
|
118
|
-
<div className='flex flex-col gap-2'>
|
|
119
|
-
<h3>Users</h3>
|
|
120
|
-
{state.users.map((user) => (
|
|
121
|
-
<div key={user.id} className='flex gap-2 items-center'>
|
|
122
|
-
{/* Bind by Array Path + Item ID */}
|
|
123
|
-
<Select {...on.select("users.role", user.id)} label='Role'>
|
|
124
|
-
<SelectItem key='admin'>Admin</SelectItem>
|
|
125
|
-
<SelectItem key='guest'>Guest</SelectItem>
|
|
126
|
-
</Select>
|
|
127
|
-
<Button
|
|
128
|
-
color='danger'
|
|
129
|
-
onPress={() => helpers.removeById("users", user.id)}
|
|
130
|
-
>
|
|
131
|
-
Remove
|
|
132
|
-
</Button>
|
|
133
|
-
</div>
|
|
134
|
-
))}
|
|
135
|
-
</div>
|
|
136
|
-
|
|
137
|
-
<div className='flex gap-2'>
|
|
138
|
-
<Button
|
|
139
|
-
onPress={() =>
|
|
140
|
-
helpers.addItem("users", { id: Date.now(), role: "guest" })
|
|
141
|
-
}
|
|
142
|
-
>
|
|
143
|
-
Add User
|
|
144
|
-
</Button>
|
|
145
|
-
<Button type='submit' color='primary'>
|
|
146
|
-
Submit
|
|
147
|
-
</Button>
|
|
148
|
-
</div>
|
|
49
|
+
<Switch {...on.switch("darkMode")}>Dark Mode</Switch>
|
|
50
|
+
<Button type='submit' color='primary'>
|
|
51
|
+
Submit
|
|
52
|
+
</Button>
|
|
149
53
|
</ControlledForm>
|
|
150
54
|
);
|
|
151
55
|
};
|
|
152
56
|
```
|
|
153
57
|
|
|
154
|
-
|
|
58
|
+
> `on.input("fullName")` spreads `value`, `onValueChange`, `isInvalid`, `errorMessage`, `isRequired`, `onBlur`, and `id` directly onto the HeroUI component. Zero boilerplate.
|
|
155
59
|
|
|
156
|
-
|
|
60
|
+
---
|
|
157
61
|
|
|
158
|
-
|
|
62
|
+
## Core Concepts
|
|
159
63
|
|
|
160
|
-
###
|
|
64
|
+
### Schema Definition
|
|
161
65
|
|
|
162
|
-
|
|
66
|
+
Pass a plain object of Yup schemas (or raw values for unvalidated state) to `useForm`. The hook infers your TypeScript types automatically.
|
|
163
67
|
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
|
|
68
|
+
```ts
|
|
69
|
+
const { on, state, helpers, ControlledForm } = useForm({
|
|
70
|
+
email: string().email().required().default(""),
|
|
71
|
+
age: number().min(18).default(18),
|
|
72
|
+
settings: {
|
|
73
|
+
theme: string().oneOf(["light", "dark"]).default("dark"),
|
|
74
|
+
notifications: boolean().default(true),
|
|
75
|
+
},
|
|
76
|
+
tags: array().of(string()).default([]),
|
|
77
|
+
users: array()
|
|
78
|
+
.of(object({ id: number().required(), role: string().required() }))
|
|
79
|
+
.default([]),
|
|
80
|
+
});
|
|
167
81
|
```
|
|
168
82
|
|
|
169
|
-
###
|
|
83
|
+
### Scalar Fields
|
|
170
84
|
|
|
171
|
-
|
|
85
|
+
Top-level primitive fields. Use dot notation to reach into nested objects.
|
|
172
86
|
|
|
173
87
|
```tsx
|
|
174
|
-
|
|
88
|
+
<Input {...on.input("email")} label="Email" />
|
|
89
|
+
<Input {...on.numberInput("age")} label="Age" />
|
|
175
90
|
<Select {...on.select("settings.theme")} label="Theme">
|
|
176
91
|
<SelectItem key="light">Light</SelectItem>
|
|
177
92
|
<SelectItem key="dark">Dark</SelectItem>
|
|
178
93
|
</Select>
|
|
179
|
-
|
|
180
|
-
// Booleans map smoothly to Switches/Checkboxes
|
|
181
|
-
<Switch {...on.input("settings.notifications")}>Enable Alerts</Switch>
|
|
182
94
|
```
|
|
183
95
|
|
|
184
|
-
###
|
|
185
|
-
|
|
186
|
-
Array management is uniquely built around tracking entries by ID (`'id'` field by default). This stops issues where UI inputs lose state references if the array gets shifted.
|
|
96
|
+
### Nested Objects
|
|
187
97
|
|
|
188
|
-
|
|
189
|
-
Combine the general array path and the item ID.
|
|
98
|
+
Dot notation reaches arbitrarily deep. TypeScript validates that the path exists and has the correct type.
|
|
190
99
|
|
|
191
100
|
```tsx
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
on.select("users", 0);
|
|
101
|
+
<Switch {...on.switch("settings.notifications")}>
|
|
102
|
+
Enable Notifications
|
|
103
|
+
</Switch>
|
|
104
|
+
<Autocomplete {...on.autocomplete("settings.theme")} label="Theme" />
|
|
197
105
|
```
|
|
198
106
|
|
|
199
|
-
|
|
200
|
-
|
|
107
|
+
### Array Fields
|
|
108
|
+
|
|
109
|
+
Arrays of objects are tracked by item `id` (configurable via `arrayIdentifiers`). This means inputs hold their state correctly even after re-ordering or deletions.
|
|
201
110
|
|
|
202
111
|
```tsx
|
|
203
|
-
|
|
112
|
+
{
|
|
113
|
+
state.users.map((user) => (
|
|
114
|
+
<div key={user.id}>
|
|
115
|
+
{/* Bind to a field inside the array item using: path + itemId */}
|
|
116
|
+
<Input {...on.input("users.name", user.id)} label='Name' />
|
|
117
|
+
<Select {...on.select("users.role", user.id)} label='Role'>
|
|
118
|
+
<SelectItem key='admin'>Admin</SelectItem>
|
|
119
|
+
<SelectItem key='guest'>Guest</SelectItem>
|
|
120
|
+
</Select>
|
|
121
|
+
<Button onPress={() => helpers.removeById("users", user.id)}>
|
|
122
|
+
Remove
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
<Button
|
|
128
|
+
onPress={() => helpers.addItem("users", { id: Date.now(), role: "guest" })}
|
|
129
|
+
>
|
|
130
|
+
Add User
|
|
131
|
+
</Button>;
|
|
132
|
+
```
|
|
204
133
|
|
|
205
|
-
|
|
206
|
-
helpers.addItem("users", { id: Date.now(), role: "guest" });
|
|
134
|
+
---
|
|
207
135
|
|
|
208
|
-
|
|
209
|
-
helpers.removeById("users", 12345);
|
|
136
|
+
## The `on` API — Component Bindings
|
|
210
137
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
138
|
+
Each `on.*` method returns a set of props that you spread directly onto a HeroUI component. Every method automatically handles:
|
|
139
|
+
|
|
140
|
+
- **Controlled value** — synced with form state
|
|
141
|
+
- **Validation** — runs on change and blur against your Yup schema
|
|
142
|
+
- **Error display** — `isInvalid` and `errorMessage` from the schema
|
|
143
|
+
- **Required indicator** — `isRequired` derived from `.required()` in your schema
|
|
214
144
|
|
|
215
|
-
|
|
145
|
+
All methods support both **scalar/nested** paths and **array item** paths (composite `"array.field"` + `itemId`).
|
|
216
146
|
|
|
217
|
-
|
|
147
|
+
| Method | HeroUI Component | Key props returned |
|
|
148
|
+
| --------------------------------- | ------------------- | ----------------------------------------------- |
|
|
149
|
+
| `on.input(path, [itemId])` | `Input`, `Textarea` | `value: string`, `onValueChange(string)` |
|
|
150
|
+
| `on.numberInput(path, [itemId])` | `NumberInput` | `value: number`, `onValueChange(number)` |
|
|
151
|
+
| `on.select(path, [itemId])` | `Select` | `selectedKeys`, `onSelectionChange` |
|
|
152
|
+
| `on.autocomplete(path, [itemId])` | `Autocomplete` | `selectedKey`, `onSelectionChange` |
|
|
153
|
+
| `on.checkbox(path, [itemId])` | `Checkbox` | `isSelected: boolean`, `onValueChange(boolean)` |
|
|
154
|
+
| `on.switch(path, [itemId])` | `Switch` | `isSelected: boolean`, `onValueChange(boolean)` |
|
|
155
|
+
| `on.radio(path, [itemId])` | `RadioGroup` | `value: string`, `onValueChange(string)` |
|
|
218
156
|
|
|
219
|
-
**
|
|
157
|
+
> **Why separate methods?** Each HeroUI component has a different prop contract (e.g. `isSelected` vs `value`, `onSelectionChange` vs `onValueChange`). Separate methods give accurate intellisense for each component.
|
|
220
158
|
|
|
221
|
-
|
|
159
|
+
### Examples
|
|
222
160
|
|
|
223
161
|
```tsx
|
|
224
|
-
|
|
162
|
+
{/* Text input */}
|
|
163
|
+
<Input {...on.input("firstName")} label="First Name" />
|
|
164
|
+
|
|
165
|
+
{/* Numeric input (NumberInput-specific) */}
|
|
166
|
+
<NumberInput {...on.numberInput("age")} label="Age" />
|
|
167
|
+
|
|
168
|
+
{/* Boolean toggle */}
|
|
169
|
+
<Checkbox {...on.checkbox("agreed")}>I agree to the terms</Checkbox>
|
|
170
|
+
<Switch {...on.switch("notifications")}>Notifications</Switch>
|
|
171
|
+
|
|
172
|
+
{/* String radio selection */}
|
|
173
|
+
<RadioGroup {...on.radio("gender")} label="Gender">
|
|
174
|
+
<Radio value="male">Male</Radio>
|
|
175
|
+
<Radio value="female">Female</Radio>
|
|
176
|
+
</RadioGroup>
|
|
177
|
+
|
|
178
|
+
{/* Dropdown selection */}
|
|
179
|
+
<Select {...on.select("country")} label="Country">
|
|
180
|
+
<SelectItem key="us">United States</SelectItem>
|
|
181
|
+
<SelectItem key="ca">Canada</SelectItem>
|
|
182
|
+
</Select>
|
|
225
183
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<Button type='submit'>Submit</Button>
|
|
230
|
-
</ControlledForm>
|
|
231
|
-
);
|
|
184
|
+
{/* Inside an array — pass item ID as 2nd arg */}
|
|
185
|
+
<Input {...on.input("users.name", user.id)} label="Name" />
|
|
186
|
+
<Switch {...on.switch("users.active", user.id)}>Active</Switch>
|
|
232
187
|
```
|
|
233
188
|
|
|
234
|
-
|
|
189
|
+
---
|
|
235
190
|
|
|
236
|
-
|
|
191
|
+
## Manual Updates
|
|
237
192
|
|
|
238
|
-
|
|
239
|
-
const { onFormSubmit, state } = useForm(mySchema);
|
|
193
|
+
For updating state outside of a component (e.g. in an event handler, after an API call), use the manual functions returned by `useForm`.
|
|
240
194
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
})
|
|
195
|
+
| Function | Use for | Signature |
|
|
196
|
+
| ------------------------ | ------------------------------- | --------------------- |
|
|
197
|
+
| `onFieldChange` | Scalar / nested object field | `(id, value)` |
|
|
198
|
+
| `onArrayItemChange` | Object array item field | `({ at, id, value })` |
|
|
199
|
+
| `onSelectionChange` | Scalar / nested selection field | `(id, value)` |
|
|
200
|
+
| `onArraySelectionChange` | Array item selection field | `({ at, id, value })` |
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const { onFieldChange, onArrayItemChange, onSelectionChange } = useForm(schema);
|
|
247
204
|
|
|
248
|
-
//
|
|
249
|
-
|
|
205
|
+
// Update a scalar field
|
|
206
|
+
onFieldChange("email", "user@example.com");
|
|
207
|
+
|
|
208
|
+
// Update a field inside an array item
|
|
209
|
+
onArrayItemChange({ at: "users.name", id: userId, value: "Alice" });
|
|
210
|
+
|
|
211
|
+
// Update a selection field inside an array item
|
|
212
|
+
onArraySelectionChange({ at: "users.role", id: userId, value: "admin" });
|
|
250
213
|
```
|
|
251
214
|
|
|
252
|
-
|
|
215
|
+
> **`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.
|
|
253
216
|
|
|
254
|
-
|
|
217
|
+
---
|
|
255
218
|
|
|
256
|
-
|
|
257
|
-
export const MyForm = () => {
|
|
258
|
-
// 1. Schema is defined organically inline inside the component execution
|
|
259
|
-
const { ControlledForm } = useForm({
|
|
260
|
-
fullName: string().required(),
|
|
261
|
-
});
|
|
219
|
+
## Array Helpers
|
|
262
220
|
|
|
263
|
-
|
|
264
|
-
const handleSubmit: React.ComponentProps<
|
|
265
|
-
typeof ControlledForm
|
|
266
|
-
>["onSubmit"] = (data, event) => {
|
|
267
|
-
// Both data.fullName and React Events are perfectly inferred natively!
|
|
268
|
-
console.log(data.fullName);
|
|
269
|
-
};
|
|
221
|
+
Extracted from `helpers` on the `useForm` return value. Items in object arrays are tracked by ID (default field: `id`).
|
|
270
222
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
223
|
+
| Method | Description |
|
|
224
|
+
| ------------------------------------------- | -------------------------------------------------- |
|
|
225
|
+
| `addItem(path, item, index?)` | Adds an item to the end (or at `index`) |
|
|
226
|
+
| `removeItemByIndexByIndex(path, index)` | Removes by index |
|
|
227
|
+
| `removeById(path, id)` | Removes by item ID |
|
|
228
|
+
| `updateByIndex(path, index, partial)` | Partially updates an item by index (shallow merge) |
|
|
229
|
+
| `updateById(path, id, partial)` | Partially updates an item by ID (shallow merge) |
|
|
230
|
+
| `moveItemByIndex(path, fromIndex, toIndex)` | Reorders by index |
|
|
231
|
+
| `moveById(path, fromId, toId)` | Reorders by ID |
|
|
232
|
+
| `getItemById(path, id)` | O(1) lookup by ID |
|
|
275
233
|
|
|
276
|
-
|
|
234
|
+
```ts
|
|
235
|
+
const { helpers } = useForm(schema);
|
|
277
236
|
|
|
278
|
-
|
|
237
|
+
helpers.addItem("users", { id: Date.now(), name: "", role: "guest" });
|
|
238
|
+
helpers.removeById("users", 123);
|
|
239
|
+
helpers.updateById("users", 123, { name: "Alice" }); // partial update — other fields preserved
|
|
240
|
+
helpers.moveById("users", 123, 456); // moves item 123 to where item 456 is
|
|
241
|
+
```
|
|
279
242
|
|
|
280
|
-
###
|
|
243
|
+
### Custom Array Identifier
|
|
281
244
|
|
|
282
|
-
|
|
245
|
+
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.
|
|
283
246
|
|
|
284
247
|
```ts
|
|
285
|
-
const
|
|
248
|
+
const { on, helpers, onArrayItemChange } = useForm(
|
|
249
|
+
{
|
|
250
|
+
employees: array()
|
|
251
|
+
.of(
|
|
252
|
+
object({
|
|
253
|
+
uuid: string().required(),
|
|
254
|
+
name: string().required(),
|
|
255
|
+
role: string().required(),
|
|
256
|
+
}),
|
|
257
|
+
)
|
|
258
|
+
.default([]),
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
arrayIdentifiers: {
|
|
262
|
+
employees: "uuid", // tells the library to use `uuid` instead of `id`
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
);
|
|
286
266
|
```
|
|
287
267
|
|
|
288
|
-
|
|
268
|
+
**Every ID-based function picks this up automatically:**
|
|
289
269
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
270
|
+
```ts
|
|
271
|
+
// on.* bindings — the second arg is the item identifier
|
|
272
|
+
<Input {...on.input("employees.name", employee.uuid)} label="Name" />
|
|
273
|
+
<Select {...on.select("employees.role", employee.uuid)} label="Role">...</Select>
|
|
274
|
+
|
|
275
|
+
// Manual setters — `id` is the item identifier
|
|
276
|
+
onArrayItemChange({ at: "employees.name", id: employee.uuid, value: "Alice" });
|
|
277
|
+
onArraySelectionChange({ at: "employees.role", id: employee.uuid, value: "admin" });
|
|
278
|
+
|
|
279
|
+
// Helpers — all `*ById` methods use the configured identifier
|
|
280
|
+
helpers.removeById("employees", employee.uuid);
|
|
281
|
+
helpers.updateById("employees", employee.uuid, { name: "Alice" }); // partial update
|
|
282
|
+
helpers.moveById("employees", fromUuid, toUuid);
|
|
283
|
+
helpers.getItemById("employees", employee.uuid);
|
|
284
|
+
```
|
|
294
285
|
|
|
295
|
-
|
|
286
|
+
> 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.
|
|
296
287
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
288
|
+
**Type inference:** `arrayIdentifiers` is typed so only valid scalar keys of each array element are accepted:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
// ✅ uuid is a string field on each employee
|
|
292
|
+
{
|
|
293
|
+
arrayIdentifiers: {
|
|
294
|
+
employees: "uuid";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ❌ TypeScript error — name is not a valid identifier (may not be unique)
|
|
299
|
+
{
|
|
300
|
+
arrayIdentifiers: {
|
|
301
|
+
employees: "name";
|
|
302
|
+
}
|
|
303
|
+
} // error if "name" is not in type
|
|
304
|
+
```
|
|
303
305
|
|
|
304
306
|
---
|
|
305
307
|
|
|
306
|
-
|
|
308
|
+
## Form Submission
|
|
307
309
|
|
|
308
|
-
|
|
310
|
+
### Option 1: `ControlledForm` (recommended)
|
|
309
311
|
|
|
310
|
-
|
|
311
|
-
| :-------------------------------- | :--------------------------------------------------------------------------- |
|
|
312
|
-
| **`on.input(path, [itemId])`** | Exports `{ id, isInvalid, errorMessage, value, onValueChange, onBlur }`. |
|
|
313
|
-
| **`on.select(path, [itemId])`** | Exports properties mapped for `<Select>` components matching `selectedKeys`. |
|
|
314
|
-
| **`on.autocomplete(path, [id])`** | Exports properties mapped for `<Autocomplete>` matching `selectedKey`. |
|
|
312
|
+
`ControlledForm` is a drop-in replacement for HeroUI's `<Form>`. It automatically validates on submit and only calls `onSubmit` if the schema passes.
|
|
315
313
|
|
|
316
|
-
|
|
314
|
+
```tsx
|
|
315
|
+
const { ControlledForm } = useForm(schema);
|
|
317
316
|
|
|
318
|
-
|
|
317
|
+
return (
|
|
318
|
+
<ControlledForm onSubmit={(data, event) => api.post("/submit", data)}>
|
|
319
|
+
<Input {...on.input("email")} label='Email' />
|
|
320
|
+
<Button type='submit'>Submit</Button>
|
|
321
|
+
</ControlledForm>
|
|
322
|
+
);
|
|
323
|
+
```
|
|
319
324
|
|
|
320
|
-
|
|
325
|
+
`data` is fully typed from your schema. No manual type annotations needed.
|
|
321
326
|
|
|
322
|
-
|
|
323
|
-
| :------------- | :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
324
|
-
| **`onSubmit`** | `(data, e) => void` | Invoked strictly post-validation of `schema` variables passing criteria. Both arguments `data` (your state payload) and `e` (the React event) are correctly typed. |
|
|
327
|
+
### Option 2: `onFormSubmit`
|
|
325
328
|
|
|
326
|
-
|
|
329
|
+
For use with plain HTML `<form>` elements:
|
|
327
330
|
|
|
328
|
-
|
|
331
|
+
```tsx
|
|
332
|
+
const { onFormSubmit } = useForm(schema);
|
|
329
333
|
|
|
330
|
-
|
|
334
|
+
const handleSubmit = onFormSubmit((data, event) => {
|
|
335
|
+
api.post("/submit", data);
|
|
336
|
+
});
|
|
331
337
|
|
|
332
|
-
|
|
338
|
+
return (
|
|
339
|
+
<form onSubmit={handleSubmit}>
|
|
340
|
+
<Input {...on.input("email")} label='Email' />
|
|
341
|
+
<button type='submit'>Submit</button>
|
|
342
|
+
</form>
|
|
343
|
+
);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Option 3: Inline handler with full type inference
|
|
347
|
+
|
|
348
|
+
When the schema is defined inline inside `useForm`, extract the handler type from `ControlledForm`:
|
|
333
349
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
350
|
+
```tsx
|
|
351
|
+
const { ControlledForm } = useForm({
|
|
352
|
+
email: string().required().default(""),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const handleSubmit: React.ComponentProps<typeof ControlledForm>["onSubmit"] = (
|
|
356
|
+
data,
|
|
357
|
+
event,
|
|
358
|
+
) => {
|
|
359
|
+
// data.email is correctly typed as string — no schema re-declaration needed
|
|
360
|
+
console.log(data.email);
|
|
361
|
+
};
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## API Reference
|
|
367
|
+
|
|
368
|
+
### `useForm(schema, options?)`
|
|
369
|
+
|
|
370
|
+
| Parameter | Type | Description |
|
|
371
|
+
| --------- | ---------------------------- | -------------------------------------------------------- |
|
|
372
|
+
| `schema` | `Record<string, ConfigType>` | Yup schemas or primitive values defining your form shape |
|
|
373
|
+
| `options` | `FormOptions` | Optional configuration |
|
|
374
|
+
|
|
375
|
+
#### `FormOptions`
|
|
376
|
+
|
|
377
|
+
| Option | Type | Description |
|
|
378
|
+
| ------------------ | ------------------------ | --------------------------------------------------------- |
|
|
379
|
+
| `arrayIdentifiers` | `Record<string, string>` | Override the ID field for object arrays (default: `"id"`) |
|
|
380
|
+
| `resetOnSubmit` | `boolean` | Reset form to defaults after successful submit |
|
|
381
|
+
| `onFormSubmit` | `Function` | Default submit handler passed via options |
|
|
382
|
+
| `keepValues` | `(keyof State)[]` | Fields to preserve during reset |
|
|
383
|
+
|
|
384
|
+
### `useForm` returns
|
|
385
|
+
|
|
386
|
+
| Property | Type | Description |
|
|
387
|
+
| ------------------------ | ----------------------------- | -------------------------------------------------- |
|
|
388
|
+
| `state` | `InferState<S>` | Current form values |
|
|
389
|
+
| `setState` | `Dispatch<SetStateAction<S>>` | Direct state setter |
|
|
390
|
+
| `metadata` | `Map<string, FieldMetadata>` | Per-field `isTouched`, `isInvalid`, `errorMessage` |
|
|
391
|
+
| `on` | `OnMethods<S>` | Component binding methods |
|
|
392
|
+
| `helpers` | `HelpersFunc<S>` | Array manipulation methods |
|
|
393
|
+
| `isDirty` | `boolean` | `true` if any field has been touched |
|
|
394
|
+
| `onFieldChange` | Function | Manual scalar field update |
|
|
395
|
+
| `onArrayItemChange` | Function | Manual array item field update |
|
|
396
|
+
| `onSelectionChange` | Function | Manual scalar selection update |
|
|
397
|
+
| `onArraySelectionChange` | Function | Manual array item selection update |
|
|
398
|
+
| `onFieldBlur` | Function | Manual blur trigger |
|
|
399
|
+
| `onFormReset` | Function | Resets state and clears metadata |
|
|
400
|
+
| `onFormSubmit` | Function | Wraps a submit handler with validation |
|
|
401
|
+
| `ControlledForm` | Component | Validated `<Form>` wrapper |
|
|
343
402
|
|
|
344
403
|
---
|
|
345
404
|
|
|
346
|
-
|
|
405
|
+
## Localization
|
|
406
|
+
|
|
407
|
+
Validation error messages are automatically localized. The library reads a `LOCALE` cookie on initialization.
|
|
347
408
|
|
|
348
|
-
|
|
409
|
+
- **Supported**: `en` (English — default), `es` (Spanish)
|
|
410
|
+
- **Setting the locale**:
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
document.cookie = "LOCALE=es; path=/; max-age=31536000";
|
|
414
|
+
```
|
|
349
415
|
|
|
350
|
-
|
|
351
|
-
| :---------------- | :---------------------------------- | :------------------------------------------------------------------ |
|
|
352
|
-
| **`state`** | `InferState<S>` | Evaluated state values matching object signatures defined. |
|
|
353
|
-
| **`setState`** | `Dispatch<SetStateAction>` | The fundamental state mutation. |
|
|
354
|
-
| **`onFormReset`** | `(opts?: { keepValues: string[] })` | Discards invalidity layers and remounts variables back to defaults. |
|
|
355
|
-
| **`isDirty`** | `boolean` | Truthy flag reflecting if state parameters altered. |
|
|
356
|
-
| **`metadata`** | `Map<string, FieldMetadata>` | Touched and Error metadata instances tracked. |
|
|
416
|
+
If no cookie is found, or the value is unrecognized, English is used.
|
|
357
417
|
|
|
358
418
|
---
|
|
359
419
|
|