@juantroconisf/lib 9.4.0 → 10.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,344 @@ 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**
235
-
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.
237
-
238
- ```tsx
239
- const { onFormSubmit, state } = useForm(mySchema);
189
+ ---
240
190
 
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
- });
191
+ ## Manual Updates
247
192
 
248
- // Used manually on form tags or button invocations
249
- <form onSubmit={submitHandler}>{/* inputs */}</form>;
250
- ```
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`.
251
194
 
252
- **3. External Handler Definitions**
195
+ | Function | Use for | Signature |
196
+ | ------------------------ | ------------------------------- | -------------------------------- |
197
+ | `onFieldChange` | Scalar / nested object field | `(id, value)` |
198
+ | `onArrayItemChange` | Object array item field | `("array.field", itemId, value)` |
199
+ | `onSelectionChange` | Scalar / nested selection field | `(id, value)` |
200
+ | `onArraySelectionChange` | Array item selection field | `("array.field", itemId, value)` |
253
201
 
254
- If you define your schemas inline inside `useForm( { ... } )` but want to extract your `submit` handler natively without destructing variables you aren't using:
202
+ ```ts
203
+ const { onFieldChange, onArrayItemChange, onSelectionChange } = useForm(schema);
255
204
 
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
- });
205
+ // Update a scalar field
206
+ onFieldChange("email", "user@example.com");
262
207
 
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
- };
208
+ // Update a field inside an array item
209
+ onArrayItemChange("users.name", userId, "Alice");
270
210
 
271
- // 3. Destructure whatever you need right on your JSX render
272
- return <ControlledForm onSubmit={handleSubmit}>...</ControlledForm>;
273
- };
211
+ // Update a selection field
212
+ onSelectionChange("theme", "dark");
274
213
  ```
275
214
 
215
+ > **Note:** `onFieldChange` only handles scalar and nested object paths. Use `onArrayItemChange` for array items — they each have a unique signature so TypeScript will guide you.
216
+
276
217
  ---
277
218
 
278
- ## API Reference
219
+ ## Array Helpers
279
220
 
280
- ### `useForm`
221
+ Extracted from `helpers` on the `useForm` return value. Items in object arrays are tracked by ID (default field: `id`).
281
222
 
282
- Instantiates state and schema configurations.
223
+ | Method | Description |
224
+ | ------------------------------------ | -------------------------------------------------- |
225
+ | `addItem(path, item, index?)` | Adds an item to the end (or at `index`) |
226
+ | `removeItem(path, index)` | Removes by index |
227
+ | `removeById(path, id)` | Removes by item ID |
228
+ | `updateItem(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
+ | `moveItem(path, fromIndex, toIndex)` | Reorders by index |
231
+ | `moveById(path, fromId, toId)` | Reorders by ID |
232
+ | `getItem(path, id)` | O(1) lookup by ID |
283
233
 
284
234
  ```ts
285
- const formApi = useForm(schema, options?);
235
+ const { helpers } = useForm(schema);
236
+
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
286
241
  ```
287
242
 
288
- #### Arguments
243
+ ### Custom Array Identifier
289
244
 
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. |
245
+ If your array items don't use an `id` field, specify the key via `arrayIdentifiers`:
294
246
 
295
- #### Options
247
+ ```ts
248
+ const { helpers } = useForm(
249
+ {
250
+ employees: array()
251
+ .of(object({ uuid: string(), name: string() }))
252
+ .default([]),
253
+ },
254
+ { arrayIdentifiers: { employees: "uuid" } },
255
+ );
296
256
 
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"]`). |
257
+ helpers.removeById("employees", "some-uuid-string");
258
+ ```
303
259
 
304
260
  ---
305
261
 
306
- ### The `on` API
262
+ ## Form Submission
307
263
 
308
- `on` hooks dynamically assign value tracks, validity checks, error text extractions, and update invocations back to the controller.
264
+ ### Option 1: `ControlledForm` (recommended)
309
265
 
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`. |
266
+ `ControlledForm` is a drop-in replacement for HeroUI's `<Form>`. It automatically validates on submit and only calls `onSubmit` if the schema passes.
315
267
 
316
- ---
268
+ ```tsx
269
+ const { ControlledForm } = useForm(schema);
270
+
271
+ return (
272
+ <ControlledForm onSubmit={(data, event) => api.post("/submit", data)}>
273
+ <Input {...on.input("email")} label='Email' />
274
+ <Button type='submit'>Submit</Button>
275
+ </ControlledForm>
276
+ );
277
+ ```
278
+
279
+ `data` is fully typed from your schema. No manual type annotations needed.
317
280
 
318
- ### ControlledForm
281
+ ### Option 2: `onFormSubmit`
319
282
 
320
- A Drop-in `<Form />` equivalent handling submit event delegation natively linked to validation triggers internally.
283
+ For use with plain HTML `<form>` elements:
321
284
 
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. |
285
+ ```tsx
286
+ const { onFormSubmit } = useForm(schema);
325
287
 
326
- _Notes: Spreads any traditional `<form>` properties to standard HeroUI mappings out of the box._
288
+ const handleSubmit = onFormSubmit((data, event) => {
289
+ api.post("/submit", data);
290
+ });
327
291
 
328
- ---
292
+ return (
293
+ <form onSubmit={handleSubmit}>
294
+ <Input {...on.input("email")} label='Email' />
295
+ <button type='submit'>Submit</button>
296
+ </form>
297
+ );
298
+ ```
299
+
300
+ ### Option 3: Inline handler with full type inference
301
+
302
+ When the schema is defined inline inside `useForm`, extract the handler type from `ControlledForm`:
303
+
304
+ ```tsx
305
+ const { ControlledForm } = useForm({
306
+ email: string().required().default(""),
307
+ });
308
+
309
+ const handleSubmit: React.ComponentProps<typeof ControlledForm>["onSubmit"] = (
310
+ data,
311
+ event,
312
+ ) => {
313
+ // data.email is correctly typed as string — no schema re-declaration needed
314
+ console.log(data.email);
315
+ };
316
+ ```
329
317
 
330
- ### Array Helpers
318
+ ---
331
319
 
332
- Array adjustments exported off the `helpers` map:
320
+ ## API Reference
333
321
 
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. |
322
+ ### `useForm(schema, options?)`
323
+
324
+ | Parameter | Type | Description |
325
+ | --------- | ---------------------------- | -------------------------------------------------------- |
326
+ | `schema` | `Record<string, ConfigType>` | Yup schemas or primitive values defining your form shape |
327
+ | `options` | `FormOptions` | Optional configuration |
328
+
329
+ #### `FormOptions`
330
+
331
+ | Option | Type | Description |
332
+ | ------------------ | ------------------------ | --------------------------------------------------------- |
333
+ | `arrayIdentifiers` | `Record<string, string>` | Override the ID field for object arrays (default: `"id"`) |
334
+ | `resetOnSubmit` | `boolean` | Reset form to defaults after successful submit |
335
+ | `onFormSubmit` | `Function` | Default submit handler passed via options |
336
+ | `keepValues` | `(keyof State)[]` | Fields to preserve during reset |
337
+
338
+ ### `useForm` returns
339
+
340
+ | Property | Type | Description |
341
+ | ------------------------ | ----------------------------- | -------------------------------------------------- |
342
+ | `state` | `InferState<S>` | Current form values |
343
+ | `setState` | `Dispatch<SetStateAction<S>>` | Direct state setter |
344
+ | `metadata` | `Map<string, FieldMetadata>` | Per-field `isTouched`, `isInvalid`, `errorMessage` |
345
+ | `on` | `OnMethods<S>` | Component binding methods |
346
+ | `helpers` | `HelpersFunc<S>` | Array manipulation methods |
347
+ | `isDirty` | `boolean` | `true` if any field has been touched |
348
+ | `onFieldChange` | Function | Manual scalar field update |
349
+ | `onArrayItemChange` | Function | Manual array item field update |
350
+ | `onSelectionChange` | Function | Manual scalar selection update |
351
+ | `onArraySelectionChange` | Function | Manual array item selection update |
352
+ | `onFieldBlur` | Function | Manual blur trigger |
353
+ | `onFormReset` | Function | Resets state and clears metadata |
354
+ | `onFormSubmit` | Function | Wraps a submit handler with validation |
355
+ | `ControlledForm` | Component | Validated `<Form>` wrapper |
343
356
 
344
357
  ---
345
358
 
346
- ### Form Controls
359
+ ## Localization
360
+
361
+ Validation error messages are automatically localized. The library reads a `LOCALE` cookie on initialization.
347
362
 
348
- Additional utilities available directly out of `useForm`:
363
+ - **Supported**: `en` (English default), `es` (Spanish)
364
+ - **Setting the locale**:
365
+
366
+ ```ts
367
+ document.cookie = "LOCALE=es; path=/; max-age=31536000";
368
+ ```
349
369
 
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. |
370
+ If no cookie is found, or the value is unrecognized, English is used.
357
371
 
358
372
  ---
359
373
 
package/dist/index.d.mts CHANGED
@@ -76,12 +76,19 @@ interface ComponentInputProps {
76
76
  onBlur: () => void;
77
77
  isInvalid: boolean;
78
78
  errorMessage: string;
79
+ /** Derived from the Yup schema — true when the field is marked .required(). */
80
+ isRequired: boolean;
79
81
  }
80
- /** Props returned by on.input() */
82
+ /** Props returned by on.input() — for Input and Textarea */
81
83
  interface ItemInputProps<V = any> extends ComponentInputProps {
82
84
  onValueChange: (value: V) => void;
83
85
  value: V;
84
86
  }
87
+ /** Props returned by on.numberInput() — for NumberInput */
88
+ interface ItemNumberInputProps extends ComponentInputProps {
89
+ onValueChange: (value: number) => void;
90
+ value: number;
91
+ }
85
92
  /** Props returned by on.select() */
86
93
  interface ItemSelectProps extends ComponentInputProps {
87
94
  onSelectionChange: NonNullable<SelectProps["onSelectionChange"]>;
@@ -92,6 +99,16 @@ interface ItemAutocompleteProps extends ComponentInputProps {
92
99
  onSelectionChange: NonNullable<AutocompleteProps["onSelectionChange"]>;
93
100
  selectedKey: SingleSelection["selectedKey"];
94
101
  }
102
+ /** Props returned by on.checkbox() and on.switch() */
103
+ interface ItemToggleProps extends ComponentInputProps {
104
+ onValueChange: (isSelected: boolean) => void;
105
+ isSelected: boolean;
106
+ }
107
+ /** Props returned by on.radio() — for RadioGroup */
108
+ interface ItemRadioGroupProps extends ComponentInputProps {
109
+ onValueChange: (value: string) => void;
110
+ value: string;
111
+ }
95
112
  /**
96
113
  * Interface for the polymorphic 'on' method handlers.
97
114
  */
@@ -102,6 +119,10 @@ interface OnMethods<O extends StateType> {
102
119
  input<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemInputProps<any>;
103
120
  /** Registers a primitive array element by index. */
104
121
  input<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemInputProps<any>;
122
+ /** Registers a numeric field for NumberInput. */
123
+ numberInput<P extends AllPaths<O>>(id: P): ItemNumberInputProps;
124
+ /** Registers a numeric field within an object array element. */
125
+ numberInput<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemNumberInputProps;
105
126
  /** Registers a scalar or nested object field. */
106
127
  select<P extends AllPaths<O>>(id: P): ItemSelectProps;
107
128
  /** Registers a complete array field for multi-selection. */
@@ -116,6 +137,18 @@ interface OnMethods<O extends StateType> {
116
137
  autocomplete<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemAutocompleteProps;
117
138
  /** Registers a primitive array element by index. */
118
139
  autocomplete<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemAutocompleteProps;
140
+ /** Registers a boolean field for Checkbox. */
141
+ checkbox<P extends AllPaths<O>>(id: P): ItemToggleProps;
142
+ /** Registers a boolean field within an object array element for Checkbox. */
143
+ checkbox<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemToggleProps;
144
+ /** Registers a boolean field for Switch. */
145
+ switch<P extends AllPaths<O>>(id: P): ItemToggleProps;
146
+ /** Registers a boolean field within an object array element for Switch. */
147
+ switch<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemToggleProps;
148
+ /** Registers a string field for RadioGroup. */
149
+ radio<P extends AllPaths<O>>(id: P): ItemRadioGroupProps;
150
+ /** Registers a string field within an object array element for RadioGroup. */
151
+ radio<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemRadioGroupProps;
119
152
  }
120
153
  /**
121
154
  * Recursive type to find all paths to arrays in the state.
@@ -123,10 +156,6 @@ interface OnMethods<O extends StateType> {
123
156
  type ArrayPaths<T> = T extends object ? {
124
157
  [K in keyof T & string]: NonNullable<T[K]> extends (infer E)[] ? K | (E extends object ? `${K}.${ArrayPaths<E>}` : never) : NonNullable<T[K]> extends object ? NonNullable<T[K]> extends Date ? never : `${K}.${ArrayPaths<NonNullable<T[K]>>}` : never;
125
158
  }[keyof T & string] : never;
126
- /** Keys whose values are arrays of objects (not primitives). */
127
- type ObjectArrayKeys<O extends StateType> = {
128
- [K in keyof O]: O[K] extends Record<string, any>[] ? K : never;
129
- }[keyof O];
130
159
  /**
131
160
  * Helper to get paths for fields within object arrays.
132
161
  * Returns paths like "arrayKey.fieldName".
@@ -135,8 +164,6 @@ type ObjectArrayFieldPaths<O extends StateType> = {
135
164
  [K in keyof O]: O[K] extends Record<string, any>[] ? `${K & string}.${FieldPaths<ArrayElement<O[K]>>}` : never;
136
165
  }[keyof O];
137
166
  type ArrayElement<T> = T extends (infer E)[] ? E : never;
138
- /** Resolves the type of the identifier field for an array element (defaults to "id"). */
139
- type ItemIdType<O extends StateType, K extends keyof O> = "id" extends keyof ArrayElement<O[K]> ? ArrayElement<O[K]>["id"] : string | number;
140
167
  type NestedFieldValue<T, F extends string> = F extends `${infer First}.${infer Rest}` ? First extends keyof T ? NestedFieldValue<NonNullable<T[First]>, Rest> : NonNullable<T> extends (infer U)[] ? NestedFieldValue<U, F> : any : F extends keyof T ? NonNullable<T[F]> : NonNullable<T> extends (infer U)[] ? F extends keyof U ? NonNullable<U[F]> : any : any;
141
168
  type FieldPaths<T> = T extends Record<string, any> ? {
142
169
  [K in keyof T & string]: T[K] extends any[] ? K : T[K] extends Record<string, any> ? `${K}.${FieldPaths<T[K]>}` : K;
@@ -159,8 +186,10 @@ interface HelpersFunc<O extends StateType> {
159
186
  removeItem: <K extends ArrayPaths<O>>(arrayKey: K, index: number) => void;
160
187
  /** Removes an item from an array by its unique identifier. */
161
188
  removeById: <K extends ArrayPaths<O>>(arrayKey: K, itemId: string | number) => void;
162
- /** Replaces an item in an array at the given index. */
163
- updateItem: <K extends ArrayPaths<O>>(arrayKey: K, index: number, value: NestedFieldValue<O, K> extends (infer E)[] ? E : never) => void;
189
+ /** Updates an item in an array at the given index (supports partial updates). */
190
+ updateItem: <K extends ArrayPaths<O>>(arrayKey: K, index: number, value: NestedFieldValue<O, K> extends (infer E)[] ? Partial<E> : never) => void;
191
+ /** Updates an item in an array by its unique identifier (supports partial updates). */
192
+ updateById: <K extends ArrayPaths<O>>(arrayKey: K, itemId: string | number, value: NestedFieldValue<O, K> extends (infer E)[] ? Partial<E> : never) => void;
164
193
  /** Moves an item within an array using indices. */
165
194
  moveItem: <K extends ArrayPaths<O>>(arrayKey: K, from: number, to: number) => void;
166
195
  /** Moves an item within an array using unique identifiers. */
@@ -176,13 +205,12 @@ interface UseFormResponse<O extends StateType> {
176
205
  onFieldBlur: BlurFunc<O>;
177
206
  /** Updates a scalar or nested object field. */
178
207
  onFieldChange<P extends AllPaths<O>>(id: P, value: NestedFieldValue<O, P & string>): void;
179
- /** Updates an object array element's field using composite syntax "array.field". */
180
- onFieldChange<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number, value: any): void;
181
- /** Updates an object array element's field by ID (explicit array key, field, and value). */
182
- onFieldChange<K extends ObjectArrayKeys<O>, F extends keyof ArrayElement<O[K]> & string>(arrayKey: K, itemId: ItemIdType<O, K>, field: F, value: ArrayElement<O[K]>[F]): void;
183
- /** Updates a primitive array element by index. */
184
- onFieldChange<K extends ArrayPaths<O>>(arrayKey: K, index: number, value: any): void;
208
+ /** Manually updates a field within an object array item using "array.field" composite path. */
209
+ onArrayItemChange<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number, value: any): void;
210
+ /** Manually updates a scalar or nested object selection field. */
185
211
  onSelectionChange: ValueChangeFunc<O, keyof O>;
212
+ /** Manually updates a selection-based field within an object array item. */
213
+ onArraySelectionChange<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number, value: any): void;
186
214
  state: O;
187
215
  setState: React.Dispatch<React.SetStateAction<O>>;
188
216
  metadata: MetadataType;
package/dist/index.d.ts CHANGED
@@ -76,12 +76,19 @@ interface ComponentInputProps {
76
76
  onBlur: () => void;
77
77
  isInvalid: boolean;
78
78
  errorMessage: string;
79
+ /** Derived from the Yup schema — true when the field is marked .required(). */
80
+ isRequired: boolean;
79
81
  }
80
- /** Props returned by on.input() */
82
+ /** Props returned by on.input() — for Input and Textarea */
81
83
  interface ItemInputProps<V = any> extends ComponentInputProps {
82
84
  onValueChange: (value: V) => void;
83
85
  value: V;
84
86
  }
87
+ /** Props returned by on.numberInput() — for NumberInput */
88
+ interface ItemNumberInputProps extends ComponentInputProps {
89
+ onValueChange: (value: number) => void;
90
+ value: number;
91
+ }
85
92
  /** Props returned by on.select() */
86
93
  interface ItemSelectProps extends ComponentInputProps {
87
94
  onSelectionChange: NonNullable<SelectProps["onSelectionChange"]>;
@@ -92,6 +99,16 @@ interface ItemAutocompleteProps extends ComponentInputProps {
92
99
  onSelectionChange: NonNullable<AutocompleteProps["onSelectionChange"]>;
93
100
  selectedKey: SingleSelection["selectedKey"];
94
101
  }
102
+ /** Props returned by on.checkbox() and on.switch() */
103
+ interface ItemToggleProps extends ComponentInputProps {
104
+ onValueChange: (isSelected: boolean) => void;
105
+ isSelected: boolean;
106
+ }
107
+ /** Props returned by on.radio() — for RadioGroup */
108
+ interface ItemRadioGroupProps extends ComponentInputProps {
109
+ onValueChange: (value: string) => void;
110
+ value: string;
111
+ }
95
112
  /**
96
113
  * Interface for the polymorphic 'on' method handlers.
97
114
  */
@@ -102,6 +119,10 @@ interface OnMethods<O extends StateType> {
102
119
  input<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemInputProps<any>;
103
120
  /** Registers a primitive array element by index. */
104
121
  input<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemInputProps<any>;
122
+ /** Registers a numeric field for NumberInput. */
123
+ numberInput<P extends AllPaths<O>>(id: P): ItemNumberInputProps;
124
+ /** Registers a numeric field within an object array element. */
125
+ numberInput<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemNumberInputProps;
105
126
  /** Registers a scalar or nested object field. */
106
127
  select<P extends AllPaths<O>>(id: P): ItemSelectProps;
107
128
  /** Registers a complete array field for multi-selection. */
@@ -116,6 +137,18 @@ interface OnMethods<O extends StateType> {
116
137
  autocomplete<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemAutocompleteProps;
117
138
  /** Registers a primitive array element by index. */
118
139
  autocomplete<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemAutocompleteProps;
140
+ /** Registers a boolean field for Checkbox. */
141
+ checkbox<P extends AllPaths<O>>(id: P): ItemToggleProps;
142
+ /** Registers a boolean field within an object array element for Checkbox. */
143
+ checkbox<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemToggleProps;
144
+ /** Registers a boolean field for Switch. */
145
+ switch<P extends AllPaths<O>>(id: P): ItemToggleProps;
146
+ /** Registers a boolean field within an object array element for Switch. */
147
+ switch<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemToggleProps;
148
+ /** Registers a string field for RadioGroup. */
149
+ radio<P extends AllPaths<O>>(id: P): ItemRadioGroupProps;
150
+ /** Registers a string field within an object array element for RadioGroup. */
151
+ radio<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemRadioGroupProps;
119
152
  }
120
153
  /**
121
154
  * Recursive type to find all paths to arrays in the state.
@@ -123,10 +156,6 @@ interface OnMethods<O extends StateType> {
123
156
  type ArrayPaths<T> = T extends object ? {
124
157
  [K in keyof T & string]: NonNullable<T[K]> extends (infer E)[] ? K | (E extends object ? `${K}.${ArrayPaths<E>}` : never) : NonNullable<T[K]> extends object ? NonNullable<T[K]> extends Date ? never : `${K}.${ArrayPaths<NonNullable<T[K]>>}` : never;
125
158
  }[keyof T & string] : never;
126
- /** Keys whose values are arrays of objects (not primitives). */
127
- type ObjectArrayKeys<O extends StateType> = {
128
- [K in keyof O]: O[K] extends Record<string, any>[] ? K : never;
129
- }[keyof O];
130
159
  /**
131
160
  * Helper to get paths for fields within object arrays.
132
161
  * Returns paths like "arrayKey.fieldName".
@@ -135,8 +164,6 @@ type ObjectArrayFieldPaths<O extends StateType> = {
135
164
  [K in keyof O]: O[K] extends Record<string, any>[] ? `${K & string}.${FieldPaths<ArrayElement<O[K]>>}` : never;
136
165
  }[keyof O];
137
166
  type ArrayElement<T> = T extends (infer E)[] ? E : never;
138
- /** Resolves the type of the identifier field for an array element (defaults to "id"). */
139
- type ItemIdType<O extends StateType, K extends keyof O> = "id" extends keyof ArrayElement<O[K]> ? ArrayElement<O[K]>["id"] : string | number;
140
167
  type NestedFieldValue<T, F extends string> = F extends `${infer First}.${infer Rest}` ? First extends keyof T ? NestedFieldValue<NonNullable<T[First]>, Rest> : NonNullable<T> extends (infer U)[] ? NestedFieldValue<U, F> : any : F extends keyof T ? NonNullable<T[F]> : NonNullable<T> extends (infer U)[] ? F extends keyof U ? NonNullable<U[F]> : any : any;
141
168
  type FieldPaths<T> = T extends Record<string, any> ? {
142
169
  [K in keyof T & string]: T[K] extends any[] ? K : T[K] extends Record<string, any> ? `${K}.${FieldPaths<T[K]>}` : K;
@@ -159,8 +186,10 @@ interface HelpersFunc<O extends StateType> {
159
186
  removeItem: <K extends ArrayPaths<O>>(arrayKey: K, index: number) => void;
160
187
  /** Removes an item from an array by its unique identifier. */
161
188
  removeById: <K extends ArrayPaths<O>>(arrayKey: K, itemId: string | number) => void;
162
- /** Replaces an item in an array at the given index. */
163
- updateItem: <K extends ArrayPaths<O>>(arrayKey: K, index: number, value: NestedFieldValue<O, K> extends (infer E)[] ? E : never) => void;
189
+ /** Updates an item in an array at the given index (supports partial updates). */
190
+ updateItem: <K extends ArrayPaths<O>>(arrayKey: K, index: number, value: NestedFieldValue<O, K> extends (infer E)[] ? Partial<E> : never) => void;
191
+ /** Updates an item in an array by its unique identifier (supports partial updates). */
192
+ updateById: <K extends ArrayPaths<O>>(arrayKey: K, itemId: string | number, value: NestedFieldValue<O, K> extends (infer E)[] ? Partial<E> : never) => void;
164
193
  /** Moves an item within an array using indices. */
165
194
  moveItem: <K extends ArrayPaths<O>>(arrayKey: K, from: number, to: number) => void;
166
195
  /** Moves an item within an array using unique identifiers. */
@@ -176,13 +205,12 @@ interface UseFormResponse<O extends StateType> {
176
205
  onFieldBlur: BlurFunc<O>;
177
206
  /** Updates a scalar or nested object field. */
178
207
  onFieldChange<P extends AllPaths<O>>(id: P, value: NestedFieldValue<O, P & string>): void;
179
- /** Updates an object array element's field using composite syntax "array.field". */
180
- onFieldChange<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number, value: any): void;
181
- /** Updates an object array element's field by ID (explicit array key, field, and value). */
182
- onFieldChange<K extends ObjectArrayKeys<O>, F extends keyof ArrayElement<O[K]> & string>(arrayKey: K, itemId: ItemIdType<O, K>, field: F, value: ArrayElement<O[K]>[F]): void;
183
- /** Updates a primitive array element by index. */
184
- onFieldChange<K extends ArrayPaths<O>>(arrayKey: K, index: number, value: any): void;
208
+ /** Manually updates a field within an object array item using "array.field" composite path. */
209
+ onArrayItemChange<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number, value: any): void;
210
+ /** Manually updates a scalar or nested object selection field. */
185
211
  onSelectionChange: ValueChangeFunc<O, keyof O>;
212
+ /** Manually updates a selection-based field within an object array item. */
213
+ onArraySelectionChange<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number, value: any): void;
186
214
  state: O;
187
215
  setState: React.Dispatch<React.SetStateAction<O>>;
188
216
  metadata: MetadataType;
package/dist/index.js CHANGED
@@ -592,10 +592,17 @@ function useForm(schema, {
592
592
  const { compositeKey, fieldPath, realPath, value } = resolution;
593
593
  const meta = metadataRef.current.get(compositeKey);
594
594
  const isTouched = meta?.isTouched;
595
+ let isRequired = false;
596
+ try {
597
+ const rule = getRule(fieldPath, validationSchema);
598
+ if (rule) isRequired = rule.describe().optional === false;
599
+ } catch {
600
+ }
595
601
  return {
596
602
  id: compositeKey,
597
603
  isInvalid: Boolean(isTouched && meta?.isInvalid),
598
604
  errorMessage: isTouched ? meta?.errorMessage || "" : "",
605
+ isRequired,
599
606
  onBlur: () => {
600
607
  if (metadataRef.current.get(compositeKey)?.isTouched) return;
601
608
  validateField(
@@ -618,7 +625,7 @@ function useForm(schema, {
618
625
  }
619
626
  };
620
627
  },
621
- [validateField]
628
+ [validateField, getRule, validationSchema]
622
629
  );
623
630
  const on = (0, import_react2.useMemo)(
624
631
  () => ({
@@ -670,6 +677,62 @@ function useForm(schema, {
670
677
  handleFieldChange(data, fixed);
671
678
  }
672
679
  };
680
+ },
681
+ numberInput: (...args) => {
682
+ const data = resolveFieldData(
683
+ args,
684
+ stateRef.current,
685
+ getIndex,
686
+ getNestedValue
687
+ );
688
+ if (!data) return {};
689
+ return {
690
+ ...createHandlers(data),
691
+ value: data.value ?? 0,
692
+ onValueChange: (v) => handleFieldChange(data, v)
693
+ };
694
+ },
695
+ checkbox: (...args) => {
696
+ const data = resolveFieldData(
697
+ args,
698
+ stateRef.current,
699
+ getIndex,
700
+ getNestedValue
701
+ );
702
+ if (!data) return {};
703
+ return {
704
+ ...createHandlers(data),
705
+ isSelected: Boolean(data.value),
706
+ onValueChange: (v) => handleFieldChange(data, v)
707
+ };
708
+ },
709
+ switch: (...args) => {
710
+ const data = resolveFieldData(
711
+ args,
712
+ stateRef.current,
713
+ getIndex,
714
+ getNestedValue
715
+ );
716
+ if (!data) return {};
717
+ return {
718
+ ...createHandlers(data),
719
+ isSelected: Boolean(data.value),
720
+ onValueChange: (v) => handleFieldChange(data, v)
721
+ };
722
+ },
723
+ radio: (...args) => {
724
+ const data = resolveFieldData(
725
+ args,
726
+ stateRef.current,
727
+ getIndex,
728
+ getNestedValue
729
+ );
730
+ if (!data) return {};
731
+ return {
732
+ ...createHandlers(data),
733
+ value: data.value ?? "",
734
+ onValueChange: (v) => handleFieldChange(data, v)
735
+ };
673
736
  }
674
737
  }),
675
738
  [createHandlers, getIndex, handleFieldChange]
@@ -729,7 +792,7 @@ function useForm(schema, {
729
792
  updateItem: (arrayKey, index, value) => {
730
793
  setState((prev) => {
731
794
  const arr = [...getNestedValue(prev, arrayKey) || []];
732
- arr[index] = value;
795
+ arr[index] = typeof arr[index] === "object" && typeof value === "object" ? { ...arr[index], ...value } : value;
733
796
  return handleNestedChange({
734
797
  state: prev,
735
798
  id: arrayKey,
@@ -738,6 +801,21 @@ function useForm(schema, {
738
801
  });
739
802
  });
740
803
  },
804
+ updateById: (arrayKey, itemId, value) => {
805
+ const index = getIndex(arrayKey, itemId);
806
+ if (index !== void 0) {
807
+ setState((prev) => {
808
+ const arr = [...getNestedValue(prev, arrayKey) || []];
809
+ arr[index] = typeof arr[index] === "object" && typeof value === "object" ? { ...arr[index], ...value } : value;
810
+ return handleNestedChange({
811
+ state: prev,
812
+ id: arrayKey,
813
+ value: arr,
814
+ hasNestedValues: String(arrayKey).includes(".")
815
+ });
816
+ });
817
+ }
818
+ },
741
819
  moveItem: (arrayKey, from, to) => {
742
820
  setState((prev) => {
743
821
  const arr = [...getNestedValue(prev, arrayKey) || []];
@@ -798,12 +876,22 @@ function useForm(schema, {
798
876
  },
799
877
  [validateField]
800
878
  );
801
- const polymorphicOnValueChange = (0, import_react2.useCallback)(
802
- (...args) => {
803
- const value = args[args.length - 1];
804
- const idArgs = args.slice(0, args.length - 1);
879
+ const scalarOnFieldChange = (0, import_react2.useCallback)(
880
+ (id, value) => {
881
+ const data = resolveFieldData(
882
+ [id],
883
+ stateRef.current,
884
+ getIndex,
885
+ getNestedValue
886
+ );
887
+ if (data) handleFieldChange(data, value);
888
+ },
889
+ [getIndex, handleFieldChange]
890
+ );
891
+ const arrayItemChange = (0, import_react2.useCallback)(
892
+ (compositePath, itemId, value) => {
805
893
  const data = resolveFieldData(
806
- idArgs,
894
+ [compositePath, itemId],
807
895
  stateRef.current,
808
896
  getIndex,
809
897
  getNestedValue
@@ -812,7 +900,7 @@ function useForm(schema, {
812
900
  },
813
901
  [getIndex, handleFieldChange]
814
902
  );
815
- const polymorphicOnSelectionChange = (0, import_react2.useCallback)(
903
+ const scalarOnSelectionChange = (0, import_react2.useCallback)(
816
904
  (id, val) => {
817
905
  const fixed = typeof val === "string" || val === null ? val : Array.from(val);
818
906
  let nextState = handleNestedChange({
@@ -826,6 +914,19 @@ function useForm(schema, {
826
914
  },
827
915
  [validateField]
828
916
  );
917
+ const arraySelectionChange = (0, import_react2.useCallback)(
918
+ (compositePath, itemId, val) => {
919
+ const fixed = typeof val === "string" || val === null ? val : Array.from(val);
920
+ const data = resolveFieldData(
921
+ [compositePath, itemId],
922
+ stateRef.current,
923
+ getIndex,
924
+ getNestedValue
925
+ );
926
+ if (data) handleFieldChange(data, fixed);
927
+ },
928
+ [getIndex, handleFieldChange]
929
+ );
829
930
  const onFormSubmit = (0, import_react2.useCallback)(
830
931
  (fn) => (e) => {
831
932
  e.preventDefault();
@@ -892,9 +993,11 @@ function useForm(schema, {
892
993
  metadata,
893
994
  on,
894
995
  helpers,
895
- onFieldChange: polymorphicOnValueChange,
996
+ onFieldChange: scalarOnFieldChange,
997
+ onArrayItemChange: arrayItemChange,
896
998
  onFieldBlur: onBlur,
897
- onSelectionChange: polymorphicOnSelectionChange,
999
+ onSelectionChange: scalarOnSelectionChange,
1000
+ onArraySelectionChange: arraySelectionChange,
898
1001
  isDirty: Array.from(metadata.values()).some((m) => m.isTouched),
899
1002
  onFormReset: handleReset,
900
1003
  onFormSubmit,
@@ -907,8 +1010,10 @@ function useForm(schema, {
907
1010
  on,
908
1011
  helpers,
909
1012
  onBlur,
910
- polymorphicOnValueChange,
911
- polymorphicOnSelectionChange,
1013
+ scalarOnFieldChange,
1014
+ arrayItemChange,
1015
+ scalarOnSelectionChange,
1016
+ arraySelectionChange,
912
1017
  validateAll,
913
1018
  onFormSubmit,
914
1019
  ControlledForm,
package/dist/index.mjs CHANGED
@@ -566,10 +566,17 @@ function useForm(schema, {
566
566
  const { compositeKey, fieldPath, realPath, value } = resolution;
567
567
  const meta = metadataRef.current.get(compositeKey);
568
568
  const isTouched = meta?.isTouched;
569
+ let isRequired = false;
570
+ try {
571
+ const rule = getRule(fieldPath, validationSchema);
572
+ if (rule) isRequired = rule.describe().optional === false;
573
+ } catch {
574
+ }
569
575
  return {
570
576
  id: compositeKey,
571
577
  isInvalid: Boolean(isTouched && meta?.isInvalid),
572
578
  errorMessage: isTouched ? meta?.errorMessage || "" : "",
579
+ isRequired,
573
580
  onBlur: () => {
574
581
  if (metadataRef.current.get(compositeKey)?.isTouched) return;
575
582
  validateField(
@@ -592,7 +599,7 @@ function useForm(schema, {
592
599
  }
593
600
  };
594
601
  },
595
- [validateField]
602
+ [validateField, getRule, validationSchema]
596
603
  );
597
604
  const on = useMemo(
598
605
  () => ({
@@ -644,6 +651,62 @@ function useForm(schema, {
644
651
  handleFieldChange(data, fixed);
645
652
  }
646
653
  };
654
+ },
655
+ numberInput: (...args) => {
656
+ const data = resolveFieldData(
657
+ args,
658
+ stateRef.current,
659
+ getIndex,
660
+ getNestedValue
661
+ );
662
+ if (!data) return {};
663
+ return {
664
+ ...createHandlers(data),
665
+ value: data.value ?? 0,
666
+ onValueChange: (v) => handleFieldChange(data, v)
667
+ };
668
+ },
669
+ checkbox: (...args) => {
670
+ const data = resolveFieldData(
671
+ args,
672
+ stateRef.current,
673
+ getIndex,
674
+ getNestedValue
675
+ );
676
+ if (!data) return {};
677
+ return {
678
+ ...createHandlers(data),
679
+ isSelected: Boolean(data.value),
680
+ onValueChange: (v) => handleFieldChange(data, v)
681
+ };
682
+ },
683
+ switch: (...args) => {
684
+ const data = resolveFieldData(
685
+ args,
686
+ stateRef.current,
687
+ getIndex,
688
+ getNestedValue
689
+ );
690
+ if (!data) return {};
691
+ return {
692
+ ...createHandlers(data),
693
+ isSelected: Boolean(data.value),
694
+ onValueChange: (v) => handleFieldChange(data, v)
695
+ };
696
+ },
697
+ radio: (...args) => {
698
+ const data = resolveFieldData(
699
+ args,
700
+ stateRef.current,
701
+ getIndex,
702
+ getNestedValue
703
+ );
704
+ if (!data) return {};
705
+ return {
706
+ ...createHandlers(data),
707
+ value: data.value ?? "",
708
+ onValueChange: (v) => handleFieldChange(data, v)
709
+ };
647
710
  }
648
711
  }),
649
712
  [createHandlers, getIndex, handleFieldChange]
@@ -703,7 +766,7 @@ function useForm(schema, {
703
766
  updateItem: (arrayKey, index, value) => {
704
767
  setState((prev) => {
705
768
  const arr = [...getNestedValue(prev, arrayKey) || []];
706
- arr[index] = value;
769
+ arr[index] = typeof arr[index] === "object" && typeof value === "object" ? { ...arr[index], ...value } : value;
707
770
  return handleNestedChange({
708
771
  state: prev,
709
772
  id: arrayKey,
@@ -712,6 +775,21 @@ function useForm(schema, {
712
775
  });
713
776
  });
714
777
  },
778
+ updateById: (arrayKey, itemId, value) => {
779
+ const index = getIndex(arrayKey, itemId);
780
+ if (index !== void 0) {
781
+ setState((prev) => {
782
+ const arr = [...getNestedValue(prev, arrayKey) || []];
783
+ arr[index] = typeof arr[index] === "object" && typeof value === "object" ? { ...arr[index], ...value } : value;
784
+ return handleNestedChange({
785
+ state: prev,
786
+ id: arrayKey,
787
+ value: arr,
788
+ hasNestedValues: String(arrayKey).includes(".")
789
+ });
790
+ });
791
+ }
792
+ },
715
793
  moveItem: (arrayKey, from, to) => {
716
794
  setState((prev) => {
717
795
  const arr = [...getNestedValue(prev, arrayKey) || []];
@@ -772,12 +850,22 @@ function useForm(schema, {
772
850
  },
773
851
  [validateField]
774
852
  );
775
- const polymorphicOnValueChange = useCallback(
776
- (...args) => {
777
- const value = args[args.length - 1];
778
- const idArgs = args.slice(0, args.length - 1);
853
+ const scalarOnFieldChange = useCallback(
854
+ (id, value) => {
855
+ const data = resolveFieldData(
856
+ [id],
857
+ stateRef.current,
858
+ getIndex,
859
+ getNestedValue
860
+ );
861
+ if (data) handleFieldChange(data, value);
862
+ },
863
+ [getIndex, handleFieldChange]
864
+ );
865
+ const arrayItemChange = useCallback(
866
+ (compositePath, itemId, value) => {
779
867
  const data = resolveFieldData(
780
- idArgs,
868
+ [compositePath, itemId],
781
869
  stateRef.current,
782
870
  getIndex,
783
871
  getNestedValue
@@ -786,7 +874,7 @@ function useForm(schema, {
786
874
  },
787
875
  [getIndex, handleFieldChange]
788
876
  );
789
- const polymorphicOnSelectionChange = useCallback(
877
+ const scalarOnSelectionChange = useCallback(
790
878
  (id, val) => {
791
879
  const fixed = typeof val === "string" || val === null ? val : Array.from(val);
792
880
  let nextState = handleNestedChange({
@@ -800,6 +888,19 @@ function useForm(schema, {
800
888
  },
801
889
  [validateField]
802
890
  );
891
+ const arraySelectionChange = useCallback(
892
+ (compositePath, itemId, val) => {
893
+ const fixed = typeof val === "string" || val === null ? val : Array.from(val);
894
+ const data = resolveFieldData(
895
+ [compositePath, itemId],
896
+ stateRef.current,
897
+ getIndex,
898
+ getNestedValue
899
+ );
900
+ if (data) handleFieldChange(data, fixed);
901
+ },
902
+ [getIndex, handleFieldChange]
903
+ );
803
904
  const onFormSubmit = useCallback(
804
905
  (fn) => (e) => {
805
906
  e.preventDefault();
@@ -866,9 +967,11 @@ function useForm(schema, {
866
967
  metadata,
867
968
  on,
868
969
  helpers,
869
- onFieldChange: polymorphicOnValueChange,
970
+ onFieldChange: scalarOnFieldChange,
971
+ onArrayItemChange: arrayItemChange,
870
972
  onFieldBlur: onBlur,
871
- onSelectionChange: polymorphicOnSelectionChange,
973
+ onSelectionChange: scalarOnSelectionChange,
974
+ onArraySelectionChange: arraySelectionChange,
872
975
  isDirty: Array.from(metadata.values()).some((m) => m.isTouched),
873
976
  onFormReset: handleReset,
874
977
  onFormSubmit,
@@ -881,8 +984,10 @@ function useForm(schema, {
881
984
  on,
882
985
  helpers,
883
986
  onBlur,
884
- polymorphicOnValueChange,
885
- polymorphicOnSelectionChange,
987
+ scalarOnFieldChange,
988
+ arrayItemChange,
989
+ scalarOnSelectionChange,
990
+ arraySelectionChange,
886
991
  validateAll,
887
992
  onFormSubmit,
888
993
  ControlledForm,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juantroconisf/lib",
3
- "version": "9.4.0",
3
+ "version": "10.0.0",
4
4
  "description": "A form validation library for HeroUI.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",