0
# Form Integration
1
2
NextUI provides comprehensive form handling capabilities with the Form component and seamless integration patterns for validation, data collection, and form state management.
3
4
## Capabilities
5
6
### Form Component
7
8
A form container component that provides validation context, error handling, and integration with form libraries for building robust form interfaces.
9
10
```typescript { .api }
11
interface FormProps {
12
/** Form content and input elements */
13
children?: React.ReactNode;
14
/** Server-side or external validation errors */
15
validationErrors?: ValidationErrors;
16
/** Validation behavior mode */
17
validationBehavior?: "aria" | "native";
18
/** Custom CSS class */
19
className?: string;
20
/** Form reset handler */
21
onReset?: () => void;
22
/** Form submission handler */
23
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;
24
/** Invalid submission handler */
25
onInvalidSubmit?: (errors: ValidationErrors) => void;
26
}
27
28
interface ValidationErrors {
29
[fieldName: string]: ValidationError;
30
}
31
32
type ValidationError = string | string[];
33
34
function Form(props: FormProps): JSX.Element;
35
```
36
37
**Basic Form Usage:**
38
39
```typescript
40
import {
41
Form, Input, Button, Checkbox, Select, SelectItem,
42
Card, CardHeader, CardBody, CardFooter
43
} from "@nextui-org/react";
44
45
function BasicFormExample() {
46
const [formData, setFormData] = useState({
47
name: "",
48
email: "",
49
password: "",
50
confirmPassword: "",
51
terms: false,
52
country: "",
53
});
54
55
const [errors, setErrors] = useState<ValidationErrors>({});
56
57
const validateForm = () => {
58
const newErrors: ValidationErrors = {};
59
60
if (!formData.name.trim()) {
61
newErrors.name = "Name is required";
62
}
63
64
if (!formData.email.trim()) {
65
newErrors.email = "Email is required";
66
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
67
newErrors.email = "Please enter a valid email";
68
}
69
70
if (!formData.password) {
71
newErrors.password = "Password is required";
72
} else if (formData.password.length < 8) {
73
newErrors.password = "Password must be at least 8 characters";
74
}
75
76
if (formData.password !== formData.confirmPassword) {
77
newErrors.confirmPassword = "Passwords do not match";
78
}
79
80
if (!formData.terms) {
81
newErrors.terms = "You must accept the terms and conditions";
82
}
83
84
return newErrors;
85
};
86
87
const handleSubmit = (e: React.FormEvent) => {
88
e.preventDefault();
89
const validationErrors = validateForm();
90
setErrors(validationErrors);
91
92
if (Object.keys(validationErrors).length === 0) {
93
console.log("Form submitted successfully:", formData);
94
// Handle successful submission
95
}
96
};
97
98
const updateField = (field: string) => (value: string | boolean) => {
99
setFormData(prev => ({ ...prev, [field]: value }));
100
// Clear error when user starts typing
101
if (errors[field]) {
102
setErrors(prev => ({ ...prev, [field]: undefined }));
103
}
104
};
105
106
return (
107
<Card className="max-w-md mx-auto">
108
<CardHeader>
109
<h2 className="text-xl font-bold">Create Account</h2>
110
</CardHeader>
111
<Form onSubmit={handleSubmit} validationErrors={errors}>
112
<CardBody className="space-y-4">
113
<Input
114
label="Full Name"
115
placeholder="Enter your full name"
116
value={formData.name}
117
onValueChange={updateField("name")}
118
isRequired
119
isInvalid={!!errors.name}
120
errorMessage={errors.name}
121
/>
122
123
<Input
124
type="email"
125
label="Email"
126
placeholder="Enter your email"
127
value={formData.email}
128
onValueChange={updateField("email")}
129
isRequired
130
isInvalid={!!errors.email}
131
errorMessage={errors.email}
132
/>
133
134
<Input
135
type="password"
136
label="Password"
137
placeholder="Enter your password"
138
value={formData.password}
139
onValueChange={updateField("password")}
140
isRequired
141
isInvalid={!!errors.password}
142
errorMessage={errors.password}
143
/>
144
145
<Input
146
type="password"
147
label="Confirm Password"
148
placeholder="Confirm your password"
149
value={formData.confirmPassword}
150
onValueChange={updateField("confirmPassword")}
151
isRequired
152
isInvalid={!!errors.confirmPassword}
153
errorMessage={errors.confirmPassword}
154
/>
155
156
<Select
157
label="Country"
158
placeholder="Select your country"
159
selectedKeys={formData.country ? [formData.country] : []}
160
onSelectionChange={(keys) => {
161
const selected = Array.from(keys)[0] as string;
162
updateField("country")(selected);
163
}}
164
isRequired
165
>
166
<SelectItem key="us">United States</SelectItem>
167
<SelectItem key="ca">Canada</SelectItem>
168
<SelectItem key="uk">United Kingdom</SelectItem>
169
<SelectItem key="de">Germany</SelectItem>
170
<SelectItem key="fr">France</SelectItem>
171
</Select>
172
173
<Checkbox
174
isSelected={formData.terms}
175
onValueChange={updateField("terms")}
176
isInvalid={!!errors.terms}
177
color={errors.terms ? "danger" : "primary"}
178
>
179
I agree to the{" "}
180
<a href="#" className="text-primary hover:underline">
181
terms and conditions
182
</a>
183
</Checkbox>
184
185
{errors.terms && (
186
<p className="text-danger text-sm mt-1">{errors.terms}</p>
187
)}
188
</CardBody>
189
<CardFooter>
190
<Button
191
type="submit"
192
color="primary"
193
className="w-full"
194
size="lg"
195
>
196
Create Account
197
</Button>
198
</CardFooter>
199
</Form>
200
</Card>
201
);
202
}
203
```
204
205
### Form Context
206
207
Context system for sharing form state and validation across form components.
208
209
```typescript { .api }
210
interface FormContext {
211
/** Current validation errors */
212
validationErrors?: ValidationErrors;
213
/** Validation behavior mode */
214
validationBehavior?: "aria" | "native";
215
/** Form submission state */
216
isSubmitting?: boolean;
217
/** Form dirty state */
218
isDirty?: boolean;
219
/** Form valid state */
220
isValid?: boolean;
221
/** Reset form */
222
reset?: () => void;
223
/** Update field validation */
224
updateValidation?: (fieldName: string, error: ValidationError | null) => void;
225
}
226
227
/**
228
* Hook to access form context
229
*/
230
function useSlottedContext<T>(context: React.Context<T>): T | undefined;
231
```
232
233
### React Hook Form Integration
234
235
NextUI components integrate seamlessly with React Hook Form for advanced form handling.
236
237
```typescript
238
import { useForm, Controller, SubmitHandler } from "react-hook-form";
239
import { zodResolver } from "@hookform/resolvers/zod";
240
import { z } from "zod";
241
import {
242
Form, Input, Button, Select, SelectItem, Checkbox,
243
Card, CardHeader, CardBody, CardFooter
244
} from "@nextui-org/react";
245
246
// Validation schema
247
const schema = z.object({
248
firstName: z.string().min(1, "First name is required"),
249
lastName: z.string().min(1, "Last name is required"),
250
email: z.string().email("Please enter a valid email"),
251
age: z.number().min(18, "Must be at least 18 years old"),
252
country: z.string().min(1, "Please select a country"),
253
newsletter: z.boolean(),
254
bio: z.string().max(500, "Bio must be less than 500 characters").optional(),
255
});
256
257
type FormData = z.infer<typeof schema>;
258
259
function ReactHookFormExample() {
260
const {
261
control,
262
handleSubmit,
263
reset,
264
formState: { errors, isSubmitting, isDirty, isValid }
265
} = useForm<FormData>({
266
resolver: zodResolver(schema),
267
defaultValues: {
268
firstName: "",
269
lastName: "",
270
email: "",
271
age: 18,
272
country: "",
273
newsletter: false,
274
bio: "",
275
},
276
mode: "onChange", // Validate on change
277
});
278
279
const onSubmit: SubmitHandler<FormData> = async (data) => {
280
try {
281
// Simulate API call
282
await new Promise(resolve => setTimeout(resolve, 1000));
283
console.log("Form submitted:", data);
284
reset();
285
} catch (error) {
286
console.error("Submission error:", error);
287
}
288
};
289
290
return (
291
<Card className="max-w-2xl mx-auto">
292
<CardHeader>
293
<h2 className="text-xl font-bold">User Profile</h2>
294
</CardHeader>
295
<form onSubmit={handleSubmit(onSubmit)}>
296
<CardBody className="space-y-4">
297
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
298
<Controller
299
name="firstName"
300
control={control}
301
render={({ field, fieldState }) => (
302
<Input
303
{...field}
304
label="First Name"
305
placeholder="Enter your first name"
306
isInvalid={fieldState.invalid}
307
errorMessage={fieldState.error?.message}
308
/>
309
)}
310
/>
311
312
<Controller
313
name="lastName"
314
control={control}
315
render={({ field, fieldState }) => (
316
<Input
317
{...field}
318
label="Last Name"
319
placeholder="Enter your last name"
320
isInvalid={fieldState.invalid}
321
errorMessage={fieldState.error?.message}
322
/>
323
)}
324
/>
325
</div>
326
327
<Controller
328
name="email"
329
control={control}
330
render={({ field, fieldState }) => (
331
<Input
332
{...field}
333
type="email"
334
label="Email"
335
placeholder="Enter your email"
336
isInvalid={fieldState.invalid}
337
errorMessage={fieldState.error?.message}
338
/>
339
)}
340
/>
341
342
<Controller
343
name="age"
344
control={control}
345
render={({ field, fieldState }) => (
346
<Input
347
{...field}
348
type="number"
349
label="Age"
350
placeholder="Enter your age"
351
value={field.value?.toString() || ""}
352
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
353
isInvalid={fieldState.invalid}
354
errorMessage={fieldState.error?.message}
355
/>
356
)}
357
/>
358
359
<Controller
360
name="country"
361
control={control}
362
render={({ field, fieldState }) => (
363
<Select
364
{...field}
365
label="Country"
366
placeholder="Select your country"
367
selectedKeys={field.value ? [field.value] : []}
368
onSelectionChange={(keys) => {
369
const selected = Array.from(keys)[0] as string;
370
field.onChange(selected);
371
}}
372
isInvalid={fieldState.invalid}
373
errorMessage={fieldState.error?.message}
374
>
375
<SelectItem key="us">United States</SelectItem>
376
<SelectItem key="ca">Canada</SelectItem>
377
<SelectItem key="uk">United Kingdom</SelectItem>
378
<SelectItem key="de">Germany</SelectItem>
379
<SelectItem key="fr">France</SelectItem>
380
</Select>
381
)}
382
/>
383
384
<Controller
385
name="bio"
386
control={control}
387
render={({ field, fieldState }) => (
388
<Textarea
389
{...field}
390
label="Bio"
391
placeholder="Tell us about yourself (optional)"
392
maxRows={3}
393
isInvalid={fieldState.invalid}
394
errorMessage={fieldState.error?.message}
395
/>
396
)}
397
/>
398
399
<Controller
400
name="newsletter"
401
control={control}
402
render={({ field }) => (
403
<Checkbox
404
isSelected={field.value}
405
onValueChange={field.onChange}
406
color="primary"
407
>
408
Subscribe to newsletter
409
</Checkbox>
410
)}
411
/>
412
</CardBody>
413
<CardFooter className="flex gap-2">
414
<Button
415
type="button"
416
variant="flat"
417
onPress={() => reset()}
418
isDisabled={!isDirty}
419
>
420
Reset
421
</Button>
422
<Button
423
type="submit"
424
color="primary"
425
isLoading={isSubmitting}
426
isDisabled={!isValid}
427
className="flex-1"
428
>
429
{isSubmitting ? "Saving..." : "Save Profile"}
430
</Button>
431
</CardFooter>
432
</form>
433
</Card>
434
);
435
}
436
```
437
438
### Formik Integration
439
440
NextUI components also work seamlessly with Formik for form state management.
441
442
```typescript
443
import { Formik, Form as FormikForm, Field } from "formik";
444
import * as Yup from "yup";
445
import {
446
Input, Button, Select, SelectItem, Textarea,
447
Card, CardHeader, CardBody, CardFooter
448
} from "@nextui-org/react";
449
450
// Validation schema
451
const validationSchema = Yup.object({
452
name: Yup.string().required("Name is required"),
453
email: Yup.string().email("Invalid email").required("Email is required"),
454
message: Yup.string()
455
.min(10, "Message must be at least 10 characters")
456
.required("Message is required"),
457
priority: Yup.string().required("Please select a priority"),
458
});
459
460
interface ContactFormValues {
461
name: string;
462
email: string;
463
message: string;
464
priority: string;
465
}
466
467
function FormikExample() {
468
const initialValues: ContactFormValues = {
469
name: "",
470
email: "",
471
message: "",
472
priority: "",
473
};
474
475
const handleSubmit = async (
476
values: ContactFormValues,
477
{ setSubmitting, resetForm }: any
478
) => {
479
try {
480
// Simulate API call
481
await new Promise(resolve => setTimeout(resolve, 1000));
482
console.log("Contact form submitted:", values);
483
resetForm();
484
} catch (error) {
485
console.error("Submission error:", error);
486
} finally {
487
setSubmitting(false);
488
}
489
};
490
491
return (
492
<Card className="max-w-lg mx-auto">
493
<CardHeader>
494
<h2 className="text-xl font-bold">Contact Us</h2>
495
</CardHeader>
496
<Formik
497
initialValues={initialValues}
498
validationSchema={validationSchema}
499
onSubmit={handleSubmit}
500
>
501
{({ isSubmitting, errors, touched, setFieldValue, values }) => (
502
<FormikForm>
503
<CardBody className="space-y-4">
504
<Field name="name">
505
{({ field, meta }: any) => (
506
<Input
507
{...field}
508
label="Name"
509
placeholder="Enter your name"
510
isInvalid={!!(meta.touched && meta.error)}
511
errorMessage={meta.touched && meta.error}
512
/>
513
)}
514
</Field>
515
516
<Field name="email">
517
{({ field, meta }: any) => (
518
<Input
519
{...field}
520
type="email"
521
label="Email"
522
placeholder="Enter your email"
523
isInvalid={!!(meta.touched && meta.error)}
524
errorMessage={meta.touched && meta.error}
525
/>
526
)}
527
</Field>
528
529
<Field name="priority">
530
{({ meta }: any) => (
531
<Select
532
label="Priority"
533
placeholder="Select priority level"
534
selectedKeys={values.priority ? [values.priority] : []}
535
onSelectionChange={(keys) => {
536
const selected = Array.from(keys)[0] as string;
537
setFieldValue("priority", selected);
538
}}
539
isInvalid={!!(meta.touched && meta.error)}
540
errorMessage={meta.touched && meta.error}
541
>
542
<SelectItem key="low">Low</SelectItem>
543
<SelectItem key="medium">Medium</SelectItem>
544
<SelectItem key="high">High</SelectItem>
545
<SelectItem key="urgent">Urgent</SelectItem>
546
</Select>
547
)}
548
</Field>
549
550
<Field name="message">
551
{({ field, meta }: any) => (
552
<Textarea
553
{...field}
554
label="Message"
555
placeholder="Enter your message"
556
minRows={4}
557
isInvalid={!!(meta.touched && meta.error)}
558
errorMessage={meta.touched && meta.error}
559
/>
560
)}
561
</Field>
562
</CardBody>
563
<CardFooter>
564
<Button
565
type="submit"
566
color="primary"
567
isLoading={isSubmitting}
568
className="w-full"
569
size="lg"
570
>
571
{isSubmitting ? "Sending..." : "Send Message"}
572
</Button>
573
</CardFooter>
574
</FormikForm>
575
)}
576
</Formik>
577
</Card>
578
);
579
}
580
```
581
582
### Field Validation Patterns
583
584
Common validation patterns and utilities for form fields.
585
586
```typescript
587
import {
588
Input, DatePicker, Select, SelectItem, Checkbox,
589
Button, Card, CardBody
590
} from "@nextui-org/react";
591
import { CalendarDate, today, getLocalTimeZone } from "@internationalized/date";
592
593
// Validation utilities
594
const validators = {
595
required: (value: any) => {
596
if (!value || (typeof value === "string" && !value.trim())) {
597
return "This field is required";
598
}
599
return true;
600
},
601
602
email: (value: string) => {
603
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
604
return "Please enter a valid email address";
605
}
606
return true;
607
},
608
609
minLength: (min: number) => (value: string) => {
610
if (value && value.length < min) {
611
return `Must be at least ${min} characters`;
612
}
613
return true;
614
},
615
616
maxLength: (max: number) => (value: string) => {
617
if (value && value.length > max) {
618
return `Must be no more than ${max} characters`;
619
}
620
return true;
621
},
622
623
phone: (value: string) => {
624
if (value && !/^\+?[\d\s\-\(\)]+$/.test(value)) {
625
return "Please enter a valid phone number";
626
}
627
return true;
628
},
629
630
url: (value: string) => {
631
if (value) {
632
try {
633
new URL(value);
634
} catch {
635
return "Please enter a valid URL";
636
}
637
}
638
return true;
639
},
640
641
futureDate: (value: CalendarDate | null) => {
642
if (value && value.compare(today(getLocalTimeZone())) <= 0) {
643
return "Date must be in the future";
644
}
645
return true;
646
},
647
648
passwordStrength: (value: string) => {
649
if (value) {
650
const hasLower = /[a-z]/.test(value);
651
const hasUpper = /[A-Z]/.test(value);
652
const hasNumber = /\d/.test(value);
653
const hasSymbol = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value);
654
const isLongEnough = value.length >= 8;
655
656
if (!isLongEnough) return "Password must be at least 8 characters";
657
if (!hasLower) return "Password must contain lowercase letters";
658
if (!hasUpper) return "Password must contain uppercase letters";
659
if (!hasNumber) return "Password must contain numbers";
660
if (!hasSymbol) return "Password must contain symbols";
661
}
662
return true;
663
},
664
665
confirmPassword: (original: string) => (value: string) => {
666
if (value && value !== original) {
667
return "Passwords do not match";
668
}
669
return true;
670
},
671
};
672
673
// Validation runner utility
674
const runValidations = (value: any, validationFns: ((value: any) => boolean | string)[]) => {
675
for (const validate of validationFns) {
676
const result = validate(value);
677
if (result !== true) {
678
return result;
679
}
680
}
681
return true;
682
};
683
684
function ValidationPatternsExample() {
685
const [formData, setFormData] = useState({
686
email: "",
687
password: "",
688
confirmPassword: "",
689
website: "",
690
birthDate: null as CalendarDate | null,
691
phone: "",
692
terms: false,
693
});
694
695
const [errors, setErrors] = useState<Record<string, string>>({});
696
697
const validateField = (name: string, value: any) => {
698
let result: boolean | string = true;
699
700
switch (name) {
701
case "email":
702
result = runValidations(value, [
703
validators.required,
704
validators.email,
705
]);
706
break;
707
case "password":
708
result = runValidations(value, [
709
validators.required,
710
validators.passwordStrength,
711
]);
712
break;
713
case "confirmPassword":
714
result = runValidations(value, [
715
validators.required,
716
validators.confirmPassword(formData.password),
717
]);
718
break;
719
case "website":
720
result = runValidations(value, [validators.url]);
721
break;
722
case "birthDate":
723
result = runValidations(value, [validators.futureDate]);
724
break;
725
case "phone":
726
result = runValidations(value, [validators.phone]);
727
break;
728
case "terms":
729
result = value ? true : "You must accept the terms";
730
break;
731
}
732
733
setErrors(prev => ({
734
...prev,
735
[name]: result === true ? "" : result
736
}));
737
738
return result === true;
739
};
740
741
const updateField = (name: string) => (value: any) => {
742
setFormData(prev => ({ ...prev, [name]: value }));
743
validateField(name, value);
744
};
745
746
return (
747
<Card className="max-w-lg mx-auto">
748
<CardBody className="space-y-4">
749
<Input
750
type="email"
751
label="Email Address"
752
placeholder="Enter your email"
753
value={formData.email}
754
onValueChange={updateField("email")}
755
isInvalid={!!errors.email}
756
errorMessage={errors.email}
757
isRequired
758
/>
759
760
<Input
761
type="password"
762
label="Password"
763
placeholder="Create a strong password"
764
value={formData.password}
765
onValueChange={updateField("password")}
766
isInvalid={!!errors.password}
767
errorMessage={errors.password}
768
description="Must contain uppercase, lowercase, numbers, and symbols"
769
isRequired
770
/>
771
772
<Input
773
type="password"
774
label="Confirm Password"
775
placeholder="Confirm your password"
776
value={formData.confirmPassword}
777
onValueChange={updateField("confirmPassword")}
778
isInvalid={!!errors.confirmPassword}
779
errorMessage={errors.confirmPassword}
780
isRequired
781
/>
782
783
<Input
784
type="url"
785
label="Website (Optional)"
786
placeholder="https://example.com"
787
value={formData.website}
788
onValueChange={updateField("website")}
789
isInvalid={!!errors.website}
790
errorMessage={errors.website}
791
/>
792
793
<Input
794
type="tel"
795
label="Phone Number (Optional)"
796
placeholder="+1 (555) 123-4567"
797
value={formData.phone}
798
onValueChange={updateField("phone")}
799
isInvalid={!!errors.phone}
800
errorMessage={errors.phone}
801
/>
802
803
<DatePicker
804
label="Event Date"
805
value={formData.birthDate}
806
onChange={updateField("birthDate")}
807
minValue={today(getLocalTimeZone()).add({ days: 1 })}
808
isInvalid={!!errors.birthDate}
809
errorMessage={errors.birthDate}
810
description="Select a future date"
811
/>
812
813
<Checkbox
814
isSelected={formData.terms}
815
onValueChange={updateField("terms")}
816
isInvalid={!!errors.terms}
817
color={errors.terms ? "danger" : "primary"}
818
>
819
I agree to the terms and conditions
820
</Checkbox>
821
822
{errors.terms && (
823
<p className="text-danger text-sm">{errors.terms}</p>
824
)}
825
826
<Button
827
color="primary"
828
className="w-full"
829
isDisabled={Object.values(errors).some(error => !!error) || !formData.terms}
830
>
831
Submit Form
832
</Button>
833
</CardBody>
834
</Card>
835
);
836
}
837
```
838
839
### Multi-Step Form Pattern
840
841
Building complex multi-step forms with NextUI components.
842
843
```typescript
844
import {
845
Card, CardHeader, CardBody, CardFooter,
846
Button, Input, Select, SelectItem, DatePicker, Textarea,
847
Progress, Divider
848
} from "@nextui-org/react";
849
import { CalendarDate } from "@internationalized/date";
850
851
interface StepData {
852
// Step 1: Personal Info
853
firstName: string;
854
lastName: string;
855
email: string;
856
phone: string;
857
birthDate: CalendarDate | null;
858
859
// Step 2: Address
860
address: string;
861
city: string;
862
state: string;
863
zipCode: string;
864
country: string;
865
866
// Step 3: Preferences
867
interests: string[];
868
bio: string;
869
newsletter: boolean;
870
}
871
872
function MultiStepFormExample() {
873
const [currentStep, setCurrentStep] = useState(1);
874
const [formData, setFormData] = useState<StepData>({
875
firstName: "", lastName: "", email: "", phone: "", birthDate: null,
876
address: "", city: "", state: "", zipCode: "", country: "",
877
interests: [], bio: "", newsletter: false,
878
});
879
const [errors, setErrors] = useState<Record<string, string>>({});
880
881
const totalSteps = 3;
882
const progress = (currentStep / totalSteps) * 100;
883
884
const validateStep = (step: number): boolean => {
885
const newErrors: Record<string, string> = {};
886
887
switch (step) {
888
case 1:
889
if (!formData.firstName.trim()) newErrors.firstName = "First name is required";
890
if (!formData.lastName.trim()) newErrors.lastName = "Last name is required";
891
if (!formData.email.trim()) newErrors.email = "Email is required";
892
break;
893
case 2:
894
if (!formData.address.trim()) newErrors.address = "Address is required";
895
if (!formData.city.trim()) newErrors.city = "City is required";
896
if (!formData.country) newErrors.country = "Country is required";
897
break;
898
case 3:
899
// Optional validation for final step
900
break;
901
}
902
903
setErrors(newErrors);
904
return Object.keys(newErrors).length === 0;
905
};
906
907
const nextStep = () => {
908
if (validateStep(currentStep)) {
909
setCurrentStep(prev => Math.min(prev + 1, totalSteps));
910
}
911
};
912
913
const prevStep = () => {
914
setCurrentStep(prev => Math.max(prev - 1, 1));
915
};
916
917
const handleSubmit = () => {
918
if (validateStep(currentStep)) {
919
console.log("Form submitted:", formData);
920
// Handle final submission
921
}
922
};
923
924
const updateField = (field: keyof StepData) => (value: any) => {
925
setFormData(prev => ({ ...prev, [field]: value }));
926
if (errors[field]) {
927
setErrors(prev => ({ ...prev, [field]: "" }));
928
}
929
};
930
931
const renderStep = () => {
932
switch (currentStep) {
933
case 1:
934
return (
935
<div className="space-y-4">
936
<div className="grid grid-cols-2 gap-4">
937
<Input
938
label="First Name"
939
placeholder="Enter first name"
940
value={formData.firstName}
941
onValueChange={updateField("firstName")}
942
isInvalid={!!errors.firstName}
943
errorMessage={errors.firstName}
944
isRequired
945
/>
946
<Input
947
label="Last Name"
948
placeholder="Enter last name"
949
value={formData.lastName}
950
onValueChange={updateField("lastName")}
951
isInvalid={!!errors.lastName}
952
errorMessage={errors.lastName}
953
isRequired
954
/>
955
</div>
956
<Input
957
type="email"
958
label="Email"
959
placeholder="Enter your email"
960
value={formData.email}
961
onValueChange={updateField("email")}
962
isInvalid={!!errors.email}
963
errorMessage={errors.email}
964
isRequired
965
/>
966
<Input
967
type="tel"
968
label="Phone Number"
969
placeholder="Enter phone number"
970
value={formData.phone}
971
onValueChange={updateField("phone")}
972
/>
973
<DatePicker
974
label="Birth Date"
975
value={formData.birthDate}
976
onChange={updateField("birthDate")}
977
/>
978
</div>
979
);
980
981
case 2:
982
return (
983
<div className="space-y-4">
984
<Input
985
label="Street Address"
986
placeholder="Enter your address"
987
value={formData.address}
988
onValueChange={updateField("address")}
989
isInvalid={!!errors.address}
990
errorMessage={errors.address}
991
isRequired
992
/>
993
<div className="grid grid-cols-2 gap-4">
994
<Input
995
label="City"
996
placeholder="Enter city"
997
value={formData.city}
998
onValueChange={updateField("city")}
999
isInvalid={!!errors.city}
1000
errorMessage={errors.city}
1001
isRequired
1002
/>
1003
<Input
1004
label="State/Province"
1005
placeholder="Enter state"
1006
value={formData.state}
1007
onValueChange={updateField("state")}
1008
/>
1009
</div>
1010
<div className="grid grid-cols-2 gap-4">
1011
<Input
1012
label="ZIP Code"
1013
placeholder="Enter ZIP code"
1014
value={formData.zipCode}
1015
onValueChange={updateField("zipCode")}
1016
/>
1017
<Select
1018
label="Country"
1019
placeholder="Select country"
1020
selectedKeys={formData.country ? [formData.country] : []}
1021
onSelectionChange={(keys) => {
1022
const selected = Array.from(keys)[0] as string;
1023
updateField("country")(selected);
1024
}}
1025
isInvalid={!!errors.country}
1026
errorMessage={errors.country}
1027
isRequired
1028
>
1029
<SelectItem key="us">United States</SelectItem>
1030
<SelectItem key="ca">Canada</SelectItem>
1031
<SelectItem key="uk">United Kingdom</SelectItem>
1032
</Select>
1033
</div>
1034
</div>
1035
);
1036
1037
case 3:
1038
return (
1039
<div className="space-y-4">
1040
<Textarea
1041
label="Bio (Optional)"
1042
placeholder="Tell us about yourself"
1043
value={formData.bio}
1044
onValueChange={updateField("bio")}
1045
maxRows={4}
1046
/>
1047
<div className="text-center">
1048
<p>Review your information and submit when ready.</p>
1049
</div>
1050
</div>
1051
);
1052
1053
default:
1054
return null;
1055
}
1056
};
1057
1058
return (
1059
<Card className="max-w-2xl mx-auto">
1060
<CardHeader className="space-y-2">
1061
<div className="flex justify-between items-center w-full">
1062
<h2 className="text-xl font-bold">Registration Form</h2>
1063
<span className="text-sm text-default-500">
1064
Step {currentStep} of {totalSteps}
1065
</span>
1066
</div>
1067
<Progress
1068
value={progress}
1069
color="primary"
1070
className="w-full"
1071
showValueLabel={false}
1072
/>
1073
</CardHeader>
1074
1075
<CardBody>
1076
{renderStep()}
1077
</CardBody>
1078
1079
<Divider />
1080
1081
<CardFooter className="flex justify-between">
1082
<Button
1083
variant="flat"
1084
onPress={prevStep}
1085
isDisabled={currentStep === 1}
1086
>
1087
Previous
1088
</Button>
1089
1090
{currentStep < totalSteps ? (
1091
<Button color="primary" onPress={nextStep}>
1092
Next Step
1093
</Button>
1094
) : (
1095
<Button color="success" onPress={handleSubmit}>
1096
Submit Form
1097
</Button>
1098
)}
1099
</CardFooter>
1100
</Card>
1101
);
1102
}
1103
```
1104
1105
## Form Integration Types
1106
1107
```typescript { .api }
1108
// Form validation types
1109
type ValidationError = string | string[];
1110
1111
interface ValidationErrors {
1112
[fieldName: string]: ValidationError;
1113
}
1114
1115
interface ValidationResult {
1116
isInvalid: boolean;
1117
validationErrors: string[];
1118
validationDetails: ValidationDetails;
1119
}
1120
1121
interface ValidationDetails {
1122
[key: string]: any;
1123
}
1124
1125
// Form state types
1126
interface FormState {
1127
values: Record<string, any>;
1128
errors: ValidationErrors;
1129
touched: Record<string, boolean>;
1130
isSubmitting: boolean;
1131
isValidating: boolean;
1132
isDirty: boolean;
1133
isValid: boolean;
1134
submitCount: number;
1135
}
1136
1137
// Form field types
1138
interface FieldProps<T = any> {
1139
name: string;
1140
value: T;
1141
onChange: (value: T) => void;
1142
onBlur?: () => void;
1143
error?: string;
1144
isInvalid?: boolean;
1145
isDisabled?: boolean;
1146
isRequired?: boolean;
1147
}
1148
1149
// Validation function types
1150
type FieldValidator<T = any> = (value: T) => boolean | string | Promise<boolean | string>;
1151
type FormValidator<T = Record<string, any>> = (values: T) => ValidationErrors | Promise<ValidationErrors>;
1152
1153
// Form hook return types
1154
interface UseFormReturn<T = Record<string, any>> {
1155
values: T;
1156
errors: ValidationErrors;
1157
touched: Record<keyof T, boolean>;
1158
isSubmitting: boolean;
1159
isValid: boolean;
1160
isDirty: boolean;
1161
1162
setFieldValue: (field: keyof T, value: any) => void;
1163
setFieldError: (field: keyof T, error: string | null) => void;
1164
setFieldTouched: (field: keyof T, touched?: boolean) => void;
1165
1166
validateField: (field: keyof T) => Promise<boolean>;
1167
validateForm: () => Promise<boolean>;
1168
1169
handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: React.FormEvent) => void;
1170
handleReset: () => void;
1171
1172
getFieldProps: (name: keyof T) => FieldProps;
1173
}
1174
1175
// Multi-step form types
1176
interface StepConfig {
1177
id: string;
1178
title: string;
1179
fields: string[];
1180
validation?: FormValidator;
1181
optional?: boolean;
1182
}
1183
1184
interface MultiStepFormState {
1185
currentStep: number;
1186
totalSteps: number;
1187
completedSteps: number[];
1188
canGoNext: boolean;
1189
canGoPrevious: boolean;
1190
isFirstStep: boolean;
1191
isLastStep: boolean;
1192
}
1193
```
1194
1195
## Integration Best Practices
1196
1197
### Form Accessibility
1198
1199
Ensuring forms are accessible and follow ARIA best practices.
1200
1201
```typescript
1202
// Accessibility patterns for forms
1203
const AccessibleFormExample = () => {
1204
return (
1205
<form role="form" aria-label="Contact form">
1206
<fieldset>
1207
<legend className="text-lg font-semibold mb-4">Personal Information</legend>
1208
1209
<Input
1210
label="Full Name"
1211
placeholder="Enter your full name"
1212
isRequired
1213
description="This will be used as your display name"
1214
// Automatically gets proper ARIA attributes
1215
/>
1216
1217
<Input
1218
type="email"
1219
label="Email Address"
1220
placeholder="Enter your email"
1221
isRequired
1222
validate={(value) => {
1223
if (!value) return "Email is required";
1224
if (!/\S+@\S+\.\S+/.test(value)) return "Invalid email format";
1225
return true;
1226
}}
1227
// Error messages are automatically announced by screen readers
1228
/>
1229
</fieldset>
1230
1231
<Button
1232
type="submit"
1233
aria-describedby="submit-help"
1234
>
1235
Submit Form
1236
</Button>
1237
1238
<p id="submit-help" className="text-sm text-default-500 mt-2">
1239
Your information will be processed securely
1240
</p>
1241
</form>
1242
);
1243
};
1244
```
1245
1246
### Performance Optimization
1247
1248
Optimizing form performance for large forms with many fields.
1249
1250
```typescript
1251
// Debounced validation for better performance
1252
import { useDeferredValue, useCallback } from "react";
1253
import { debounce } from "lodash";
1254
1255
const useOptimizedValidation = (validator: FieldValidator, delay = 300) => {
1256
const debouncedValidator = useCallback(
1257
debounce(validator, delay),
1258
[validator, delay]
1259
);
1260
1261
return debouncedValidator;
1262
};
1263
1264
// Memoized form fields to prevent unnecessary re-renders
1265
const MemoizedFormField = React.memo(({ field, value, onChange, error }: any) => {
1266
return (
1267
<Input
1268
label={field.label}
1269
value={value}
1270
onValueChange={onChange}
1271
isInvalid={!!error}
1272
errorMessage={error}
1273
{...field.props}
1274
/>
1275
);
1276
});
1277
```