@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 CHANGED
@@ -1,40 +1,24 @@
1
1
  # @juantroconisf/lib
2
2
 
3
- A powerful, type-safe form management and validation library tailored for **HeroUI** and **React**.
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
- - [Usage Guide](#usage-guide)
11
+ - [Core Concepts](#core-concepts)
12
+ - [Schema Definition](#schema-definition)
16
13
  - [Scalar Fields](#scalar-fields)
17
14
  - [Nested Objects](#nested-objects)
18
- - [Managing Arrays](#managing-arrays)
19
- - [Form Submission](#form-submission)
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
- - [useForm](#useform)
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, number, array, object, boolean } from "yup";
70
- import { Input, Switch, Select, SelectItem, Button } from "@heroui/react";
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
- // 2. Initialize the hook with fully inferred types
92
- const { on, state, helpers, ControlledForm } = useForm(mySchema);
93
-
94
- // 3. Types are perfectly inferred from your Yup schema
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={handleSubmit} className='flex flex-col gap-4'>
104
- {/* Scalar Fields */}
47
+ <ControlledForm onSubmit={(data) => console.log(data)}>
105
48
  <Input {...on.input("fullName")} label='Full Name' />
106
- <Input {...on.input("age")} type='number' label='Age' />
107
-
108
- {/* Nested Object Fields */}
109
- <Switch {...on.input("settings.notifications")}>
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
- ## Usage Guide
60
+ ---
157
61
 
158
- All examples below continue referencing the `mySchema` definition from the Quick Start.
62
+ ## Core Concepts
159
63
 
160
- ### Scalar Fields
64
+ ### Schema Definition
161
65
 
162
- Scalar fields map directly to your schema definitions. Use `on.input`, `on.select`, or `on.autocomplete` to yield binding props instantly compatible with HeroUI.
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
- ```tsx
165
- <Input {...on.input("fullName")} label="Full Name" />
166
- <Input {...on.input("age")} type="number" label="Age" />
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
- ### Nested Objects
83
+ ### Scalar Fields
170
84
 
171
- Nested state is managed intuitively via dot-notation directly on your identifiers. Your TypeScript types will validate that the path points to real endpoints on your data shape.
85
+ Top-level primitive fields. Use dot notation to reach into nested objects.
172
86
 
173
87
  ```tsx
174
- // Binds seamlessly to state.settings.theme
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
- ### Managing Arrays
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
- **Binding UI to Items:**
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
- // ✅ Correct: Binds perfectly regardless of list index changes
193
- on.select("users.role", userId);
194
-
195
- // ❌ Warning (Unless primitives array): Prevents index-shifting stability
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
- **Using the `helpers` Library:**
200
- Extract `helpers` out of `useForm(...)` to manipulate array state securely.
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
- const { helpers } = useForm(mySchema);
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
- // Add an item (Optionally provide index as a 3rd argument to insert)
206
- helpers.addItem("users", { id: Date.now(), role: "guest" });
134
+ ---
207
135
 
208
- // Remove item by ID
209
- helpers.removeById("users", 12345);
136
+ ## The `on` API — Component Bindings
210
137
 
211
- // Reorder items by ID
212
- helpers.moveById("users", fromId, toId);
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
- ### Form Submission
145
+ All methods support both **scalar/nested** paths and **array item** paths (composite `"array.field"` + `itemId`).
216
146
 
217
- There are two primary ways to submit, validate, and handle your logic, both fully type-safe.
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
- **1. The `ControlledForm` Wrapper (Recommended)**
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
- `ControlledForm` behaves exactly like a HeroUI `Form` component. It automatically intercepts submit events, validates all bindings through your schema, and invokes `onSubmit` with the validated dataset.
159
+ ### Examples
222
160
 
223
161
  ```tsx
224
- const { ControlledForm } = useForm(mySchema);
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
- return (
227
- <ControlledForm onSubmit={(data, event) => console.log(data.fullName)}>
228
- <Input {...on.input("fullName")} />
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
- **2. The `onFormSubmit` Handler Wrapper**
189
+ ---
235
190
 
236
- If you prefer using native HTML `<form>` tags, you can pass your callback into `onFormSubmit`. The params exposed to your callback are tightly inferred from the schema.
191
+ ## Manual Updates
237
192
 
238
- ```tsx
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
- // Automatically infers your final structured data (data) and the React form event (e)
242
- const submitHandler = onFormSubmit((data, e) => {
243
- // Validation passed successfully!
244
- // 'data.settings.theme' is correctly typed as string.
245
- api.post("/endpoint", data);
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
- // Used manually on form tags or button invocations
249
- <form onSubmit={submitHandler}>{/* inputs */}</form>;
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
- **3. External Handler Definitions**
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
- If you define your schemas inline inside `useForm( { ... } )` but want to extract your `submit` handler natively without destructing variables you aren't using:
217
+ ---
255
218
 
256
- ```tsx
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
- // 2. Type your extract handler capturing React.ComponentProps directly on the wrapper
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
- // 3. Destructure whatever you need right on your JSX render
272
- return <ControlledForm onSubmit={handleSubmit}>...</ControlledForm>;
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
- ## API Reference
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
- ### `useForm`
243
+ ### Custom Array Identifier
281
244
 
282
- Instantiates state and schema configurations.
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 formApi = useForm(schema, options?);
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
- #### Arguments
268
+ **Every ID-based function picks this up automatically:**
289
269
 
290
- | Parameter | Type | Description |
291
- | :------------ | :--------------------------- | :------------------------------------------------------------------------------- |
292
- | **`schema`** | `Record<string, ConfigType>` | Your form shape configuration containing `Yup` schemas or primitive constraints. |
293
- | **`options`** | `FormOptions` | Configuration behaviors. |
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
- #### Options
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
- | Option | Type | Description |
298
- | :--------------------- | :--------- | :------------------------------------------------------------------------------ |
299
- | **`arrayIdentifiers`** | `Object` | Identifier maps for objects missing `id` (e.g. `{ users: "uuid" }`). |
300
- | **`resetOnSubmit`** | `boolean` | Instantly resets all values tracking arrays to initialization configurations. |
301
- | **`onFormSubmit`** | `Function` | Fallback default submission pipeline. |
302
- | **`keepValues`** | `string[]` | Keys in the root state to preserve during a reset (e.g. `["settings", "age"]`). |
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
- ### The `on` API
308
+ ## Form Submission
307
309
 
308
- `on` hooks dynamically assign value tracks, validity checks, error text extractions, and update invocations back to the controller.
310
+ ### Option 1: `ControlledForm` (recommended)
309
311
 
310
- | Method | Description |
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
- ### ControlledForm
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
- A Drop-in `<Form />` equivalent handling submit event delegation natively linked to validation triggers internally.
325
+ `data` is fully typed from your schema. No manual type annotations needed.
321
326
 
322
- | Prop | Type | Description |
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
- _Notes: Spreads any traditional `<form>` properties to standard HeroUI mappings out of the box._
329
+ For use with plain HTML `<form>` elements:
327
330
 
328
- ---
331
+ ```tsx
332
+ const { onFormSubmit } = useForm(schema);
329
333
 
330
- ### Array Helpers
334
+ const handleSubmit = onFormSubmit((data, event) => {
335
+ api.post("/submit", data);
336
+ });
331
337
 
332
- Array adjustments exported off the `helpers` map:
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
- | Utility | Description |
335
- | :--------------------------------- | :---------------------------------------------------------------------------- |
336
- | **`addItem(path, item, index?)`** | Submits entries safely. |
337
- | **`removeItem(path, index)`** | Excludes entry mapped at index map offset. |
338
- | **`removeById(path, id)`** | Standard and secure ID withdrawal. |
339
- | **`updateItem(path, index, val)`** | Hot swap content without re-render destruction. |
340
- | **`moveItem(path, current, to)`** | Re-order index slots. |
341
- | **`moveById(path, fromId, toId)`** | Shift lists seamlessly referencing fixed ID targets. |
342
- | **`getItem(path, id)`** | Read items immediately via O(1) indexed lookups without array mapping cycles. |
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
- ### Form Controls
405
+ ## Localization
406
+
407
+ Validation error messages are automatically localized. The library reads a `LOCALE` cookie on initialization.
347
408
 
348
- Additional utilities available directly out of `useForm`:
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
- | Method | Signatures / Types | Info |
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