Forms
Form handling, validation, and user input management in WI-CARPOOL
Form Architecture
WI-CARPOOL uses React Hook Form with Zod validation for robust form handling with excellent performance and developer experience.
📋 React Hook Form
Performant forms with minimal re-renders and easy validation.
🔍 Zod Validation
Type-safe schema validation with excellent TypeScript integration.
🎨 Shadcn Form Components
Pre-built form components with consistent styling and behavior.
♿ Accessibility
ARIA labels, error announcements, and keyboard navigation support.
Basic Form Setup
Form Component Structure
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
const formSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
})
type FormData = z.infer<typeof formSchema>
function LoginForm() {
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
password: "",
},
})
const onSubmit = (data: FormData) => {
console.log(data)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Enter your email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="Enter password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Sign In
</Button>
</form>
</Form>
)
}
Advanced Form Examples
Ride Search Form
Complex form with location autocomplete and date/time selection.
SearchForm Implementation
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { CalendarIcon } from "lucide-react"
import { format } from "date-fns"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { PlacesAutocomplete } from "@/components/PlacesAutocomplete"
const searchSchema = z.object({
from: z.string().min(1, "Starting location is required"),
to: z.string().min(1, "Destination is required"),
date: z.date({
required_error: "Please select a date",
}),
passengers: z.number().min(1).max(8),
})
type SearchData = z.infer<typeof searchSchema>
function RideSearchForm() {
const form = useForm<SearchData>({
resolver: zodResolver(searchSchema),
defaultValues: {
from: "",
to: "",
date: new Date(),
passengers: 1,
},
})
const onSubmit = (data: SearchData) => {
// Handle search logic
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="from"
render={({ field }) => (
<FormItem>
<FormLabel>From</FormLabel>
<FormControl>
<PlacesAutocomplete
placeholder="Enter starting location"
value={field.value}
onSelect={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="to"
render={({ field }) => (
<FormItem>
<FormLabel>To</FormLabel>
<FormControl>
<PlacesAutocomplete
placeholder="Enter destination"
value={field.value}
onSelect={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Date</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className="w-full pl-3 text-left font-normal"
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
disabled={(date) => date < new Date()}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Search Rides
</Button>
</form>
</Form>
)
}
Form Validation Patterns
Common Validation Schemas
User Registration Schema
import { z } from "zod"
const userRegistrationSchema = z.object({
firstName: z
.string()
.min(2, "First name must be at least 2 characters")
.max(50, "First name must be less than 50 characters"),
lastName: z
.string()
.min(2, "Last name must be at least 2 characters")
.max(50, "Last name must be less than 50 characters"),
email: z
.string()
.email("Please enter a valid email address"),
phone: z
.string()
.regex(/^\+[1-9]\d{1,14}$/, "Please enter a valid phone number"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/(?=.*[a-z])/, "Password must contain at least one lowercase letter")
.regex(/(?=.*[A-Z])/, "Password must contain at least one uppercase letter")
.regex(/(?=.*\d)/, "Password must contain at least one number"),
confirmPassword: z.string(),
dateOfBirth: z
.date()
.refine((date) => {
const age = new Date().getFullYear() - date.getFullYear()
return age >= 18
}, "You must be at least 18 years old"),
acceptTerms: z
.boolean()
.refine((val) => val === true, "You must accept the terms and conditions"),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
})
Vehicle Registration Schema
Vehicle Form Validation
const vehicleSchema = z.object({
make: z.string().min(1, "Vehicle make is required"),
model: z.string().min(1, "Vehicle model is required"),
year: z
.number()
.min(2000, "Vehicle must be from 2000 or later")
.max(new Date().getFullYear() + 1, "Invalid year"),
licensePlate: z
.string()
.min(1, "License plate is required")
.regex(/^[A-Z0-9-\s]+$/i, "Invalid license plate format"),
color: z.string().min(1, "Vehicle color is required"),
seats: z
.number()
.min(2, "Vehicle must have at least 2 seats")
.max(9, "Vehicle cannot have more than 9 seats"),
fuelType: z.enum(["gasoline", "diesel", "electric", "hybrid"], {
required_error: "Please select a fuel type",
}),
features: z.array(z.string()).optional(),
images: z
.array(z.instanceof(File))
.min(1, "At least one image is required")
.max(5, "Maximum 5 images allowed"),
})
Form Components
Custom Form Fields
Phone Number Input
import { useState } from "react"
import { CountryCodePicker } from "@/components/CountryCodePicker"
import { Input } from "@/components/ui/input"
interface PhoneInputProps {
value: string
onChange: (value: string) => void
placeholder?: string
}
function PhoneInput({ value, onChange, placeholder }: PhoneInputProps) {
const [countryCode, setCountryCode] = useState("+1")
const [phoneNumber, setPhoneNumber] = useState("")
const handlePhoneChange = (phone: string) => {
setPhoneNumber(phone)
onChange(`${countryCode}${phone}`)
}
return (
<div className="flex">
<CountryCodePicker
value={countryCode}
onChange={setCountryCode}
/>
<Input
type="tel"
placeholder={placeholder}
value={phoneNumber}
onChange={(e) => handlePhoneChange(e.target.value)}
className="flex-1"
/>
</div>
)
}
File Upload Field
Image Upload Component
import { useCallback } from "react"
import { useDropzone } from "react-dropzone"
import { Upload, X } from "lucide-react"
import { Button } from "@/components/ui/button"
interface FileUploadProps {
files: File[]
onFilesChange: (files: File[]) => void
maxFiles?: number
accept?: Record<string, string[]>
}
function FileUpload({ files, onFilesChange, maxFiles = 5, accept }: FileUploadProps) {
const onDrop = useCallback((acceptedFiles: File[]) => {
const newFiles = [...files, ...acceptedFiles].slice(0, maxFiles)
onFilesChange(newFiles)
}, [files, onFilesChange, maxFiles])
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept,
maxFiles: maxFiles - files.length,
})
const removeFile = (index: number) => {
const newFiles = files.filter((_, i) => i !== index)
onFilesChange(newFiles)
}
return (
<div className="space-y-4">
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
isDragActive
? "border-primary bg-primary/10"
: "border-gray-300 hover:border-primary"
}`}
>
<input {...getInputProps()} />
<Upload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
<p className="text-sm text-gray-600">
{isDragActive
? "Drop the files here..."
: "Drag 'n' drop files here, or click to select"}
</p>
</div>
{files.length > 0 && (
<div className="space-y-2">
{files.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-2 bg-gray-50 rounded"
>
<span className="text-sm truncate">{file.name}</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeFile(index)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</div>
)
}
Form State Management
Multi-Step Forms
Managing complex forms with multiple steps and state persistence.
Multi-Step Form Hook
import { useState } from "react"
function useMultiStepForm<T>(initialData: T) {
const [currentStep, setCurrentStep] = useState(0)
const [formData, setFormData] = useState(initialData)
const nextStep = () => setCurrentStep((prev) => prev + 1)
const prevStep = () => setCurrentStep((prev) => Math.max(0, prev - 1))
const goToStep = (step: number) => setCurrentStep(step)
const updateFormData = (data: Partial<T>) => {
setFormData((prev) => ({ ...prev, ...data }))
}
const resetForm = () => {
setCurrentStep(0)
setFormData(initialData)
}
return {
currentStep,
formData,
nextStep,
prevStep,
goToStep,
updateFormData,
resetForm,
}
}
Form Persistence
Saving form data to localStorage for recovery.
Form Persistence Hook
import { useEffect } from "react"
import { UseFormReturn } from "react-hook-form"
function useFormPersistence<T>(
form: UseFormReturn<T>,
key: string,
exclude: (keyof T)[] = []
) {
// Save form data to localStorage
useEffect(() => {
const subscription = form.watch((data) => {
const dataToSave = { ...data }
exclude.forEach((field) => {
delete dataToSave[field]
})
localStorage.setItem(key, JSON.stringify(dataToSave))
})
return () => subscription.unsubscribe()
}, [form, key, exclude])
// Load form data from localStorage
useEffect(() => {
const savedData = localStorage.getItem(key)
if (savedData) {
try {
const parsedData = JSON.parse(savedData)
Object.keys(parsedData).forEach((field) => {
if (!exclude.includes(field as keyof T)) {
form.setValue(field as keyof T, parsedData[field])
}
})
} catch (error) {
console.error("Failed to load saved form data:", error)
}
}
}, [form, key, exclude])
const clearSavedData = () => {
localStorage.removeItem(key)
}
return { clearSavedData }
}
Best Practices
Performance Optimization
- Use React Hook Form's uncontrolled components when possible
- Debounce expensive validation operations
- Implement field-level validation for better UX
- Use form context for deeply nested form components
User Experience
- Provide clear, actionable error messages
- Show validation status in real-time
- Use proper loading states during submission
- Implement autosave for long forms
Accessibility
- Associate labels with form controls
- Use ARIA attributes for complex form interactions
- Provide clear focus indicators
- Announce validation errors to screen readers