@juantroconisf/lib 9.1.0 → 9.3.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,8 +1,8 @@
1
1
  # @juantroconisf/lib
2
2
 
3
- A powerful, type-safe form management and validation library optimized for **HeroUI** (formerly NextUI) and **React**.
3
+ A powerful, type-safe form management and validation library tailored for **HeroUI** and **React**.
4
4
 
5
- Designed for complex applications, it provides **O(1) value updates**, stable **ID-based array management**, and deep nesting support, all while fully integrating with **Yup** for schema validation.
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.
6
6
 
7
7
  ---
8
8
 
@@ -10,30 +10,31 @@ Designed for complex applications, it provides **O(1) value updates**, stable **
10
10
 
11
11
  - [Features](#features)
12
12
  - [Installation](#installation)
13
+ - [Localization](#localization)
13
14
  - [Quick Start](#quick-start)
14
15
  - [Usage Guide](#usage-guide)
15
16
  - [Scalar Fields](#scalar-fields)
16
17
  - [Nested Objects](#nested-objects)
17
18
  - [Managing Arrays](#managing-arrays)
18
- - [Validation](#validation)
19
- - [Submitting Forms](#submitting-forms)
19
+ - [Form Submission](#form-submission)
20
20
  - [API Reference](#api-reference)
21
21
  - [useForm](#useform)
22
- - [The `on` Object](#the-on-object)
22
+ - [The `on` API](#the-on-api)
23
+ - [ControlledForm](#controlledform)
23
24
  - [Array Helpers](#array-helpers)
24
- - [Metadata & Reset](#metadata--reset)
25
+ - [Form Controls](#form-controls)
25
26
 
26
27
  ---
27
28
 
28
29
  ## Features
29
30
 
30
- - 🎯 **Polymorphic `on` API**: Single consistent interface for `input`, `select`, and `autocomplete`.
31
- - 🧩 **Deep Nesting**: Effortlessly manage complex objects with dot-notation support (e.g., `address.city`).
32
- - 🔢 **ID-Based Arrays**: Stable state management for dynamic lists. Items are tracked by unique identifiers (default: `id`), preventing state loss during reordering or deletions.
33
- - **O(1) Performance**: Internal mapping for array items ensures lightning-fast updates regardless of list size.
34
- - 🛡️ **Total Type Safety**: Best-in-class Intellisense for paths, types, and array identifiers.
35
- - 🎨 **HeroUI Optimized**: Returns props ready to be spread directly onto HeroUI components (`isInvalid`, `errorMessage`, `onFieldBlur`, etc.).
36
- - **Yup Integration**: Built-in support for Yup schemas for validation.
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.
37
38
 
38
39
  ---
39
40
 
@@ -45,69 +46,107 @@ pnpm add @juantroconisf/lib yup
45
46
 
46
47
  ---
47
48
 
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
+
48
63
  ## Quick Start
49
64
 
50
- Here is a complete example demonstrating scalar fields, object arrays, and validation.
65
+ Throughout this documentation, we will refer to a common, diverse schema. It includes simple scalars, deep objects, and complex arrays.
51
66
 
52
67
  ```tsx
53
68
  import { useForm } from "@juantroconisf/lib";
54
- import { string, number, array, object } from "yup";
55
- import { Input, Button } from "@heroui/react";
69
+ import { string, number, array, object, boolean } from "yup";
70
+ import { Input, Switch, Select, SelectItem, Button } from "@heroui/react";
56
71
 
57
- const MyForm = () => {
58
- const { on, state, helpers, onSubmit } = useForm({
59
- name: string().required("Name is required").default(""),
60
- items: array()
61
- .of(
62
- object().shape({
63
- id: number().required(),
64
- label: string().required("Label is required"),
65
- }),
66
- )
67
- .default([{ id: 1, label: "Initial Item" }]),
68
- });
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
+ };
69
89
 
70
- const handleSubmit = (data) => {
71
- console.log("Submitted:", data);
90
+ 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
72
100
  };
73
101
 
74
102
  return (
75
- <form onSubmit={onSubmit(handleSubmit)} className='flex flex-col gap-4'>
76
- {/* 1. Scalar Registration */}
77
- <Input {...on.input("name")} label='Name' />
78
-
79
- {/* 2. Array Registration */}
80
- {state.items.map((item) => (
81
- <div key={item.id} className='flex gap-2 items-center'>
82
-
83
- {/*
84
- Bind by Path + ID
85
- This ensures that even if the array order changes,
86
- the input remains bound to the correct item.
87
- */}
88
- <Input
89
- {...on.input("items.label", item.id)}
90
- label='Item Label'
91
- />
92
-
93
- <Button
94
- color="danger"
95
- onClick={() => helpers.removeById("items", item.id)}
96
- >
97
- Remove
98
- </Button>
99
- </div>
100
- ))}
101
-
102
- <div className="flex gap-2">
103
+ <ControlledForm onSubmit={handleSubmit} className='flex flex-col gap-4'>
104
+ {/* Scalar Fields */}
105
+ <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'>
103
138
  <Button
104
- onClick={() => helpers.addItem("items", { id: Date.now(), label: "" })}
139
+ onPress={() =>
140
+ helpers.addItem("users", { id: Date.now(), role: "guest" })
141
+ }
105
142
  >
106
- Add Item
143
+ Add User
144
+ </Button>
145
+ <Button type='submit' color='primary'>
146
+ Submit
107
147
  </Button>
108
- <Button type='submit' color="primary">Submit</Button>
109
148
  </div>
110
- </form>
149
+ </ControlledForm>
111
150
  );
112
151
  };
113
152
  ```
@@ -116,86 +155,122 @@ const MyForm = () => {
116
155
 
117
156
  ## Usage Guide
118
157
 
158
+ All examples below continue referencing the `mySchema` definition from the Quick Start.
159
+
119
160
  ### Scalar Fields
120
161
 
121
- Scalar fields are the bread and butter of any form. Use `on.input`, `on.select`, or `on.autocomplete` to bind them.
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.
122
163
 
123
164
  ```tsx
124
165
  <Input {...on.input("fullName")} label="Full Name" />
125
- <Input {...on.input("email")} type="email" label="Email" />
166
+ <Input {...on.input("age")} type="number" label="Age" />
126
167
  ```
127
168
 
128
169
  ### Nested Objects
129
170
 
130
- No special configuration needed. Just use standard dot notation.
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.
131
172
 
132
173
  ```tsx
133
- const { on } = useForm({
134
- settings: object({
135
- theme: object({
136
- mode: string().default("dark"),
137
- }),
138
- }),
139
- });
140
-
141
- <Input {...on.input("settings.theme.mode")} />
174
+ // Binds seamlessly to state.settings.theme
175
+ <Select {...on.select("settings.theme")} label="Theme">
176
+ <SelectItem key="light">Light</SelectItem>
177
+ <SelectItem key="dark">Dark</SelectItem>
178
+ </Select>
179
+
180
+ // Booleans map smoothly to Switches/Checkboxes
181
+ <Switch {...on.input("settings.notifications")}>Enable Alerts</Switch>
142
182
  ```
143
183
 
144
184
  ### Managing Arrays
145
185
 
146
- The library shines with arrays. It tracks items by ID, so you can reorder or delete items without losing focus or state.
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.
147
187
 
148
- **1. Binding Inputs:**
188
+ **Binding UI to Items:**
189
+ Combine the general array path and the item ID.
149
190
 
150
- Always bind using the composite syntax: `path.field` + `itemId`.
191
+ ```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);
197
+ ```
198
+
199
+ **Using the `helpers` Library:**
200
+ Extract `helpers` out of `useForm(...)` to manipulate array state securely.
151
201
 
152
202
  ```tsx
153
- // Correct: Binds to the item with ID 123
154
- on.input("users.name", 123)
203
+ const { helpers } = useForm(mySchema);
204
+
205
+ // Add an item (Optionally provide index as a 3rd argument to insert)
206
+ helpers.addItem("users", { id: Date.now(), role: "guest" });
207
+
208
+ // Remove item by ID
209
+ helpers.removeById("users", 12345);
155
210
 
156
- // Avoid (unless primitive array): Binds to index 0
157
- on.input("users", 0)
211
+ // Reorder items by ID
212
+ helpers.moveById("users", fromId, toId);
158
213
  ```
159
214
 
160
- **2. Manipulating the Array:**
215
+ ### Form Submission
161
216
 
162
- Use the `helpers` object.
217
+ There are two primary ways to submit, validate, and handle your logic, both fully type-safe.
163
218
 
164
- ```tsx
165
- // Add
166
- helpers.addItem("users", { id: "new-id", name: "" });
219
+ **1. The `ControlledForm` Wrapper (Recommended)**
167
220
 
168
- // Remove by ID (Safe)
169
- helpers.removeById("users", "user-id-123");
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.
170
222
 
171
- // Move (Reorder)
172
- helpers.moveById("users", "from-id", "to-id");
223
+ ```tsx
224
+ const { ControlledForm } = useForm(mySchema);
225
+
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
+ );
173
232
  ```
174
233
 
175
- ### Validation
234
+ **2. The `onFormSubmit` Handler Wrapper**
176
235
 
177
- Validation is "Schema-First" via Yup.
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);
178
240
 
179
- 1. **Define Rules**: Pass a Yup schema to `useForm`.
180
- 2. **Auto-Validation**:
181
- - Fields validate on `blur`.
182
- - If a field is `touched` and `invalid`, it re-validates on every keystroke (`onChange`).
183
- 3. **Submit Validation**: Use `onSubmit` wrapper to validate all fields before executing your handler.
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
+ });
247
+
248
+ // Used manually on form tags or button invocations
249
+ <form onSubmit={submitHandler}>{/* inputs */}</form>;
250
+ ```
184
251
 
185
- ### Submitting Forms
252
+ **3. External Handler Definitions**
186
253
 
187
- The `onSubmit` wrapper handles `preventDefault`, runs full validation, and only executes your callback if valid.
254
+ If you define your schemas inline inside `useForm( { ... } )` but want to extract your `submit` handler natively without destructing variables you aren't using:
188
255
 
189
256
  ```tsx
190
- const { onSubmit } = useForm(...);
257
+ export const MyForm = () => {
258
+ // 1. Schema is defined organically inline inside the component execution
259
+ const { ControlledForm } = useForm({
260
+ fullName: string().required(),
261
+ });
191
262
 
192
- const handleSubmit = (data) => {
193
- // 1. Validation has already passed!
194
- // 2. Proceed with submission
195
- api.post("/users", data);
196
- };
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
+ };
197
270
 
198
- return <form onSubmit={onSubmit(handleSubmit)}>...</form>;
271
+ // 3. Destructure whatever you need right on your JSX render
272
+ return <ControlledForm onSubmit={handleSubmit}>...</ControlledForm>;
273
+ };
199
274
  ```
200
275
 
201
276
  ---
@@ -204,76 +279,81 @@ return <form onSubmit={onSubmit(handleSubmit)}>...</form>;
204
279
 
205
280
  ### `useForm`
206
281
 
282
+ Instantiates state and schema configurations.
283
+
207
284
  ```ts
208
- const {
209
- state,
210
- on,
211
- helpers,
212
- metadata,
213
- reset,
214
- onSubmit,
215
- reset,
216
- onSubmit,
217
- isDirty,
218
- onFieldChange,
219
- onFieldBlur
220
- } = useForm(schema, options?);
285
+ const formApi = useForm(schema, options?);
221
286
  ```
222
287
 
223
288
  #### Arguments
224
289
 
225
- | Argument | Type | Description |
226
- | :--- | :--- | :--- |
227
- | **`schema`** | `Yup.Schema` | A Yup schema object defining structure, default values, and validation rules. |
228
- | **`options`** | `FormOptions` | Configuration object. |
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. |
229
294
 
230
295
  #### Options
231
296
 
232
- - **`arrayIdentifiers`**: Mapping of array paths to their unique ID property (default: `"id"`).
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"]`). |
233
303
 
234
- ### The `on` Object
304
+ ---
235
305
 
236
- The `on` object generates props for your components: `value`, `onValueChange`, `isInvalid`, `errorMessage`, `onBlur`.
306
+ ### The `on` API
237
307
 
238
- | Method | Signature | Description |
239
- | :--- | :--- | :--- |
240
- | **`on.input`** | `(path, [id])` | Generic input handler. Returns standard input props. |
241
- | **`on.select`** | `(path, [id])` | Returns props compatible with `Select` (handles `selectedKeys`). |
242
- | **`on.autocomplete`** | `(path, [id])` | Returns props compatible with `Autocomplete` (handles `selectedKey`). |
308
+ `on` hooks dynamically assign value tracks, validity checks, error text extractions, and update invocations back to the controller.
243
309
 
244
- ### Array Helpers
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`. |
245
315
 
246
- Utilities for manipulating array state.
316
+ ---
247
317
 
248
- | Method | Description |
249
- | :--- | :--- |
250
- | **`addItem(key, item, index?)`** | Adds an item to the array. |
251
- | **`removeItem(key, index)`** | Removes an item at a specific index. |
252
- | **`removeById(key, id)`** | **Recommended**. Removes an item by its unique ID. |
253
- | **`updateItem(key, index, val)`** | Replaces an item at a specific index. |
254
- | **`moveItem(key, from, to)`** | Moves an item from one index to another. |
255
- | **`moveById(key, fromId, toId)`** | Moves an item by ID. |
256
- | **`getItem(key, id)`** | Retrieval helper. |
318
+ ### ControlledForm
257
319
 
258
- ### Metadata & Reset
320
+ A Drop-in `<Form />` equivalent handling submit event delegation natively linked to validation triggers internally.
259
321
 
260
- | Property | Type | Description |
261
- | :--- | :--- | :--- |
262
- | **`metadata`** | `Map<string, FieldMetadata>` | Contains validation state (`isInvalid`, `errorMessage`) and `isTouched`. |
263
- | **`reset`** | `(options?) => void` | Resets form state and metadata to initial values. |
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. |
264
325
 
265
- **Reset Options:**
326
+ _Notes: Spreads any traditional `<form>` properties to standard HeroUI mappings out of the box._
266
327
 
267
- ```ts
268
- // Reset everything
269
- reset();
328
+ ---
270
329
 
271
- // Keep specific fields
272
- reset({ keys: ["organizationId"] });
330
+ ### Array Helpers
273
331
 
274
- // Only clear touched status (keep values)
275
- reset({ onlyTouched: true });
276
- ```
332
+ Array adjustments exported off the `helpers` map:
333
+
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. |
343
+
344
+ ---
345
+
346
+ ### Form Controls
347
+
348
+ Additional utilities available directly out of `useForm`:
349
+
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. |
277
357
 
278
358
  ---
279
359
 
package/dist/index.d.mts CHANGED
@@ -61,6 +61,11 @@ type FieldMetadata = {
61
61
  };
62
62
  type MetadataType = Map<string, FieldMetadata>;
63
63
  type FormSubmitHandler<O extends StateType> = (data: O, e: React.FormEvent) => void;
64
+ /**
65
+ * A utility type for inferring the submit handler's signature directly from a Yup schema object.
66
+ * This saves developers from manually mapping `InferType` back to `FormSubmitHandler` when extracting sumbits.
67
+ */
68
+ type InferSubmitHandler<S extends Record<string, ConfigType>> = (data: InferState<S>, e: React.FormEvent) => void;
64
69
  interface FormResetOptions<O> {
65
70
  keepValues?: (keyof O)[];
66
71
  }
@@ -205,4 +210,4 @@ interface UseFormResponse<O extends StateType> {
205
210
 
206
211
  declare function useForm<S extends Record<string, ConfigType>>(schema: S, { arrayIdentifiers, onFormSubmit: onFormSubmitProp, resetOnSubmit, keepValues: keepValuesProp, }?: FormOptions<InferState<S>>): UseFormResponse<InferState<S>>;
207
212
 
208
- export { type BlurFunc, type ConfigType, type FieldMetadata, type FormOptions, type FormSubmitHandler, type HelpersFunc, type InferState, type MetadataType, type NestedChangeProps, NextUIError, type OnMethods, type StateType, type UseFormResponse, type ValueChangeFunc, useForm };
213
+ export { type BlurFunc, type ConfigType, type FieldMetadata, type FormOptions, type FormSubmitHandler, type HelpersFunc, type InferState, type InferSubmitHandler, type MetadataType, type NestedChangeProps, NextUIError, type OnMethods, type StateType, type UseFormResponse, type ValueChangeFunc, useForm };
package/dist/index.d.ts CHANGED
@@ -61,6 +61,11 @@ type FieldMetadata = {
61
61
  };
62
62
  type MetadataType = Map<string, FieldMetadata>;
63
63
  type FormSubmitHandler<O extends StateType> = (data: O, e: React.FormEvent) => void;
64
+ /**
65
+ * A utility type for inferring the submit handler's signature directly from a Yup schema object.
66
+ * This saves developers from manually mapping `InferType` back to `FormSubmitHandler` when extracting sumbits.
67
+ */
68
+ type InferSubmitHandler<S extends Record<string, ConfigType>> = (data: InferState<S>, e: React.FormEvent) => void;
64
69
  interface FormResetOptions<O> {
65
70
  keepValues?: (keyof O)[];
66
71
  }
@@ -205,4 +210,4 @@ interface UseFormResponse<O extends StateType> {
205
210
 
206
211
  declare function useForm<S extends Record<string, ConfigType>>(schema: S, { arrayIdentifiers, onFormSubmit: onFormSubmitProp, resetOnSubmit, keepValues: keepValuesProp, }?: FormOptions<InferState<S>>): UseFormResponse<InferState<S>>;
207
212
 
208
- export { type BlurFunc, type ConfigType, type FieldMetadata, type FormOptions, type FormSubmitHandler, type HelpersFunc, type InferState, type MetadataType, type NestedChangeProps, NextUIError, type OnMethods, type StateType, type UseFormResponse, type ValueChangeFunc, useForm };
213
+ export { type BlurFunc, type ConfigType, type FieldMetadata, type FormOptions, type FormSubmitHandler, type HelpersFunc, type InferState, type InferSubmitHandler, type MetadataType, type NestedChangeProps, NextUIError, type OnMethods, type StateType, type UseFormResponse, type ValueChangeFunc, useForm };
package/dist/index.js CHANGED
@@ -227,6 +227,7 @@ function resolveFieldData(args, state, getIndex, getNestedValue2) {
227
227
  type: "scalar" /* Scalar */,
228
228
  compositeKey: id,
229
229
  fieldPath: id,
230
+ realPath: id,
230
231
  value: getNestedValue2(state, id),
231
232
  id
232
233
  };
@@ -249,6 +250,7 @@ function resolveFieldData(args, state, getIndex, getNestedValue2) {
249
250
  compositeKey: `${arrayKey2}.${itemId}.${field}`,
250
251
  fieldPath: arg0,
251
252
  // "array.field"
253
+ realPath: `${arrayKey2}.${index2}.${field}`,
252
254
  value,
253
255
  arrayKey: arrayKey2,
254
256
  itemId,
@@ -264,6 +266,7 @@ function resolveFieldData(args, state, getIndex, getNestedValue2) {
264
266
  type: "primitiveArray" /* PrimitiveArray */,
265
267
  compositeKey: `${arrayKey}.@${index}`,
266
268
  fieldPath: `${arrayKey}`,
269
+ realPath: `${arrayKey}.${index}`,
267
270
  value: arr?.[index],
268
271
  arrayKey,
269
272
  index
@@ -281,6 +284,7 @@ function resolveFieldData(args, state, getIndex, getNestedValue2) {
281
284
  compositeKey: `${arrayKey}.${itemId}.${field}`,
282
285
  fieldPath: `${arrayKey}.${field}`,
283
286
  // Normalized path for rules
287
+ realPath: `${arrayKey}.${index}.${field}`,
284
288
  value,
285
289
  arrayKey,
286
290
  itemId,
@@ -301,6 +305,7 @@ function resolveFieldData(args, state, getIndex, getNestedValue2) {
301
305
  compositeKey: `${parentKey}.${parentId}.${field}.@${index}`,
302
306
  fieldPath: `${parentKey}.${field}`,
303
307
  // Roughly?
308
+ realPath: `${parentKey}.${parentIndex}.${field}.${index}`,
304
309
  value,
305
310
  parentKey,
306
311
  parentId,
@@ -403,10 +408,14 @@ function useForm(schema, {
403
408
  return rule;
404
409
  }, []);
405
410
  const runValidation = (0, import_react2.useCallback)(
406
- (ruleDef, value, compositeKey) => {
411
+ (ruleDef, value, compositeKey, realPath, fullState) => {
407
412
  if ((0, import_yup2.isSchema)(ruleDef)) {
408
413
  try {
409
- ruleDef.validateSync(value);
414
+ if (validationSchema && realPath && fullState !== void 0) {
415
+ validationSchema.validateSyncAt(realPath, fullState);
416
+ } else {
417
+ ruleDef.validateSync(value);
418
+ }
410
419
  return false;
411
420
  } catch (err) {
412
421
  const error = {
@@ -428,13 +437,14 @@ function useForm(schema, {
428
437
  }
429
438
  return false;
430
439
  },
431
- []
440
+ [validationSchema]
432
441
  );
433
442
  const validateField = (0, import_react2.useCallback)(
434
- (compositeKey, fieldPath, value) => {
443
+ (compositeKey, fieldPath, realPath, value, fullState) => {
435
444
  let schemaRule = getRule(fieldPath, validationSchema);
436
445
  if (schemaRule) {
437
- if (runValidation(schemaRule, value, compositeKey)) return true;
446
+ if (runValidation(schemaRule, value, compositeKey, realPath, fullState))
447
+ return true;
438
448
  }
439
449
  setMetadata((prev) => {
440
450
  const newMap = new Map(prev);
@@ -530,53 +540,56 @@ function useForm(schema, {
530
540
  } catch {
531
541
  }
532
542
  }
533
- setState((prev) => {
534
- if (type === "scalar" /* Scalar */) {
535
- return handleNestedChange({
536
- state: prev,
537
- id: compositeKey,
538
- value: finalValue,
539
- hasNestedValues: compositeKey.includes(".")
540
- });
541
- }
542
- if (type === "primitiveArray" /* PrimitiveArray */) {
543
- return handleNestedChange({
544
- state: prev,
545
- id: `${arrayKey}.${index}`,
546
- value: finalValue,
547
- hasNestedValues: true
548
- });
549
- }
550
- if (type === "objectArray" /* ObjectArray */) {
551
- return handleArrayItemChange({
552
- state: prev,
553
- arrayKey,
554
- index,
555
- field: resolution.field,
556
- value: finalValue
557
- });
558
- }
559
- if (type === "nestedPrimitiveArray" /* NestedPrimitiveArray */) {
560
- const pIndex = getIndex(parentKey, parentId);
561
- if (pIndex === void 0) return prev;
562
- const parentArr = [...prev[parentKey]];
543
+ let nextState = stateRef.current;
544
+ if (type === "scalar" /* Scalar */) {
545
+ nextState = handleNestedChange({
546
+ state: nextState,
547
+ id: compositeKey,
548
+ value: finalValue,
549
+ hasNestedValues: compositeKey.includes(".")
550
+ });
551
+ } else if (type === "primitiveArray" /* PrimitiveArray */) {
552
+ nextState = handleNestedChange({
553
+ state: nextState,
554
+ id: `${arrayKey}.${index}`,
555
+ value: finalValue,
556
+ hasNestedValues: true
557
+ });
558
+ } else if (type === "objectArray" /* ObjectArray */) {
559
+ nextState = handleArrayItemChange({
560
+ state: nextState,
561
+ arrayKey,
562
+ index,
563
+ field: resolution.field,
564
+ value: finalValue
565
+ });
566
+ } else if (type === "nestedPrimitiveArray" /* NestedPrimitiveArray */) {
567
+ const pIndex = getIndex(parentKey, parentId);
568
+ if (pIndex !== void 0) {
569
+ const parentArr = [...nextState[parentKey]];
563
570
  const pItem = { ...parentArr[pIndex] };
564
571
  const nestedArr = [...getNestedValue(pItem, nestedField) || []];
565
572
  nestedArr[index] = finalValue;
566
573
  pItem[nestedField] = nestedArr;
567
574
  parentArr[pIndex] = pItem;
568
- return { ...prev, [parentKey]: parentArr };
575
+ nextState = { ...nextState, [parentKey]: parentArr };
569
576
  }
570
- return prev;
571
- });
572
- validateField(compositeKey, fieldPath, finalValue);
577
+ }
578
+ setState(nextState);
579
+ validateField(
580
+ compositeKey,
581
+ fieldPath,
582
+ resolution.realPath,
583
+ finalValue,
584
+ nextState
585
+ );
573
586
  },
574
587
  [getIndex, validateField, getRule, validationSchema]
575
588
  );
576
589
  const createHandlers = (0, import_react2.useCallback)(
577
590
  (resolution) => {
578
591
  if (!resolution) return {};
579
- const { compositeKey, fieldPath, value } = resolution;
592
+ const { compositeKey, fieldPath, realPath, value } = resolution;
580
593
  const meta = metadataRef.current.get(compositeKey);
581
594
  const isTouched = meta?.isTouched;
582
595
  return {
@@ -585,7 +598,13 @@ function useForm(schema, {
585
598
  errorMessage: isTouched ? meta?.errorMessage || "" : "",
586
599
  onBlur: () => {
587
600
  if (metadataRef.current.get(compositeKey)?.isTouched) return;
588
- validateField(compositeKey, fieldPath, value);
601
+ validateField(
602
+ compositeKey,
603
+ fieldPath,
604
+ realPath,
605
+ value,
606
+ stateRef.current
607
+ );
589
608
  setMetadata((prev) => {
590
609
  const newMap = new Map(prev);
591
610
  const current = newMap.get(compositeKey) || {
@@ -759,7 +778,13 @@ function useForm(schema, {
759
778
  );
760
779
  const onBlur = (0, import_react2.useCallback)(
761
780
  (id) => {
762
- validateField(String(id), String(id), stateRef.current[id]);
781
+ validateField(
782
+ String(id),
783
+ String(id),
784
+ String(id),
785
+ stateRef.current[id],
786
+ stateRef.current
787
+ );
763
788
  setMetadata((prev) => {
764
789
  const newMap = new Map(prev);
765
790
  const current = newMap.get(String(id)) || {
@@ -790,8 +815,14 @@ function useForm(schema, {
790
815
  const polymorphicOnSelectionChange = (0, import_react2.useCallback)(
791
816
  (id, val) => {
792
817
  const fixed = typeof val === "string" || val === null ? val : Array.from(val);
793
- setState((prev) => handleNestedChange({ state: prev, id, value: fixed }));
794
- validateField(String(id), String(id), fixed);
818
+ let nextState = handleNestedChange({
819
+ state: stateRef.current,
820
+ id,
821
+ value: fixed,
822
+ hasNestedValues: String(id).includes(".")
823
+ });
824
+ setState(nextState);
825
+ validateField(String(id), String(id), String(id), fixed, nextState);
795
826
  },
796
827
  [validateField]
797
828
  );
package/dist/index.mjs CHANGED
@@ -201,6 +201,7 @@ function resolveFieldData(args, state, getIndex, getNestedValue2) {
201
201
  type: "scalar" /* Scalar */,
202
202
  compositeKey: id,
203
203
  fieldPath: id,
204
+ realPath: id,
204
205
  value: getNestedValue2(state, id),
205
206
  id
206
207
  };
@@ -223,6 +224,7 @@ function resolveFieldData(args, state, getIndex, getNestedValue2) {
223
224
  compositeKey: `${arrayKey2}.${itemId}.${field}`,
224
225
  fieldPath: arg0,
225
226
  // "array.field"
227
+ realPath: `${arrayKey2}.${index2}.${field}`,
226
228
  value,
227
229
  arrayKey: arrayKey2,
228
230
  itemId,
@@ -238,6 +240,7 @@ function resolveFieldData(args, state, getIndex, getNestedValue2) {
238
240
  type: "primitiveArray" /* PrimitiveArray */,
239
241
  compositeKey: `${arrayKey}.@${index}`,
240
242
  fieldPath: `${arrayKey}`,
243
+ realPath: `${arrayKey}.${index}`,
241
244
  value: arr?.[index],
242
245
  arrayKey,
243
246
  index
@@ -255,6 +258,7 @@ function resolveFieldData(args, state, getIndex, getNestedValue2) {
255
258
  compositeKey: `${arrayKey}.${itemId}.${field}`,
256
259
  fieldPath: `${arrayKey}.${field}`,
257
260
  // Normalized path for rules
261
+ realPath: `${arrayKey}.${index}.${field}`,
258
262
  value,
259
263
  arrayKey,
260
264
  itemId,
@@ -275,6 +279,7 @@ function resolveFieldData(args, state, getIndex, getNestedValue2) {
275
279
  compositeKey: `${parentKey}.${parentId}.${field}.@${index}`,
276
280
  fieldPath: `${parentKey}.${field}`,
277
281
  // Roughly?
282
+ realPath: `${parentKey}.${parentIndex}.${field}.${index}`,
278
283
  value,
279
284
  parentKey,
280
285
  parentId,
@@ -377,10 +382,14 @@ function useForm(schema, {
377
382
  return rule;
378
383
  }, []);
379
384
  const runValidation = useCallback(
380
- (ruleDef, value, compositeKey) => {
385
+ (ruleDef, value, compositeKey, realPath, fullState) => {
381
386
  if (isSchema(ruleDef)) {
382
387
  try {
383
- ruleDef.validateSync(value);
388
+ if (validationSchema && realPath && fullState !== void 0) {
389
+ validationSchema.validateSyncAt(realPath, fullState);
390
+ } else {
391
+ ruleDef.validateSync(value);
392
+ }
384
393
  return false;
385
394
  } catch (err) {
386
395
  const error = {
@@ -402,13 +411,14 @@ function useForm(schema, {
402
411
  }
403
412
  return false;
404
413
  },
405
- []
414
+ [validationSchema]
406
415
  );
407
416
  const validateField = useCallback(
408
- (compositeKey, fieldPath, value) => {
417
+ (compositeKey, fieldPath, realPath, value, fullState) => {
409
418
  let schemaRule = getRule(fieldPath, validationSchema);
410
419
  if (schemaRule) {
411
- if (runValidation(schemaRule, value, compositeKey)) return true;
420
+ if (runValidation(schemaRule, value, compositeKey, realPath, fullState))
421
+ return true;
412
422
  }
413
423
  setMetadata((prev) => {
414
424
  const newMap = new Map(prev);
@@ -504,53 +514,56 @@ function useForm(schema, {
504
514
  } catch {
505
515
  }
506
516
  }
507
- setState((prev) => {
508
- if (type === "scalar" /* Scalar */) {
509
- return handleNestedChange({
510
- state: prev,
511
- id: compositeKey,
512
- value: finalValue,
513
- hasNestedValues: compositeKey.includes(".")
514
- });
515
- }
516
- if (type === "primitiveArray" /* PrimitiveArray */) {
517
- return handleNestedChange({
518
- state: prev,
519
- id: `${arrayKey}.${index}`,
520
- value: finalValue,
521
- hasNestedValues: true
522
- });
523
- }
524
- if (type === "objectArray" /* ObjectArray */) {
525
- return handleArrayItemChange({
526
- state: prev,
527
- arrayKey,
528
- index,
529
- field: resolution.field,
530
- value: finalValue
531
- });
532
- }
533
- if (type === "nestedPrimitiveArray" /* NestedPrimitiveArray */) {
534
- const pIndex = getIndex(parentKey, parentId);
535
- if (pIndex === void 0) return prev;
536
- const parentArr = [...prev[parentKey]];
517
+ let nextState = stateRef.current;
518
+ if (type === "scalar" /* Scalar */) {
519
+ nextState = handleNestedChange({
520
+ state: nextState,
521
+ id: compositeKey,
522
+ value: finalValue,
523
+ hasNestedValues: compositeKey.includes(".")
524
+ });
525
+ } else if (type === "primitiveArray" /* PrimitiveArray */) {
526
+ nextState = handleNestedChange({
527
+ state: nextState,
528
+ id: `${arrayKey}.${index}`,
529
+ value: finalValue,
530
+ hasNestedValues: true
531
+ });
532
+ } else if (type === "objectArray" /* ObjectArray */) {
533
+ nextState = handleArrayItemChange({
534
+ state: nextState,
535
+ arrayKey,
536
+ index,
537
+ field: resolution.field,
538
+ value: finalValue
539
+ });
540
+ } else if (type === "nestedPrimitiveArray" /* NestedPrimitiveArray */) {
541
+ const pIndex = getIndex(parentKey, parentId);
542
+ if (pIndex !== void 0) {
543
+ const parentArr = [...nextState[parentKey]];
537
544
  const pItem = { ...parentArr[pIndex] };
538
545
  const nestedArr = [...getNestedValue(pItem, nestedField) || []];
539
546
  nestedArr[index] = finalValue;
540
547
  pItem[nestedField] = nestedArr;
541
548
  parentArr[pIndex] = pItem;
542
- return { ...prev, [parentKey]: parentArr };
549
+ nextState = { ...nextState, [parentKey]: parentArr };
543
550
  }
544
- return prev;
545
- });
546
- validateField(compositeKey, fieldPath, finalValue);
551
+ }
552
+ setState(nextState);
553
+ validateField(
554
+ compositeKey,
555
+ fieldPath,
556
+ resolution.realPath,
557
+ finalValue,
558
+ nextState
559
+ );
547
560
  },
548
561
  [getIndex, validateField, getRule, validationSchema]
549
562
  );
550
563
  const createHandlers = useCallback(
551
564
  (resolution) => {
552
565
  if (!resolution) return {};
553
- const { compositeKey, fieldPath, value } = resolution;
566
+ const { compositeKey, fieldPath, realPath, value } = resolution;
554
567
  const meta = metadataRef.current.get(compositeKey);
555
568
  const isTouched = meta?.isTouched;
556
569
  return {
@@ -559,7 +572,13 @@ function useForm(schema, {
559
572
  errorMessage: isTouched ? meta?.errorMessage || "" : "",
560
573
  onBlur: () => {
561
574
  if (metadataRef.current.get(compositeKey)?.isTouched) return;
562
- validateField(compositeKey, fieldPath, value);
575
+ validateField(
576
+ compositeKey,
577
+ fieldPath,
578
+ realPath,
579
+ value,
580
+ stateRef.current
581
+ );
563
582
  setMetadata((prev) => {
564
583
  const newMap = new Map(prev);
565
584
  const current = newMap.get(compositeKey) || {
@@ -733,7 +752,13 @@ function useForm(schema, {
733
752
  );
734
753
  const onBlur = useCallback(
735
754
  (id) => {
736
- validateField(String(id), String(id), stateRef.current[id]);
755
+ validateField(
756
+ String(id),
757
+ String(id),
758
+ String(id),
759
+ stateRef.current[id],
760
+ stateRef.current
761
+ );
737
762
  setMetadata((prev) => {
738
763
  const newMap = new Map(prev);
739
764
  const current = newMap.get(String(id)) || {
@@ -764,8 +789,14 @@ function useForm(schema, {
764
789
  const polymorphicOnSelectionChange = useCallback(
765
790
  (id, val) => {
766
791
  const fixed = typeof val === "string" || val === null ? val : Array.from(val);
767
- setState((prev) => handleNestedChange({ state: prev, id, value: fixed }));
768
- validateField(String(id), String(id), fixed);
792
+ let nextState = handleNestedChange({
793
+ state: stateRef.current,
794
+ id,
795
+ value: fixed,
796
+ hasNestedValues: String(id).includes(".")
797
+ });
798
+ setState(nextState);
799
+ validateField(String(id), String(id), String(id), fixed, nextState);
769
800
  },
770
801
  [validateField]
771
802
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juantroconisf/lib",
3
- "version": "9.1.0",
3
+ "version": "9.3.0",
4
4
  "description": "A form validation library for HeroUI.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",