rfhook 1.1.2 → 1.2.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/.husky/commit-msg +1 -0
- package/.prettierignore +3 -0
- package/.prettierrc +10 -0
- package/CHANGELOG.md +19 -0
- package/README.md +167 -6
- package/commitlint.config.ts +20 -0
- package/dist/index.cjs +59 -6
- package/dist/index.js +59 -6
- package/dist/useForm.hook.d.ts +42 -4
- package/eslint.config.mjs +50 -0
- package/package.json +20 -3
- package/src/index.ts +1 -1
- package/src/useForm.hook.ts +99 -38
- package/src/utils/parseFormData.ts +26 -22
- package/tsconfig.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx --no -- commitlint --edit
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
|
+
|
|
5
|
+
## 1.2.0 (2026-01-21)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* enhance useForm hook with improved type safety and additional utility functions ([73bee0e](https://github.com/evandroishikawa/rfhook/commit/73bee0ee58522deb2c21d8a25ada531c5411e339))
|
|
11
|
+
* initial commit for form-hook ([67f2812](https://github.com/evandroishikawa/rfhook/commit/67f28128b51236ee24727cb1378f679ad646765e))
|
|
12
|
+
* update package name to "rfhook" and add cSpell configuration ([7035768](https://github.com/evandroishikawa/rfhook/commit/7035768698e046c01248bcb93fe2f2e9909ec51f))
|
|
13
|
+
* update README to clarify automatic prevention of default form submission ([4cf0cb6](https://github.com/evandroishikawa/rfhook/commit/4cf0cb657421eb65891d8108b6fab2a27f943ee5))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* bump version to 1.0.1 in package.json ([537382c](https://github.com/evandroishikawa/rfhook/commit/537382c4a080ec9308da4b733e31cfbcc3d37d57))
|
|
19
|
+
* update package name from "@eji/form-hook" to "rfhook" in README.md ([d65177a](https://github.com/evandroishikawa/rfhook/commit/d65177a3cb5fecbb4509414972f786b21fbf9b9e))
|
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ A lightweight React hook for handling form submissions with advanced form data p
|
|
|
9
9
|
- :wrench: **Nested Objects** - Parse nested form data with dot notation (`user.name`)
|
|
10
10
|
- :clipboard: **Array Support** - Handle arrays with indexed notation (`items[0]`)
|
|
11
11
|
- :floppy_disk: **Automatic Prevention** - Prevents default form submission behavior by default
|
|
12
|
+
- :seedling: **Form Initialization** - Pre-populate forms with initial data or load data dynamically
|
|
13
|
+
- :arrows_counterclockwise: **Smart Reset** - Reset to initial state, custom data, or clear completely
|
|
12
14
|
- :money_with_wings: **Lightweight** - Zero dependencies (except React)
|
|
13
15
|
- :memo: **Framework Agnostic** - Works with any form structure
|
|
14
16
|
|
|
@@ -28,6 +30,8 @@ yarn add rfhook
|
|
|
28
30
|
|
|
29
31
|
## Quick Start
|
|
30
32
|
|
|
33
|
+
### Basic Usage
|
|
34
|
+
|
|
31
35
|
```tsx
|
|
32
36
|
import React from 'react';
|
|
33
37
|
import { useForm } from 'rfhook';
|
|
@@ -52,7 +56,40 @@ function LoginForm() {
|
|
|
52
56
|
</form>
|
|
53
57
|
);
|
|
54
58
|
}
|
|
55
|
-
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### With Initial Data
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import React, { useEffect } from 'react';
|
|
65
|
+
import { useForm } from 'rfhook';
|
|
66
|
+
|
|
67
|
+
function EditUserForm({ userId }) {
|
|
68
|
+
const form = useForm<FormData>({
|
|
69
|
+
submit: (data) => updateUser(userId, data),
|
|
70
|
+
initialData: { name: '', email: '', phone: '' }
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Load user data and initialize form
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const loadUser = async () => {
|
|
76
|
+
if (userId) {
|
|
77
|
+
const userData = await fetchUser(userId);
|
|
78
|
+
form.initialize(userData);
|
|
79
|
+
} else {
|
|
80
|
+
form.initialize(); // Use initialData
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
loadUser();
|
|
85
|
+
}, [userId]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<form ref={form.ref} onSubmit={form.onSubmit}>
|
|
89
|
+
<input name="name" placeholder="Name" />
|
|
90
|
+
<input name="email" type="email" placeholder="Email" />
|
|
91
|
+
<input name="phone" placeholder="Phone" />
|
|
92
|
+
<button type="submit">Save</button>
|
|
56
93
|
</form>
|
|
57
94
|
);
|
|
58
95
|
}
|
|
@@ -155,27 +192,124 @@ function TodoForm() {
|
|
|
155
192
|
#### Parameters
|
|
156
193
|
|
|
157
194
|
- `options.submit: (data: T) => void` - Callback function called when form is submitted with parsed form data
|
|
195
|
+
- `options.initialData?: T` - Optional initial data to populate the form fields
|
|
158
196
|
|
|
159
197
|
#### Returns
|
|
160
198
|
|
|
161
199
|
- `ref: React.RefObject<HTMLFormElement>` - React ref to attach to your form element
|
|
162
200
|
- `onSubmit: (event: React.FormEvent<HTMLFormElement>) => void` - Form submission handler that prevents default behavior and calls submit with parsed data
|
|
163
|
-
- `reset: () => void` - Resets the form to
|
|
201
|
+
- `reset: (data?: T, shouldUseInitialData?: boolean) => void` - Resets the form. If `data` is provided, sets form to that data. If `shouldUseInitialData` is true (default), falls back to initial data when no data is provided
|
|
164
202
|
- `getFormData: () => T | null` - Gets current form data without triggering submission (returns null if form ref is not available)
|
|
165
203
|
- `setValue: (name: string, value: string) => void` - Sets the value of a specific form field by name
|
|
204
|
+
- `initialize: (data?: T) => void` - Initializes the form with provided data or falls back to initial data from options
|
|
205
|
+
|
|
206
|
+
### Form Initialization Examples
|
|
207
|
+
|
|
208
|
+
#### Static Initial Data
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
const form = useForm<UserData>({
|
|
212
|
+
submit: (data) => saveUser(data),
|
|
213
|
+
initialData: {
|
|
214
|
+
name: 'John Doe',
|
|
215
|
+
email: 'john@example.com',
|
|
216
|
+
role: 'user'
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Initialize with predefined data on component mount
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
form.initialize(); // Uses initialData
|
|
223
|
+
}, []);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### Dynamic Data Loading
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
function EditProfile({ userId }) {
|
|
230
|
+
const form = useForm<ProfileData>({
|
|
231
|
+
submit: (data) => updateProfile(userId, data)
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
const loadProfile = async () => {
|
|
236
|
+
try {
|
|
237
|
+
const profile = await fetchProfile(userId);
|
|
238
|
+
form.initialize(profile);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error('Failed to load profile:', error);
|
|
241
|
+
// Initialize with empty data or defaults
|
|
242
|
+
form.initialize({ name: '', email: '', bio: '' });
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (userId) loadProfile();
|
|
247
|
+
}, [userId]);
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<form ref={form.ref} onSubmit={form.onSubmit}>
|
|
251
|
+
<input name="name" placeholder="Full Name" />
|
|
252
|
+
<input name="email" type="email" placeholder="Email" />
|
|
253
|
+
<textarea name="bio" placeholder="Bio" />
|
|
254
|
+
<button type="submit">Save Profile</button>
|
|
255
|
+
</form>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Conditional Reset Behavior
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
function FormWithMultipleResets() {
|
|
264
|
+
const form = useForm<FormData>({
|
|
265
|
+
submit: (data) => console.log(data),
|
|
266
|
+
initialData: { name: 'Default Name', email: '' }
|
|
267
|
+
});
|
|
166
268
|
|
|
167
|
-
|
|
269
|
+
return (
|
|
270
|
+
<>
|
|
271
|
+
<form ref={form.ref} onSubmit={form.onSubmit}>
|
|
272
|
+
<input name="name" placeholder="Name" />
|
|
273
|
+
<input name="email" placeholder="Email" />
|
|
274
|
+
<button type="submit">Submit</button>
|
|
275
|
+
</form>
|
|
276
|
+
|
|
277
|
+
<div>
|
|
278
|
+
{/* Reset to initial data */}
|
|
279
|
+
<button onClick={() => form.reset()}>Reset to Defaults</button>
|
|
280
|
+
|
|
281
|
+
{/* Reset to custom data */}
|
|
282
|
+
<button onClick={() => form.reset({ name: 'Custom Name', email: 'custom@example.com' })}>
|
|
283
|
+
Reset to Custom
|
|
284
|
+
</button>
|
|
285
|
+
|
|
286
|
+
{/* Clear form completely */}
|
|
287
|
+
<button onClick={() => form.reset({}, false)}>Clear Form</button>
|
|
288
|
+
</div>
|
|
289
|
+
</>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
```
|
|
168
293
|
|
|
169
294
|
#### Using Form Utilities
|
|
170
295
|
|
|
171
296
|
```tsx
|
|
172
297
|
function MyForm() {
|
|
173
298
|
const form = useForm<FormData>({
|
|
174
|
-
submit: (data) => console.log('Submitted:', data)
|
|
299
|
+
submit: (data) => console.log('Submitted:', data),
|
|
300
|
+
initialData: { name: '', email: '', age: '' }
|
|
175
301
|
});
|
|
176
302
|
|
|
177
303
|
const handleReset = () => {
|
|
178
|
-
form.reset(); // Reset
|
|
304
|
+
form.reset(); // Reset to initial data
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const handleClearForm = () => {
|
|
308
|
+
form.reset({}, false); // Clear form completely
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const handleResetToDefaults = () => {
|
|
312
|
+
form.reset({ name: 'Default User', email: '', age: '25' }); // Reset to specific data
|
|
179
313
|
};
|
|
180
314
|
|
|
181
315
|
const handlePreview = () => {
|
|
@@ -190,18 +324,27 @@ function MyForm() {
|
|
|
190
324
|
form.setValue('name', 'John Doe');
|
|
191
325
|
};
|
|
192
326
|
|
|
327
|
+
const handleLoadFromAPI = async () => {
|
|
328
|
+
const userData = await fetchUserData();
|
|
329
|
+
form.initialize(userData);
|
|
330
|
+
};
|
|
331
|
+
|
|
193
332
|
return (
|
|
194
333
|
<>
|
|
195
334
|
<form ref={form.ref} onSubmit={form.onSubmit}>
|
|
196
335
|
<input name="name" placeholder="Name" />
|
|
197
336
|
<input name="email" type="email" placeholder="Email" />
|
|
337
|
+
<input name="age" type="number" placeholder="Age" />
|
|
198
338
|
<button type="submit">Submit</button>
|
|
199
339
|
</form>
|
|
200
340
|
|
|
201
341
|
<div>
|
|
202
|
-
<button onClick={handleReset}>Reset
|
|
342
|
+
<button onClick={handleReset}>Reset to Initial</button>
|
|
343
|
+
<button onClick={handleClearForm}>Clear Form</button>
|
|
344
|
+
<button onClick={handleResetToDefaults}>Reset to Defaults</button>
|
|
203
345
|
<button onClick={handlePreview}>Preview Data</button>
|
|
204
346
|
<button onClick={handlePrefill}>Prefill Form</button>
|
|
347
|
+
<button onClick={handleLoadFromAPI}>Load from API</button>
|
|
205
348
|
</div>
|
|
206
349
|
</>
|
|
207
350
|
);
|
|
@@ -318,5 +461,23 @@ pnpm install
|
|
|
318
461
|
# Build the package
|
|
319
462
|
pnpm run build
|
|
320
463
|
|
|
464
|
+
# Code quality
|
|
465
|
+
pnpm run lint # Check for linting issues
|
|
466
|
+
pnpm run lint:fix # Auto-fix linting issues
|
|
467
|
+
pnpm run format # Format code with Prettier
|
|
468
|
+
pnpm run format:check # Check code formatting
|
|
469
|
+
|
|
321
470
|
# The built files will be in the `dist/` directory
|
|
322
471
|
```
|
|
472
|
+
|
|
473
|
+
### Code Style
|
|
474
|
+
|
|
475
|
+
This project uses:
|
|
476
|
+
|
|
477
|
+
- **ESLint** for code linting with TypeScript and React support
|
|
478
|
+
- **Prettier** for code formatting with these rules:
|
|
479
|
+
- Single quotes
|
|
480
|
+
- No semicolons
|
|
481
|
+
- Trailing commas
|
|
482
|
+
- 2-space indentation
|
|
483
|
+
- 80 character line width
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const commitTypes = [
|
|
2
|
+
'chore',
|
|
3
|
+
'fix',
|
|
4
|
+
'feat',
|
|
5
|
+
'docs',
|
|
6
|
+
'style',
|
|
7
|
+
'refactor',
|
|
8
|
+
'test',
|
|
9
|
+
'ci',
|
|
10
|
+
'build',
|
|
11
|
+
'revert',
|
|
12
|
+
'wip',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
extends: ['@commitlint/config-conventional'],
|
|
17
|
+
rules: {
|
|
18
|
+
'type-enum': [2, 'always', commitTypes],
|
|
19
|
+
},
|
|
20
|
+
}
|
package/dist/index.cjs
CHANGED
|
@@ -40,7 +40,9 @@ function parseFormData(formData) {
|
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
42
42
|
// Initialize nested object and move to it
|
|
43
|
-
if (!current[key] ||
|
|
43
|
+
if (!current[key] ||
|
|
44
|
+
typeof current[key] !== 'object' ||
|
|
45
|
+
Array.isArray(current[key])) {
|
|
44
46
|
current[key] = {};
|
|
45
47
|
}
|
|
46
48
|
current = current[key];
|
|
@@ -50,13 +52,14 @@ function parseFormData(formData) {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
|
-
* A React hook for handling form state and submission
|
|
55
|
+
* A React hook for handling form state and submission with initialization support
|
|
54
56
|
*
|
|
55
57
|
* @template T - The expected shape of the parsed form data
|
|
56
58
|
* @param options - Configuration options for the form
|
|
57
59
|
* @returns Object containing form ref, handlers, and utility functions
|
|
58
60
|
*
|
|
59
61
|
* @example
|
|
62
|
+
* Basic usage:
|
|
60
63
|
* ```typescript
|
|
61
64
|
* interface LoginData {
|
|
62
65
|
* email: string;
|
|
@@ -75,9 +78,43 @@ function parseFormData(formData) {
|
|
|
75
78
|
* </form>
|
|
76
79
|
* );
|
|
77
80
|
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* With initial data:
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const form = useForm<LoginData>({
|
|
86
|
+
* submit: (data) => console.log(data),
|
|
87
|
+
* initialData: { email: 'user@example.com', password: '' }
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* // Initialize form with initial data on mount
|
|
91
|
+
* useEffect(() => {
|
|
92
|
+
* form.initialize();
|
|
93
|
+
* }, []);
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* Dynamic initialization:
|
|
98
|
+
* ```typescript
|
|
99
|
+
* const form = useForm<UserData>({
|
|
100
|
+
* submit: (data) => saveUser(data)
|
|
101
|
+
* });
|
|
102
|
+
*
|
|
103
|
+
* // Load and set user data from API
|
|
104
|
+
* const loadUser = async (userId: string) => {
|
|
105
|
+
* const userData = await fetchUser(userId);
|
|
106
|
+
* form.initialize(userData);
|
|
107
|
+
* };
|
|
108
|
+
*
|
|
109
|
+
* // Reset to specific data
|
|
110
|
+
* const resetToDefaults = () => {
|
|
111
|
+
* form.reset({ name: '', email: '', age: '' });
|
|
112
|
+
* };
|
|
113
|
+
* ```
|
|
78
114
|
*/
|
|
79
|
-
function useForm({ submit }) {
|
|
115
|
+
function useForm({ submit, initialData, }) {
|
|
80
116
|
const ref = react.useRef(null);
|
|
117
|
+
const initialDataRef = react.useRef(initialData);
|
|
81
118
|
const onSubmit = (event) => {
|
|
82
119
|
event.preventDefault();
|
|
83
120
|
if (!ref.current)
|
|
@@ -86,10 +123,16 @@ function useForm({ submit }) {
|
|
|
86
123
|
const data = parseFormData(formData);
|
|
87
124
|
submit(data);
|
|
88
125
|
};
|
|
89
|
-
const reset = () => {
|
|
126
|
+
const reset = (data, shouldUseInitialData = true) => {
|
|
90
127
|
if (!ref.current)
|
|
91
128
|
return;
|
|
92
129
|
ref.current.reset();
|
|
130
|
+
if (data) {
|
|
131
|
+
return initialize(data);
|
|
132
|
+
}
|
|
133
|
+
if (initialDataRef.current && shouldUseInitialData) {
|
|
134
|
+
initialize(initialDataRef.current);
|
|
135
|
+
}
|
|
93
136
|
};
|
|
94
137
|
const getFormData = () => {
|
|
95
138
|
if (!ref.current)
|
|
@@ -106,12 +149,22 @@ function useForm({ submit }) {
|
|
|
106
149
|
element.value = value;
|
|
107
150
|
}
|
|
108
151
|
};
|
|
152
|
+
const initialize = (data) => {
|
|
153
|
+
const dataToUse = data || initialDataRef.current;
|
|
154
|
+
if (!ref.current || !dataToUse)
|
|
155
|
+
return;
|
|
156
|
+
Object.entries(dataToUse).forEach(([key, value]) => {
|
|
157
|
+
if (value)
|
|
158
|
+
setValue(key, String(value));
|
|
159
|
+
});
|
|
160
|
+
};
|
|
109
161
|
return {
|
|
110
162
|
ref,
|
|
163
|
+
getFormData,
|
|
164
|
+
initialize,
|
|
111
165
|
onSubmit,
|
|
112
166
|
reset,
|
|
113
|
-
|
|
114
|
-
setValue
|
|
167
|
+
setValue,
|
|
115
168
|
};
|
|
116
169
|
}
|
|
117
170
|
|
package/dist/index.js
CHANGED
|
@@ -38,7 +38,9 @@ function parseFormData(formData) {
|
|
|
38
38
|
continue;
|
|
39
39
|
}
|
|
40
40
|
// Initialize nested object and move to it
|
|
41
|
-
if (!current[key] ||
|
|
41
|
+
if (!current[key] ||
|
|
42
|
+
typeof current[key] !== 'object' ||
|
|
43
|
+
Array.isArray(current[key])) {
|
|
42
44
|
current[key] = {};
|
|
43
45
|
}
|
|
44
46
|
current = current[key];
|
|
@@ -48,13 +50,14 @@ function parseFormData(formData) {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
|
-
* A React hook for handling form state and submission
|
|
53
|
+
* A React hook for handling form state and submission with initialization support
|
|
52
54
|
*
|
|
53
55
|
* @template T - The expected shape of the parsed form data
|
|
54
56
|
* @param options - Configuration options for the form
|
|
55
57
|
* @returns Object containing form ref, handlers, and utility functions
|
|
56
58
|
*
|
|
57
59
|
* @example
|
|
60
|
+
* Basic usage:
|
|
58
61
|
* ```typescript
|
|
59
62
|
* interface LoginData {
|
|
60
63
|
* email: string;
|
|
@@ -73,9 +76,43 @@ function parseFormData(formData) {
|
|
|
73
76
|
* </form>
|
|
74
77
|
* );
|
|
75
78
|
* ```
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* With initial data:
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const form = useForm<LoginData>({
|
|
84
|
+
* submit: (data) => console.log(data),
|
|
85
|
+
* initialData: { email: 'user@example.com', password: '' }
|
|
86
|
+
* });
|
|
87
|
+
*
|
|
88
|
+
* // Initialize form with initial data on mount
|
|
89
|
+
* useEffect(() => {
|
|
90
|
+
* form.initialize();
|
|
91
|
+
* }, []);
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* Dynamic initialization:
|
|
96
|
+
* ```typescript
|
|
97
|
+
* const form = useForm<UserData>({
|
|
98
|
+
* submit: (data) => saveUser(data)
|
|
99
|
+
* });
|
|
100
|
+
*
|
|
101
|
+
* // Load and set user data from API
|
|
102
|
+
* const loadUser = async (userId: string) => {
|
|
103
|
+
* const userData = await fetchUser(userId);
|
|
104
|
+
* form.initialize(userData);
|
|
105
|
+
* };
|
|
106
|
+
*
|
|
107
|
+
* // Reset to specific data
|
|
108
|
+
* const resetToDefaults = () => {
|
|
109
|
+
* form.reset({ name: '', email: '', age: '' });
|
|
110
|
+
* };
|
|
111
|
+
* ```
|
|
76
112
|
*/
|
|
77
|
-
function useForm({ submit }) {
|
|
113
|
+
function useForm({ submit, initialData, }) {
|
|
78
114
|
const ref = useRef(null);
|
|
115
|
+
const initialDataRef = useRef(initialData);
|
|
79
116
|
const onSubmit = (event) => {
|
|
80
117
|
event.preventDefault();
|
|
81
118
|
if (!ref.current)
|
|
@@ -84,10 +121,16 @@ function useForm({ submit }) {
|
|
|
84
121
|
const data = parseFormData(formData);
|
|
85
122
|
submit(data);
|
|
86
123
|
};
|
|
87
|
-
const reset = () => {
|
|
124
|
+
const reset = (data, shouldUseInitialData = true) => {
|
|
88
125
|
if (!ref.current)
|
|
89
126
|
return;
|
|
90
127
|
ref.current.reset();
|
|
128
|
+
if (data) {
|
|
129
|
+
return initialize(data);
|
|
130
|
+
}
|
|
131
|
+
if (initialDataRef.current && shouldUseInitialData) {
|
|
132
|
+
initialize(initialDataRef.current);
|
|
133
|
+
}
|
|
91
134
|
};
|
|
92
135
|
const getFormData = () => {
|
|
93
136
|
if (!ref.current)
|
|
@@ -104,12 +147,22 @@ function useForm({ submit }) {
|
|
|
104
147
|
element.value = value;
|
|
105
148
|
}
|
|
106
149
|
};
|
|
150
|
+
const initialize = (data) => {
|
|
151
|
+
const dataToUse = data || initialDataRef.current;
|
|
152
|
+
if (!ref.current || !dataToUse)
|
|
153
|
+
return;
|
|
154
|
+
Object.entries(dataToUse).forEach(([key, value]) => {
|
|
155
|
+
if (value)
|
|
156
|
+
setValue(key, String(value));
|
|
157
|
+
});
|
|
158
|
+
};
|
|
107
159
|
return {
|
|
108
160
|
ref,
|
|
161
|
+
getFormData,
|
|
162
|
+
initialize,
|
|
109
163
|
onSubmit,
|
|
110
164
|
reset,
|
|
111
|
-
|
|
112
|
-
setValue
|
|
165
|
+
setValue,
|
|
113
166
|
};
|
|
114
167
|
}
|
|
115
168
|
|
package/dist/useForm.hook.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
interface UseFormOptions<T> {
|
|
5
5
|
/** Callback function called when form is submitted with parsed form data */
|
|
6
6
|
submit: (data: T) => void;
|
|
7
|
+
/** Initial data to populate the form fields */
|
|
8
|
+
initialData?: T;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* Return type of the useForm hook
|
|
@@ -13,21 +15,24 @@ interface UseFormReturn<T> {
|
|
|
13
15
|
ref: React.RefObject<HTMLFormElement | null>;
|
|
14
16
|
/** Form submission handler - prevents default and calls submit with parsed data */
|
|
15
17
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
|
16
|
-
/** Resets the form to
|
|
17
|
-
reset: () => void;
|
|
18
|
+
/** Resets the form and optionally sets it to provided data or initial data */
|
|
19
|
+
reset: (data?: T, shouldUseInitialData?: boolean) => void;
|
|
18
20
|
/** Gets current form data without triggering submission */
|
|
19
21
|
getFormData: () => T | null;
|
|
20
22
|
/** Sets the value of a specific form field by name */
|
|
21
23
|
setValue: (name: string, value: string) => void;
|
|
24
|
+
/** Initializes the form with provided data or initial data from options */
|
|
25
|
+
initialize: (data?: T) => void;
|
|
22
26
|
}
|
|
23
27
|
/**
|
|
24
|
-
* A React hook for handling form state and submission
|
|
28
|
+
* A React hook for handling form state and submission with initialization support
|
|
25
29
|
*
|
|
26
30
|
* @template T - The expected shape of the parsed form data
|
|
27
31
|
* @param options - Configuration options for the form
|
|
28
32
|
* @returns Object containing form ref, handlers, and utility functions
|
|
29
33
|
*
|
|
30
34
|
* @example
|
|
35
|
+
* Basic usage:
|
|
31
36
|
* ```typescript
|
|
32
37
|
* interface LoginData {
|
|
33
38
|
* email: string;
|
|
@@ -46,6 +51,39 @@ interface UseFormReturn<T> {
|
|
|
46
51
|
* </form>
|
|
47
52
|
* );
|
|
48
53
|
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* With initial data:
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const form = useForm<LoginData>({
|
|
59
|
+
* submit: (data) => console.log(data),
|
|
60
|
+
* initialData: { email: 'user@example.com', password: '' }
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* // Initialize form with initial data on mount
|
|
64
|
+
* useEffect(() => {
|
|
65
|
+
* form.initialize();
|
|
66
|
+
* }, []);
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* Dynamic initialization:
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const form = useForm<UserData>({
|
|
73
|
+
* submit: (data) => saveUser(data)
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* // Load and set user data from API
|
|
77
|
+
* const loadUser = async (userId: string) => {
|
|
78
|
+
* const userData = await fetchUser(userId);
|
|
79
|
+
* form.initialize(userData);
|
|
80
|
+
* };
|
|
81
|
+
*
|
|
82
|
+
* // Reset to specific data
|
|
83
|
+
* const resetToDefaults = () => {
|
|
84
|
+
* form.reset({ name: '', email: '', age: '' });
|
|
85
|
+
* };
|
|
86
|
+
* ```
|
|
49
87
|
*/
|
|
50
|
-
export declare function useForm<T = Record<string, unknown>>({ submit }: UseFormOptions<T>): UseFormReturn<T>;
|
|
88
|
+
export declare function useForm<T = Record<string, unknown>>({ submit, initialData, }: UseFormOptions<T>): UseFormReturn<T>;
|
|
51
89
|
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import tseslint from '@typescript-eslint/eslint-plugin'
|
|
3
|
+
import tsparser from '@typescript-eslint/parser'
|
|
4
|
+
import react from 'eslint-plugin-react'
|
|
5
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
6
|
+
import prettier from 'eslint-plugin-prettier'
|
|
7
|
+
|
|
8
|
+
export default [
|
|
9
|
+
js.configs.recommended,
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx,js}'],
|
|
12
|
+
languageOptions: {
|
|
13
|
+
parser: tsparser,
|
|
14
|
+
parserOptions: {
|
|
15
|
+
ecmaVersion: 'latest',
|
|
16
|
+
sourceType: 'module',
|
|
17
|
+
ecmaFeatures: {
|
|
18
|
+
jsx: true,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
plugins: {
|
|
23
|
+
'@typescript-eslint': tseslint,
|
|
24
|
+
prettier,
|
|
25
|
+
react: react,
|
|
26
|
+
'react-hooks': reactHooks,
|
|
27
|
+
},
|
|
28
|
+
rules: {
|
|
29
|
+
...tseslint.configs.recommended.rules,
|
|
30
|
+
...react.configs.recommended.rules,
|
|
31
|
+
...reactHooks.configs.recommended.rules,
|
|
32
|
+
'react/react-in-jsx-scope': 'off',
|
|
33
|
+
'react/prop-types': 'off',
|
|
34
|
+
'@typescript-eslint/no-unused-vars': [
|
|
35
|
+
'error',
|
|
36
|
+
{ argsIgnorePattern: '^_' },
|
|
37
|
+
],
|
|
38
|
+
'no-undef': 'off',
|
|
39
|
+
'prettier/prettier': 'error',
|
|
40
|
+
},
|
|
41
|
+
settings: {
|
|
42
|
+
react: {
|
|
43
|
+
version: 'detect',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
ignores: ['dist/**', 'node_modules/**', '*.config.js'],
|
|
49
|
+
},
|
|
50
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rfhook",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -18,15 +18,32 @@
|
|
|
18
18
|
"react-dom": "^19.2.3"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
+
"@commitlint/cli": "^20.3.1",
|
|
22
|
+
"@commitlint/config-conventional": "^20.3.1",
|
|
23
|
+
"@eslint/js": "^9.39.2",
|
|
21
24
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
22
25
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
23
|
-
"@types/react": "^19.2.
|
|
26
|
+
"@types/react": "^19.2.9",
|
|
24
27
|
"@types/react-dom": "^19.2.3",
|
|
28
|
+
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
|
29
|
+
"@typescript-eslint/parser": "^8.53.1",
|
|
30
|
+
"eslint": "^9.39.2",
|
|
31
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
32
|
+
"eslint-plugin-react": "^7.37.5",
|
|
33
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
34
|
+
"husky": "^9.1.7",
|
|
35
|
+
"prettier": "^3.8.0",
|
|
25
36
|
"rollup": "^4.55.2",
|
|
37
|
+
"standard-version": "^9.5.0",
|
|
26
38
|
"tslib": "^2.8.1",
|
|
27
39
|
"typescript": "^5.9.3"
|
|
28
40
|
},
|
|
29
41
|
"scripts": {
|
|
30
|
-
"build": "rollup -c"
|
|
42
|
+
"build": "rollup -c",
|
|
43
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
44
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
45
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
46
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
|
|
47
|
+
"release": "standard-version"
|
|
31
48
|
}
|
|
32
49
|
}
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { useForm } from './useForm.hook'
|
|
1
|
+
export { useForm } from './useForm.hook'
|
package/src/useForm.hook.ts
CHANGED
|
@@ -1,44 +1,49 @@
|
|
|
1
|
-
import { useRef } from 'react'
|
|
1
|
+
import { useRef } from 'react'
|
|
2
2
|
|
|
3
|
-
import { parseFormData } from './utils/parseFormData'
|
|
3
|
+
import { parseFormData } from './utils/parseFormData'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Configuration options for the useForm hook
|
|
7
7
|
*/
|
|
8
8
|
interface UseFormOptions<T> {
|
|
9
9
|
/** Callback function called when form is submitted with parsed form data */
|
|
10
|
-
submit: (data: T) => void
|
|
10
|
+
submit: (data: T) => void
|
|
11
|
+
/** Initial data to populate the form fields */
|
|
12
|
+
initialData?: T
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Form element types that can have their values set programmatically
|
|
15
17
|
*/
|
|
16
|
-
type FormElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
18
|
+
type FormElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Return type of the useForm hook
|
|
20
22
|
*/
|
|
21
23
|
interface UseFormReturn<T> {
|
|
22
24
|
/** React ref to be attached to the form element */
|
|
23
|
-
ref: React.RefObject<HTMLFormElement | null
|
|
25
|
+
ref: React.RefObject<HTMLFormElement | null>
|
|
24
26
|
/** Form submission handler - prevents default and calls submit with parsed data */
|
|
25
|
-
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
|
26
|
-
/** Resets the form to
|
|
27
|
-
reset: () => void
|
|
27
|
+
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
|
28
|
+
/** Resets the form and optionally sets it to provided data or initial data */
|
|
29
|
+
reset: (data?: T, shouldUseInitialData?: boolean) => void
|
|
28
30
|
/** Gets current form data without triggering submission */
|
|
29
|
-
getFormData: () => T | null
|
|
31
|
+
getFormData: () => T | null
|
|
30
32
|
/** Sets the value of a specific form field by name */
|
|
31
|
-
setValue: (name: string, value: string) => void
|
|
33
|
+
setValue: (name: string, value: string) => void
|
|
34
|
+
/** Initializes the form with provided data or initial data from options */
|
|
35
|
+
initialize: (data?: T) => void
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
|
-
* A React hook for handling form state and submission
|
|
39
|
+
* A React hook for handling form state and submission with initialization support
|
|
36
40
|
*
|
|
37
41
|
* @template T - The expected shape of the parsed form data
|
|
38
42
|
* @param options - Configuration options for the form
|
|
39
43
|
* @returns Object containing form ref, handlers, and utility functions
|
|
40
44
|
*
|
|
41
45
|
* @example
|
|
46
|
+
* Basic usage:
|
|
42
47
|
* ```typescript
|
|
43
48
|
* interface LoginData {
|
|
44
49
|
* email: string;
|
|
@@ -57,55 +62,111 @@ interface UseFormReturn<T> {
|
|
|
57
62
|
* </form>
|
|
58
63
|
* );
|
|
59
64
|
* ```
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* With initial data:
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const form = useForm<LoginData>({
|
|
70
|
+
* submit: (data) => console.log(data),
|
|
71
|
+
* initialData: { email: 'user@example.com', password: '' }
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* // Initialize form with initial data on mount
|
|
75
|
+
* useEffect(() => {
|
|
76
|
+
* form.initialize();
|
|
77
|
+
* }, []);
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* Dynamic initialization:
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const form = useForm<UserData>({
|
|
84
|
+
* submit: (data) => saveUser(data)
|
|
85
|
+
* });
|
|
86
|
+
*
|
|
87
|
+
* // Load and set user data from API
|
|
88
|
+
* const loadUser = async (userId: string) => {
|
|
89
|
+
* const userData = await fetchUser(userId);
|
|
90
|
+
* form.initialize(userData);
|
|
91
|
+
* };
|
|
92
|
+
*
|
|
93
|
+
* // Reset to specific data
|
|
94
|
+
* const resetToDefaults = () => {
|
|
95
|
+
* form.reset({ name: '', email: '', age: '' });
|
|
96
|
+
* };
|
|
97
|
+
* ```
|
|
60
98
|
*/
|
|
61
|
-
export function useForm<T = Record<string, unknown>>(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
99
|
+
export function useForm<T = Record<string, unknown>>({
|
|
100
|
+
submit,
|
|
101
|
+
initialData,
|
|
102
|
+
}: UseFormOptions<T>): UseFormReturn<T> {
|
|
103
|
+
const ref = useRef<HTMLFormElement>(null)
|
|
104
|
+
const initialDataRef = useRef<T | undefined>(initialData)
|
|
65
105
|
|
|
66
106
|
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
67
|
-
|
|
107
|
+
event.preventDefault()
|
|
68
108
|
|
|
69
|
-
|
|
109
|
+
if (!ref.current) return
|
|
70
110
|
|
|
71
|
-
|
|
111
|
+
const formData = new FormData(ref.current)
|
|
72
112
|
|
|
73
|
-
|
|
113
|
+
const data = parseFormData(formData) as T
|
|
74
114
|
|
|
75
|
-
|
|
76
|
-
|
|
115
|
+
submit(data)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const reset = (data?: T, shouldUseInitialData: boolean = true) => {
|
|
119
|
+
if (!ref.current) return
|
|
77
120
|
|
|
78
|
-
|
|
79
|
-
if (!ref.current) return;
|
|
121
|
+
ref.current.reset()
|
|
80
122
|
|
|
81
|
-
|
|
82
|
-
|
|
123
|
+
if (data) {
|
|
124
|
+
return initialize(data)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (initialDataRef.current && shouldUseInitialData) {
|
|
128
|
+
initialize(initialDataRef.current)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
83
131
|
|
|
84
132
|
const getFormData = (): T | null => {
|
|
85
|
-
if (!ref.current) return null
|
|
133
|
+
if (!ref.current) return null
|
|
86
134
|
|
|
87
|
-
const formData = new FormData(ref.current)
|
|
135
|
+
const formData = new FormData(ref.current)
|
|
88
136
|
|
|
89
|
-
const data = parseFormData(formData) as T
|
|
137
|
+
const data = parseFormData(formData) as T
|
|
90
138
|
|
|
91
|
-
return data
|
|
92
|
-
}
|
|
139
|
+
return data
|
|
140
|
+
}
|
|
93
141
|
|
|
94
142
|
const setValue = (name: string, value: string) => {
|
|
95
|
-
if (!ref.current) return
|
|
143
|
+
if (!ref.current) return
|
|
96
144
|
|
|
97
|
-
const element = ref.current.elements.namedItem(name) as FormElement | null
|
|
145
|
+
const element = ref.current.elements.namedItem(name) as FormElement | null
|
|
98
146
|
|
|
99
147
|
if (element && 'value' in element) {
|
|
100
|
-
element.value = value
|
|
148
|
+
element.value = value
|
|
101
149
|
}
|
|
102
|
-
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const initialize = (data?: T) => {
|
|
153
|
+
const dataToUse = data || initialDataRef.current
|
|
154
|
+
|
|
155
|
+
if (!ref.current || !dataToUse) return
|
|
156
|
+
|
|
157
|
+
Object.entries(dataToUse as Record<string, unknown>).forEach(
|
|
158
|
+
([key, value]) => {
|
|
159
|
+
if (value) setValue(key, String(value))
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
}
|
|
103
163
|
|
|
104
164
|
return {
|
|
105
165
|
ref,
|
|
166
|
+
getFormData,
|
|
167
|
+
initialize,
|
|
106
168
|
onSubmit,
|
|
107
169
|
reset,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
170
|
+
setValue,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -1,60 +1,64 @@
|
|
|
1
|
-
const ARRAY_KEY_REGEX = /^(\w+)\[(\d+)\]
|
|
1
|
+
const ARRAY_KEY_REGEX = /^(\w+)\[(\d+)\]$/
|
|
2
2
|
|
|
3
3
|
export function parseFormData(formData: FormData): Record<string, unknown> {
|
|
4
|
-
const data = Object.fromEntries(formData.entries())
|
|
5
|
-
const result: Record<string, unknown> = {}
|
|
4
|
+
const data = Object.fromEntries(formData.entries())
|
|
5
|
+
const result: Record<string, unknown> = {}
|
|
6
6
|
|
|
7
7
|
for (const [path, value] of Object.entries(data)) {
|
|
8
8
|
const keys = path.split('.')
|
|
9
|
-
let current = result
|
|
9
|
+
let current = result
|
|
10
10
|
|
|
11
11
|
for (let i = 0; i < keys.length; i++) {
|
|
12
|
-
const key = keys[i]
|
|
13
|
-
const isLastKey = i === keys.length - 1
|
|
14
|
-
const arrayMatch = key.match(ARRAY_KEY_REGEX)
|
|
12
|
+
const key = keys[i]
|
|
13
|
+
const isLastKey = i === keys.length - 1
|
|
14
|
+
const arrayMatch = key.match(ARRAY_KEY_REGEX)
|
|
15
15
|
|
|
16
16
|
// Handle array notation like items[0]
|
|
17
17
|
if (arrayMatch) {
|
|
18
|
-
const [, arrayKey, indexStr] = arrayMatch
|
|
19
|
-
const index = parseInt(indexStr, 10)
|
|
18
|
+
const [, arrayKey, indexStr] = arrayMatch
|
|
19
|
+
const index = parseInt(indexStr, 10)
|
|
20
20
|
|
|
21
21
|
// Initialize array if it doesn't exist
|
|
22
22
|
if (!Array.isArray(current[arrayKey])) {
|
|
23
|
-
current[arrayKey] = []
|
|
23
|
+
current[arrayKey] = []
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const array = current[arrayKey] as unknown[]
|
|
26
|
+
const array = current[arrayKey] as unknown[]
|
|
27
27
|
|
|
28
28
|
// Set value and continue if this is the last key
|
|
29
29
|
if (isLastKey) {
|
|
30
|
-
array[index] = value
|
|
31
|
-
continue
|
|
30
|
+
array[index] = value
|
|
31
|
+
continue
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// Initialize nested object and move to it
|
|
35
35
|
if (!array[index] || typeof array[index] !== 'object') {
|
|
36
|
-
array[index] = {}
|
|
36
|
+
array[index] = {}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
current = array[index] as Record<string, unknown
|
|
39
|
+
current = array[index] as Record<string, unknown>
|
|
40
40
|
|
|
41
|
-
continue
|
|
41
|
+
continue
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Handle regular keys - set value and continue if this is the last key
|
|
45
45
|
if (isLastKey) {
|
|
46
|
-
current[key] = value
|
|
47
|
-
continue
|
|
46
|
+
current[key] = value
|
|
47
|
+
continue
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// Initialize nested object and move to it
|
|
51
|
-
if (
|
|
52
|
-
current[key]
|
|
51
|
+
if (
|
|
52
|
+
!current[key] ||
|
|
53
|
+
typeof current[key] !== 'object' ||
|
|
54
|
+
Array.isArray(current[key])
|
|
55
|
+
) {
|
|
56
|
+
current[key] = {}
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
current = current[key] as Record<string, unknown
|
|
59
|
+
current = current[key] as Record<string, unknown>
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
return result
|
|
63
|
+
return result
|
|
60
64
|
}
|