rfhook 1.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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "cSpell.words": [
3
+ "rfhook"
4
+ ]
5
+ }
package/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # @eji/form-hook
2
+
3
+ A lightweight React hook for handling form submissions with advanced form data parsing capabilities. Supports nested objects, arrays, and dot notation for complex form structures.
4
+
5
+ ## Features
6
+
7
+ - 🚀 **Simple API** - Just one hook to handle all your form needs
8
+ - 🎯 **TypeScript Support** - Fully typed with generic support
9
+ - 🔧 **Nested Objects** - Parse nested form data with dot notation (`user.name`)
10
+ - 📋 **Array Support** - Handle arrays with indexed notation (`items[0]`)
11
+ - 📦 **Lightweight** - Zero dependencies (except React)
12
+ - 🎨 **Framework Agnostic** - Works with any form structure
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @eji/form-hook
18
+ ```
19
+
20
+ ```bash
21
+ pnpm add @eji/form-hook
22
+ ```
23
+
24
+ ```bash
25
+ yarn add @eji/form-hook
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```tsx
31
+ import React from 'react';
32
+ import { useForm } from '@eji/form-hook';
33
+
34
+ interface FormData {
35
+ email: string;
36
+ password: string;
37
+ }
38
+
39
+ function LoginForm() {
40
+ const { ref, submit } = useForm<FormData>({
41
+ handleSubmit: (data) => {
42
+ console.log(data); // { email: "user@example.com", password: "secret" }
43
+ }
44
+ });
45
+
46
+ return (
47
+ <form ref={ref} onSubmit={submit}>
48
+ <input name="email" type="email" />
49
+ <input name="password" type="password" />
50
+ <button type="submit">Login</button>
51
+ </form>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ## Advanced Usage
57
+
58
+ ### Nested Objects
59
+
60
+ Use dot notation to create nested objects:
61
+
62
+ ```tsx
63
+ interface UserForm {
64
+ user: {
65
+ profile: {
66
+ name: string;
67
+ age: number;
68
+ };
69
+ preferences: {
70
+ theme: string;
71
+ };
72
+ };
73
+ }
74
+
75
+ function UserForm() {
76
+ const { ref, submit } = useForm<UserForm>({
77
+ handleSubmit: (data) => {
78
+ console.log(data);
79
+ // {
80
+ // user: {
81
+ // profile: {
82
+ // name: "John Doe",
83
+ // age: "30"
84
+ // },
85
+ // preferences: {
86
+ // theme: "dark"
87
+ // }
88
+ // }
89
+ // }
90
+ }
91
+ });
92
+
93
+ return (
94
+ <form ref={ref} onSubmit={submit}>
95
+ <input name="user.profile.name" placeholder="Name" />
96
+ <input name="user.profile.age" type="number" placeholder="Age" />
97
+ <select name="user.preferences.theme">
98
+ <option value="light">Light</option>
99
+ <option value="dark">Dark</option>
100
+ </select>
101
+ <button type="submit">Submit</button>
102
+ </form>
103
+ );
104
+ }
105
+ ```
106
+
107
+ ### Arrays
108
+
109
+ Use bracket notation to handle arrays:
110
+
111
+ ```tsx
112
+ interface TodoForm {
113
+ todos: Array<{
114
+ title: string;
115
+ completed: boolean;
116
+ }>;
117
+ }
118
+
119
+ function TodoForm() {
120
+ const { ref, submit } = useForm<TodoForm>({
121
+ handleSubmit: (data) => {
122
+ console.log(data);
123
+ // {
124
+ // todos: [
125
+ // { title: "Buy groceries", completed: false },
126
+ // { title: "Walk the dog", completed: true }
127
+ // ]
128
+ // }
129
+ }
130
+ });
131
+
132
+ return (
133
+ <form ref={ref} onSubmit={submit}>
134
+ <input name="todos[0].title" placeholder="First todo" />
135
+ <input name="todos[0].completed" type="checkbox" />
136
+
137
+ <input name="todos[1].title" placeholder="Second todo" />
138
+ <input name="todos[1].completed" type="checkbox" />
139
+
140
+ <button type="submit">Submit</button>
141
+ </form>
142
+ );
143
+ }
144
+ ```
145
+
146
+ ## API Reference
147
+
148
+ ### `useForm<T>(options)`
149
+
150
+ #### Parameters
151
+
152
+ - `options.handleSubmit: (data: T) => void` - Callback function called when form is submitted
153
+
154
+ #### Returns
155
+
156
+ - `ref: RefObject<HTMLFormElement>` - React ref to attach to your form element
157
+ - `submit: (event: React.FormEvent) => void` - Submit handler to attach to form's `onSubmit`
158
+
159
+ ### Form Data Parsing Rules
160
+
161
+ The `parseFormData` utility converts FormData into structured objects using these rules:
162
+
163
+ 1. **Dot notation** creates nested objects: `user.name` → `{ user: { name: "value" } }`
164
+ 2. **Bracket notation** creates arrays: `items[0]` → `{ items: ["value"] }`
165
+ 3. **Combined notation** works: `users[0].name` → `{ users: [{ name: "value" }] }`
166
+
167
+ ## Examples
168
+
169
+ ### Contact Form
170
+
171
+ ```tsx
172
+ import { useForm } from '@eji/form-hook';
173
+
174
+ interface ContactData {
175
+ name: string;
176
+ email: string;
177
+ message: string;
178
+ preferences: {
179
+ newsletter: boolean;
180
+ notifications: boolean;
181
+ };
182
+ }
183
+
184
+ function ContactForm() {
185
+ const { ref, submit } = useForm<ContactData>({
186
+ handleSubmit: async (data) => {
187
+ try {
188
+ const response = await fetch('/api/contact', {
189
+ method: 'POST',
190
+ headers: { 'Content-Type': 'application/json' },
191
+ body: JSON.stringify(data)
192
+ });
193
+
194
+ if (response.ok) {
195
+ alert('Message sent successfully!');
196
+ }
197
+ } catch (error) {
198
+ console.error('Failed to send message:', error);
199
+ }
200
+ }
201
+ });
202
+
203
+ return (
204
+ <form ref={ref} onSubmit={submit}>
205
+ <input name="name" placeholder="Your Name" required />
206
+ <input name="email" type="email" placeholder="Your Email" required />
207
+ <textarea name="message" placeholder="Your Message" required />
208
+
209
+ <fieldset>
210
+ <legend>Preferences</legend>
211
+ <label>
212
+ <input name="preferences.newsletter" type="checkbox" />
213
+ Subscribe to newsletter
214
+ </label>
215
+ <label>
216
+ <input name="preferences.notifications" type="checkbox" />
217
+ Enable notifications
218
+ </label>
219
+ </fieldset>
220
+
221
+ <button type="submit">Send Message</button>
222
+ </form>
223
+ );
224
+ }
225
+ ```
226
+
227
+ ## TypeScript Support
228
+
229
+ The hook is fully typed and supports generic type parameters for form data:
230
+
231
+ ```tsx
232
+ interface MyFormData {
233
+ // Define your form structure here
234
+ }
235
+
236
+ const { ref, submit } = useForm<MyFormData>({
237
+ handleSubmit: (data) => {
238
+ // data is typed as MyFormData
239
+ }
240
+ });
241
+ ```
242
+
243
+ ## Browser Support
244
+
245
+ - All modern browsers that support ES2020
246
+ - React 16.8+ (hooks support required)
247
+
248
+ ## Contributing
249
+
250
+ 1. Fork the repository
251
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
252
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
253
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
254
+ 5. Open a Pull Request
255
+
256
+ ## License
257
+
258
+ ISC
259
+
260
+ ## Development
261
+
262
+ ```bash
263
+ # Install dependencies
264
+ pnpm install
265
+
266
+ # Build the package
267
+ pnpm run build
268
+
269
+ # The built files will be in the `dist/` directory
270
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ const ARRAY_KEY_REGEX = /^(\w+)\[(\d+)\]$/;
6
+ function parseFormData(formData) {
7
+ const data = Object.fromEntries(formData.entries());
8
+ const result = {};
9
+ for (const [path, value] of Object.entries(data)) {
10
+ const keys = path.split('.');
11
+ let current = result;
12
+ for (let i = 0; i < keys.length; i++) {
13
+ const key = keys[i];
14
+ const isLastKey = i === keys.length - 1;
15
+ const arrayMatch = key.match(ARRAY_KEY_REGEX);
16
+ // Handle array notation like items[0]
17
+ if (arrayMatch) {
18
+ const [, arrayKey, indexStr] = arrayMatch;
19
+ const index = parseInt(indexStr, 10);
20
+ // Initialize array if it doesn't exist
21
+ if (!Array.isArray(current[arrayKey])) {
22
+ current[arrayKey] = [];
23
+ }
24
+ const array = current[arrayKey];
25
+ // Set value and continue if this is the last key
26
+ if (isLastKey) {
27
+ array[index] = value;
28
+ continue;
29
+ }
30
+ // Initialize nested object and move to it
31
+ if (!array[index] || typeof array[index] !== 'object') {
32
+ array[index] = {};
33
+ }
34
+ current = array[index];
35
+ continue;
36
+ }
37
+ // Handle regular keys - set value and continue if this is the last key
38
+ if (isLastKey) {
39
+ current[key] = value;
40
+ continue;
41
+ }
42
+ // Initialize nested object and move to it
43
+ if (!current[key] || typeof current[key] !== 'object' || Array.isArray(current[key])) {
44
+ current[key] = {};
45
+ }
46
+ current = current[key];
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+
52
+ function useForm({ handleSubmit }) {
53
+ const ref = react.useRef(null);
54
+ const submit = (event) => {
55
+ event.preventDefault();
56
+ if (!ref.current)
57
+ return;
58
+ const formData = new FormData(ref.current);
59
+ const data = parseFormData(formData);
60
+ handleSubmit(data);
61
+ };
62
+ return { ref, submit };
63
+ }
64
+
65
+ exports.useForm = useForm;
@@ -0,0 +1 @@
1
+ export { useForm } from './useForm.hook';
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ import { useRef } from 'react';
2
+
3
+ const ARRAY_KEY_REGEX = /^(\w+)\[(\d+)\]$/;
4
+ function parseFormData(formData) {
5
+ const data = Object.fromEntries(formData.entries());
6
+ const result = {};
7
+ for (const [path, value] of Object.entries(data)) {
8
+ const keys = path.split('.');
9
+ let current = result;
10
+ for (let i = 0; i < keys.length; i++) {
11
+ const key = keys[i];
12
+ const isLastKey = i === keys.length - 1;
13
+ const arrayMatch = key.match(ARRAY_KEY_REGEX);
14
+ // Handle array notation like items[0]
15
+ if (arrayMatch) {
16
+ const [, arrayKey, indexStr] = arrayMatch;
17
+ const index = parseInt(indexStr, 10);
18
+ // Initialize array if it doesn't exist
19
+ if (!Array.isArray(current[arrayKey])) {
20
+ current[arrayKey] = [];
21
+ }
22
+ const array = current[arrayKey];
23
+ // Set value and continue if this is the last key
24
+ if (isLastKey) {
25
+ array[index] = value;
26
+ continue;
27
+ }
28
+ // Initialize nested object and move to it
29
+ if (!array[index] || typeof array[index] !== 'object') {
30
+ array[index] = {};
31
+ }
32
+ current = array[index];
33
+ continue;
34
+ }
35
+ // Handle regular keys - set value and continue if this is the last key
36
+ if (isLastKey) {
37
+ current[key] = value;
38
+ continue;
39
+ }
40
+ // Initialize nested object and move to it
41
+ if (!current[key] || typeof current[key] !== 'object' || Array.isArray(current[key])) {
42
+ current[key] = {};
43
+ }
44
+ current = current[key];
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+
50
+ function useForm({ handleSubmit }) {
51
+ const ref = useRef(null);
52
+ const submit = (event) => {
53
+ event.preventDefault();
54
+ if (!ref.current)
55
+ return;
56
+ const formData = new FormData(ref.current);
57
+ const data = parseFormData(formData);
58
+ handleSubmit(data);
59
+ };
60
+ return { ref, submit };
61
+ }
62
+
63
+ export { useForm };
@@ -0,0 +1,8 @@
1
+ interface UseForm<T> {
2
+ handleSubmit: (data: T) => void;
3
+ }
4
+ export declare function useForm<T = object>({ handleSubmit }: UseForm<T>): {
5
+ ref: import("react").RefObject<HTMLFormElement | null>;
6
+ submit: (event: React.FormEvent) => void;
7
+ };
8
+ export {};
@@ -0,0 +1 @@
1
+ export declare function parseFormData(formData: FormData): Record<string, unknown>;
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "rfhook",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.js",
7
+ "type": "module",
8
+ "types": "dist/index.d.ts",
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "dependencies": {
13
+ "react": "^19.2.3",
14
+ "react-dom": "^19.2.3"
15
+ },
16
+ "devDependencies": {
17
+ "@rollup/plugin-node-resolve": "^16.0.3",
18
+ "@rollup/plugin-typescript": "^12.3.0",
19
+ "@types/react": "^19.2.8",
20
+ "@types/react-dom": "^19.2.3",
21
+ "rollup": "^4.55.2",
22
+ "tslib": "^2.8.1",
23
+ "typescript": "^5.9.3"
24
+ },
25
+ "scripts": {
26
+ "build": "rollup -c"
27
+ }
28
+ }
@@ -0,0 +1,18 @@
1
+ import typescript from '@rollup/plugin-typescript';
2
+ import nodeResolve from '@rollup/plugin-node-resolve';
3
+
4
+ export default {
5
+ input: 'src/index.ts',
6
+ external: ['react'],
7
+ plugins: [typescript(), nodeResolve()],
8
+ output: [
9
+ {
10
+ file: 'dist/index.js',
11
+ format: 'esm'
12
+ },
13
+ {
14
+ file: 'dist/index.cjs',
15
+ format: 'cjs'
16
+ }
17
+ ]
18
+ };
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { useForm } from './useForm.hook';
@@ -0,0 +1,25 @@
1
+ import { useRef } from 'react';
2
+
3
+ import { parseFormData } from './utils/parseFormData';
4
+
5
+ interface UseForm<T> {
6
+ handleSubmit: (data: T) => void;
7
+ }
8
+
9
+ export function useForm<T = object>({ handleSubmit }: UseForm<T>) {
10
+ const ref = useRef<HTMLFormElement>(null);
11
+
12
+ const submit = (event: React.FormEvent) => {
13
+ event.preventDefault();
14
+
15
+ if (!ref.current) return
16
+
17
+ const formData = new FormData(ref.current);
18
+
19
+ const data: object = parseFormData(formData);
20
+
21
+ handleSubmit(data as T);
22
+ }
23
+
24
+ return { ref, submit };
25
+ }
@@ -0,0 +1,60 @@
1
+ const ARRAY_KEY_REGEX = /^(\w+)\[(\d+)\]$/;
2
+
3
+ export function parseFormData(formData: FormData): Record<string, unknown> {
4
+ const data = Object.fromEntries(formData.entries());
5
+ const result: Record<string, unknown> = {};
6
+
7
+ for (const [path, value] of Object.entries(data)) {
8
+ const keys = path.split('.')
9
+ let current = result;
10
+
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);
15
+
16
+ // Handle array notation like items[0]
17
+ if (arrayMatch) {
18
+ const [, arrayKey, indexStr] = arrayMatch;
19
+ const index = parseInt(indexStr, 10);
20
+
21
+ // Initialize array if it doesn't exist
22
+ if (!Array.isArray(current[arrayKey])) {
23
+ current[arrayKey] = [];
24
+ }
25
+
26
+ const array = current[arrayKey] as unknown[];
27
+
28
+ // Set value and continue if this is the last key
29
+ if (isLastKey) {
30
+ array[index] = value;
31
+ continue;
32
+ }
33
+
34
+ // Initialize nested object and move to it
35
+ if (!array[index] || typeof array[index] !== 'object') {
36
+ array[index] = {};
37
+ }
38
+
39
+ current = array[index] as Record<string, unknown>;
40
+
41
+ continue;
42
+ }
43
+
44
+ // Handle regular keys - set value and continue if this is the last key
45
+ if (isLastKey) {
46
+ current[key] = value;
47
+ continue;
48
+ }
49
+
50
+ // Initialize nested object and move to it
51
+ if (!current[key] || typeof current[key] !== 'object' || Array.isArray(current[key])) {
52
+ current[key] = {};
53
+ }
54
+
55
+ current = current[key] as Record<string, unknown>;
56
+ }
57
+ }
58
+
59
+ return result;
60
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "node",
7
+ "declaration": true,
8
+ "outDir": "dist",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "jsx": "react-jsx"
15
+ },
16
+ "include": [
17
+ "src/**/*"
18
+ ],
19
+ "exclude": [
20
+ "node_modules",
21
+ "dist"
22
+ ]
23
+ }