@n8x/react-form-utils 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.
- package/README.md +1238 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +11 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,1238 @@
|
|
|
1
|
+
# n8x-form-utils
|
|
2
|
+
|
|
3
|
+
A powerful React form library that simplifies form creation with instant JavaScript object-to-form generation, Zod validation, controlled form submission events, and state management using React Hook Form. <b>A fastest way to create and manage forms in react</b>.
|
|
4
|
+
|
|
5
|
+
**Features:**
|
|
6
|
+
- Rapid form generation from simple field configuration
|
|
7
|
+
- Built-in Zod validation with type safety
|
|
8
|
+
- Multiple pre-built styling themes
|
|
9
|
+
- Grouped form support for complex layouts
|
|
10
|
+
- Controlled form submission handling
|
|
11
|
+
- Query hooks for API integration
|
|
12
|
+
- Support for 10+ field types
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Setup](#setup)
|
|
20
|
+
- [Quick Start](#quick-start)
|
|
21
|
+
- [Field Types](#field-types)
|
|
22
|
+
- [Validation](#validation)
|
|
23
|
+
- [Styling Themes](#styling-themes)
|
|
24
|
+
- [Grouped Forms](#grouped-forms)
|
|
25
|
+
- [Form Submission](#form-submission)
|
|
26
|
+
- [Data Fetching Hooks](#data-fetching-hooks)
|
|
27
|
+
- [Error Handling](#error-handling)
|
|
28
|
+
- [API Reference](#api-reference)
|
|
29
|
+
- [Contribution](#Contributing)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install n8x-form-utils
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Dependencies (auto-installed):**
|
|
40
|
+
- React 18+
|
|
41
|
+
- React Hook Form 7+
|
|
42
|
+
- Zod 4+
|
|
43
|
+
- Tailwind CSS 4+
|
|
44
|
+
- Axios
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Setup
|
|
49
|
+
|
|
50
|
+
### 1. Wrap Your App with FormContextProvider
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { ReactNode } from 'react'
|
|
54
|
+
import { FormContext } from 'n8x-form-utils'
|
|
55
|
+
import FormContextProvider from 'n8x-form-utils'
|
|
56
|
+
|
|
57
|
+
export default function App() {
|
|
58
|
+
return (
|
|
59
|
+
<FormContextProvider>
|
|
60
|
+
{/* Your routes and components */}
|
|
61
|
+
</FormContextProvider>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The `FormContextProvider` manages the global form state and validation schema. It should wrap your entire application or at least the components using N8xForm.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
### Create a Simple Login Form
|
|
73
|
+
|
|
74
|
+
**Step 1: Define your form fields**
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// forms/login.ts
|
|
78
|
+
import { z } from 'n8x-form-utils'
|
|
79
|
+
import { FormFieldTypes, FormFieldWidth, type FormFields } from 'n8x-form-utils'
|
|
80
|
+
|
|
81
|
+
export const loginForm: FormFields[] = [
|
|
82
|
+
{
|
|
83
|
+
name: 'email',
|
|
84
|
+
type: FormFieldTypes.EMAIL,
|
|
85
|
+
label: 'Email',
|
|
86
|
+
placeholder: 'hello@example.com',
|
|
87
|
+
required: true,
|
|
88
|
+
width: FormFieldWidth.FULL,
|
|
89
|
+
validationScheme: z.string().email('Please enter a valid email')
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'password',
|
|
93
|
+
type: FormFieldTypes.PASSWORD,
|
|
94
|
+
label: 'Password',
|
|
95
|
+
placeholder: 'Enter your password',
|
|
96
|
+
required: true,
|
|
97
|
+
width: FormFieldWidth.FULL,
|
|
98
|
+
validationScheme: z.string().min(6, 'Password must be at least 6 characters')
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Step 2: Use the form in your component**
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// pages/auth/Login.tsx
|
|
107
|
+
import { useContext } from 'react'
|
|
108
|
+
import { N8xForm, useN8xFormQuery, EDefaultFieldStyles } from 'n8x-form-utils'
|
|
109
|
+
import { loginForm } from '../../forms/login'
|
|
110
|
+
import { login } from '../../service/auth-service'
|
|
111
|
+
|
|
112
|
+
export default function LoginPage() {
|
|
113
|
+
const { data, execute, error, isLoading } = useN8xFormQuery(login)
|
|
114
|
+
|
|
115
|
+
const handleLoginSubmit = async (formData: any) => {
|
|
116
|
+
execute(formData)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="w-full min-h-screen flex justify-center items-center">
|
|
121
|
+
<div className="md:w-[600px] w-full sm:p-6 p-3">
|
|
122
|
+
<h1 className="text-4xl tracking-tighter">Account Login</h1>
|
|
123
|
+
|
|
124
|
+
<N8xForm
|
|
125
|
+
fields={loginForm}
|
|
126
|
+
onShubmit={handleLoginSubmit}
|
|
127
|
+
defaultFieldStyles={EDefaultFieldStyles.FLOATING_LABEL}
|
|
128
|
+
>
|
|
129
|
+
<button
|
|
130
|
+
disabled={isLoading}
|
|
131
|
+
type="submit"
|
|
132
|
+
className="col-span-4 bg-blue-500 disabled:bg-zinc-400 text-white py-2 px-4 rounded font-medium"
|
|
133
|
+
>
|
|
134
|
+
{isLoading ? 'Logging in...' : 'Login'}
|
|
135
|
+
</button>
|
|
136
|
+
</N8xForm>
|
|
137
|
+
|
|
138
|
+
{error && <p className="text-red-500 text-center mt-4">{error}</p>}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Field Types
|
|
148
|
+
|
|
149
|
+
N8x Form Utils supports 10+ field types. Here's a comprehensive example showing all available field types:
|
|
150
|
+
|
|
151
|
+
### Basic Fields
|
|
152
|
+
|
|
153
|
+
#### TEXT Field
|
|
154
|
+
```tsx
|
|
155
|
+
{
|
|
156
|
+
name: 'username',
|
|
157
|
+
type: FormFieldTypes.TEXT,
|
|
158
|
+
label: 'Username',
|
|
159
|
+
placeholder: 'Enter your username',
|
|
160
|
+
required: true,
|
|
161
|
+
width: FormFieldWidth.FULL,
|
|
162
|
+
validationScheme: z.string().min(3, 'Username must be at least 3 characters')
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### EMAIL Field
|
|
167
|
+
```tsx
|
|
168
|
+
{
|
|
169
|
+
name: 'email',
|
|
170
|
+
type: FormFieldTypes.EMAIL,
|
|
171
|
+
label: 'Email Address',
|
|
172
|
+
placeholder: 'you@example.com',
|
|
173
|
+
required: true,
|
|
174
|
+
width: FormFieldWidth.FULL,
|
|
175
|
+
validationScheme: z.string().email('Invalid email address')
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### PASSWORD Field
|
|
180
|
+
```tsx
|
|
181
|
+
{
|
|
182
|
+
name: 'password',
|
|
183
|
+
type: FormFieldTypes.PASSWORD,
|
|
184
|
+
label: 'Password',
|
|
185
|
+
placeholder: 'Create a strong password',
|
|
186
|
+
required: true,
|
|
187
|
+
width: FormFieldWidth.FULL,
|
|
188
|
+
validationScheme: z.string()
|
|
189
|
+
.min(6, 'Password must be at least 6 characters')
|
|
190
|
+
.max(36, 'Password too long')
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### NUMBER Field
|
|
195
|
+
```tsx
|
|
196
|
+
{
|
|
197
|
+
name: 'age',
|
|
198
|
+
type: FormFieldTypes.NUMBER,
|
|
199
|
+
label: 'Age',
|
|
200
|
+
placeholder: 'Enter your age',
|
|
201
|
+
min: 18,
|
|
202
|
+
max: 100,
|
|
203
|
+
required: true,
|
|
204
|
+
width: FormFieldWidth.HALF,
|
|
205
|
+
validationScheme: z.number().min(18, 'Must be 18 or older')
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### DATE Field
|
|
210
|
+
```tsx
|
|
211
|
+
{
|
|
212
|
+
name: 'birthDate',
|
|
213
|
+
type: FormFieldTypes.DATE,
|
|
214
|
+
label: 'Date of Birth',
|
|
215
|
+
required: true,
|
|
216
|
+
width: FormFieldWidth.HALF,
|
|
217
|
+
validationScheme: z.string().refine(
|
|
218
|
+
(date) => new Date(date) < new Date(),
|
|
219
|
+
'Date must be in the past'
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Composite Fields
|
|
225
|
+
|
|
226
|
+
#### SELECT Field
|
|
227
|
+
```tsx
|
|
228
|
+
{
|
|
229
|
+
name: 'country',
|
|
230
|
+
type: FormFieldTypes.SELECT,
|
|
231
|
+
label: 'Country',
|
|
232
|
+
required: true,
|
|
233
|
+
width: FormFieldWidth.FULL,
|
|
234
|
+
options: [
|
|
235
|
+
{ label: 'United States', value: 'US' },
|
|
236
|
+
{ label: 'Canada', value: 'CA' },
|
|
237
|
+
{ label: 'United Kingdom', value: 'GB' },
|
|
238
|
+
{ label: 'Australia', value: 'AU' }
|
|
239
|
+
],
|
|
240
|
+
validationScheme: z.string().min(1, 'Please select a country')
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### RADIO Field
|
|
245
|
+
```tsx
|
|
246
|
+
{
|
|
247
|
+
name: 'accountType',
|
|
248
|
+
type: FormFieldTypes.RADIO,
|
|
249
|
+
label: 'Account Type',
|
|
250
|
+
required: true,
|
|
251
|
+
width: FormFieldWidth.FULL,
|
|
252
|
+
options: [
|
|
253
|
+
{ label: 'Personal', value: 'personal' },
|
|
254
|
+
{ label: 'Business', value: 'business' },
|
|
255
|
+
{ label: 'Enterprise', value: 'enterprise' }
|
|
256
|
+
],
|
|
257
|
+
validationScheme: z.string().min(1, 'Please select an account type')
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### CHECKBOX Field
|
|
262
|
+
```tsx
|
|
263
|
+
{
|
|
264
|
+
name: 'interests',
|
|
265
|
+
type: FormFieldTypes.CHECKBOX,
|
|
266
|
+
label: 'Interests',
|
|
267
|
+
width: FormFieldWidth.FULL,
|
|
268
|
+
options: [
|
|
269
|
+
{ label: 'Technology', value: 'tech' },
|
|
270
|
+
{ label: 'Finance', value: 'finance' },
|
|
271
|
+
{ label: 'Health', value: 'health' },
|
|
272
|
+
{ label: 'Travel', value: 'travel' }
|
|
273
|
+
],
|
|
274
|
+
validationScheme: z.array(z.string()).min(1, 'Select at least one interest')
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### TEXTAREA Field
|
|
279
|
+
```tsx
|
|
280
|
+
{
|
|
281
|
+
name: 'bio',
|
|
282
|
+
type: FormFieldTypes.TEXTAREA,
|
|
283
|
+
label: 'Bio',
|
|
284
|
+
placeholder: 'Tell us about yourself',
|
|
285
|
+
width: FormFieldWidth.FULL,
|
|
286
|
+
validationScheme: z.string()
|
|
287
|
+
.min(10, 'Bio must be at least 10 characters')
|
|
288
|
+
.max(500, 'Bio must not exceed 500 characters')
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### FILE Field
|
|
293
|
+
```tsx
|
|
294
|
+
{
|
|
295
|
+
name: 'profilePhoto',
|
|
296
|
+
type: FormFieldTypes.FILE,
|
|
297
|
+
label: 'Profile Photo',
|
|
298
|
+
width: FormFieldWidth.FULL,
|
|
299
|
+
validationScheme: z.any()
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Validation
|
|
306
|
+
|
|
307
|
+
N8x Form Utils uses **Zod** for type-safe schema validation. All fields support a `validationScheme` property where you define your validation rules.
|
|
308
|
+
|
|
309
|
+
### Basic Validation Examples
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
import { z } from 'n8x-form-utils'
|
|
313
|
+
|
|
314
|
+
// Email validation
|
|
315
|
+
validationScheme: z.string().email('Invalid email address')
|
|
316
|
+
|
|
317
|
+
// Required string with minimum length
|
|
318
|
+
validationScheme: z.string().min(3, 'Minimum 3 characters required')
|
|
319
|
+
|
|
320
|
+
// Number with range
|
|
321
|
+
validationScheme: z.number().min(0).max(100, 'Value must be between 0 and 100')
|
|
322
|
+
|
|
323
|
+
// Custom refine validation
|
|
324
|
+
validationScheme: z.string().refine(
|
|
325
|
+
(value) => value !== 'admin',
|
|
326
|
+
'Username cannot be "admin"'
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
// Conditional validation
|
|
330
|
+
validationScheme: z.string().or(z.number())
|
|
331
|
+
|
|
332
|
+
// Array validation
|
|
333
|
+
validationScheme: z.array(z.string()).min(1, 'Select at least one option')
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Complete Registration Form with Validation
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
// forms/register.ts
|
|
340
|
+
import { z } from 'n8x-form-utils'
|
|
341
|
+
import { FormFieldTypes, FormFieldWidth, type FormFields } from 'n8x-form-utils'
|
|
342
|
+
|
|
343
|
+
export const registerForm: FormFields[] = [
|
|
344
|
+
{
|
|
345
|
+
name: 'email',
|
|
346
|
+
type: FormFieldTypes.EMAIL,
|
|
347
|
+
label: 'Email',
|
|
348
|
+
placeholder: 'hello@example.com',
|
|
349
|
+
required: true,
|
|
350
|
+
width: FormFieldWidth.FULL,
|
|
351
|
+
validationScheme: z.string().email('Please enter a valid email')
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: 'password',
|
|
355
|
+
type: FormFieldTypes.PASSWORD,
|
|
356
|
+
label: 'Password',
|
|
357
|
+
placeholder: 'Create a strong password',
|
|
358
|
+
required: true,
|
|
359
|
+
width: FormFieldWidth.FULL,
|
|
360
|
+
validationScheme: z.string()
|
|
361
|
+
.min(6, 'Password must be at least 6 characters')
|
|
362
|
+
.max(36, 'Password too long')
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: 'confirmPassword',
|
|
366
|
+
type: FormFieldTypes.PASSWORD,
|
|
367
|
+
label: 'Confirm Password',
|
|
368
|
+
placeholder: 'Re-enter your password',
|
|
369
|
+
required: true,
|
|
370
|
+
width: FormFieldWidth.FULL,
|
|
371
|
+
validationScheme: z.string().min(6, 'Password must be at least 6 characters')
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: 'fullName',
|
|
375
|
+
type: FormFieldTypes.TEXT,
|
|
376
|
+
label: 'Full Name',
|
|
377
|
+
placeholder: 'John Doe',
|
|
378
|
+
required: true,
|
|
379
|
+
width: FormFieldWidth.FULL,
|
|
380
|
+
validationScheme: z.string().min(2, 'Name must be at least 2 characters')
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: 'agreeToTerms',
|
|
384
|
+
type: FormFieldTypes.CHECKBOX,
|
|
385
|
+
label: 'Terms',
|
|
386
|
+
width: FormFieldWidth.FULL,
|
|
387
|
+
options: [
|
|
388
|
+
{ label: 'I agree to the Terms and Conditions', value: 'agree' }
|
|
389
|
+
],
|
|
390
|
+
validationScheme: z.array(z.string()).min(1, 'You must agree to the terms')
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Styling Themes
|
|
398
|
+
|
|
399
|
+
N8x Form Utils provides 4 pre-built styling themes via `EDefaultFieldStyles`. You can apply a theme through the `defaultFieldStyles` prop in the N8xForm component.
|
|
400
|
+
|
|
401
|
+
### Available Themes
|
|
402
|
+
|
|
403
|
+
#### CLEAN (Minimal, professional)
|
|
404
|
+
```tsx
|
|
405
|
+
<N8xForm
|
|
406
|
+
fields={loginForm}
|
|
407
|
+
defaultFieldStyles={EDefaultFieldStyles.CLEAN}
|
|
408
|
+
onShubmit={handleSubmit}
|
|
409
|
+
/>
|
|
410
|
+
```
|
|
411
|
+
Clean white inputs with thin borders, subtle focus effects, and a professional appearance.
|
|
412
|
+
|
|
413
|
+
#### SOFT_GLASS (Modern, glassmorphism)
|
|
414
|
+
```tsx
|
|
415
|
+
<N8xForm
|
|
416
|
+
fields={loginForm}
|
|
417
|
+
defaultFieldStyles={EDefaultFieldStyles.SOFT_GLASS}
|
|
418
|
+
onShubmit={handleSubmit}
|
|
419
|
+
/>
|
|
420
|
+
```
|
|
421
|
+
Soft, frosted appearance with light background and smooth transitions. Default theme.
|
|
422
|
+
|
|
423
|
+
#### DARK (Dark mode)
|
|
424
|
+
```tsx
|
|
425
|
+
<N8xForm
|
|
426
|
+
fields={loginForm}
|
|
427
|
+
defaultFieldStyles={EDefaultFieldStyles.DARK}
|
|
428
|
+
onShubmit={handleSubmit}
|
|
429
|
+
/>
|
|
430
|
+
```
|
|
431
|
+
Dark background inputs suitable for dark mode interfaces.
|
|
432
|
+
|
|
433
|
+
#### FLOATING_LABEL (Material Design)
|
|
434
|
+
```tsx
|
|
435
|
+
<N8xForm
|
|
436
|
+
fields={loginForm}
|
|
437
|
+
defaultFieldStyles={EDefaultFieldStyles.FLOATING_LABEL}
|
|
438
|
+
onShubmit={handleSubmit}
|
|
439
|
+
/>
|
|
440
|
+
```
|
|
441
|
+
Floating label style with animated labels on focus/input. Material Design inspired.
|
|
442
|
+
|
|
443
|
+
### Custom Styling
|
|
444
|
+
|
|
445
|
+
You can also apply custom Tailwind CSS classes to individual fields:
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
{
|
|
449
|
+
name: 'customField',
|
|
450
|
+
type: FormFieldTypes.TEXT,
|
|
451
|
+
label: 'Custom Styled',
|
|
452
|
+
_className: 'bg-gradient-to-r from-blue-400 to-blue-600 text-white rounded-xl'
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Or pass a custom string:
|
|
457
|
+
|
|
458
|
+
```tsx
|
|
459
|
+
<N8xForm
|
|
460
|
+
fields={loginForm}
|
|
461
|
+
defaultFieldStyles="custom-class-string"
|
|
462
|
+
onShubmit={handleSubmit}
|
|
463
|
+
/>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Grouped Forms
|
|
469
|
+
|
|
470
|
+
For complex forms with multiple sections, organize fields into logical groups. Grouped forms automatically render section headers.
|
|
471
|
+
|
|
472
|
+
### Example: User Profile Form with Groups
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
// forms/userProfile.ts
|
|
476
|
+
import { z } from 'n8x-form-utils'
|
|
477
|
+
import { FormFieldTypes, FormFieldWidth, type FormFields } from 'n8x-form-utils'
|
|
478
|
+
|
|
479
|
+
export const userProfileForm = {
|
|
480
|
+
'Personal Information': [
|
|
481
|
+
{
|
|
482
|
+
name: 'firstName',
|
|
483
|
+
type: FormFieldTypes.TEXT,
|
|
484
|
+
label: 'First Name',
|
|
485
|
+
required: true,
|
|
486
|
+
width: FormFieldWidth.HALF,
|
|
487
|
+
validationScheme: z.string().min(2, 'First name is required')
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: 'lastName',
|
|
491
|
+
type: FormFieldTypes.TEXT,
|
|
492
|
+
label: 'Last Name',
|
|
493
|
+
required: true,
|
|
494
|
+
width: FormFieldWidth.HALF,
|
|
495
|
+
validationScheme: z.string().min(2, 'Last name is required')
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: 'email',
|
|
499
|
+
type: FormFieldTypes.EMAIL,
|
|
500
|
+
label: 'Email Address',
|
|
501
|
+
required: true,
|
|
502
|
+
width: FormFieldWidth.FULL,
|
|
503
|
+
validationScheme: z.string().email('Invalid email')
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: 'birthDate',
|
|
507
|
+
type: FormFieldTypes.DATE,
|
|
508
|
+
label: 'Date of Birth',
|
|
509
|
+
required: false,
|
|
510
|
+
width: FormFieldWidth.HALF,
|
|
511
|
+
validationScheme: z.string().optional()
|
|
512
|
+
}
|
|
513
|
+
],
|
|
514
|
+
'Contact Information': [
|
|
515
|
+
{
|
|
516
|
+
name: 'phone',
|
|
517
|
+
type: FormFieldTypes.TEXT,
|
|
518
|
+
label: 'Phone Number',
|
|
519
|
+
placeholder: '+1 (555) 123-4567',
|
|
520
|
+
width: FormFieldWidth.FULL,
|
|
521
|
+
validationScheme: z.string().optional()
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
name: 'country',
|
|
525
|
+
type: FormFieldTypes.SELECT,
|
|
526
|
+
label: 'Country',
|
|
527
|
+
required: true,
|
|
528
|
+
width: FormFieldWidth.HALF,
|
|
529
|
+
options: [
|
|
530
|
+
{ label: 'United States', value: 'US' },
|
|
531
|
+
{ label: 'Canada', value: 'CA' },
|
|
532
|
+
{ label: 'United Kingdom', value: 'GB' }
|
|
533
|
+
],
|
|
534
|
+
validationScheme: z.string().min(1, 'Select a country')
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: 'city',
|
|
538
|
+
type: FormFieldTypes.TEXT,
|
|
539
|
+
label: 'City',
|
|
540
|
+
required: true,
|
|
541
|
+
width: FormFieldWidth.HALF,
|
|
542
|
+
validationScheme: z.string().min(2, 'City is required')
|
|
543
|
+
}
|
|
544
|
+
],
|
|
545
|
+
'Preferences': [
|
|
546
|
+
{
|
|
547
|
+
name: 'language',
|
|
548
|
+
type: FormFieldTypes.SELECT,
|
|
549
|
+
label: 'Preferred Language',
|
|
550
|
+
width: FormFieldWidth.FULL,
|
|
551
|
+
options: [
|
|
552
|
+
{ label: 'English', value: 'en' },
|
|
553
|
+
{ label: 'Spanish', value: 'es' },
|
|
554
|
+
{ label: 'French', value: 'fr' },
|
|
555
|
+
{ label: 'German', value: 'de' }
|
|
556
|
+
],
|
|
557
|
+
validationScheme: z.string().optional()
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
name: 'newsletter',
|
|
561
|
+
type: FormFieldTypes.CHECKBOX,
|
|
562
|
+
label: 'Email Preferences',
|
|
563
|
+
width: FormFieldWidth.FULL,
|
|
564
|
+
options: [
|
|
565
|
+
{ label: 'Subscribe to newsletter', value: 'newsletter' },
|
|
566
|
+
{ label: 'Receive promotional emails', value: 'promo' }
|
|
567
|
+
],
|
|
568
|
+
validationScheme: z.array(z.string()).optional()
|
|
569
|
+
}
|
|
570
|
+
]
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Using Grouped Forms
|
|
575
|
+
|
|
576
|
+
```tsx
|
|
577
|
+
// pages/profile/UserProfile.tsx
|
|
578
|
+
import { useContext } from 'react'
|
|
579
|
+
import { N8xForm, useN8xFormQuery, EDefaultFieldStyles } from 'n8x-form-utils'
|
|
580
|
+
import { userProfileForm } from '../../forms/userProfile'
|
|
581
|
+
import { updateUserProfile } from '../../service/user-service'
|
|
582
|
+
|
|
583
|
+
export default function UserProfilePage() {
|
|
584
|
+
const { data, execute, error, isLoading } = useN8xFormQuery(updateUserProfile)
|
|
585
|
+
|
|
586
|
+
const handleProfileUpdate = async (formData: any) => {
|
|
587
|
+
execute(formData)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return (
|
|
591
|
+
<div className="w-full p-6">
|
|
592
|
+
<h1 className="text-3xl font-bold mb-6">Edit Profile</h1>
|
|
593
|
+
|
|
594
|
+
<N8xForm
|
|
595
|
+
fields={userProfileForm}
|
|
596
|
+
onShubmit={handleProfileUpdate}
|
|
597
|
+
defaultFieldStyles={EDefaultFieldStyles.SOFT_GLASS}
|
|
598
|
+
>
|
|
599
|
+
<button
|
|
600
|
+
disabled={isLoading}
|
|
601
|
+
type="submit"
|
|
602
|
+
className="col-span-4 bg-blue-500 disabled:bg-zinc-400 text-white py-2 px-4 rounded font-medium"
|
|
603
|
+
>
|
|
604
|
+
{isLoading ? 'Saving...' : 'Save Changes'}
|
|
605
|
+
</button>
|
|
606
|
+
</N8xForm>
|
|
607
|
+
|
|
608
|
+
{error && <p className="text-red-500 mt-4">{error}</p>}
|
|
609
|
+
</div>
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Form Submission
|
|
617
|
+
|
|
618
|
+
### useN8xFormQuery Hook
|
|
619
|
+
|
|
620
|
+
Execute async operations when the form is submitted. Perfect for API calls.
|
|
621
|
+
|
|
622
|
+
```tsx
|
|
623
|
+
import { useN8xFormQuery } from 'n8x-form-utils'
|
|
624
|
+
import { login } from '../service/auth-service'
|
|
625
|
+
|
|
626
|
+
export default function LoginComponent() {
|
|
627
|
+
const { data, execute, error, isLoading } = useN8xFormQuery(login)
|
|
628
|
+
|
|
629
|
+
const handleSubmit = async (formData: any) => {
|
|
630
|
+
execute(formData) // Calls the login service
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
<N8xForm
|
|
635
|
+
fields={loginForm}
|
|
636
|
+
onShubmit={handleSubmit}
|
|
637
|
+
>
|
|
638
|
+
<button disabled={isLoading} type="submit">
|
|
639
|
+
{isLoading ? 'Loading...' : 'Login'}
|
|
640
|
+
</button>
|
|
641
|
+
</N8xForm>
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Hook Signature
|
|
647
|
+
|
|
648
|
+
```tsx
|
|
649
|
+
const { data, execute, error, isLoading } = useN8xFormQuery(
|
|
650
|
+
queryService: (payload: any, signal?: AbortSignal) => Promise<any>
|
|
651
|
+
)
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
**Properties:**
|
|
655
|
+
- `data`: Response data from the service after successful execution
|
|
656
|
+
- `execute`: Function to call the service with form payload
|
|
657
|
+
- `error`: Error message if the request fails
|
|
658
|
+
- `isLoading`: Boolean indicating if the request is in progress
|
|
659
|
+
|
|
660
|
+
### Service Function Example
|
|
661
|
+
|
|
662
|
+
```tsx
|
|
663
|
+
// service/auth-service.ts
|
|
664
|
+
import axios from 'axios'
|
|
665
|
+
|
|
666
|
+
export const login = async (payload: any, signal?: AbortSignal) => {
|
|
667
|
+
return axios.post('/api/auth/login', payload, { signal })
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export const register = async (payload: any, signal?: AbortSignal) => {
|
|
671
|
+
return axios.post('/api/auth/register', payload, { signal })
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Complete Submission Example with Side Effects
|
|
676
|
+
|
|
677
|
+
```tsx
|
|
678
|
+
import { useContext, useEffect } from 'react'
|
|
679
|
+
import { useNavigate } from 'react-router-dom'
|
|
680
|
+
import { N8xForm, useN8xFormQuery, EDefaultFieldStyles } from 'n8x-form-utils'
|
|
681
|
+
import { AuthContext } from '../context/AuthContext'
|
|
682
|
+
import { loginForm } from '../forms/login'
|
|
683
|
+
import { login } from '../service/auth-service'
|
|
684
|
+
|
|
685
|
+
export default function LoginPage() {
|
|
686
|
+
const { setUser } = useContext(AuthContext)
|
|
687
|
+
const navigate = useNavigate()
|
|
688
|
+
const { data, execute, error, isLoading } = useN8xFormQuery(login)
|
|
689
|
+
|
|
690
|
+
const handleLoginSubmit = async (formData: any) => {
|
|
691
|
+
execute(formData)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Handle successful login
|
|
695
|
+
useEffect(() => {
|
|
696
|
+
if (data) {
|
|
697
|
+
setUser(data)
|
|
698
|
+
navigate('/home')
|
|
699
|
+
}
|
|
700
|
+
}, [data, setUser, navigate])
|
|
701
|
+
|
|
702
|
+
return (
|
|
703
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
704
|
+
<div className="w-full max-w-md">
|
|
705
|
+
<h1 className="text-4xl font-bold mb-6">Login</h1>
|
|
706
|
+
|
|
707
|
+
<N8xForm
|
|
708
|
+
fields={loginForm}
|
|
709
|
+
onShubmit={handleLoginSubmit}
|
|
710
|
+
defaultFieldStyles={EDefaultFieldStyles.FLOATING_LABEL}
|
|
711
|
+
>
|
|
712
|
+
<button
|
|
713
|
+
disabled={isLoading}
|
|
714
|
+
type="submit"
|
|
715
|
+
className="col-span-4 bg-blue-500 hover:bg-blue-600 disabled:bg-zinc-400 text-white py-2 rounded font-medium transition"
|
|
716
|
+
>
|
|
717
|
+
{isLoading ? 'Logging in...' : 'Login'}
|
|
718
|
+
</button>
|
|
719
|
+
</N8xForm>
|
|
720
|
+
|
|
721
|
+
{error && <p className="text-red-500 text-center mt-4">{error}</p>}
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## Data Fetching Hooks
|
|
731
|
+
|
|
732
|
+
### useN8xQuery Hook
|
|
733
|
+
|
|
734
|
+
Auto-execute queries on component mount. Use this for fetching initial data to populate forms.
|
|
735
|
+
|
|
736
|
+
```tsx
|
|
737
|
+
import { useN8xQuery } from 'n8x-form-utils'
|
|
738
|
+
import { fetchUserProfile } from '../service/user-service'
|
|
739
|
+
|
|
740
|
+
export default function ProfilePage() {
|
|
741
|
+
const { data: profile, error, isLoading } = useN8xQuery(fetchUserProfile, userId)
|
|
742
|
+
|
|
743
|
+
if (isLoading) return <div>Loading profile...</div>
|
|
744
|
+
if (error) return <div>Error: {error}</div>
|
|
745
|
+
|
|
746
|
+
return (
|
|
747
|
+
<N8xForm
|
|
748
|
+
fields={userProfileForm}
|
|
749
|
+
onShubmit={handleProfileUpdate}
|
|
750
|
+
/>
|
|
751
|
+
)
|
|
752
|
+
}
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Hook Signature
|
|
756
|
+
|
|
757
|
+
```tsx
|
|
758
|
+
const { data, error, isLoading } = useN8xQuery(
|
|
759
|
+
queryService: (payload: any, signal?: AbortSignal) => Promise<any>,
|
|
760
|
+
...queryParams: any[]
|
|
761
|
+
)
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
**Properties:**
|
|
765
|
+
- `data`: Response data from the service
|
|
766
|
+
- `error`: Error message if the request fails
|
|
767
|
+
- `isLoading`: Boolean indicating if the request is loading
|
|
768
|
+
|
|
769
|
+
### Key Differences
|
|
770
|
+
|
|
771
|
+
| Feature | useN8xFormQuery | useN8xQuery |
|
|
772
|
+
|---------|-----------------|------------|
|
|
773
|
+
| Runs on mount | ❌ No | ✅ Yes |
|
|
774
|
+
| Triggered by | Manual call | Auto on mount |
|
|
775
|
+
| Return state | `data, error, isLoading, execute` | `data, error, isLoading` |
|
|
776
|
+
| Use case | Form submissions | Initial data fetch |
|
|
777
|
+
|
|
778
|
+
---
|
|
779
|
+
|
|
780
|
+
## Error Handling
|
|
781
|
+
|
|
782
|
+
### Display Validation Errors
|
|
783
|
+
|
|
784
|
+
Validation errors from Zod are automatically displayed below each field with error styling.
|
|
785
|
+
|
|
786
|
+
```tsx
|
|
787
|
+
<N8xForm
|
|
788
|
+
fields={loginForm}
|
|
789
|
+
onShubmit={handleSubmit}
|
|
790
|
+
/>
|
|
791
|
+
// Errors appear in real-time as user types (default: "onChange" mode)
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Display API Errors
|
|
795
|
+
|
|
796
|
+
Handle errors returned from `useN8xFormQuery`:
|
|
797
|
+
|
|
798
|
+
```tsx
|
|
799
|
+
const { data, execute, error, isLoading } = useN8xFormQuery(login)
|
|
800
|
+
|
|
801
|
+
return (
|
|
802
|
+
<>
|
|
803
|
+
<N8xForm
|
|
804
|
+
fields={loginForm}
|
|
805
|
+
onShubmit={handleSubmit}
|
|
806
|
+
/>
|
|
807
|
+
|
|
808
|
+
{error && (
|
|
809
|
+
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded">
|
|
810
|
+
<p className="text-red-700 font-semibold">Login failed</p>
|
|
811
|
+
<p className="text-red-600 text-sm">{error}</p>
|
|
812
|
+
</div>
|
|
813
|
+
)}
|
|
814
|
+
</>
|
|
815
|
+
)
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### Custom Error Component
|
|
819
|
+
|
|
820
|
+
```tsx
|
|
821
|
+
interface ErrorProps {
|
|
822
|
+
message: string
|
|
823
|
+
onDismiss?: () => void
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function ErrorAlert({ message, onDismiss }: ErrorProps) {
|
|
827
|
+
return (
|
|
828
|
+
<div className="p-4 bg-red-50 border-l-4 border-red-500 rounded">
|
|
829
|
+
<p className="text-red-700">{message}</p>
|
|
830
|
+
{onDismiss && (
|
|
831
|
+
<button onClick={onDismiss} className="mt-2 text-sm text-red-600">
|
|
832
|
+
Dismiss
|
|
833
|
+
</button>
|
|
834
|
+
)}
|
|
835
|
+
</div>
|
|
836
|
+
)
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export default function LoginPage() {
|
|
840
|
+
const { data, execute, error, isLoading } = useN8xFormQuery(login)
|
|
841
|
+
const [dismissError, setDismissError] = React.useState(false)
|
|
842
|
+
|
|
843
|
+
return (
|
|
844
|
+
<>
|
|
845
|
+
{error && !dismissError && (
|
|
846
|
+
<ErrorAlert
|
|
847
|
+
message={error}
|
|
848
|
+
onDismiss={() => setDismissError(true)}
|
|
849
|
+
/>
|
|
850
|
+
)}
|
|
851
|
+
<N8xForm
|
|
852
|
+
fields={loginForm}
|
|
853
|
+
onShubmit={(data) => {
|
|
854
|
+
setDismissError(false)
|
|
855
|
+
execute(data)
|
|
856
|
+
}}
|
|
857
|
+
/>
|
|
858
|
+
</>
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
### Error Recovery
|
|
864
|
+
|
|
865
|
+
```tsx
|
|
866
|
+
const [errorKey, setErrorKey] = React.useState(0)
|
|
867
|
+
|
|
868
|
+
const handleRetry = () => {
|
|
869
|
+
setErrorKey(prev => prev + 1) // Force form re-render
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return (
|
|
873
|
+
<>
|
|
874
|
+
{error && (
|
|
875
|
+
<button onClick={handleRetry} className="text-blue-600">
|
|
876
|
+
Retry
|
|
877
|
+
</button>
|
|
878
|
+
)}
|
|
879
|
+
<N8xForm
|
|
880
|
+
key={errorKey}
|
|
881
|
+
fields={loginForm}
|
|
882
|
+
onShubmit={handleSubmit}
|
|
883
|
+
/>
|
|
884
|
+
</>
|
|
885
|
+
)
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
## API Reference
|
|
891
|
+
|
|
892
|
+
### N8xForm Component
|
|
893
|
+
|
|
894
|
+
The main form component that renders fields and handles submission.
|
|
895
|
+
|
|
896
|
+
```tsx
|
|
897
|
+
interface FormProps {
|
|
898
|
+
fields?: FormFields[] | GroupedFormFields
|
|
899
|
+
defaultFieldStyles?: EDefaultFieldStyles | string
|
|
900
|
+
onShubmit: (data: any) => void
|
|
901
|
+
mode?: Mode
|
|
902
|
+
children?: React.ReactNode
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
<N8xForm
|
|
906
|
+
fields={loginForm}
|
|
907
|
+
defaultFieldStyles={EDefaultFieldStyles.FLOATING_LABEL}
|
|
908
|
+
onShubmit={handleSubmit}
|
|
909
|
+
mode="onBlur"
|
|
910
|
+
>
|
|
911
|
+
<button type="submit">Submit</button>
|
|
912
|
+
</N8xForm>
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
**Props:**
|
|
916
|
+
- `fields`: Array or object of FormFields to render
|
|
917
|
+
- `defaultFieldStyles`: Theme from EDefaultFieldStyles or custom Tailwind string
|
|
918
|
+
- `onShubmit`: Callback when form is successfully submitted
|
|
919
|
+
- `mode`: React Hook Form validation mode ('onChange', 'onBlur', 'onTouched', 'onSubmit', 'all')
|
|
920
|
+
- `children`: Additional JSX elements (e.g., submit button, custom elements)
|
|
921
|
+
|
|
922
|
+
### FormFields Type
|
|
923
|
+
|
|
924
|
+
Configuration for a single form field.
|
|
925
|
+
|
|
926
|
+
```tsx
|
|
927
|
+
type FormFields = {
|
|
928
|
+
name: string // Unique field identifier
|
|
929
|
+
type: FormFieldTypes // Input type
|
|
930
|
+
validationScheme?: ZodTypeAny // Zod validation schema
|
|
931
|
+
label?: string // Display label
|
|
932
|
+
placeholder?: string // Input placeholder
|
|
933
|
+
options?: { label: string; value: string }[] // For select/radio/checkbox
|
|
934
|
+
min?: number // Min value for number fields
|
|
935
|
+
max?: number // Max value for number fields
|
|
936
|
+
required?: boolean // Is field required?
|
|
937
|
+
width?: FormFieldWidth // Grid column span
|
|
938
|
+
watch?: boolean // Watch for changes
|
|
939
|
+
default?: string // Default value
|
|
940
|
+
_className?: string // Custom Tailwind classes
|
|
941
|
+
}
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
### FormFieldTypes Enum
|
|
945
|
+
|
|
946
|
+
```tsx
|
|
947
|
+
enum FormFieldTypes {
|
|
948
|
+
TEXT = 'text'
|
|
949
|
+
NUMBER = 'number'
|
|
950
|
+
EMAIL = 'email'
|
|
951
|
+
PASSWORD = 'password'
|
|
952
|
+
SELECT = 'select'
|
|
953
|
+
RADIO = 'radio'
|
|
954
|
+
CHECKBOX = 'checkbox'
|
|
955
|
+
TEXTAREA = 'textarea'
|
|
956
|
+
DATE = 'date'
|
|
957
|
+
FILE = 'file'
|
|
958
|
+
SUBMIT = 'submit'
|
|
959
|
+
}
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
### FormFieldWidth Enum
|
|
963
|
+
|
|
964
|
+
```tsx
|
|
965
|
+
enum FormFieldWidth {
|
|
966
|
+
FULL = 'col-span-4' // 100% width
|
|
967
|
+
HALF = 'col-span-2' // 50% width
|
|
968
|
+
THIRD = 'col-span-3' // 75% width
|
|
969
|
+
QUARTER = 'col-span-1' // 25% width
|
|
970
|
+
}
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
### EDefaultFieldStyles Enum
|
|
974
|
+
|
|
975
|
+
```tsx
|
|
976
|
+
enum EDefaultFieldStyles {
|
|
977
|
+
CLEAN // Minimal white inputs
|
|
978
|
+
SOFT_GLASS // Glassmorphism (default)
|
|
979
|
+
DARK // Dark mode inputs
|
|
980
|
+
FLOATING_LABEL // Material Design floating labels
|
|
981
|
+
}
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
### useN8xFormQuery Hook
|
|
985
|
+
|
|
986
|
+
```tsx
|
|
987
|
+
const {
|
|
988
|
+
data, // Response data after successful request
|
|
989
|
+
execute, // Function to execute the query
|
|
990
|
+
error, // Error message string or null
|
|
991
|
+
isLoading // Boolean loading state
|
|
992
|
+
} = useN8xFormQuery(
|
|
993
|
+
queryService: (payload: any, signal?: AbortSignal) => Promise<any>
|
|
994
|
+
)
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
**Example:**
|
|
998
|
+
```tsx
|
|
999
|
+
const { data, execute, error, isLoading } = useN8xFormQuery(login)
|
|
1000
|
+
|
|
1001
|
+
// Later in form submission:
|
|
1002
|
+
const handleSubmit = (formData: any) => {
|
|
1003
|
+
execute(formData)
|
|
1004
|
+
}
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
### useN8xQuery Hook
|
|
1008
|
+
|
|
1009
|
+
```tsx
|
|
1010
|
+
const {
|
|
1011
|
+
data, // Response data after successful request
|
|
1012
|
+
error, // Error message string or null
|
|
1013
|
+
isLoading // Boolean loading state
|
|
1014
|
+
} = useN8xQuery(
|
|
1015
|
+
queryService: (payload: any, signal?: AbortSignal) => Promise<any>,
|
|
1016
|
+
...queryParams: any[]
|
|
1017
|
+
)
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
**Example:**
|
|
1021
|
+
```tsx
|
|
1022
|
+
const { data, error, isLoading } = useN8xQuery(fetchUserProfile, userId)
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
### FormContextType Interface
|
|
1026
|
+
|
|
1027
|
+
```tsx
|
|
1028
|
+
interface FormContextType {
|
|
1029
|
+
formUtils: ReturnType<typeof useForm> // React Hook Form utilities
|
|
1030
|
+
setValidationSchema: (props: ZodSchema) => void // Update validation schema
|
|
1031
|
+
setMode: (props: Mode) => void // Update validation mode
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
---
|
|
1036
|
+
|
|
1037
|
+
## Complete Real-World Example
|
|
1038
|
+
|
|
1039
|
+
Here's a complete registration flow combining everything:
|
|
1040
|
+
|
|
1041
|
+
```tsx
|
|
1042
|
+
// forms/registration.ts
|
|
1043
|
+
import { z } from 'n8x-form-utils'
|
|
1044
|
+
import { FormFieldTypes, FormFieldWidth, type FormFields } from 'n8x-form-utils'
|
|
1045
|
+
|
|
1046
|
+
export const registrationForm: FormFields[] = [
|
|
1047
|
+
{
|
|
1048
|
+
name: 'email',
|
|
1049
|
+
type: FormFieldTypes.EMAIL,
|
|
1050
|
+
label: 'Email Address',
|
|
1051
|
+
placeholder: 'hello@example.com',
|
|
1052
|
+
required: true,
|
|
1053
|
+
width: FormFieldWidth.FULL,
|
|
1054
|
+
validationScheme: z.string().email('Invalid email address')
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
name: 'password',
|
|
1058
|
+
type: FormFieldTypes.PASSWORD,
|
|
1059
|
+
label: 'Password',
|
|
1060
|
+
placeholder: 'Create a strong password',
|
|
1061
|
+
required: true,
|
|
1062
|
+
width: FormFieldWidth.FULL,
|
|
1063
|
+
validationScheme: z.string()
|
|
1064
|
+
.min(6, 'Password must be at least 6 characters')
|
|
1065
|
+
.max(36, 'Password too long')
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
name: 'confirmPassword',
|
|
1069
|
+
type: FormFieldTypes.PASSWORD,
|
|
1070
|
+
label: 'Confirm Password',
|
|
1071
|
+
placeholder: 'Re-enter your password',
|
|
1072
|
+
required: true,
|
|
1073
|
+
width: FormFieldWidth.FULL,
|
|
1074
|
+
validationScheme: z.string()
|
|
1075
|
+
.min(6, 'Password must match')
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
name: 'fullName',
|
|
1079
|
+
type: FormFieldTypes.TEXT,
|
|
1080
|
+
label: 'Full Name',
|
|
1081
|
+
placeholder: 'John Doe',
|
|
1082
|
+
required: true,
|
|
1083
|
+
width: FormFieldWidth.FULL,
|
|
1084
|
+
validationScheme: z.string().min(2, 'Name too short')
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
name: 'accountType',
|
|
1088
|
+
type: FormFieldTypes.RADIO,
|
|
1089
|
+
label: 'Account Type',
|
|
1090
|
+
required: true,
|
|
1091
|
+
width: FormFieldWidth.FULL,
|
|
1092
|
+
options: [
|
|
1093
|
+
{ label: 'Personal', value: 'personal' },
|
|
1094
|
+
{ label: 'Business', value: 'business' }
|
|
1095
|
+
],
|
|
1096
|
+
validationScheme: z.string()
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
name: 'interests',
|
|
1100
|
+
type: FormFieldTypes.CHECKBOX,
|
|
1101
|
+
label: 'Select Your Interests',
|
|
1102
|
+
width: FormFieldWidth.FULL,
|
|
1103
|
+
options: [
|
|
1104
|
+
{ label: 'Technology', value: 'tech' },
|
|
1105
|
+
{ label: 'Finance', value: 'finance' },
|
|
1106
|
+
{ label: 'Health', value: 'health' }
|
|
1107
|
+
],
|
|
1108
|
+
validationScheme: z.array(z.string()).min(1, 'Select at least one interest')
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
name: 'terms',
|
|
1112
|
+
type: FormFieldTypes.CHECKBOX,
|
|
1113
|
+
label: 'Agreement',
|
|
1114
|
+
required: true,
|
|
1115
|
+
width: FormFieldWidth.FULL,
|
|
1116
|
+
options: [
|
|
1117
|
+
{ label: 'I agree to Terms and Conditions', value: 'agreed' }
|
|
1118
|
+
],
|
|
1119
|
+
validationScheme: z.array(z.string()).min(1, 'You must agree to terms')
|
|
1120
|
+
}
|
|
1121
|
+
]
|
|
1122
|
+
|
|
1123
|
+
// pages/auth/Register.tsx
|
|
1124
|
+
import { useContext, useEffect, useState } from 'react'
|
|
1125
|
+
import { Link, useNavigate } from 'react-router-dom'
|
|
1126
|
+
import { N8xForm, useN8xFormQuery, EDefaultFieldStyles } from 'n8x-form-utils'
|
|
1127
|
+
import { AuthContext, type IAuthContext } from '../../context/AuthContext/context'
|
|
1128
|
+
import { registrationForm } from '../../forms/registration'
|
|
1129
|
+
import { register } from '../../service/auth-service'
|
|
1130
|
+
|
|
1131
|
+
export default function RegisterPage() {
|
|
1132
|
+
const { user, setUser } = useContext(AuthContext) as IAuthContext
|
|
1133
|
+
const navigate = useNavigate()
|
|
1134
|
+
const [dismissError, setDismissError] = useState(false)
|
|
1135
|
+
const { data, execute, error, isLoading } = useN8xFormQuery(register)
|
|
1136
|
+
|
|
1137
|
+
const handleRegisterSubmit = async (formData: any) => {
|
|
1138
|
+
// Validate passwords match
|
|
1139
|
+
if (formData.password !== formData.confirmPassword) {
|
|
1140
|
+
// You could set a custom error here or let Zod handle it
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1143
|
+
setDismissError(false)
|
|
1144
|
+
execute(formData)
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Handle successful registration
|
|
1148
|
+
useEffect(() => {
|
|
1149
|
+
if (data) {
|
|
1150
|
+
setUser(data)
|
|
1151
|
+
navigate('/home')
|
|
1152
|
+
}
|
|
1153
|
+
}, [data, setUser, navigate])
|
|
1154
|
+
|
|
1155
|
+
// Redirect if already logged in
|
|
1156
|
+
useEffect(() => {
|
|
1157
|
+
if (user) {
|
|
1158
|
+
navigate('/home')
|
|
1159
|
+
}
|
|
1160
|
+
}, [user, navigate])
|
|
1161
|
+
|
|
1162
|
+
return (
|
|
1163
|
+
<div className="w-full min-h-screen flex justify-center items-center bg-gradient-to-br from-zinc-50 to-zinc-100 p-4">
|
|
1164
|
+
<div className="md:w-[600px] w-full">
|
|
1165
|
+
<div className="mb-8">
|
|
1166
|
+
<h1 className="text-4xl font-bold tracking-tight mb-2">Create Your Account</h1>
|
|
1167
|
+
<p className="text-zinc-600">Join us today and get started</p>
|
|
1168
|
+
</div>
|
|
1169
|
+
|
|
1170
|
+
{error && !dismissError && (
|
|
1171
|
+
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
|
1172
|
+
<p className="text-red-700 font-semibold">Registration failed</p>
|
|
1173
|
+
<p className="text-red-600 text-sm mt-1">{error}</p>
|
|
1174
|
+
<button
|
|
1175
|
+
onClick={() => setDismissError(true)}
|
|
1176
|
+
className="mt-2 text-xs text-red-600 hover:text-red-700"
|
|
1177
|
+
>
|
|
1178
|
+
Dismiss
|
|
1179
|
+
</button>
|
|
1180
|
+
</div>
|
|
1181
|
+
)}
|
|
1182
|
+
|
|
1183
|
+
<N8xForm
|
|
1184
|
+
fields={registrationForm}
|
|
1185
|
+
onShubmit={handleRegisterSubmit}
|
|
1186
|
+
defaultFieldStyles={EDefaultFieldStyles.FLOATING_LABEL}
|
|
1187
|
+
>
|
|
1188
|
+
<button
|
|
1189
|
+
disabled={isLoading}
|
|
1190
|
+
type="submit"
|
|
1191
|
+
className={`col-span-4 py-3 px-4 rounded-lg font-semibold text-white transition-all ${
|
|
1192
|
+
isLoading
|
|
1193
|
+
? 'bg-zinc-400 cursor-not-allowed'
|
|
1194
|
+
: 'bg-blue-500 hover:bg-blue-600 active:bg-blue-700'
|
|
1195
|
+
}`}
|
|
1196
|
+
>
|
|
1197
|
+
{isLoading ? (
|
|
1198
|
+
<span className="inline-flex items-center gap-2">
|
|
1199
|
+
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
1200
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
1201
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
1202
|
+
</svg>
|
|
1203
|
+
Creating account...
|
|
1204
|
+
</span>
|
|
1205
|
+
) : (
|
|
1206
|
+
'Create Account'
|
|
1207
|
+
)}
|
|
1208
|
+
</button>
|
|
1209
|
+
</N8xForm>
|
|
1210
|
+
|
|
1211
|
+
<p className="text-center text-sm text-zinc-600 mt-6">
|
|
1212
|
+
Already have an account?{' '}
|
|
1213
|
+
<Link to="/login" className="text-blue-500 hover:text-blue-600 font-semibold">
|
|
1214
|
+
Login here
|
|
1215
|
+
</Link>
|
|
1216
|
+
</p>
|
|
1217
|
+
</div>
|
|
1218
|
+
</div>
|
|
1219
|
+
)
|
|
1220
|
+
}
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
---
|
|
1224
|
+
|
|
1225
|
+
## Browser Support
|
|
1226
|
+
|
|
1227
|
+
- Chrome (latest)
|
|
1228
|
+
- Firefox (latest)
|
|
1229
|
+
- Safari (latest)
|
|
1230
|
+
- Edge (latest)
|
|
1231
|
+
|
|
1232
|
+
## License
|
|
1233
|
+
|
|
1234
|
+
ISC License - Created by Syed Shayan Ali
|
|
1235
|
+
|
|
1236
|
+
## Contributing
|
|
1237
|
+
|
|
1238
|
+
This project is currently not open for external contributions. It will be made open source very soon in the future once it has matured and gained wider adoption.
|