@juantroconisf/lib 9.3.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 +259 -245
- package/dist/index.d.mts +49 -19
- package/dist/index.d.ts +49 -19
- package/dist/index.js +117 -12
- package/dist/index.mjs +117 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,40 +1,24 @@
|
|
|
1
1
|
# @juantroconisf/lib
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
Designed for complex applications, it provides **O(1) value updates**, stable **ID-based array management**, and deep nesting support, all seamlessly integrated with **Yup** schemas.
|
|
3
|
+
A type-safe form management library for **React** and **HeroUI**. It eliminates boilerplate through a declarative `on.*` API that bridges your [Yup](https://github.com/jquense/yup) schema directly to HeroUI component props — complete with validation, error messages, dirty tracking, and ID-based array management.
|
|
6
4
|
|
|
7
5
|
---
|
|
8
6
|
|
|
9
7
|
## Table of Contents
|
|
10
8
|
|
|
11
|
-
- [Features](#features)
|
|
12
9
|
- [Installation](#installation)
|
|
13
|
-
- [Localization](#localization)
|
|
14
10
|
- [Quick Start](#quick-start)
|
|
15
|
-
- [
|
|
11
|
+
- [Core Concepts](#core-concepts)
|
|
12
|
+
- [Schema Definition](#schema-definition)
|
|
16
13
|
- [Scalar Fields](#scalar-fields)
|
|
17
14
|
- [Nested Objects](#nested-objects)
|
|
18
|
-
- [
|
|
19
|
-
|
|
15
|
+
- [Array Fields](#array-fields)
|
|
16
|
+
- [The `on` API — Component Bindings](#the-on-api--component-bindings)
|
|
17
|
+
- [Manual Updates](#manual-updates)
|
|
18
|
+
- [Array Helpers](#array-helpers)
|
|
19
|
+
- [Form Submission](#form-submission)
|
|
20
20
|
- [API Reference](#api-reference)
|
|
21
|
-
|
|
22
|
-
- [The `on` API](#the-on-api)
|
|
23
|
-
- [ControlledForm](#controlledform)
|
|
24
|
-
- [Array Helpers](#array-helpers)
|
|
25
|
-
- [Form Controls](#form-controls)
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Features
|
|
30
|
-
|
|
31
|
-
- 🎯 **Polymorphic `on` API**: A unified interface (`on.input`, `on.select`, `on.autocomplete`) saving lines of boilerplate.
|
|
32
|
-
- 🧱 **ControlledForm Component**: Zero-boilerplate validated submissions leveraging the `@heroui/react` Form component directly.
|
|
33
|
-
- 🧩 **Deep Nesting**: Dot-notation support (e.g., `settings.theme`) out of the box.
|
|
34
|
-
- 🔢 **ID-Based Arrays**: List items are tracked by unique identifiers (default: `id`), ensuring states survive mapping, reordering, and deletions.
|
|
35
|
-
- ⚡ **O(1) Performance**: Internal mapping for array updates offers incredible speed regardless of data density.
|
|
36
|
-
- 🛡️ **Complete Type Safety**: TypeScript infers all acceptable paths, data structures, and outputs natively.
|
|
37
|
-
- 🌍 **Built-in Localization**: Ships with English and Spanish validation translations via runtime cookies.
|
|
21
|
+
- [Localization](#localization)
|
|
38
22
|
|
|
39
23
|
---
|
|
40
24
|
|
|
@@ -46,314 +30,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,
|
|
70
|
-
import { Input, Switch,
|
|
71
|
-
|
|
72
|
-
// 1. Define your diverse schema structure
|
|
73
|
-
const mySchema = {
|
|
74
|
-
fullName: string().required("Name is required").default(""),
|
|
75
|
-
age: number().min(18).default(18),
|
|
76
|
-
settings: object({
|
|
77
|
-
theme: string().oneOf(["light", "dark"]).default("dark"),
|
|
78
|
-
notifications: boolean().default(true),
|
|
79
|
-
}),
|
|
80
|
-
users: array()
|
|
81
|
-
.of(
|
|
82
|
-
object().shape({
|
|
83
|
-
id: number().required(),
|
|
84
|
-
role: string().required(),
|
|
85
|
-
}),
|
|
86
|
-
)
|
|
87
|
-
.default([{ id: Date.now(), role: "admin" }]),
|
|
88
|
-
};
|
|
37
|
+
import { string, boolean } from "yup";
|
|
38
|
+
import { Input, Switch, Button } from "@heroui/react";
|
|
89
39
|
|
|
90
40
|
const MyForm = () => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const handleSubmit = (data, event) => {
|
|
96
|
-
console.log("Submitted payload:", data);
|
|
97
|
-
// data.fullName -> string
|
|
98
|
-
// data.settings.theme -> "light" | "dark"
|
|
99
|
-
// data.users[0].role -> string
|
|
100
|
-
};
|
|
41
|
+
const { on, ControlledForm } = useForm({
|
|
42
|
+
fullName: string().required().default(""),
|
|
43
|
+
darkMode: boolean().default(false),
|
|
44
|
+
});
|
|
101
45
|
|
|
102
46
|
return (
|
|
103
|
-
<ControlledForm onSubmit={
|
|
104
|
-
{/* Scalar Fields */}
|
|
47
|
+
<ControlledForm onSubmit={(data) => console.log(data)}>
|
|
105
48
|
<Input {...on.input("fullName")} label='Full Name' />
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Enable Notifications
|
|
111
|
-
</Switch>
|
|
112
|
-
<Select {...on.select("settings.theme")} label='Theme'>
|
|
113
|
-
<SelectItem key='light'>Light</SelectItem>
|
|
114
|
-
<SelectItem key='dark'>Dark</SelectItem>
|
|
115
|
-
</Select>
|
|
116
|
-
|
|
117
|
-
{/* Array Fields */}
|
|
118
|
-
<div className='flex flex-col gap-2'>
|
|
119
|
-
<h3>Users</h3>
|
|
120
|
-
{state.users.map((user) => (
|
|
121
|
-
<div key={user.id} className='flex gap-2 items-center'>
|
|
122
|
-
{/* Bind by Array Path + Item ID */}
|
|
123
|
-
<Select {...on.select("users.role", user.id)} label='Role'>
|
|
124
|
-
<SelectItem key='admin'>Admin</SelectItem>
|
|
125
|
-
<SelectItem key='guest'>Guest</SelectItem>
|
|
126
|
-
</Select>
|
|
127
|
-
<Button
|
|
128
|
-
color='danger'
|
|
129
|
-
onPress={() => helpers.removeById("users", user.id)}
|
|
130
|
-
>
|
|
131
|
-
Remove
|
|
132
|
-
</Button>
|
|
133
|
-
</div>
|
|
134
|
-
))}
|
|
135
|
-
</div>
|
|
136
|
-
|
|
137
|
-
<div className='flex gap-2'>
|
|
138
|
-
<Button
|
|
139
|
-
onPress={() =>
|
|
140
|
-
helpers.addItem("users", { id: Date.now(), role: "guest" })
|
|
141
|
-
}
|
|
142
|
-
>
|
|
143
|
-
Add User
|
|
144
|
-
</Button>
|
|
145
|
-
<Button type='submit' color='primary'>
|
|
146
|
-
Submit
|
|
147
|
-
</Button>
|
|
148
|
-
</div>
|
|
49
|
+
<Switch {...on.switch("darkMode")}>Dark Mode</Switch>
|
|
50
|
+
<Button type='submit' color='primary'>
|
|
51
|
+
Submit
|
|
52
|
+
</Button>
|
|
149
53
|
</ControlledForm>
|
|
150
54
|
);
|
|
151
55
|
};
|
|
152
56
|
```
|
|
153
57
|
|
|
154
|
-
|
|
58
|
+
> `on.input("fullName")` spreads `value`, `onValueChange`, `isInvalid`, `errorMessage`, `isRequired`, `onBlur`, and `id` directly onto the HeroUI component. Zero boilerplate.
|
|
155
59
|
|
|
156
|
-
|
|
60
|
+
---
|
|
157
61
|
|
|
158
|
-
|
|
62
|
+
## Core Concepts
|
|
159
63
|
|
|
160
|
-
###
|
|
64
|
+
### Schema Definition
|
|
161
65
|
|
|
162
|
-
|
|
66
|
+
Pass a plain object of Yup schemas (or raw values for unvalidated state) to `useForm`. The hook infers your TypeScript types automatically.
|
|
163
67
|
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
|
|
68
|
+
```ts
|
|
69
|
+
const { on, state, helpers, ControlledForm } = useForm({
|
|
70
|
+
email: string().email().required().default(""),
|
|
71
|
+
age: number().min(18).default(18),
|
|
72
|
+
settings: {
|
|
73
|
+
theme: string().oneOf(["light", "dark"]).default("dark"),
|
|
74
|
+
notifications: boolean().default(true),
|
|
75
|
+
},
|
|
76
|
+
tags: array().of(string()).default([]),
|
|
77
|
+
users: array()
|
|
78
|
+
.of(object({ id: number().required(), role: string().required() }))
|
|
79
|
+
.default([]),
|
|
80
|
+
});
|
|
167
81
|
```
|
|
168
82
|
|
|
169
|
-
###
|
|
83
|
+
### Scalar Fields
|
|
170
84
|
|
|
171
|
-
|
|
85
|
+
Top-level primitive fields. Use dot notation to reach into nested objects.
|
|
172
86
|
|
|
173
87
|
```tsx
|
|
174
|
-
|
|
88
|
+
<Input {...on.input("email")} label="Email" />
|
|
89
|
+
<Input {...on.numberInput("age")} label="Age" />
|
|
175
90
|
<Select {...on.select("settings.theme")} label="Theme">
|
|
176
91
|
<SelectItem key="light">Light</SelectItem>
|
|
177
92
|
<SelectItem key="dark">Dark</SelectItem>
|
|
178
93
|
</Select>
|
|
179
|
-
|
|
180
|
-
// Booleans map smoothly to Switches/Checkboxes
|
|
181
|
-
<Switch {...on.input("settings.notifications")}>Enable Alerts</Switch>
|
|
182
94
|
```
|
|
183
95
|
|
|
184
|
-
###
|
|
185
|
-
|
|
186
|
-
Array management is uniquely built around tracking entries by ID (`'id'` field by default). This stops issues where UI inputs lose state references if the array gets shifted.
|
|
96
|
+
### Nested Objects
|
|
187
97
|
|
|
188
|
-
|
|
189
|
-
Combine the general array path and the item ID.
|
|
98
|
+
Dot notation reaches arbitrarily deep. TypeScript validates that the path exists and has the correct type.
|
|
190
99
|
|
|
191
100
|
```tsx
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
on.select("users", 0);
|
|
101
|
+
<Switch {...on.switch("settings.notifications")}>
|
|
102
|
+
Enable Notifications
|
|
103
|
+
</Switch>
|
|
104
|
+
<Autocomplete {...on.autocomplete("settings.theme")} label="Theme" />
|
|
197
105
|
```
|
|
198
106
|
|
|
199
|
-
|
|
200
|
-
|
|
107
|
+
### Array Fields
|
|
108
|
+
|
|
109
|
+
Arrays of objects are tracked by item `id` (configurable via `arrayIdentifiers`). This means inputs hold their state correctly even after re-ordering or deletions.
|
|
201
110
|
|
|
202
111
|
```tsx
|
|
203
|
-
|
|
112
|
+
{
|
|
113
|
+
state.users.map((user) => (
|
|
114
|
+
<div key={user.id}>
|
|
115
|
+
{/* Bind to a field inside the array item using: path + itemId */}
|
|
116
|
+
<Input {...on.input("users.name", user.id)} label='Name' />
|
|
117
|
+
<Select {...on.select("users.role", user.id)} label='Role'>
|
|
118
|
+
<SelectItem key='admin'>Admin</SelectItem>
|
|
119
|
+
<SelectItem key='guest'>Guest</SelectItem>
|
|
120
|
+
</Select>
|
|
121
|
+
<Button onPress={() => helpers.removeById("users", user.id)}>
|
|
122
|
+
Remove
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
<Button
|
|
128
|
+
onPress={() => helpers.addItem("users", { id: Date.now(), role: "guest" })}
|
|
129
|
+
>
|
|
130
|
+
Add User
|
|
131
|
+
</Button>;
|
|
132
|
+
```
|
|
204
133
|
|
|
205
|
-
|
|
206
|
-
helpers.addItem("users", { id: Date.now(), role: "guest" });
|
|
134
|
+
---
|
|
207
135
|
|
|
208
|
-
|
|
209
|
-
helpers.removeById("users", 12345);
|
|
136
|
+
## The `on` API — Component Bindings
|
|
210
137
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
138
|
+
Each `on.*` method returns a set of props that you spread directly onto a HeroUI component. Every method automatically handles:
|
|
139
|
+
|
|
140
|
+
- **Controlled value** — synced with form state
|
|
141
|
+
- **Validation** — runs on change and blur against your Yup schema
|
|
142
|
+
- **Error display** — `isInvalid` and `errorMessage` from the schema
|
|
143
|
+
- **Required indicator** — `isRequired` derived from `.required()` in your schema
|
|
214
144
|
|
|
215
|
-
|
|
145
|
+
All methods support both **scalar/nested** paths and **array item** paths (composite `"array.field"` + `itemId`).
|
|
216
146
|
|
|
217
|
-
|
|
147
|
+
| Method | HeroUI Component | Key props returned |
|
|
148
|
+
| --------------------------------- | ------------------- | ----------------------------------------------- |
|
|
149
|
+
| `on.input(path, [itemId])` | `Input`, `Textarea` | `value: string`, `onValueChange(string)` |
|
|
150
|
+
| `on.numberInput(path, [itemId])` | `NumberInput` | `value: number`, `onValueChange(number)` |
|
|
151
|
+
| `on.select(path, [itemId])` | `Select` | `selectedKeys`, `onSelectionChange` |
|
|
152
|
+
| `on.autocomplete(path, [itemId])` | `Autocomplete` | `selectedKey`, `onSelectionChange` |
|
|
153
|
+
| `on.checkbox(path, [itemId])` | `Checkbox` | `isSelected: boolean`, `onValueChange(boolean)` |
|
|
154
|
+
| `on.switch(path, [itemId])` | `Switch` | `isSelected: boolean`, `onValueChange(boolean)` |
|
|
155
|
+
| `on.radio(path, [itemId])` | `RadioGroup` | `value: string`, `onValueChange(string)` |
|
|
218
156
|
|
|
219
|
-
**
|
|
157
|
+
> **Why separate methods?** Each HeroUI component has a different prop contract (e.g. `isSelected` vs `value`, `onSelectionChange` vs `onValueChange`). Separate methods give accurate intellisense for each component.
|
|
220
158
|
|
|
221
|
-
|
|
159
|
+
### Examples
|
|
222
160
|
|
|
223
161
|
```tsx
|
|
224
|
-
|
|
162
|
+
{/* Text input */}
|
|
163
|
+
<Input {...on.input("firstName")} label="First Name" />
|
|
164
|
+
|
|
165
|
+
{/* Numeric input (NumberInput-specific) */}
|
|
166
|
+
<NumberInput {...on.numberInput("age")} label="Age" />
|
|
167
|
+
|
|
168
|
+
{/* Boolean toggle */}
|
|
169
|
+
<Checkbox {...on.checkbox("agreed")}>I agree to the terms</Checkbox>
|
|
170
|
+
<Switch {...on.switch("notifications")}>Notifications</Switch>
|
|
171
|
+
|
|
172
|
+
{/* String radio selection */}
|
|
173
|
+
<RadioGroup {...on.radio("gender")} label="Gender">
|
|
174
|
+
<Radio value="male">Male</Radio>
|
|
175
|
+
<Radio value="female">Female</Radio>
|
|
176
|
+
</RadioGroup>
|
|
177
|
+
|
|
178
|
+
{/* Dropdown selection */}
|
|
179
|
+
<Select {...on.select("country")} label="Country">
|
|
180
|
+
<SelectItem key="us">United States</SelectItem>
|
|
181
|
+
<SelectItem key="ca">Canada</SelectItem>
|
|
182
|
+
</Select>
|
|
225
183
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<Button type='submit'>Submit</Button>
|
|
230
|
-
</ControlledForm>
|
|
231
|
-
);
|
|
184
|
+
{/* Inside an array — pass item ID as 2nd arg */}
|
|
185
|
+
<Input {...on.input("users.name", user.id)} label="Name" />
|
|
186
|
+
<Switch {...on.switch("users.active", user.id)}>Active</Switch>
|
|
232
187
|
```
|
|
233
188
|
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
+
```ts
|
|
203
|
+
const { onFieldChange, onArrayItemChange, onSelectionChange } = useForm(schema);
|
|
255
204
|
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
##
|
|
219
|
+
## Array Helpers
|
|
279
220
|
|
|
280
|
-
|
|
221
|
+
Extracted from `helpers` on the `useForm` return value. Items in object arrays are tracked by ID (default field: `id`).
|
|
281
222
|
|
|
282
|
-
|
|
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
|
|
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
|
-
|
|
243
|
+
### Custom Array Identifier
|
|
289
244
|
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
+
## Form Submission
|
|
307
263
|
|
|
308
|
-
|
|
264
|
+
### Option 1: `ControlledForm` (recommended)
|
|
309
265
|
|
|
310
|
-
|
|
311
|
-
| :-------------------------------- | :--------------------------------------------------------------------------- |
|
|
312
|
-
| **`on.input(path, [itemId])`** | Exports `{ id, isInvalid, errorMessage, value, onValueChange, onBlur }`. |
|
|
313
|
-
| **`on.select(path, [itemId])`** | Exports properties mapped for `<Select>` components matching `selectedKeys`. |
|
|
314
|
-
| **`on.autocomplete(path, [id])`** | Exports properties mapped for `<Autocomplete>` matching `selectedKey`. |
|
|
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
|
-
###
|
|
281
|
+
### Option 2: `onFormSubmit`
|
|
319
282
|
|
|
320
|
-
|
|
283
|
+
For use with plain HTML `<form>` elements:
|
|
321
284
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
| **`onSubmit`** | `(data, e) => void` | Invoked strictly post-validation of `schema` variables passing criteria. Both arguments `data` (your state payload) and `e` (the React event) are correctly typed. |
|
|
285
|
+
```tsx
|
|
286
|
+
const { onFormSubmit } = useForm(schema);
|
|
325
287
|
|
|
326
|
-
|
|
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
|
-
|
|
318
|
+
---
|
|
331
319
|
|
|
332
|
-
|
|
320
|
+
## API Reference
|
|
333
321
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
|
337
|
-
|
|
|
338
|
-
|
|
|
339
|
-
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
359
|
+
## Localization
|
|
360
|
+
|
|
361
|
+
Validation error messages are automatically localized. The library reads a `LOCALE` cookie on initialization.
|
|
347
362
|
|
|
348
|
-
|
|
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
|
-
|
|
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,32 +99,56 @@ 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
|
*/
|
|
98
115
|
interface OnMethods<O extends StateType> {
|
|
99
116
|
/** Registers a scalar or nested object field. */
|
|
100
117
|
input<P extends AllPaths<O>>(id: P): ItemInputProps<NestedFieldValue<O, P & string>>;
|
|
101
|
-
/** Registers an array element — adapts to primitive arrays (by index) or object arrays (by ID + field). */
|
|
102
|
-
/** Registers an array element — adapts to primitive arrays (by index). */
|
|
103
|
-
input<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemInputProps<any>;
|
|
104
118
|
/** Registers an object array element's field using composite syntax "array.field". */
|
|
105
119
|
input<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemInputProps<any>;
|
|
120
|
+
/** Registers a primitive array element by index. */
|
|
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;
|
|
106
126
|
/** Registers a scalar or nested object field. */
|
|
107
127
|
select<P extends AllPaths<O>>(id: P): ItemSelectProps;
|
|
108
128
|
/** Registers a complete array field for multi-selection. */
|
|
109
129
|
select<K extends ArrayPaths<O>>(arrayKey: K): ItemSelectProps;
|
|
110
|
-
/** Registers an array element — adapts to primitive arrays (by index) or object arrays (by ID + field). */
|
|
111
|
-
select<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemSelectProps;
|
|
112
130
|
/** Registers an object array element's field using composite syntax "array.field". */
|
|
113
131
|
select<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemSelectProps;
|
|
132
|
+
/** Registers a primitive array element by index. */
|
|
133
|
+
select<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemSelectProps;
|
|
114
134
|
/** Registers a scalar or nested object field. */
|
|
115
135
|
autocomplete<P extends AllPaths<O>>(id: P): ItemAutocompleteProps;
|
|
116
|
-
/** Registers an array element — adapts to primitive arrays (by index) or object arrays (by ID + field). */
|
|
117
|
-
/** Registers an array element — adapts to primitive arrays (by index). */
|
|
118
|
-
autocomplete<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemAutocompleteProps;
|
|
119
136
|
/** Registers an object array element's field using composite syntax "array.field". */
|
|
120
137
|
autocomplete<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemAutocompleteProps;
|
|
138
|
+
/** Registers a primitive array element by index. */
|
|
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;
|
|
121
152
|
}
|
|
122
153
|
/**
|
|
123
154
|
* Recursive type to find all paths to arrays in the state.
|
|
@@ -125,10 +156,6 @@ interface OnMethods<O extends StateType> {
|
|
|
125
156
|
type ArrayPaths<T> = T extends object ? {
|
|
126
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;
|
|
127
158
|
}[keyof T & string] : never;
|
|
128
|
-
/** Keys whose values are arrays of objects (not primitives). */
|
|
129
|
-
type ObjectArrayKeys<O extends StateType> = {
|
|
130
|
-
[K in keyof O]: O[K] extends Record<string, any>[] ? K : never;
|
|
131
|
-
}[keyof O];
|
|
132
159
|
/**
|
|
133
160
|
* Helper to get paths for fields within object arrays.
|
|
134
161
|
* Returns paths like "arrayKey.fieldName".
|
|
@@ -137,8 +164,6 @@ type ObjectArrayFieldPaths<O extends StateType> = {
|
|
|
137
164
|
[K in keyof O]: O[K] extends Record<string, any>[] ? `${K & string}.${FieldPaths<ArrayElement<O[K]>>}` : never;
|
|
138
165
|
}[keyof O];
|
|
139
166
|
type ArrayElement<T> = T extends (infer E)[] ? E : never;
|
|
140
|
-
/** Resolves the type of the identifier field for an array element (defaults to "id"). */
|
|
141
|
-
type ItemIdType<O extends StateType, K extends keyof O> = "id" extends keyof ArrayElement<O[K]> ? ArrayElement<O[K]>["id"] : string | number;
|
|
142
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;
|
|
143
168
|
type FieldPaths<T> = T extends Record<string, any> ? {
|
|
144
169
|
[K in keyof T & string]: T[K] extends any[] ? K : T[K] extends Record<string, any> ? `${K}.${FieldPaths<T[K]>}` : K;
|
|
@@ -161,8 +186,10 @@ interface HelpersFunc<O extends StateType> {
|
|
|
161
186
|
removeItem: <K extends ArrayPaths<O>>(arrayKey: K, index: number) => void;
|
|
162
187
|
/** Removes an item from an array by its unique identifier. */
|
|
163
188
|
removeById: <K extends ArrayPaths<O>>(arrayKey: K, itemId: string | number) => void;
|
|
164
|
-
/**
|
|
165
|
-
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;
|
|
166
193
|
/** Moves an item within an array using indices. */
|
|
167
194
|
moveItem: <K extends ArrayPaths<O>>(arrayKey: K, from: number, to: number) => void;
|
|
168
195
|
/** Moves an item within an array using unique identifiers. */
|
|
@@ -176,11 +203,14 @@ interface HelpersFunc<O extends StateType> {
|
|
|
176
203
|
*/
|
|
177
204
|
interface UseFormResponse<O extends StateType> {
|
|
178
205
|
onFieldBlur: BlurFunc<O>;
|
|
179
|
-
/** Updates an object array element's field by ID. */
|
|
180
|
-
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;
|
|
181
206
|
/** Updates a scalar or nested object field. */
|
|
182
207
|
onFieldChange<P extends AllPaths<O>>(id: P, value: NestedFieldValue<O, P & string>): 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. */
|
|
183
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;
|
|
184
214
|
state: O;
|
|
185
215
|
setState: React.Dispatch<React.SetStateAction<O>>;
|
|
186
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,32 +99,56 @@ 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
|
*/
|
|
98
115
|
interface OnMethods<O extends StateType> {
|
|
99
116
|
/** Registers a scalar or nested object field. */
|
|
100
117
|
input<P extends AllPaths<O>>(id: P): ItemInputProps<NestedFieldValue<O, P & string>>;
|
|
101
|
-
/** Registers an array element — adapts to primitive arrays (by index) or object arrays (by ID + field). */
|
|
102
|
-
/** Registers an array element — adapts to primitive arrays (by index). */
|
|
103
|
-
input<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemInputProps<any>;
|
|
104
118
|
/** Registers an object array element's field using composite syntax "array.field". */
|
|
105
119
|
input<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemInputProps<any>;
|
|
120
|
+
/** Registers a primitive array element by index. */
|
|
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;
|
|
106
126
|
/** Registers a scalar or nested object field. */
|
|
107
127
|
select<P extends AllPaths<O>>(id: P): ItemSelectProps;
|
|
108
128
|
/** Registers a complete array field for multi-selection. */
|
|
109
129
|
select<K extends ArrayPaths<O>>(arrayKey: K): ItemSelectProps;
|
|
110
|
-
/** Registers an array element — adapts to primitive arrays (by index) or object arrays (by ID + field). */
|
|
111
|
-
select<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemSelectProps;
|
|
112
130
|
/** Registers an object array element's field using composite syntax "array.field". */
|
|
113
131
|
select<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemSelectProps;
|
|
132
|
+
/** Registers a primitive array element by index. */
|
|
133
|
+
select<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemSelectProps;
|
|
114
134
|
/** Registers a scalar or nested object field. */
|
|
115
135
|
autocomplete<P extends AllPaths<O>>(id: P): ItemAutocompleteProps;
|
|
116
|
-
/** Registers an array element — adapts to primitive arrays (by index) or object arrays (by ID + field). */
|
|
117
|
-
/** Registers an array element — adapts to primitive arrays (by index). */
|
|
118
|
-
autocomplete<K extends ArrayPaths<O>>(arrayKey: K, index: number): ItemAutocompleteProps;
|
|
119
136
|
/** Registers an object array element's field using composite syntax "array.field". */
|
|
120
137
|
autocomplete<P extends ObjectArrayFieldPaths<O>>(compositePath: P, itemId: string | number): ItemAutocompleteProps;
|
|
138
|
+
/** Registers a primitive array element by index. */
|
|
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;
|
|
121
152
|
}
|
|
122
153
|
/**
|
|
123
154
|
* Recursive type to find all paths to arrays in the state.
|
|
@@ -125,10 +156,6 @@ interface OnMethods<O extends StateType> {
|
|
|
125
156
|
type ArrayPaths<T> = T extends object ? {
|
|
126
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;
|
|
127
158
|
}[keyof T & string] : never;
|
|
128
|
-
/** Keys whose values are arrays of objects (not primitives). */
|
|
129
|
-
type ObjectArrayKeys<O extends StateType> = {
|
|
130
|
-
[K in keyof O]: O[K] extends Record<string, any>[] ? K : never;
|
|
131
|
-
}[keyof O];
|
|
132
159
|
/**
|
|
133
160
|
* Helper to get paths for fields within object arrays.
|
|
134
161
|
* Returns paths like "arrayKey.fieldName".
|
|
@@ -137,8 +164,6 @@ type ObjectArrayFieldPaths<O extends StateType> = {
|
|
|
137
164
|
[K in keyof O]: O[K] extends Record<string, any>[] ? `${K & string}.${FieldPaths<ArrayElement<O[K]>>}` : never;
|
|
138
165
|
}[keyof O];
|
|
139
166
|
type ArrayElement<T> = T extends (infer E)[] ? E : never;
|
|
140
|
-
/** Resolves the type of the identifier field for an array element (defaults to "id"). */
|
|
141
|
-
type ItemIdType<O extends StateType, K extends keyof O> = "id" extends keyof ArrayElement<O[K]> ? ArrayElement<O[K]>["id"] : string | number;
|
|
142
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;
|
|
143
168
|
type FieldPaths<T> = T extends Record<string, any> ? {
|
|
144
169
|
[K in keyof T & string]: T[K] extends any[] ? K : T[K] extends Record<string, any> ? `${K}.${FieldPaths<T[K]>}` : K;
|
|
@@ -161,8 +186,10 @@ interface HelpersFunc<O extends StateType> {
|
|
|
161
186
|
removeItem: <K extends ArrayPaths<O>>(arrayKey: K, index: number) => void;
|
|
162
187
|
/** Removes an item from an array by its unique identifier. */
|
|
163
188
|
removeById: <K extends ArrayPaths<O>>(arrayKey: K, itemId: string | number) => void;
|
|
164
|
-
/**
|
|
165
|
-
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;
|
|
166
193
|
/** Moves an item within an array using indices. */
|
|
167
194
|
moveItem: <K extends ArrayPaths<O>>(arrayKey: K, from: number, to: number) => void;
|
|
168
195
|
/** Moves an item within an array using unique identifiers. */
|
|
@@ -176,11 +203,14 @@ interface HelpersFunc<O extends StateType> {
|
|
|
176
203
|
*/
|
|
177
204
|
interface UseFormResponse<O extends StateType> {
|
|
178
205
|
onFieldBlur: BlurFunc<O>;
|
|
179
|
-
/** Updates an object array element's field by ID. */
|
|
180
|
-
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;
|
|
181
206
|
/** Updates a scalar or nested object field. */
|
|
182
207
|
onFieldChange<P extends AllPaths<O>>(id: P, value: NestedFieldValue<O, P & string>): 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. */
|
|
183
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;
|
|
184
214
|
state: O;
|
|
185
215
|
setState: React.Dispatch<React.SetStateAction<O>>;
|
|
186
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
|
|
802
|
-
(
|
|
803
|
-
const
|
|
804
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
996
|
+
onFieldChange: scalarOnFieldChange,
|
|
997
|
+
onArrayItemChange: arrayItemChange,
|
|
896
998
|
onFieldBlur: onBlur,
|
|
897
|
-
onSelectionChange:
|
|
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
|
-
|
|
911
|
-
|
|
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
|
|
776
|
-
(
|
|
777
|
-
const
|
|
778
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
970
|
+
onFieldChange: scalarOnFieldChange,
|
|
971
|
+
onArrayItemChange: arrayItemChange,
|
|
870
972
|
onFieldBlur: onBlur,
|
|
871
|
-
onSelectionChange:
|
|
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
|
-
|
|
885
|
-
|
|
987
|
+
scalarOnFieldChange,
|
|
988
|
+
arrayItemChange,
|
|
989
|
+
scalarOnSelectionChange,
|
|
990
|
+
arraySelectionChange,
|
|
886
991
|
validateAll,
|
|
887
992
|
onFormSubmit,
|
|
888
993
|
ControlledForm,
|