
Building Forms
- 66 installs
- 426 repo stars
- Updated December 11, 2025
- ancoleman/ai-design-components
Building-forms is a Claude Code skill that builds form components and data-collection interfaces with 50+ input types, validation strategies, and WCAG 2.1 accessibility patterns.
About
Building-forms is a Claude Code skill for building form components and data-collection interfaces such as contact forms, registration flows, checkout processes, and surveys. A developer uses it when collecting user input, implementing validation, or ensuring form accessibility. It provides component-selection decision trees, validation-timing guidance, WCAG 2.1 AA accessibility patterns, and multi-step wizard structures.
- 50+ input types with data-type-to-component decision trees
- Validation timing strategies (on-blur, on-change, debounced, progressive)
- WCAG 2.1 AA accessibility patterns and multi-step wizards
Building Forms by the numbers
- 66 all-time installs (skills.sh)
- Ranked #1,170 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
building-forms capabilities & compatibility
- Capabilities
- building forms · building tables · creating dashboards · building ai chat
- Use cases
- frontend · ui design · web design
What building-forms says it does
Includes 50+ input types, validation strategies, accessibility patterns (WCAG 2.1), multi-step wizards, and UX best practices.
The Golden Rule:** Data Type → Input Component → Validation Pattern
npx skills add https://github.com/ancoleman/ai-design-components --skill building-formsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 66 |
|---|---|
| repo stars | ★ 426 |
| Last updated | December 11, 2025 |
| Repository | ancoleman/ai-design-components ↗ |
What it does
Build accessible forms with the right input types, validation timing, and multi-step wizard flows.
Who is it for?
Building accessible forms, checkout flows, and surveys with proper validation.
Skip if: Backend form-submission processing or database schema.
When should I use this skill?
Creating forms, collecting user input, or implementing validation and multi-step workflows.
What you get
Accessible, user-friendly forms with appropriate inputs, validation timing, and WCAG 2.1 AA compliance.
- Input component selection
- Validation strategy
- Accessible form markup (WCAG 2.1 AA)
By the numbers
- 50+ input types
- 5 validation modes (On Submit, On Blur, On Change, Debounced, Progressive)
Files
Form Systems & Input Patterns
Build accessible, user-friendly forms with systematic component selection, validation strategies, and UX best practices.
Purpose
Forms are the primary mechanism for user data input in web applications. This skill provides systematic guidance for:
- Selecting appropriate input types based on data requirements
- Implementing validation strategies that enhance user experience
- Ensuring WCAG 2.1 AA accessibility compliance
- Creating complex patterns (multi-step wizards, conditional fields, dynamic forms)
When to Use This Skill
Triggers:
- Building contact forms, login/registration flows, checkout processes
- Implementing surveys, questionnaires, or settings pages
- Adding validation to user inputs
- Creating multi-step workflows or wizards
- Ensuring form accessibility
- Collecting structured data (addresses, credit cards, dates)
Common Requests:
- "Create a registration form with validation"
- "Build a multi-step checkout flow"
- "Add inline validation to email input"
- "Make this form accessible for screen readers"
- "Implement a survey with conditional questions"
Universal Form Concepts
Component Selection Framework
The Golden Rule: Data Type → Input Component → Validation Pattern
Start by identifying the data type to collect, then select the appropriate component:
Quick Reference:
- Short text (<100 chars) → Text input, Email input, Password input
- Long text (>100 chars) → Textarea, Rich text editor, Code editor
- Numeric → Number input, Currency input, Slider
- Date/Time → Date picker, Time picker, Date range picker
- Boolean → Checkbox, Toggle switch
- Single choice → Radio group (2-7 options), Select dropdown (>7 options), Autocomplete (>15 options)
- Multiple choice → Checkbox group, Multi-select, Tag input
- File/Media → File upload, Image upload
- Structured → Address input, Credit card input, Phone number input
For detailed decision tree: See references/decision-tree.md
Validation Timing Strategies
Recommended Default: On Blur with Progressive Enhancement
Field pristine (never touched): No validation
User typing: No errors shown
On blur (field loses focus): Validate and show errors
After first error: Switch to onChange for that field
On fix: Show success immediatelyValidation Modes: 1. On Submit - Validate when form submitted (simple forms) 2. On Blur - Validate when field loses focus (RECOMMENDED for most forms) 3. On Change - Validate as user types (password strength, availability checks) 4. Debounced - Validate after user stops typing (API-based validation) 5. Progressive - Start with on-blur, switch to on-change after first error
For complete validation guide: See references/validation-concepts.md
Accessibility Requirements (WCAG 2.1 AA)
Critical Accessibility Patterns:
Labels and Instructions:
- Every input must have an associated
<label>oraria-label - Labels must be visible and descriptive
- Required fields clearly indicated (not by color alone)
- Never use placeholder text as label replacement
- Provide help text for complex inputs
Keyboard Navigation:
- Logical, sequential tab order
- All inputs keyboard accessible
- Custom components support arrow keys
- Escape key dismisses modals/popovers
- Focus visible (outline or custom indicator)
Error Handling:
- Errors programmatically associated with inputs (
aria-describedby) - Error messages clear and actionable
- Errors announced by screen readers (
aria-live) - Focus moves to first error on submit
- Errors not conveyed by color alone
ARIA Attributes:
aria-required="true"for required fieldsaria-invalid="true"when validation failsaria-describedbylinking to help/error textrole="group"for related inputsaria-live="polite"for validation messages
For complete accessibility checklist: See references/accessibility-forms.md
UX Best Practices
Modern Form UX Principles (2024-2025):
1. Progressive Disclosure - Show only essential fields initially, reveal advanced options on demand 2. Smart Defaults - Pre-fill known information, suggest values based on context 3. Inline Validation with Positive Feedback - Show green checkmark on valid input, provide helpful error messages 4. Mobile-First - Large touch targets (44px minimum), appropriate keyboard types 5. Reduce Cognitive Load - Group related fields, use clear labels, provide examples 6. Error Prevention - Constraints prevent invalid input, autocomplete reduces typos 7. Autosave and Recovery - Save draft state automatically, warn before losing data
For detailed UX patterns: See references/ux-patterns.md
Error Message Best Practices
Good Error Message Formula: 1. What's wrong - "Email address is not valid" 2. Why it matters - "We need this to send your receipt" 3. How to fix - "Format: name@example.com"
Examples:
❌ Bad: "Invalid input" ✅ Good: "Email address must include @ symbol (e.g., name@example.com)"
❌ Bad: "Error" ✅ Good: "Password must be at least 8 characters long"
❌ Bad: "Field required" ✅ Good: "Please enter your email address so we can send order confirmation"
Tone Guidelines:
- Conversational, not robotic
- Helpful, not blaming
- Specific, not generic
- Actionable, not just descriptive
Language-Specific Implementations
This skill provides universal form concepts above, with language-specific implementations below.
JavaScript/React (PRIMARY)
Recommended Stack:
- React Hook Form - Form state management (best performance, 8KB bundle)
- Zod - TypeScript-first schema validation
- Radix UI or React Aria - Accessible component primitives
Quick Start:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
// Define validation schema
const schema = 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 schema>;
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
mode: 'onBlur', // Validate on blur (recommended)
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Email</label>
<input id="email" {...register('email')} type="email" />
{errors.email && <span role="alert">{errors.email.message}</span>}
<label htmlFor="password">Password</label>
<input id="password" {...register('password')} type="password" />
{errors.password && <span role="alert">{errors.password.message}</span>}
<button type="submit">Login</button>
</form>
);
}Detailed JavaScript/React Documentation:
references/javascript/react-hook-form.md- Complete React Hook Form guidereferences/javascript/zod-validation.md- Zod schema validation patternsreferences/javascript/examples/- Working code examples
Python (PRIMARY)
Recommended Stack:
- Pydantic - Data validation and settings management (runtime validation, type-safe)
- FastAPI - Modern async web framework with automatic validation
- WTForms - Flask/Django form handling (when using traditional frameworks)
Quick Start (FastAPI + Pydantic):
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field, validator
app = FastAPI()
# Define validation schema
class LoginForm(BaseModel):
email: EmailStr # Validates email format
password: str = Field(..., min_length=8, description="Password must be at least 8 characters")
@validator('password')
def validate_password_strength(cls, v):
if not any(char.isdigit() for char in v):
raise ValueError('Password must contain at least one number')
if not any(char.isupper() for char in v):
raise ValueError('Password must contain at least one uppercase letter')
return v
@app.post("/api/login")
async def login(form_data: LoginForm):
# Pydantic automatically validates incoming data
# If validation fails, returns 422 with error details
return {"message": "Login successful", "email": form_data.email}
# Example error response (automatic):
# {
# "detail": [
# {
# "loc": ["body", "email"],
# "msg": "value is not a valid email address",
# "type": "value_error.email"
# }
# ]
# }Detailed Python Documentation:
references/python/pydantic-forms.md- Pydantic validation patternsreferences/python/wtforms.md- WTForms for Flask/Djangoreferences/python/examples/- Working code examples
Rust (FUTURE)
Planned Libraries:
- validator - Struct field validation
- Leptos / Yew - Reactive web frameworks
Rust implementation will be added when needed.
Go (FUTURE)
Planned Libraries:
- Templ - Type-safe HTML templating
- html/template - Standard library templating
Go implementation will be added when needed.
Component Tiers
Tier 1: Basic Input Components
Text-Based:
- Text field (single-line)
- Textarea (multi-line)
- Email input (with validation)
- Password input (with visibility toggle)
- Number input (with step controls)
- Tel input (with formatting)
- URL input (with protocol validation)
- Search input (with clear button)
Selection:
- Radio group (2-7 options)
- Checkbox (boolean or multiple)
- Toggle switch (clear on/off states)
- Select dropdown (many options)
- Multi-select (multiple selections)
Date & Time:
- Date picker (calendar interface)
- Time picker (hour/minute)
- Date range picker (start and end)
- DateTime picker (combined)
Tier 2: Rich Input Components
Advanced Selection:
- Autocomplete/Combobox (type to filter)
- Tag input (multiple tags)
- Transfer list (move items between lists)
- Listbox (keyboard-navigable)
Specialized:
- Color picker (hex, RGB, HSL)
- File uploader (single, multiple, drag-drop)
- Image uploader (crop, resize, preview)
- Slider/Range (single or range)
- Rating input (stars, numeric, emoji)
- Rich text editor (formatting, media)
- Code editor (syntax highlighting)
- Markdown editor (preview, toolbar)
Structured Data:
- Address input (multi-field)
- Credit card input (formatted)
- Phone number (international)
- Currency input (symbol, decimal)
Tier 3: Complex Form Patterns
Multi-Step Forms:
- Linear wizard (step 1 → 2 → 3)
- Branching wizard (conditional steps)
- Progress indicators
- Save and resume (draft state)
- Review and submit page
Dynamic Forms:
- Conditional fields (show/hide)
- Repeating sections (add/remove)
- Field arrays (dynamic list)
- Nested forms (complex objects)
Advanced Patterns:
- Inline editing (click to edit)
- Bulk editing (multiple records)
- Autosave (periodic or on change)
- Optimistic updates
- Undo/redo functionality
Integration with Design Tokens
All form components use the design-tokens skill for visual styling, enabling theme switching (light/dark/high-contrast/custom brands).
Key Token Categories:
- Color - Input backgrounds, borders, text, error/success states
- Spacing - Padding, gaps between fields, label margins
- Typography - Font sizes, weights for inputs, labels, errors
- Borders - Border width, radius, focus ring
- Shadows - Focus indicators, elevation
See: skills/design-tokens/ for complete theming documentation.
Common Use Cases
Contact Form
// Basic contact form with validation
// See: references/javascript/examples/basic-form.tsxRegistration Flow
// Multi-step registration with password strength
// See: references/javascript/examples/multi-step-wizard.tsxInline Validation
// Real-time validation with debouncing
// See: references/javascript/examples/inline-validation.tsxSurvey with Conditional Logic
// Dynamic form with conditional fields
// See: references/javascript/examples/conditional-form.tsxSettings Page
// Mixed input types with autosave
// See: references/javascript/examples/settings-form.tsxQuick Decision Guide
Question: What input should I use? → See references/decision-tree.md for complete decision tree
Question: When should I validate? → Use on-blur with progressive enhancement (on-change after first error) → See references/validation-concepts.md for all strategies
Question: How do I make my form accessible? → Use semantic HTML, label all inputs, support keyboard navigation → See references/accessibility-forms.md for WCAG 2.1 checklist
Question: How do I handle complex validation? → Use schema validation (Zod for TypeScript, Yup for JavaScript) → See references/javascript/zod-validation.md for patterns
Question: How do I build a multi-step form? → Use state management with progress tracking → See references/javascript/examples/multi-step-wizard.tsx
Best Practices Summary
1. Start with semantic HTML - Use native <input>, <select>, <textarea> when possible 2. Label everything - Every input needs a visible, descriptive label 3. Validate on blur - Best UX balance for most forms 4. Provide helpful errors - Explain what's wrong and how to fix it 5. Support keyboard navigation - Tab order, arrow keys, escape to dismiss 6. Mobile-first - Large touch targets, appropriate keyboards 7. Progressive disclosure - Don't overwhelm with all fields at once 8. Autosave when possible - Prevent data loss 9. Test with screen readers - Ensure ARIA attributes work correctly 10. Use design tokens - Consistent styling, theme support
Additional Resources
references/decision-tree.md- Complete component selection frameworkreferences/validation-concepts.md- All validation strategies and patternsreferences/accessibility-forms.md- WCAG 2.1 AA compliance checklistreferences/ux-patterns.md- Modern form UX best practicesreferences/javascript/- JavaScript/React implementation guides
{
"required": "{field} is required",
"email": "Please enter a valid email address",
"minLength": "{field} must be at least {min} characters",
"maxLength": "{field} must be less than {max} characters",
"min": "{field} must be at least {min}",
"max": "{field} must be at most {max}",
"pattern": "{field} format is invalid",
"url": "Please enter a valid URL (e.g., https://example.com)",
"date": "Please enter a valid date",
"integer": "{field} must be a whole number",
"positive": "{field} must be a positive number",
"future": "{field} must be a future date",
"past": "{field} must be a past date",
"password": {
"minLength": "Password must be at least 8 characters long",
"uppercase": "Password must contain at least one uppercase letter (A-Z)",
"lowercase": "Password must contain at least one lowercase letter (a-z)",
"number": "Password must contain at least one number (0-9)",
"special": "Password must contain at least one special character (!@#$%^&*)",
"common": "This password is too common. Please choose a stronger password.",
"match": "Passwords do not match"
},
"username": {
"taken": "This username is already taken. Please choose another.",
"invalid": "Username can only contain letters, numbers, underscores, and hyphens",
"minLength": "Username must be at least 3 characters long",
"maxLength": "Username must be less than 20 characters long",
"reserved": "This username is reserved. Please choose another."
},
"phone": {
"invalid": "Please enter a valid phone number",
"invalidFormat": "Phone number format is invalid for selected country",
"tooShort": "Phone number is too short",
"tooLong": "Phone number is too long"
},
"creditCard": {
"invalid": "Please enter a valid credit card number",
"luhn": "Credit card number failed validation check",
"expired": "This card has expired",
"unsupportedType": "This card type is not accepted"
},
"file": {
"tooLarge": "File size must be less than {maxSize}",
"invalidType": "File type not allowed. Accepted: {allowedTypes}",
"tooMany": "Maximum {max} files allowed"
},
"age": {
"tooYoung": "You must be at least {min} years old",
"invalidDate": "Please enter a valid date of birth"
},
"common": {
"serverError": "An error occurred. Please try again.",
"networkError": "Network error. Please check your connection.",
"validating": "Validating...",
"success": "Looks good!",
"optional": "(Optional)"
}
}
{
"required": "{field} es obligatorio",
"email": "Por favor ingrese una dirección de correo válida",
"minLength": "{field} debe tener al menos {min} caracteres",
"maxLength": "{field} debe tener menos de {max} caracteres",
"min": "{field} debe ser al menos {min}",
"max": "{field} debe ser como máximo {max}",
"pattern": "El formato de {field} no es válido",
"url": "Por favor ingrese una URL válida (ej: https://ejemplo.com)",
"date": "Por favor ingrese una fecha válida",
"integer": "{field} debe ser un número entero",
"positive": "{field} debe ser un número positivo",
"future": "{field} debe ser una fecha futura",
"past": "{field} debe ser una fecha pasada",
"password": {
"minLength": "La contraseña debe tener al menos 8 caracteres",
"uppercase": "La contraseña debe contener al menos una letra mayúscula (A-Z)",
"lowercase": "La contraseña debe contener al menos una letra minúscula (a-z)",
"number": "La contraseña debe contener al menos un número (0-9)",
"special": "La contraseña debe contener al menos un carácter especial (!@#$%^&*)",
"common": "Esta contraseña es demasiado común. Por favor elija una más segura.",
"match": "Las contraseñas no coinciden"
},
"username": {
"taken": "Este nombre de usuario ya está en uso. Por favor elija otro.",
"invalid": "El nombre de usuario solo puede contener letras, números, guiones bajos y guiones",
"minLength": "El nombre de usuario debe tener al menos 3 caracteres",
"maxLength": "El nombre de usuario debe tener menos de 20 caracteres",
"reserved": "Este nombre de usuario está reservado. Por favor elija otro."
},
"phone": {
"invalid": "Por favor ingrese un número de teléfono válido",
"invalidFormat": "El formato del número de teléfono no es válido para el país seleccionado",
"tooShort": "El número de teléfono es demasiado corto",
"tooLong": "El número de teléfono es demasiado largo"
},
"creditCard": {
"invalid": "Por favor ingrese un número de tarjeta de crédito válido",
"luhn": "El número de tarjeta de crédito no pasó la validación",
"expired": "Esta tarjeta ha expirado",
"unsupportedType": "Este tipo de tarjeta no es aceptado"
},
"file": {
"tooLarge": "El tamaño del archivo debe ser menor a {maxSize}",
"invalidType": "Tipo de archivo no permitido. Aceptados: {allowedTypes}",
"tooMany": "Máximo {max} archivos permitidos"
},
"age": {
"tooYoung": "Debes tener al menos {min} años",
"invalidDate": "Por favor ingrese una fecha de nacimiento válida"
},
"common": {
"serverError": "Ocurrió un error. Por favor intente de nuevo.",
"networkError": "Error de conexión. Por favor verifique su conexión.",
"validating": "Validando...",
"success": "¡Se ve bien!",
"optional": "(Opcional)"
}
}
{
"required": "{field} est requis",
"email": "Veuillez saisir une adresse e-mail valide",
"minLength": "{field} doit contenir au moins {min} caractères",
"maxLength": "{field} doit contenir moins de {max} caractères",
"min": "{field} doit être au moins {min}",
"max": "{field} doit être au plus {max}",
"pattern": "Le format de {field} n'est pas valide",
"url": "Veuillez saisir une URL valide (ex: https://exemple.com)",
"date": "Veuillez saisir une date valide",
"integer": "{field} doit être un nombre entier",
"positive": "{field} doit être un nombre positif",
"future": "{field} doit être une date future",
"past": "{field} doit être une date passée",
"password": {
"minLength": "Le mot de passe doit contenir au moins 8 caractères",
"uppercase": "Le mot de passe doit contenir au moins une lettre majuscule (A-Z)",
"lowercase": "Le mot de passe doit contenir au moins une lettre minuscule (a-z)",
"number": "Le mot de passe doit contenir au moins un chiffre (0-9)",
"special": "Le mot de passe doit contenir au moins un caractère spécial (!@#$%^&*)",
"common": "Ce mot de passe est trop commun. Veuillez en choisir un plus fort.",
"match": "Les mots de passe ne correspondent pas"
},
"username": {
"taken": "Ce nom d'utilisateur est déjà pris. Veuillez en choisir un autre.",
"invalid": "Le nom d'utilisateur ne peut contenir que des lettres, chiffres, traits de soulignement et tirets",
"minLength": "Le nom d'utilisateur doit contenir au moins 3 caractères",
"maxLength": "Le nom d'utilisateur doit contenir moins de 20 caractères",
"reserved": "Ce nom d'utilisateur est réservé. Veuillez en choisir un autre."
},
"phone": {
"invalid": "Veuillez saisir un numéro de téléphone valide",
"invalidFormat": "Le format du numéro n'est pas valide pour le pays sélectionné",
"tooShort": "Le numéro de téléphone est trop court",
"tooLong": "Le numéro de téléphone est trop long"
},
"creditCard": {
"invalid": "Veuillez saisir un numéro de carte de crédit valide",
"luhn": "Le numéro de carte de crédit n'a pas passé la validation",
"expired": "Cette carte a expiré",
"unsupportedType": "Ce type de carte n'est pas accepté"
},
"file": {
"tooLarge": "La taille du fichier doit être inférieure à {maxSize}",
"invalidType": "Type de fichier non autorisé. Acceptés: {allowedTypes}",
"tooMany": "Maximum {max} fichiers autorisés"
},
"age": {
"tooYoung": "Vous devez avoir au moins {min} ans",
"invalidDate": "Veuillez saisir une date de naissance valide"
},
"common": {
"serverError": "Une erreur s'est produite. Veuillez réessayer.",
"networkError": "Erreur réseau. Veuillez vérifier votre connexion.",
"validating": "Validation en cours...",
"success": "Parfait!",
"optional": "(Optionnel)"
}
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Contact Form",
"description": "Basic contact form for customer inquiries",
"properties": {
"name": {
"type": "string",
"title": "Full Name",
"minLength": 2,
"maxLength": 100,
"description": "Customer's full name"
},
"email": {
"type": "string",
"format": "email",
"title": "Email Address",
"description": "Valid email address for response"
},
"subject": {
"type": "string",
"title": "Subject",
"minLength": 5,
"maxLength": 200,
"description": "Brief summary of inquiry"
},
"message": {
"type": "string",
"title": "Message",
"minLength": 20,
"maxLength": 2000,
"description": "Detailed message content"
},
"category": {
"type": "string",
"title": "Inquiry Category",
"enum": ["sales", "support", "billing", "other"],
"description": "Type of inquiry"
},
"newsletter": {
"type": "boolean",
"title": "Subscribe to Newsletter",
"default": false,
"description": "Opt-in to marketing communications"
}
},
"required": ["name", "email", "subject", "message", "category"],
"additionalProperties": false
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "User Registration Form",
"description": "New user account registration",
"properties": {
"username": {
"type": "string",
"title": "Username",
"minLength": 3,
"maxLength": 20,
"pattern": "^[a-zA-Z0-9_-]+$",
"description": "Unique username (letters, numbers, underscore, hyphen only)"
},
"email": {
"type": "string",
"format": "email",
"title": "Email Address",
"description": "Primary email for account"
},
"password": {
"type": "string",
"title": "Password",
"minLength": 8,
"maxLength": 100,
"description": "Secure password (min 8 chars, must include uppercase, lowercase, number)"
},
"confirmPassword": {
"type": "string",
"title": "Confirm Password",
"description": "Must match password field"
},
"firstName": {
"type": "string",
"title": "First Name",
"minLength": 1,
"maxLength": 50
},
"lastName": {
"type": "string",
"title": "Last Name",
"minLength": 1,
"maxLength": 50
},
"dateOfBirth": {
"type": "string",
"format": "date",
"title": "Date of Birth",
"description": "Must be 13 years or older"
},
"country": {
"type": "string",
"title": "Country",
"description": "Country of residence"
},
"terms": {
"type": "boolean",
"title": "Accept Terms and Conditions",
"const": true,
"description": "Must accept to register"
},
"marketing": {
"type": "boolean",
"title": "Receive Marketing Emails",
"default": false
}
},
"required": ["username", "email", "password", "confirmPassword", "firstName", "lastName", "terms"],
"additionalProperties": false
}
{
"password": {
"minLength": 8,
"patterns": {
"uppercase": "[A-Z]",
"lowercase": "[a-z]",
"number": "[0-9]",
"special": "[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]"
},
"commonPasswords": [
"password", "123456", "123456789", "12345678", "12345",
"1234567", "password1", "qwerty", "abc123", "111111"
],
"strength": {
"weak": "Less than 8 chars or missing requirements",
"medium": "8+ chars with 2-3 requirements",
"strong": "12+ chars with all requirements + special chars"
}
},
"username": {
"minLength": 3,
"maxLength": 20,
"pattern": "^[a-zA-Z0-9_-]+$",
"reservedWords": [
"admin", "administrator", "root", "system", "api", "test",
"user", "guest", "null", "undefined", "moderator"
]
},
"name": {
"minLength": 2,
"maxLength": 50,
"pattern": "^[a-zA-ZÀ-ÿ\\s'-]+$",
"description": "Allows letters, spaces, hyphens, apostrophes, and accented characters"
},
"age": {
"minimum": 13,
"maximum": 120,
"description": "Typical age constraints (13+ for COPPA compliance)"
},
"zipCode": {
"US": {
"pattern": "^\\d{5}(?:-\\d{4})?$",
"examples": ["12345", "12345-6789"]
},
"UK": {
"pattern": "^[A-Z]{1,2}\\d[A-Z\\d]? ?\\d[A-Z]{2}$",
"examples": ["SW1A 1AA", "W1A 0AX"]
},
"CA": {
"pattern": "^[A-Z]\\d[A-Z] ?\\d[A-Z]\\d$",
"examples": ["K1A 0B1", "M5H 2N2"]
}
},
"url": {
"pattern": "^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)$",
"requireHttps": true,
"allowedProtocols": ["http", "https"]
}
}
{
"patterns": {
"basic": {
"regex": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
"description": "Basic email validation (most permissive)",
"examples": {
"valid": ["user@example.com", "test.user+tag@domain.co.uk"],
"invalid": ["invalid", "@example.com", "user@", "user@domain"]
}
},
"strict": {
"regex": "^[a-zA-Z0-9][a-zA-Z0-9._%+-]*@[a-zA-Z0-9][a-zA-Z0-9.-]*\\.[a-zA-Z]{2,}$",
"description": "Strict email validation (no leading special chars)",
"examples": {
"valid": ["user@example.com", "test.user@domain.com"],
"invalid": [".user@example.com", "-user@example.com"]
}
},
"rfc5322": {
"regex": "^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])$",
"description": "RFC 5322 compliant (most comprehensive)",
"note": "Very complex, use for strict compliance only"
}
},
"commonDomains": {
"free": ["gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "icloud.com"],
"business": ["company.com", "corporation.com", "enterprise.com"]
},
"typos": {
"gmail.com": ["gmai.com", "gmial.com", "gmai.co"],
"yahoo.com": ["yaho.com", "yahooo.com"],
"hotmail.com": ["hotmai.com", "hotmial.com"]
}
}
{
"formats": {
"E164": {
"regex": "^\\+[1-9]\\d{1,14}$",
"description": "International standard format",
"examples": ["+14155552671", "+442071838750", "+81312345678"]
},
"US": {
"regex": "^(?:\\+1)?\\s?\\(?([0-9]{3})\\)?[-\\s]?([0-9]{3})[-\\s]?([0-9]{4})$",
"description": "US phone number (various formats)",
"examples": ["(415) 555-2671", "415-555-2671", "4155552671", "+1 415 555 2671"]
},
"UK": {
"regex": "^(?:(?:\\(?(?:0(?:0|11)\\)?[\\s-]?\\(?|\\+)44\\)?[\\s-]?(?:\\(?0\\)?[\\s-]?)?)|(?:\\(?0))(?:(?:\\d{5}\\)?[\\s-]?\\d{4,5})|(?:\\d{4}\\)?[\\s-]?(?:\\d{5}|\\d{3}[\\s-]?\\d{3}))|(?:\\d{3}\\)?[\\s-]?\\d{3}[\\s-]?\\d{3,4})|(?:\\d{2}\\)?[\\s-]?\\d{4}[\\s-]?\\d{4}))(?:[\\s-]?(?:x|ext\\.?|\\#)\\d{3,4})?$",
"description": "UK phone number",
"examples": ["020 7183 8750", "+44 20 7183 8750", "07700 900123"]
}
},
"countryPatterns": {
"US": {
"code": "+1",
"length": 10,
"format": "(XXX) XXX-XXXX"
},
"UK": {
"code": "+44",
"length": 10,
"format": "XXXX XXX XXXX"
},
"DE": {
"code": "+49",
"length": 11,
"format": "XXX XXXXXXXX"
},
"FR": {
"code": "+33",
"length": 9,
"format": "X XX XX XX XX"
},
"JP": {
"code": "+81",
"length": 10,
"format": "XX-XXXX-XXXX"
}
}
}
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Zod schema for validation
const formSchema = z.object({
fullName: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
type FormData = z.infer<typeof formSchema>;
export function BasicForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm<FormData>({
resolver: zodResolver(formSchema),
});
const onSubmit = async (data: FormData) => {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Form submitted:', data);
alert('Form submitted successfully!');
reset();
};
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6 text-gray-800">Create Account</h2>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* Full Name Field */}
<div>
<label
htmlFor="fullName"
className="block text-sm font-medium text-gray-700 mb-1"
>
Full Name
</label>
<input
id="fullName"
type="text"
{...register('fullName')}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.fullName ? 'border-red-500' : 'border-gray-300'
}`}
aria-invalid={errors.fullName ? 'true' : 'false'}
aria-describedby={errors.fullName ? 'fullName-error' : undefined}
/>
{errors.fullName && (
<p id="fullName-error" className="mt-1 text-sm text-red-600" role="alert">
{errors.fullName.message}
</p>
)}
</div>
{/* Email Field */}
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1"
>
Email Address
</label>
<input
id="email"
type="email"
{...register('email')}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.email ? 'border-red-500' : 'border-gray-300'
}`}
aria-invalid={errors.email ? 'true' : 'false'}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" className="mt-1 text-sm text-red-600" role="alert">
{errors.email.message}
</p>
)}
</div>
{/* Password Field */}
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 mb-1"
>
Password
</label>
<input
id="password"
type="password"
{...register('password')}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.password ? 'border-red-500' : 'border-gray-300'
}`}
aria-invalid={errors.password ? 'true' : 'false'}
aria-describedby={errors.password ? 'password-error' : undefined}
/>
{errors.password && (
<p id="password-error" className="mt-1 text-sm text-red-600" role="alert">
{errors.password.message}
</p>
)}
</div>
{/* Confirm Password Field */}
<div>
<label
htmlFor="confirmPassword"
className="block text-sm font-medium text-gray-700 mb-1"
>
Confirm Password
</label>
<input
id="confirmPassword"
type="password"
{...register('confirmPassword')}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.confirmPassword ? 'border-red-500' : 'border-gray-300'
}`}
aria-invalid={errors.confirmPassword ? 'true' : 'false'}
aria-describedby={errors.confirmPassword ? 'confirmPassword-error' : undefined}
/>
{errors.confirmPassword && (
<p id="confirmPassword-error" className="mt-1 text-sm text-red-600" role="alert">
{errors.confirmPassword.message}
</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isSubmitting ? 'Creating Account...' : 'Create Account'}
</button>
</form>
</div>
);
}
import React from 'react';
import { useForm, useWatch, Controller, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Dynamic schema that adapts based on user type and business type
const createFormSchema = (userType?: string, businessType?: string) => {
const baseSchema = z.object({
userType: z.enum(['individual', 'business'], {
required_error: 'Please select a user type',
}),
});
// Individual-specific fields
const individualSchema = z.object({
firstName: z.string().min(2, 'First name required'),
lastName: z.string().min(2, 'Last name required'),
dateOfBirth: z.string().min(1, 'Date of birth required'),
});
// Business-specific fields
const businessSchema = z.object({
businessType: z.enum(['llc', 'corporation', 'nonprofit'], {
required_error: 'Please select a business type',
}),
businessName: z.string().min(2, 'Business name required'),
taxId: z.string().regex(/^\d{2}-\d{7}$/, 'Tax ID must be in format XX-XXXXXXX'),
});
// LLC-specific fields
const llcSchema = z.object({
owners: z.array(
z.object({
name: z.string().min(1, 'Owner name required'),
percentage: z.number().min(1).max(100),
})
).min(1, 'At least one owner required'),
});
// Corporation-specific fields
const corporationSchema = z.object({
stockSymbol: z.string().optional(),
boardMembers: z.array(
z.object({
name: z.string().min(1, 'Board member name required'),
title: z.string().min(1, 'Title required'),
})
).min(1, 'At least one board member required'),
});
// Nonprofit-specific fields
const nonprofitSchema = z.object({
missionStatement: z.string().min(10, 'Mission statement required (min 10 characters)'),
ein: z.string().regex(/^\d{2}-\d{7}$/, 'EIN must be in format XX-XXXXXXX'),
});
// Common fields for both types
const commonSchema = z.object({
email: z.string().email('Invalid email'),
phone: z.string().regex(/^\+?[\d\s-()]+$/, 'Invalid phone number'),
country: z.string().min(1, 'Country required'),
state: z.string().optional(),
});
// Build schema based on selections
if (userType === 'individual') {
return baseSchema.merge(individualSchema).merge(commonSchema);
}
if (userType === 'business') {
let businessFullSchema = baseSchema.merge(businessSchema).merge(commonSchema);
if (businessType === 'llc') {
businessFullSchema = businessFullSchema.merge(llcSchema) as any;
} else if (businessType === 'corporation') {
businessFullSchema = businessFullSchema.merge(corporationSchema) as any;
} else if (businessType === 'nonprofit') {
businessFullSchema = businessFullSchema.merge(nonprofitSchema) as any;
}
return businessFullSchema;
}
return baseSchema.merge(commonSchema);
};
// Type for the most complete form
type FormData = z.infer<ReturnType<typeof createFormSchema>>;
export function ConditionalForm() {
const {
register,
handleSubmit,
control,
formState: { errors },
setValue,
} = useForm<FormData>({
resolver: zodResolver(createFormSchema()),
mode: 'onChange',
});
// Watch fields to show/hide conditional sections
const userType = useWatch({ control, name: 'userType' });
const businessType = useWatch({ control, name: 'businessType' as any });
const country = useWatch({ control, name: 'country' });
// Field arrays for dynamic lists
const {
fields: ownerFields,
append: appendOwner,
remove: removeOwner,
} = useFieldArray({
control,
name: 'owners' as any,
});
const {
fields: boardFields,
append: appendBoard,
remove: removeBoard,
} = useFieldArray({
control,
name: 'boardMembers' as any,
});
const onSubmit = async (data: FormData) => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Form submitted:', data);
alert('Registration submitted successfully!');
};
return (
<div className="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6 text-gray-800">Account Registration</h2>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* User Type Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
I am registering as <span className="text-red-500">*</span>
</label>
<div className="space-y-2">
<label className="flex items-start p-3 border rounded-md hover:bg-gray-50 cursor-pointer">
<input
type="radio"
value="individual"
{...register('userType')}
className="h-4 w-4 mt-0.5 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<div className="ml-3">
<span className="text-sm font-medium text-gray-700">Individual</span>
<p className="text-xs text-gray-500">Personal account for individual use</p>
</div>
</label>
<label className="flex items-start p-3 border rounded-md hover:bg-gray-50 cursor-pointer">
<input
type="radio"
value="business"
{...register('userType')}
className="h-4 w-4 mt-0.5 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<div className="ml-3">
<span className="text-sm font-medium text-gray-700">Business</span>
<p className="text-xs text-gray-500">Business or organization account</p>
</div>
</label>
</div>
{errors.userType && (
<p className="mt-1 text-sm text-red-600">{errors.userType.message}</p>
)}
</div>
{/* Individual-Specific Fields */}
{userType === 'individual' && (
<div className="p-4 bg-blue-50 rounded-md space-y-4">
<h3 className="font-semibold text-gray-700">Personal Information</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="firstName" className="block text-sm font-medium text-gray-700 mb-1">
First Name <span className="text-red-500">*</span>
</label>
<input
id="firstName"
{...register('firstName' as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.firstName && (
<p className="mt-1 text-sm text-red-600">{(errors.firstName as any).message}</p>
)}
</div>
<div>
<label htmlFor="lastName" className="block text-sm font-medium text-gray-700 mb-1">
Last Name <span className="text-red-500">*</span>
</label>
<input
id="lastName"
{...register('lastName' as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.lastName && (
<p className="mt-1 text-sm text-red-600">{(errors.lastName as any).message}</p>
)}
</div>
</div>
<div>
<label htmlFor="dateOfBirth" className="block text-sm font-medium text-gray-700 mb-1">
Date of Birth <span className="text-red-500">*</span>
</label>
<input
id="dateOfBirth"
type="date"
{...register('dateOfBirth' as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.dateOfBirth && (
<p className="mt-1 text-sm text-red-600">{(errors.dateOfBirth as any).message}</p>
)}
</div>
</div>
)}
{/* Business-Specific Fields */}
{userType === 'business' && (
<div className="p-4 bg-green-50 rounded-md space-y-4">
<h3 className="font-semibold text-gray-700">Business Information</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Business Type <span className="text-red-500">*</span>
</label>
<select
{...register('businessType' as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select a type...</option>
<option value="llc">LLC</option>
<option value="corporation">Corporation</option>
<option value="nonprofit">Nonprofit</option>
</select>
{errors.businessType && (
<p className="mt-1 text-sm text-red-600">{(errors.businessType as any).message}</p>
)}
</div>
<div>
<label htmlFor="businessName" className="block text-sm font-medium text-gray-700 mb-1">
Business Name <span className="text-red-500">*</span>
</label>
<input
id="businessName"
{...register('businessName' as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.businessName && (
<p className="mt-1 text-sm text-red-600">{(errors.businessName as any).message}</p>
)}
</div>
<div>
<label htmlFor="taxId" className="block text-sm font-medium text-gray-700 mb-1">
Tax ID <span className="text-red-500">*</span>
<span className="text-xs text-gray-500 ml-1">(Format: XX-XXXXXXX)</span>
</label>
<input
id="taxId"
{...register('taxId' as any)}
placeholder="12-3456789"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.taxId && (
<p className="mt-1 text-sm text-red-600">{(errors.taxId as any).message}</p>
)}
</div>
{/* LLC-Specific Fields */}
{businessType === 'llc' && (
<div className="p-3 bg-white rounded-md border border-gray-200">
<h4 className="font-semibold text-gray-700 mb-3">LLC Owners</h4>
{ownerFields.map((field, index) => (
<div key={field.id} className="flex gap-2 mb-2">
<div className="flex-1">
<input
{...register(`owners.${index}.name` as any)}
placeholder="Owner name"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="w-24">
<input
type="number"
{...register(`owners.${index}.percentage` as any, { valueAsNumber: true })}
placeholder="%"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="button"
onClick={() => removeOwner(index)}
className="px-3 py-2 text-red-600 hover:bg-red-50 rounded-md"
>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => appendOwner({ name: '', percentage: 0 })}
className="mt-2 px-4 py-2 text-sm text-blue-600 border border-blue-600 rounded-md hover:bg-blue-50"
>
Add Owner
</button>
</div>
)}
{/* Corporation-Specific Fields */}
{businessType === 'corporation' && (
<div className="p-3 bg-white rounded-md border border-gray-200">
<div className="mb-3">
<label htmlFor="stockSymbol" className="block text-sm font-medium text-gray-700 mb-1">
Stock Symbol (if publicly traded)
</label>
<input
id="stockSymbol"
{...register('stockSymbol' as any)}
placeholder="AAPL"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<h4 className="font-semibold text-gray-700 mb-3">Board Members</h4>
{boardFields.map((field, index) => (
<div key={field.id} className="flex gap-2 mb-2">
<div className="flex-1">
<input
{...register(`boardMembers.${index}.name` as any)}
placeholder="Board member name"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex-1">
<input
{...register(`boardMembers.${index}.title` as any)}
placeholder="Title"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="button"
onClick={() => removeBoard(index)}
className="px-3 py-2 text-red-600 hover:bg-red-50 rounded-md"
>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => appendBoard({ name: '', title: '' })}
className="mt-2 px-4 py-2 text-sm text-blue-600 border border-blue-600 rounded-md hover:bg-blue-50"
>
Add Board Member
</button>
</div>
)}
{/* Nonprofit-Specific Fields */}
{businessType === 'nonprofit' && (
<div className="p-3 bg-white rounded-md border border-gray-200">
<div className="mb-3">
<label htmlFor="missionStatement" className="block text-sm font-medium text-gray-700 mb-1">
Mission Statement <span className="text-red-500">*</span>
</label>
<textarea
id="missionStatement"
rows={3}
{...register('missionStatement' as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.missionStatement && (
<p className="mt-1 text-sm text-red-600">{(errors.missionStatement as any).message}</p>
)}
</div>
<div>
<label htmlFor="ein" className="block text-sm font-medium text-gray-700 mb-1">
EIN (Employer Identification Number) <span className="text-red-500">*</span>
</label>
<input
id="ein"
{...register('ein' as any)}
placeholder="12-3456789"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.ein && (
<p className="mt-1 text-sm text-red-600">{(errors.ein as any).message}</p>
)}
</div>
</div>
)}
</div>
)}
{/* Common Contact Fields (shown for both types) */}
{userType && (
<div className="space-y-4">
<h3 className="font-semibold text-gray-700">Contact Information</h3>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email <span className="text-red-500">*</span>
</label>
<input
id="email"
type="email"
{...register('email')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-1">
Phone <span className="text-red-500">*</span>
</label>
<input
id="phone"
type="tel"
{...register('phone')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.phone && (
<p className="mt-1 text-sm text-red-600">{errors.phone.message}</p>
)}
</div>
<div>
<label htmlFor="country" className="block text-sm font-medium text-gray-700 mb-1">
Country <span className="text-red-500">*</span>
</label>
<select
id="country"
{...register('country')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select a country...</option>
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="UK">United Kingdom</option>
<option value="AU">Australia</option>
</select>
{errors.country && (
<p className="mt-1 text-sm text-red-600">{errors.country.message}</p>
)}
</div>
{/* Conditional State Field (only for US and CA) */}
{(country === 'US' || country === 'CA') && (
<div>
<label htmlFor="state" className="block text-sm font-medium text-gray-700 mb-1">
State/Province
</label>
<select
id="state"
{...register('state')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select a state...</option>
{country === 'US' && (
<>
<option value="CA">California</option>
<option value="NY">New York</option>
<option value="TX">Texas</option>
<option value="FL">Florida</option>
</>
)}
{country === 'CA' && (
<>
<option value="ON">Ontario</option>
<option value="QC">Quebec</option>
<option value="BC">British Columbia</option>
<option value="AB">Alberta</option>
</>
)}
</select>
</div>
)}
</div>
)}
{/* Submit Button */}
<button
type="submit"
disabled={!userType}
className="w-full bg-blue-600 text-white py-3 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
>
Submit Registration
</button>
</form>
</div>
);
}
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Simulate async username check
const checkUsernameAvailability = async (username: string): Promise<boolean> => {
await new Promise(resolve => setTimeout(resolve, 1000));
// Simulate taken usernames
const takenUsernames = ['admin', 'user', 'test', 'demo'];
return !takenUsernames.includes(username.toLowerCase());
};
// Zod schema with custom async validation
const formSchema = z.object({
username: z
.string()
.min(3, 'Username must be at least 3 characters')
.max(20, 'Username must be less than 20 characters')
.regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, underscores, and hyphens'),
email: z.string().email('Invalid email address'),
website: z
.string()
.url('Must be a valid URL')
.or(z.literal('')),
bio: z
.string()
.max(500, 'Bio must be less than 500 characters')
.optional(),
});
type FormData = z.infer<typeof formSchema>;
// Field validation state
type FieldStatus = 'idle' | 'validating' | 'valid' | 'invalid';
export function InlineValidationForm() {
const [usernameStatus, setUsernameStatus] = useState<FieldStatus>('idle');
const [usernameError, setUsernameError] = useState<string>('');
const {
register,
handleSubmit,
formState: { errors, dirtyFields },
watch,
trigger,
} = useForm<FormData>({
resolver: zodResolver(formSchema),
mode: 'onBlur', // Validate on blur
reValidateMode: 'onChange', // Re-validate on change after first validation
});
const watchedUsername = watch('username');
const watchedBio = watch('bio');
// Debounced async username validation
React.useEffect(() => {
if (!watchedUsername || watchedUsername.length < 3) {
setUsernameStatus('idle');
return;
}
// Check schema validation first
const schemaValidation = formSchema.shape.username.safeParse(watchedUsername);
if (!schemaValidation.success) {
setUsernameStatus('idle');
return;
}
setUsernameStatus('validating');
setUsernameError('');
const timeoutId = setTimeout(async () => {
try {
const isAvailable = await checkUsernameAvailability(watchedUsername);
if (isAvailable) {
setUsernameStatus('valid');
setUsernameError('');
} else {
setUsernameStatus('invalid');
setUsernameError('Username is already taken');
}
} catch (error) {
setUsernameStatus('invalid');
setUsernameError('Error checking username availability');
}
}, 500); // Debounce for 500ms
return () => clearTimeout(timeoutId);
}, [watchedUsername]);
const onSubmit = async (data: FormData) => {
// Final check for username availability
if (usernameStatus !== 'valid') {
setUsernameError('Please wait for username validation to complete');
return;
}
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Form submitted:', data);
alert('Profile created successfully!');
};
// Get status icon for username field
const getUsernameStatusIcon = () => {
if (usernameStatus === 'validating') {
return (
<svg className="animate-spin h-5 w-5 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<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"></path>
</svg>
);
}
if (usernameStatus === 'valid') {
return (
<svg className="h-5 w-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
);
}
if (usernameStatus === 'invalid') {
return (
<svg className="h-5 w-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
);
}
return null;
};
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6 text-gray-800">Create Profile</h2>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* Username Field with Async Validation */}
<div>
<label
htmlFor="username"
className="block text-sm font-medium text-gray-700 mb-1"
>
Username
</label>
<div className="relative">
<input
id="username"
type="text"
{...register('username')}
className={`w-full px-3 py-2 pr-10 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.username || usernameStatus === 'invalid'
? 'border-red-500'
: usernameStatus === 'valid'
? 'border-green-500'
: 'border-gray-300'
}`}
aria-invalid={errors.username || usernameStatus === 'invalid' ? 'true' : 'false'}
aria-describedby="username-error username-status"
/>
{/* Status Icon */}
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
{getUsernameStatusIcon()}
</div>
</div>
{/* Error Messages */}
{errors.username && (
<p id="username-error" className="mt-1 text-sm text-red-600" role="alert">
{errors.username.message}
</p>
)}
{usernameError && !errors.username && (
<p id="username-error" className="mt-1 text-sm text-red-600" role="alert">
{usernameError}
</p>
)}
{/* Success Message */}
{usernameStatus === 'valid' && (
<p id="username-status" className="mt-1 text-sm text-green-600">
Username is available!
</p>
)}
{/* Loading Message */}
{usernameStatus === 'validating' && (
<p id="username-status" className="mt-1 text-sm text-blue-600">
Checking availability...
</p>
)}
</div>
{/* Email Field with Real-time Validation */}
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1"
>
Email Address
</label>
<div className="relative">
<input
id="email"
type="email"
{...register('email')}
onBlur={() => trigger('email')}
className={`w-full px-3 py-2 pr-10 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.email
? 'border-red-500'
: dirtyFields.email && !errors.email
? 'border-green-500'
: 'border-gray-300'
}`}
aria-invalid={errors.email ? 'true' : 'false'}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{/* Success Icon */}
{dirtyFields.email && !errors.email && (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
)}
</div>
{errors.email && (
<p id="email-error" className="mt-1 text-sm text-red-600" role="alert">
{errors.email.message}
</p>
)}
</div>
{/* Website Field (Optional) */}
<div>
<label
htmlFor="website"
className="block text-sm font-medium text-gray-700 mb-1"
>
Website <span className="text-gray-500 text-xs">(optional)</span>
</label>
<div className="relative">
<input
id="website"
type="url"
{...register('website')}
onBlur={() => trigger('website')}
placeholder="https://example.com"
className={`w-full px-3 py-2 pr-10 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.website
? 'border-red-500'
: dirtyFields.website && !errors.website
? 'border-green-500'
: 'border-gray-300'
}`}
aria-invalid={errors.website ? 'true' : 'false'}
aria-describedby={errors.website ? 'website-error' : undefined}
/>
{/* Success Icon */}
{dirtyFields.website && !errors.website && watch('website') && (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
)}
</div>
{errors.website && (
<p id="website-error" className="mt-1 text-sm text-red-600" role="alert">
{errors.website.message}
</p>
)}
</div>
{/* Bio Field with Character Count */}
<div>
<label
htmlFor="bio"
className="block text-sm font-medium text-gray-700 mb-1"
>
Bio <span className="text-gray-500 text-xs">(optional)</span>
</label>
<textarea
id="bio"
rows={4}
{...register('bio')}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.bio ? 'border-red-500' : 'border-gray-300'
}`}
aria-invalid={errors.bio ? 'true' : 'false'}
aria-describedby="bio-error bio-count"
/>
<div className="flex justify-between items-center mt-1">
<div>
{errors.bio && (
<p id="bio-error" className="text-sm text-red-600" role="alert">
{errors.bio.message}
</p>
)}
</div>
<p
id="bio-count"
className={`text-sm ${
(watchedBio?.length || 0) > 500 ? 'text-red-600' : 'text-gray-500'
}`}
>
{watchedBio?.length || 0} / 500
</p>
</div>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={usernameStatus === 'validating' || usernameStatus === 'invalid'}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Create Profile
</button>
</form>
</div>
);
}
/**
* Async Validation Example: Username Availability Check
*
* Demonstrates:
* - Async validation (API call to check username)
* - Debouncing to prevent excessive API calls
* - Loading states during validation
* - Real-time feedback
* - Error handling for network failures
*/
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { useState, useCallback } from 'react';
import { debounce } from 'lodash';
// Validation schema with async refinement
const signupSchema = z.object({
username: z.string()
.min(3, 'Username must be at least 3 characters')
.max(20, 'Username must be less than 20 characters')
.regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, underscores, and hyphens'),
email: z.string().email('Please enter a valid email address'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain at least one uppercase letter')
.regex(/[a-z]/, 'Must contain at least one lowercase letter')
.regex(/[0-9]/, 'Must contain at least one number'),
});
type SignupFormData = z.infer<typeof signupSchema>;
export default function AsyncValidationForm() {
const [isCheckingUsername, setIsCheckingUsername] = useState(false);
const [usernameAvailable, setUsernameAvailable] = useState<boolean | null>(null);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
setError,
clearErrors,
watch,
} = useForm<SignupFormData>({
resolver: zodResolver(signupSchema),
mode: 'onBlur',
reValidateMode: 'onChange',
});
// Mock API call to check username availability
const checkUsernameAPI = async (username: string): Promise<boolean> => {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 800));
// Simulate some taken usernames
const takenUsernames = ['admin', 'user', 'test', 'demo', 'support'];
return !takenUsernames.includes(username.toLowerCase());
};
// Debounced username check (500ms delay)
const debouncedCheckUsername = useCallback(
debounce(async (username: string) => {
if (username.length < 3) {
setUsernameAvailable(null);
return;
}
setIsCheckingUsername(true);
try {
const available = await checkUsernameAPI(username);
setUsernameAvailable(available);
if (!available) {
setError('username', {
type: 'manual',
message: 'This username is already taken. Please choose another.',
});
} else {
clearErrors('username');
}
} catch (error) {
console.error('Username check failed:', error);
setError('username', {
type: 'manual',
message: 'Could not verify username. Please try again.',
});
} finally {
setIsCheckingUsername(false);
}
}, 500),
[]
);
// Watch username field for changes
const username = watch('username');
// Trigger check on username change
React.useEffect(() => {
if (username) {
debouncedCheckUsername(username);
} else {
setUsernameAvailable(null);
}
}, [username, debouncedCheckUsername]);
const onSubmit = async (data: SignupFormData) => {
// Final check before submission
const available = await checkUsernameAPI(data.username);
if (!available) {
setError('username', {
type: 'manual',
message: 'Username is no longer available',
});
return;
}
console.log('Form submitted:', data);
// Submit to API...
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2>Sign Up</h2>
{/* Username with async validation */}
<div style={{ marginBottom: '24px' }}>
<label htmlFor="username" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
Username *
</label>
<div style={{ position: 'relative' }}>
<input
id="username"
{...register('username')}
placeholder="Choose a username"
aria-invalid={errors.username ? 'true' : 'false'}
aria-describedby={errors.username ? 'username-error' : undefined}
style={{
width: '100%',
padding: '12px',
border: `2px solid ${
errors.username ? '#EF4444' :
usernameAvailable === true ? '#10B981' :
'#D1D5DB'
}`,
borderRadius: '8px',
fontSize: '16px',
}}
/>
{/* Loading indicator */}
{isCheckingUsername && (
<div style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
}}>
<div className="spinner" style={{
width: '20px',
height: '20px',
border: '2px solid #D1D5DB',
borderTopColor: '#3B82F6',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
}} />
</div>
)}
{/* Success indicator */}
{usernameAvailable === true && !isCheckingUsername && (
<div style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
color: '#10B981',
fontSize: '20px',
}}>
✓
</div>
)}
</div>
{/* Error message */}
{errors.username && (
<p id="username-error" role="alert" style={{
color: '#EF4444',
fontSize: '14px',
marginTop: '8px',
}}>
{errors.username.message}
</p>
)}
{/* Success message */}
{usernameAvailable === true && !errors.username && (
<p style={{
color: '#10B981',
fontSize: '14px',
marginTop: '8px',
}}>
✓ Username is available!
</p>
)}
</div>
{/* Email */}
<div style={{ marginBottom: '24px' }}>
<label htmlFor="email" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
Email *
</label>
<input
id="email"
type="email"
{...register('email')}
placeholder="you@example.com"
aria-invalid={errors.email ? 'true' : 'false'}
aria-describedby={errors.email ? 'email-error' : undefined}
style={{
width: '100%',
padding: '12px',
border: `2px solid ${errors.email ? '#EF4444' : '#D1D5DB'}`,
borderRadius: '8px',
fontSize: '16px',
}}
/>
{errors.email && (
<p id="email-error" role="alert" style={{
color: '#EF4444',
fontSize: '14px',
marginTop: '8px',
}}>
{errors.email.message}
</p>
)}
</div>
{/* Password */}
<div style={{ marginBottom: '24px' }}>
<label htmlFor="password" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
Password *
</label>
<input
id="password"
type="password"
{...register('password')}
placeholder="At least 8 characters"
aria-invalid={errors.password ? 'true' : 'false'}
aria-describedby={errors.password ? 'password-error' : undefined}
style={{
width: '100%',
padding: '12px',
border: `2px solid ${errors.password ? '#EF4444' : '#D1D5DB'}`,
borderRadius: '8px',
fontSize: '16px',
}}
/>
{errors.password && (
<p id="password-error" role="alert" style={{
color: '#EF4444',
fontSize: '14px',
marginTop: '8px',
}}>
{errors.password.message}
</p>
)}
</div>
{/* Submit */}
<button
type="submit"
disabled={isSubmitting || isCheckingUsername}
style={{
width: '100%',
padding: '12px 24px',
backgroundColor: '#3B82F6',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
fontWeight: '500',
cursor: 'pointer',
opacity: (isSubmitting || isCheckingUsername) ? 0.6 : 1,
}}
>
{isSubmitting ? 'Creating Account...' : 'Sign Up'}
</button>
</form>
);
}
// Add CSS animation for spinner
const styles = `
@keyframes spin {
to { transform: rotate(360deg); }
}
`;
```
/**
* File Upload with Preview Example
*
* Demonstrates:
* - Image upload with preview
* - File type validation
* - File size validation
* - Drag-and-drop support
* - Multiple file handling
* - Accessibility (keyboard, screen readers)
*/
import { useForm, Controller } from 'react-hook-form';
import { useState } from 'react';
interface FileWithPreview extends File {
preview?: string;
}
interface FormData {
images: FileList;
title: string;
description: string;
}
export default function FileUploadForm() {
const [previews, setPreviews] = useState<string[]>([]);
const [isDragging, setIsDragging] = useState(false);
const {
register,
handleSubmit,
control,
formState: { errors },
setValue,
watch,
} = useForm<FormData>();
const files = watch('images');
const handleFileChange = (fileList: FileList | null) => {
if (!fileList || fileList.length === 0) {
setPreviews([]);
return;
}
// Validate file types and sizes
const validFiles: File[] = [];
const newPreviews: string[] = [];
Array.from(fileList).forEach((file) => {
// Check file type
if (!file.type.startsWith('image/')) {
alert(`${file.name} is not an image file`);
return;
}
// Check file size (5MB limit)
if (file.size > 5 * 1024 * 1024) {
alert(`${file.name} is too large. Maximum size is 5MB`);
return;
}
validFiles.push(file);
// Create preview
const reader = new FileReader();
reader.onloadend = () => {
newPreviews.push(reader.result as string);
setPreviews([...newPreviews]);
};
reader.readAsDataURL(file);
});
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = () => {
setIsDragging(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const droppedFiles = e.dataTransfer.files;
setValue('images', droppedFiles);
handleFileChange(droppedFiles);
};
const removeImage = (index: number) => {
const newPreviews = previews.filter((_, i) => i !== index);
setPreviews(newPreviews);
// Note: Removing from FileList is complex, better to track separately
// In production, maintain separate array of File objects
};
const onSubmit = (data: FormData) => {
console.log('Form data:', data);
console.log('Files:', Array.from(data.images));
// Upload files to server...
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2>Upload Images</h2>
{/* Title */}
<div style={{ marginBottom: '24px' }}>
<label htmlFor="title" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
Title *
</label>
<input
id="title"
{...register('title', {
required: 'Title is required',
minLength: { value: 3, message: 'Title must be at least 3 characters' },
})}
style={{
width: '100%',
padding: '12px',
border: `2px solid ${errors.title ? '#EF4444' : '#D1D5DB'}`,
borderRadius: '8px',
}}
/>
{errors.title && (
<p role="alert" style={{ color: '#EF4444', fontSize: '14px', marginTop: '8px' }}>
{errors.title.message}
</p>
)}
</div>
{/* File Upload */}
<div style={{ marginBottom: '24px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
Images * (Max 5MB each, JPG/PNG only)
</label>
{/* Drag-and-drop zone */}
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
style={{
border: `2px dashed ${isDragging ? '#3B82F6' : '#D1D5DB'}`,
borderRadius: '8px',
padding: '32px',
textAlign: 'center',
backgroundColor: isDragging ? '#EFF6FF' : '#F9FAFB',
cursor: 'pointer',
transition: 'all 0.2s',
}}
onClick={() => document.getElementById('file-input')?.click()}
>
<input
id="file-input"
type="file"
accept="image/*"
multiple
{...register('images', {
required: 'At least one image is required',
onChange: (e) => handleFileChange(e.target.files),
})}
style={{ display: 'none' }}
/>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📁</div>
<p style={{ margin: 0, fontSize: '16px', color: '#6B7280' }}>
{isDragging ? 'Drop files here' : 'Drag and drop images here, or click to browse'}
</p>
<p style={{ margin: '8px 0 0 0', fontSize: '14px', color: '#9CA3AF' }}>
Supports: JPG, PNG, GIF (Max 5MB per file)
</p>
</div>
{errors.images && (
<p role="alert" style={{ color: '#EF4444', fontSize: '14px', marginTop: '8px' }}>
{errors.images.message}
</p>
)}
</div>
{/* Image Previews */}
{previews.length > 0 && (
<div style={{ marginBottom: '24px' }}>
<p style={{ fontWeight: '500', marginBottom: '12px' }}>
Previews ({previews.length} image{previews.length > 1 ? 's' : ''})
</p>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
gap: '16px',
}}>
{previews.map((preview, index) => (
<div
key={index}
style={{
position: 'relative',
aspectRatio: '1',
borderRadius: '8px',
overflow: 'hidden',
border: '2px solid #D1D5DB',
}}
>
<img
src={preview}
alt={`Preview ${index + 1}`}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
<button
type="button"
onClick={() => removeImage(index)}
aria-label={`Remove image ${index + 1}`}
style={{
position: 'absolute',
top: '8px',
right: '8px',
width: '32px',
height: '32px',
borderRadius: '50%',
border: 'none',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
color: 'white',
fontSize: '18px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
×
</button>
</div>
))}
</div>
</div>
)}
{/* Description */}
<div style={{ marginBottom: '24px' }}>
<label htmlFor="description" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
Description
</label>
<textarea
id="description"
{...register('description')}
rows={4}
placeholder="Describe your images..."
style={{
width: '100%',
padding: '12px',
border: '2px solid #D1D5DB',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
}}
/>
</div>
{/* Submit */}
<button
type="submit"
disabled={isSubmitting}
style={{
width: '100%',
padding: '12px 24px',
backgroundColor: '#3B82F6',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
fontWeight: '500',
cursor: 'pointer',
opacity: isSubmitting ? 0.6 : 1,
}}
>
{isSubmitting ? 'Uploading...' : 'Upload Images'}
</button>
</form>
);
}
```
import React, { useState } from 'react';
import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Zod schemas for each step
const step1Schema = z.object({
firstName: z.string().min(2, 'First name required'),
lastName: z.string().min(2, 'Last name required'),
email: z.string().email('Invalid email'),
});
const step2Schema = z.object({
company: z.string().min(2, 'Company name required'),
jobTitle: z.string().min(2, 'Job title required'),
phone: z.string().regex(/^\+?[\d\s-()]+$/, 'Invalid phone number'),
});
const step3Schema = z.object({
skills: z.array(
z.object({
name: z.string().min(1, 'Skill name required'),
level: z.enum(['beginner', 'intermediate', 'advanced']),
})
).min(1, 'Add at least one skill'),
});
// Combined schema for final validation
const wizardSchema = z.object({
...step1Schema.shape,
...step2Schema.shape,
...step3Schema.shape,
});
type WizardFormData = z.infer<typeof wizardSchema>;
const steps = [
{ id: 1, name: 'Personal Info', schema: step1Schema },
{ id: 2, name: 'Professional Info', schema: step2Schema },
{ id: 3, name: 'Skills', schema: step3Schema },
{ id: 4, name: 'Review', schema: wizardSchema },
];
export function MultiStepWizard() {
const [currentStep, setCurrentStep] = useState(1);
const {
register,
handleSubmit,
control,
formState: { errors },
trigger,
getValues,
} = useForm<WizardFormData>({
resolver: zodResolver(wizardSchema),
defaultValues: {
skills: [{ name: '', level: 'beginner' }],
},
mode: 'onBlur',
});
const { fields, append, remove } = useFieldArray({
control,
name: 'skills',
});
// Validate current step before proceeding
const validateStep = async (step: number): Promise<boolean> => {
let fieldsToValidate: (keyof WizardFormData)[] = [];
if (step === 1) fieldsToValidate = ['firstName', 'lastName', 'email'];
if (step === 2) fieldsToValidate = ['company', 'jobTitle', 'phone'];
if (step === 3) fieldsToValidate = ['skills'];
const result = await trigger(fieldsToValidate as any);
return result;
};
const handleNext = async () => {
const isValid = await validateStep(currentStep);
if (isValid) {
setCurrentStep(prev => Math.min(prev + 1, steps.length));
}
};
const handlePrevious = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
};
const onSubmit = async (data: WizardFormData) => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Wizard completed:', data);
alert('Registration completed successfully!');
};
const formData = getValues();
return (
<div className="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md">
{/* Progress Indicator */}
<div className="mb-8">
<div className="flex justify-between items-center mb-2">
{steps.map((step, index) => (
<React.Fragment key={step.id}>
<div className="flex flex-col items-center">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold ${
currentStep >= step.id
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-600'
}`}
>
{step.id}
</div>
<span className="text-xs mt-1 text-gray-600">{step.name}</span>
</div>
{index < steps.length - 1 && (
<div
className={`flex-1 h-1 mx-2 ${
currentStep > step.id ? 'bg-blue-600' : 'bg-gray-200'
}`}
/>
)}
</React.Fragment>
))}
</div>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
{/* Step 1: Personal Info */}
{currentStep === 1 && (
<div className="space-y-4">
<h3 className="text-xl font-semibold mb-4">Personal Information</h3>
<div>
<label htmlFor="firstName" className="block text-sm font-medium text-gray-700 mb-1">
First Name
</label>
<input
id="firstName"
{...register('firstName')}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.firstName ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.firstName && (
<p className="mt-1 text-sm text-red-600">{errors.firstName.message}</p>
)}
</div>
<div>
<label htmlFor="lastName" className="block text-sm font-medium text-gray-700 mb-1">
Last Name
</label>
<input
id="lastName"
{...register('lastName')}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.lastName ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.lastName && (
<p className="mt-1 text-sm text-red-600">{errors.lastName.message}</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
id="email"
type="email"
{...register('email')}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.email ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
</div>
)}
{/* Step 2: Professional Info */}
{currentStep === 2 && (
<div className="space-y-4">
<h3 className="text-xl font-semibold mb-4">Professional Information</h3>
<div>
<label htmlFor="company" className="block text-sm font-medium text-gray-700 mb-1">
Company
</label>
<input
id="company"
{...register('company')}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.company ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.company && (
<p className="mt-1 text-sm text-red-600">{errors.company.message}</p>
)}
</div>
<div>
<label htmlFor="jobTitle" className="block text-sm font-medium text-gray-700 mb-1">
Job Title
</label>
<input
id="jobTitle"
{...register('jobTitle')}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.jobTitle ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.jobTitle && (
<p className="mt-1 text-sm text-red-600">{errors.jobTitle.message}</p>
)}
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-1">
Phone Number
</label>
<input
id="phone"
type="tel"
{...register('phone')}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.phone ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.phone && (
<p className="mt-1 text-sm text-red-600">{errors.phone.message}</p>
)}
</div>
</div>
)}
{/* Step 3: Skills */}
{currentStep === 3 && (
<div className="space-y-4">
<h3 className="text-xl font-semibold mb-4">Your Skills</h3>
{fields.map((field, index) => (
<div key={field.id} className="flex gap-2 items-start">
<div className="flex-1">
<label htmlFor={`skills.${index}.name`} className="block text-sm font-medium text-gray-700 mb-1">
Skill Name
</label>
<input
id={`skills.${index}.name`}
{...register(`skills.${index}.name` as const)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.skills?.[index]?.name && (
<p className="mt-1 text-sm text-red-600">{errors.skills[index]?.name?.message}</p>
)}
</div>
<div className="flex-1">
<label htmlFor={`skills.${index}.level`} className="block text-sm font-medium text-gray-700 mb-1">
Level
</label>
<select
id={`skills.${index}.level`}
{...register(`skills.${index}.level` as const)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
</div>
<button
type="button"
onClick={() => remove(index)}
disabled={fields.length === 1}
className="mt-7 px-3 py-2 text-red-600 hover:bg-red-50 rounded-md disabled:opacity-50"
>
Remove
</button>
</div>
))}
{errors.skills && typeof errors.skills.message === 'string' && (
<p className="text-sm text-red-600">{errors.skills.message}</p>
)}
<button
type="button"
onClick={() => append({ name: '', level: 'beginner' })}
className="px-4 py-2 text-blue-600 border border-blue-600 rounded-md hover:bg-blue-50"
>
Add Skill
</button>
</div>
)}
{/* Step 4: Review */}
{currentStep === 4 && (
<div className="space-y-4">
<h3 className="text-xl font-semibold mb-4">Review Your Information</h3>
<div className="bg-gray-50 p-4 rounded-md space-y-3">
<div>
<h4 className="font-semibold text-gray-700">Personal Information</h4>
<p>Name: {formData.firstName} {formData.lastName}</p>
<p>Email: {formData.email}</p>
</div>
<div>
<h4 className="font-semibold text-gray-700">Professional Information</h4>
<p>Company: {formData.company}</p>
<p>Job Title: {formData.jobTitle}</p>
<p>Phone: {formData.phone}</p>
</div>
<div>
<h4 className="font-semibold text-gray-700">Skills</h4>
<ul className="list-disc list-inside">
{formData.skills?.map((skill, index) => (
<li key={index}>
{skill.name} - {skill.level}
</li>
))}
</ul>
</div>
</div>
</div>
)}
{/* Navigation Buttons */}
<div className="flex justify-between mt-6">
<button
type="button"
onClick={handlePrevious}
disabled={currentStep === 1}
className="px-6 py-2 border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
{currentStep < steps.length ? (
<button
type="button"
onClick={handleNext}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Next
</button>
) : (
<button
type="submit"
className="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
>
Submit
</button>
)}
</div>
</form>
</div>
);
}
"""
Async Database Validation Example
Demonstrates:
- Async/await validation
- Database lookups for uniqueness
- Custom async validators
- Debouncing database queries
"""
from pydantic import BaseModel, EmailStr, validator, Field
from typing import Optional
import asyncio
import re
# Mock database (in production, use SQLAlchemy, MongoDB, etc.)
MOCK_DB = {
'usernames': ['admin', 'user', 'test', 'demo'],
'emails': ['admin@example.com', 'test@example.com'],
}
# Async validation functions
async def check_username_available(username: str) -> bool:
"""Simulate async database check"""
await asyncio.sleep(0.1) # Simulate DB query
return username.lower() not in MOCK_DB['usernames']
async def check_email_available(email: str) -> bool:
"""Simulate async database check"""
await asyncio.sleep(0.1) # Simulate DB query
return email.lower() not in MOCK_DB['emails']
class UserRegistrationAsync(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
email: EmailStr
password: str = Field(..., min_length=8)
class Config:
# Enable validation on assignment for better UX
validate_assignment = True
@validator('username')
def validate_username_format(cls, v):
"""Synchronous format validation"""
if not re.match(r'^[a-zA-Z0-9_-]+$', v):
raise ValueError('Username can only contain letters, numbers, underscores, and hyphens')
return v
@validator('password')
def validate_password_strength(cls, v):
"""Synchronous password strength validation"""
if not re.search(r'[A-Z]', v):
raise ValueError('Must contain uppercase letter')
if not re.search(r'[a-z]', v):
raise ValueError('Must contain lowercase letter')
if not re.search(r'[0-9]', v):
raise ValueError('Must contain number')
return v
# Async validation (separate from Pydantic model)
async def validate_user_registration(data: dict) -> tuple[bool, dict]:
"""
Async validation including database checks
Returns:
(is_valid, errors_dict)
"""
errors = {}
# Check username availability
if 'username' in data:
available = await check_username_available(data['username'])
if not available:
errors['username'] = 'Username is already taken'
# Check email availability
if 'email' in data:
available = await check_email_available(data['email'])
if not available:
errors['email'] = 'Email is already registered'
is_valid = len(errors) == 0
return is_valid, errors
# Usage example
async def register_user_example():
"""Complete async validation workflow"""
# User input
user_data = {
'username': 'newuser',
'email': 'newuser@example.com',
'password': 'SecurePass123'
}
try:
# Step 1: Synchronous validation (Pydantic model)
user = UserRegistrationAsync(**user_data)
print("✅ Synchronous validation passed")
# Step 2: Async validation (database checks)
is_valid, errors = await validate_user_registration(user_data)
if not is_valid:
print("❌ Async validation failed:")
for field, error in errors.items():
print(f" {field}: {error}")
return None
print("✅ Async validation passed")
# Step 3: Create user in database
print(f"✅ User {user.username} registered successfully")
return user
except ValueError as e:
print(f"❌ Validation error: {e}")
return None
# With FastAPI
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.post("/api/register-async")
async def register_with_async_validation(user_data: UserRegistrationAsync):
"""
Registration endpoint with async validation
Performs both Pydantic validation (sync) and database checks (async)
"""
# Pydantic validation happens automatically
# Additional async validation
is_valid, errors = await validate_user_registration(user_data.dict())
if not is_valid:
raise HTTPException(
status_code=400,
detail=errors
)
# Create user
# In production: await database.users.insert_one(user_data.dict())
return {
"success": True,
"message": "User registered successfully",
"username": user_data.username
}
# Run async example
if __name__ == "__main__":
asyncio.run(register_user_example())
```
"""
Basic FastAPI Form Handling Example
Demonstrates:
- FastAPI with Pydantic validation
- User registration endpoint
- Contact form endpoint
- Login endpoint
- Custom error responses
- Email validation
- Password validation
- Cross-field validation
Installation:
pip install fastapi uvicorn 'pydantic[email]'
Run:
uvicorn basic_form:app --reload
Then visit: http://localhost:8000/docs for Swagger UI
"""
from fastapi import FastAPI, HTTPException, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
from typing import Optional
from datetime import date, datetime
app = FastAPI(
title="Form API",
description="FastAPI form handling with Pydantic validation",
version="1.0.0"
)
# ============================================================================
# 1. Contact Form
# ============================================================================
class ContactForm(BaseModel):
"""Contact form submission"""
name: str = Field(..., min_length=2, max_length=100, description="Full name")
email: EmailStr = Field(..., description="Valid email address")
subject: str = Field(..., min_length=5, max_length=200, description="Email subject")
message: str = Field(
...,
min_length=20,
max_length=2000,
description="Message content (20-2000 characters)"
)
newsletter: bool = Field(default=False, description="Subscribe to newsletter")
@field_validator('name')
@classmethod
def validate_name(cls, v: str) -> str:
"""Ensure name doesn't contain special characters"""
v = v.strip()
if not all(char.isalpha() or char.isspace() for char in v):
raise ValueError('Name can only contain letters and spaces')
return v
@field_validator('email')
@classmethod
def email_lowercase(cls, v: str) -> str:
"""Convert email to lowercase"""
return v.lower()
@app.post("/api/contact", status_code=status.HTTP_200_OK)
async def submit_contact_form(form_data: ContactForm):
"""
Submit a contact form
Validation:
- Name: 2-100 characters, letters and spaces only
- Email: Valid email format
- Subject: 5-200 characters
- Message: 20-2000 characters
"""
# Simulate processing (e.g., send email, save to database)
print(f"Contact form submission from {form_data.name} ({form_data.email})")
print(f"Subject: {form_data.subject}")
print(f"Message: {form_data.message}")
print(f"Newsletter subscription: {form_data.newsletter}")
return {
"message": "Thank you for contacting us! We'll get back to you within 24 hours.",
"email": form_data.email,
"newsletter_subscribed": form_data.newsletter
}
# ============================================================================
# 2. User Registration
# ============================================================================
class UserRegistration(BaseModel):
"""User registration form"""
username: str = Field(
...,
min_length=3,
max_length=20,
pattern=r'^[a-zA-Z0-9_]+$',
description="Username (3-20 characters, alphanumeric and underscore only)"
)
email: EmailStr = Field(..., description="Valid email address")
password: str = Field(..., min_length=8, max_length=100, description="Password (min 8 characters)")
confirm_password: str = Field(..., description="Password confirmation")
first_name: str = Field(..., min_length=2, max_length=50)
last_name: str = Field(..., min_length=2, max_length=50)
date_of_birth: date = Field(..., description="Date of birth (YYYY-MM-DD)")
terms_accepted: bool = Field(..., description="Must accept terms and conditions")
@field_validator('username')
@classmethod
def validate_username(cls, v: str) -> str:
"""Username validation and transformation"""
v = v.lower() # Convert to lowercase
# Check reserved usernames
reserved = ['admin', 'root', 'administrator', 'system', 'user']
if v in reserved:
raise ValueError('Username is reserved')
return v
@field_validator('password')
@classmethod
def validate_password_strength(cls, v: str) -> str:
"""Password strength validation"""
if not any(char.isupper() for char in v):
raise ValueError('Password must contain at least one uppercase letter')
if not any(char.islower() for char in v):
raise ValueError('Password must contain at least one lowercase letter')
if not any(char.isdigit() for char in v):
raise ValueError('Password must contain at least one number')
if not any(char in '!@#$%^&*(),.?":{}|<>' for char in v):
raise ValueError('Password must contain at least one special character')
return v
@field_validator('date_of_birth')
@classmethod
def validate_age(cls, v: date) -> date:
"""Ensure user is at least 18 years old"""
from datetime import timedelta
min_age_date = date.today() - timedelta(days=365 * 18)
if v > min_age_date:
raise ValueError('You must be at least 18 years old to register')
return v
@model_validator(mode='after')
def validate_passwords_match(self) -> 'UserRegistration':
"""Ensure password and confirm_password match"""
if self.password != self.confirm_password:
raise ValueError('Passwords do not match')
return self
@model_validator(mode='after')
def validate_terms(self) -> 'UserRegistration':
"""Ensure terms are accepted"""
if not self.terms_accepted:
raise ValueError('You must accept the terms and conditions')
return self
@app.post("/api/register", status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserRegistration):
"""
Register a new user
Validation:
- Username: 3-20 characters, alphanumeric and underscore only, not reserved
- Email: Valid email format
- Password: Min 8 characters, must contain uppercase, lowercase, number, and special character
- Passwords must match
- Age: Must be 18 or older
- Terms: Must be accepted
"""
# Simulate user creation (in real app: hash password, save to database)
print(f"Registering user: {user_data.username}")
print(f"Email: {user_data.email}")
print(f"Name: {user_data.first_name} {user_data.last_name}")
print(f"Date of birth: {user_data.date_of_birth}")
return {
"message": "Registration successful!",
"username": user_data.username,
"email": user_data.email,
"created_at": datetime.now().isoformat()
}
# ============================================================================
# 3. Login Form
# ============================================================================
class LoginForm(BaseModel):
"""User login form"""
email: EmailStr = Field(..., description="Email address")
password: str = Field(..., min_length=8, description="Password")
remember_me: bool = Field(default=False, description="Remember me")
@field_validator('email')
@classmethod
def email_lowercase(cls, v: str) -> str:
return v.lower()
@app.post("/api/login")
async def login(credentials: LoginForm):
"""
User login endpoint
In a real application, you would:
1. Query database for user by email
2. Verify password hash matches
3. Generate and return JWT token
"""
# Simulate authentication (in real app: verify against database)
# This is just an example - NEVER hardcode credentials!
if credentials.email == "test@example.com" and credentials.password == "Password123!":
return {
"message": "Login successful",
"token": "example_jwt_token_here",
"token_type": "bearer",
"remember_me": credentials.remember_me
}
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password"
)
# ============================================================================
# 4. Custom Error Handling
# ============================================================================
from fastapi.exceptions import RequestValidationError
from fastapi import Request
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""
Custom validation error response format
Converts Pydantic validation errors to user-friendly format:
{
"errors": {
"field_name": "Error message"
}
}
"""
errors = {}
for error in exc.errors():
field = error['loc'][-1] # Get field name
message = error['msg']
# Customize error messages based on error type
if error['type'] == 'string_too_short':
ctx = error.get('ctx', {})
min_length = ctx.get('min_length', 0)
message = f"Must be at least {min_length} characters long"
elif error['type'] == 'string_too_long':
ctx = error.get('ctx', {})
max_length = ctx.get('max_length', 0)
message = f"Must be less than {max_length} characters long"
elif error['type'] == 'value_error.email':
message = "Please enter a valid email address"
elif error['type'] == 'value_error':
# Custom validation error messages (from validators)
message = str(error.get('ctx', {}).get('error', message))
errors[field] = message
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"detail": "Validation failed",
"errors": errors
}
)
# ============================================================================
# 5. Health Check Endpoint
# ============================================================================
@app.get("/")
async def root():
"""API health check and info"""
return {
"name": "Form API",
"version": "1.0.0",
"status": "healthy",
"endpoints": {
"contact": "/api/contact",
"register": "/api/register",
"login": "/api/login",
"docs": "/docs",
"redoc": "/redoc"
}
}
# ============================================================================
# Example Usage (Client Side)
# ============================================================================
"""
# Example using httpx or requests
import httpx
# Contact form submission
contact_data = {
"name": "John Doe",
"email": "john@example.com",
"subject": "Question about your service",
"message": "I would like to know more about your premium plan features.",
"newsletter": True
}
response = httpx.post("http://localhost:8000/api/contact", json=contact_data)
print(response.json())
# User registration
registration_data = {
"username": "johndoe",
"email": "john@example.com",
"password": "SecurePass123!",
"confirm_password": "SecurePass123!",
"first_name": "John",
"last_name": "Doe",
"date_of_birth": "1990-01-01",
"terms_accepted": True
}
response = httpx.post("http://localhost:8000/api/register", json=registration_data)
print(response.json())
# Login
login_data = {
"email": "test@example.com",
"password": "Password123!",
"remember_me": True
}
response = httpx.post("http://localhost:8000/api/login", json=login_data)
print(response.json())
"""
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
"""
Django Forms Example
Demonstrates:
- Django Form classes
- ModelForm for database models
- Custom validators
- Clean methods
- Form rendering in templates
"""
from django import forms
from django.core.exceptions import ValidationError
from django.core.validators import EmailValidator, RegexValidator
from django.contrib.auth.models import User
import re
# Custom Validators
def validate_username_available(value):
"""Check if username is available"""
if User.objects.filter(username=value).exists():
raise ValidationError(
'This username is already taken',
code='username_taken'
)
def validate_strong_password(value):
"""Validate password strength"""
if len(value) < 8:
raise ValidationError('Password must be at least 8 characters long')
if not re.search(r'[A-Z]', value):
raise ValidationError('Password must contain at least one uppercase letter')
if not re.search(r'[a-z]', value):
raise ValidationError('Password must contain at least one lowercase letter')
if not re.search(r'[0-9]', value):
raise ValidationError('Password must contain at least one number')
def validate_age(value):
"""Validate user age"""
if value < 13:
raise ValidationError('You must be at least 13 years old')
if value > 120:
raise ValidationError('Please enter a valid age')
# Contact Form
class ContactForm(forms.Form):
name = forms.CharField(
max_length=100,
min_length=2,
required=True,
widget=forms.TextInput(attrs={
'placeholder': 'Your full name',
'class': 'form-control',
'aria-label': 'Full name'
}),
error_messages={
'required': 'Please enter your name',
'min_length': 'Name must be at least 2 characters',
'max_length': 'Name must be less than 100 characters'
}
)
email = forms.EmailField(
required=True,
validators=[EmailValidator(message='Please enter a valid email address')],
widget=forms.EmailInput(attrs={
'placeholder': 'you@example.com',
'class': 'form-control',
'aria-label': 'Email address'
})
)
subject = forms.CharField(
max_length=200,
min_length=5,
required=True,
widget=forms.TextInput(attrs={
'placeholder': 'Subject of your inquiry',
'class': 'form-control'
})
)
message = forms.CharField(
max_length=2000,
min_length=20,
required=True,
widget=forms.Textarea(attrs={
'placeholder': 'Your message...',
'class': 'form-control',
'rows': 5
})
)
category = forms.ChoiceField(
choices=[
('', 'Select a category'),
('sales', 'Sales Inquiry'),
('support', 'Technical Support'),
('billing', 'Billing Question'),
('other', 'Other'),
],
required=True,
widget=forms.Select(attrs={'class': 'form-control'})
)
newsletter = forms.BooleanField(
required=False,
initial=False,
label='Subscribe to newsletter'
)
def clean_message(self):
"""Custom cleaning for message field"""
message = self.cleaned_data.get('message')
if message:
# Remove excessive whitespace
message = ' '.join(message.split())
# Check for spam patterns (simple example)
spam_words = ['viagra', 'casino', 'lottery']
if any(word in message.lower() for word in spam_words):
raise ValidationError('Your message appears to contain spam content')
return message
# Registration Form
class UserRegistrationForm(forms.Form):
username = forms.CharField(
max_length=20,
min_length=3,
required=True,
validators=[
RegexValidator(
r'^[a-zA-Z0-9_-]+$',
message='Username can only contain letters, numbers, underscores, and hyphens'
),
validate_username_available
],
widget=forms.TextInput(attrs={
'placeholder': 'Choose a username',
'class': 'form-control'
})
)
email = forms.EmailField(
required=True,
widget=forms.EmailInput(attrs={
'placeholder': 'your.email@example.com',
'class': 'form-control'
})
)
password = forms.CharField(
max_length=100,
min_length=8,
required=True,
validators=[validate_strong_password],
widget=forms.PasswordInput(attrs={
'placeholder': 'At least 8 characters',
'class': 'form-control'
})
)
confirm_password = forms.CharField(
required=True,
widget=forms.PasswordInput(attrs={
'placeholder': 'Confirm your password',
'class': 'form-control'
})
)
first_name = forms.CharField(max_length=50, required=True)
last_name = forms.CharField(max_length=50, required=True)
age = forms.IntegerField(
required=True,
validators=[validate_age],
widget=forms.NumberInput(attrs={
'min': 13,
'max': 120,
'class': 'form-control'
})
)
terms = forms.BooleanField(
required=True,
error_messages={
'required': 'You must accept the terms and conditions to register'
}
)
def clean(self):
"""Cross-field validation"""
cleaned_data = super().clean()
password = cleaned_data.get('password')
confirm_password = cleaned_data.get('confirm_password')
if password and confirm_password:
if password != confirm_password:
raise ValidationError('Passwords do not match')
return cleaned_data
def clean_email(self):
"""Check if email is already registered"""
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise ValidationError('This email is already registered')
return email
# Views example
from django.shortcuts import render, redirect
from django.contrib import messages
def register_view(request):
if request.method == 'POST':
form = UserRegistrationForm(request.POST)
if form.is_valid():
# Create user
user = User.objects.create_user(
username=form.cleaned_data['username'],
email=form.cleaned_data['email'],
password=form.cleaned_data['password'],
first_name=form.cleaned_data['first_name'],
last_name=form.cleaned_data['last_name'],
)
messages.success(request, 'Account created successfully!')
return redirect('login')
else:
form = UserRegistrationForm()
return render(request, 'registration/register.html', {'form': form})
```
"""
FastAPI Form Handling Example
Demonstrates:
- FastAPI form endpoints
- Pydantic validation
- File upload handling
- Async validation
- Error responses
"""
from fastapi import FastAPI, Form, File, UploadFile, HTTPException
from pydantic import BaseModel, EmailStr, validator, Field
from typing import Optional, List
import re
app = FastAPI()
# Pydantic models for validation
class ContactForm(BaseModel):
name: str = Field(..., min_length=2, max_length=100, description="Full name")
email: EmailStr = Field(..., description="Valid email address")
subject: str = Field(..., min_length=5, max_length=200)
message: str = Field(..., min_length=20, max_length=2000)
newsletter: bool = Field(default=False)
@validator('name')
def validate_name(cls, v):
if not re.match(r"^[a-zA-ZÀ-ÿ\s'-]+$", v):
raise ValueError('Name can only contain letters, spaces, hyphens, and apostrophes')
return v.strip()
class UserRegistration(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
email: EmailStr
password: str = Field(..., min_length=8, max_length=100)
confirm_password: str
first_name: str = Field(..., min_length=1, max_length=50)
last_name: str = Field(..., min_length=1, max_length=50)
age: int = Field(..., ge=13, le=120)
terms: bool = Field(..., description="Must accept terms")
@validator('username')
def validate_username(cls, v):
if not re.match(r'^[a-zA-Z0-9_-]+$', v):
raise ValueError('Username can only contain letters, numbers, underscores, and hyphens')
# Check reserved words
reserved = ['admin', 'administrator', 'root', 'system']
if v.lower() in reserved:
raise ValueError('This username is reserved')
return v
@validator('password')
def validate_password(cls, v):
if not re.search(r'[A-Z]', v):
raise ValueError('Password must contain at least one uppercase letter')
if not re.search(r'[a-z]', v):
raise ValueError('Password must contain at least one lowercase letter')
if not re.search(r'[0-9]', v):
raise ValueError('Password must contain at least one number')
if not re.search(r'[!@#$%^&*()_+\-=\[\]{};:\'",.<>?]', v):
raise ValueError('Password must contain at least one special character')
return v
@validator('confirm_password')
def passwords_match(cls, v, values):
if 'password' in values and v != values['password']:
raise ValueError('Passwords do not match')
return v
@validator('terms')
def terms_accepted(cls, v):
if not v:
raise ValueError('You must accept the terms and conditions')
return v
# POST endpoints
@app.post("/api/contact")
async def submit_contact_form(form: ContactForm):
"""
Contact form submission endpoint
Returns:
200: Form submitted successfully
422: Validation errors
"""
try:
# Process form data
# In production: save to database, send email, etc.
return {
"success": True,
"message": "Thank you for contacting us!",
"data": form.dict()
}
except Exception as e:
raise HTTPException(status_code=500, detail="Server error processing form")
@app.post("/api/register")
async def register_user(user: UserRegistration):
"""
User registration endpoint with comprehensive validation
Returns:
201: User created successfully
400: Username already taken
422: Validation errors
"""
# Check username availability (mock)
taken_usernames = ['admin', 'test', 'demo']
if user.username.lower() in taken_usernames:
raise HTTPException(
status_code=400,
detail="Username is already taken"
)
# In production: create user in database, send welcome email, etc.
return {
"success": True,
"message": "Account created successfully",
"user_id": "generated-uuid",
"username": user.username
}
@app.post("/api/upload")
async def upload_file(
file: UploadFile = File(...),
title: str = Form(...),
description: Optional[str] = Form(None)
):
"""
File upload endpoint with validation
Accepts:
- Images (JPG, PNG, GIF)
- Max size: 5MB
- Required metadata: title
"""
# Validate file type
allowed_types = ['image/jpeg', 'image/png', 'image/gif']
if file.content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}"
)
# Validate file size (5MB)
contents = await file.read()
if len(contents) > 5 * 1024 * 1024:
raise HTTPException(
status_code=400,
detail="File too large. Maximum size is 5MB"
)
# In production: save file to storage, process image, create thumbnail, etc.
return {
"success": True,
"message": "File uploaded successfully",
"filename": file.filename,
"size": len(contents),
"content_type": file.content_type
}
# Async validation endpoints
@app.get("/api/check-username/{username}")
async def check_username_availability(username: str):
"""
Check if username is available (for async validation)
Returns:
200: {"available": true/false}
"""
# Simulate database check
taken_usernames = ['admin', 'user', 'test', 'demo', 'support']
available = username.lower() not in taken_usernames
return {"available": available}
@app.get("/api/check-email/{email}")
async def check_email_availability(email: str):
"""
Check if email is available
Returns:
200: {"available": true/false}
"""
# Simulate database check
taken_emails = ['admin@example.com', 'test@example.com']
available = email.lower() not in taken_emails
return {"available": available}
# Run with: uvicorn fastapi_forms:app --reload
```
Related skills
FAQ
What validation timing does it recommend?
On-blur with progressive enhancement: validate when a field loses focus, then switch to on-change for that field after its first error.
How does it pick input components?
By the golden rule Data Type -> Input Component -> Validation Pattern, using a component-selection decision tree.