14 KiB
KLZ Forms System
A comprehensive, reusable form system for Next.js applications with full TypeScript support, accessibility features, and consistent styling.
Features
- Complete Form Components: All essential form inputs with consistent styling
- Validation System: Built-in validation with custom rules
- Type Safety: Full TypeScript support
- Accessibility: ARIA attributes and keyboard navigation
- Internationalization: Ready for i18n
- Customizable: Flexible props for different use cases
- Animation: Smooth transitions and animations
- Error Handling: Multiple error display modes
- Auto-resize: Smart textarea resizing
- Character Count: Built-in character counting
Installation
The form system is already included in the project. All components use the existing design system tokens.
Components
FormField
Wrapper component that provides consistent form field experience.
<FormField
type="text"
name="email"
label="Email Address"
required
placeholder="your@email.com"
value={value}
error={error}
onChange={(e) => setValue(e.target.value)}
/>
Supported Types: text, email, tel, textarea, select, checkbox, radio, number, password, date, time, url
FormInput
Base input component with icon support and clear button.
<FormInput
type="email"
name="email"
label="Email"
prefix={<EmailIcon />}
showClear
value={value}
onChange={handleChange}
/>
FormTextarea
Textarea with auto-resize and character counting.
<FormTextarea
name="message"
label="Message"
rows={5}
showCharCount
maxLength={500}
autoResize
value={value}
onChange={handleChange}
/>
FormSelect
Select dropdown with search and multi-select support.
<FormSelect
name="country"
label="Country"
options={[
{ value: 'de', label: 'Germany' },
{ value: 'us', label: 'United States' }
]}
value={value}
onChange={handleChange}
/>
FormCheckbox
Single checkbox or checkbox group with indeterminate state.
// Single checkbox
<FormCheckbox
name="agree"
label="I agree to terms"
checked={checked}
onChange={(values) => setChecked(values.length > 0)}
/>
// Checkbox group
<FormCheckbox
name="services"
label="Services"
options={[
{ value: 'consulting', label: 'Consulting' },
{ value: 'support', label: 'Support' }
]}
value={selectedValues}
onChange={(values) => setSelectedValues(values)}
/>
FormRadio
Radio button group with custom styling.
<FormRadio
name="payment"
label="Payment Method"
options={[
{ value: 'credit-card', label: 'Credit Card' },
{ value: 'paypal', label: 'PayPal' }
]}
value={value}
onChange={(value) => setValue(value)}
/>
FormError
Error message display with multiple variants.
<FormError
errors={errors}
variant="block"
showIcon
/>
FormSuccess
Success message with auto-dismiss option.
<FormSuccess
message="Form submitted successfully!"
autoDismiss
onClose={() => setShowSuccess(false)}
/>
Hooks
useForm
Main form state management hook with validation and submission handling.
const form = useForm({
initialValues: {
name: '',
email: '',
},
validationRules: {
name: { required: true, minLength: { value: 2, message: 'Too short' } },
email: { required: true, email: true },
},
onSubmit: async (values) => {
// Handle submission
await api.submit(values);
},
});
// In your component
<form {...form.getFormProps()}>
<input
value={form.values.name}
onChange={(e) => form.setFieldValue('name', e.target.value)}
/>
{form.errors.name && <FormError errors={form.errors.name} />}
<button type="submit" disabled={!form.isValid || form.isSubmitting}>
Submit
</button>
</form>
useFormField
Hook for managing individual field state.
const field = useFormField({
initialValue: '',
validate: (value) => value.length < 2 ? 'Too short' : null,
});
// field.value, field.error, field.touched, field.handleChange, etc.
useFormValidation
Validation logic and utilities.
const { validateField, validateForm } = useFormValidation();
const errors = validateField(value, {
required: true,
email: true,
}, 'email');
Validation Rules
Available validation rules:
{
required: boolean | string; // Required field
minLength: { value: number, message: string };
maxLength: { value: number, message: string };
pattern: { value: RegExp, message: string };
min: { value: number, message: string };
max: { value: number, message: string };
email: boolean | string;
url: boolean | string;
number: boolean | string;
custom: (value) => string | null; // Custom validation
}
Examples
Simple Contact Form
import { useForm, FormField, Button } from '@/components/forms';
export function ContactForm() {
const form = useForm({
initialValues: { name: '', email: '', message: '' },
validationRules: {
name: { required: true, minLength: { value: 2 } },
email: { required: true, email: true },
message: { required: true },
},
onSubmit: async (values) => {
await sendEmail(values);
alert('Sent!');
form.reset();
},
});
return (
<form {...form.getFormProps()}>
<FormField
name="name"
label="Name"
required
value={form.values.name}
error={form.errors.name?.[0]}
onChange={(e) => form.setFieldValue('name', e.target.value)}
/>
<FormField
type="email"
name="email"
label="Email"
required
value={form.values.email}
error={form.errors.email?.[0]}
onChange={(e) => form.setFieldValue('email', e.target.value)}
/>
<FormField
type="textarea"
name="message"
label="Message"
required
rows={5}
value={form.values.message}
error={form.errors.message?.[0]}
onChange={(e) => form.setFieldValue('message', e.target.value)}
/>
<Button
type="submit"
variant="primary"
disabled={!form.isValid || form.isSubmitting}
loading={form.isSubmitting}
>
Send
</Button>
</form>
);
}
Registration Form
import { useForm, FormField, Button } from '@/components/forms';
export function RegistrationForm() {
const form = useForm({
initialValues: {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
terms: false,
},
validationRules: {
firstName: { required: true, minLength: { value: 2 } },
lastName: { required: true, minLength: { value: 2 } },
email: { required: true, email: true },
password: {
required: true,
minLength: { value: 8 },
pattern: { value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/ }
},
confirmPassword: {
required: true,
custom: (value) => value === form.values.password ? null : 'Passwords do not match'
},
terms: {
required: 'You must accept terms',
custom: (value) => value ? null : 'Required'
},
},
onSubmit: async (values) => {
await registerUser(values);
alert('Registered!');
},
});
return (
<form {...form.getFormProps()}>
<div className="grid grid-cols-2 gap-4">
<FormField
name="firstName"
label="First Name"
required
value={form.values.firstName}
error={form.errors.firstName?.[0]}
onChange={(e) => form.setFieldValue('firstName', e.target.value)}
/>
<FormField
name="lastName"
label="Last Name"
required
value={form.values.lastName}
error={form.errors.lastName?.[0]}
onChange={(e) => form.setFieldValue('lastName', e.target.value)}
/>
</div>
<FormField
type="email"
name="email"
label="Email"
required
value={form.values.email}
error={form.errors.email?.[0]}
onChange={(e) => form.setFieldValue('email', e.target.value)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
type="password"
name="password"
label="Password"
required
helpText="Min 8 chars with uppercase, lowercase, and number"
value={form.values.password}
error={form.errors.password?.[0]}
onChange={(e) => form.setFieldValue('password', e.target.value)}
/>
<FormField
type="password"
name="confirmPassword"
label="Confirm Password"
required
value={form.values.confirmPassword}
error={form.errors.confirmPassword?.[0]}
onChange={(e) => form.setFieldValue('confirmPassword', e.target.value)}
/>
</div>
<FormField
type="checkbox"
name="terms"
label="I accept the terms and conditions"
required
checked={form.values.terms}
error={form.errors.terms?.[0]}
onChange={(values) => form.setFieldValue('terms', values.length > 0)}
/>
<Button
type="submit"
variant="primary"
disabled={!form.isValid || form.isSubmitting}
loading={form.isSubmitting}
>
Register
</Button>
</form>
);
}
Search and Filter Form
import { useForm, FormField, Button } from '@/components/forms';
export function SearchForm() {
const form = useForm({
initialValues: {
search: '',
category: '',
status: '',
},
validationRules: {},
onSubmit: async (values) => {
await performSearch(values);
},
});
const categoryOptions = [
{ value: '', label: 'All' },
{ value: 'cables', label: 'Cables' },
{ value: 'connectors', label: 'Connectors' },
];
return (
<form {...form.getFormProps()} className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<FormField
type="text"
name="search"
label="Search"
placeholder="Search..."
prefix={<SearchIcon />}
showClear
value={form.values.search}
onChange={(e) => form.setFieldValue('search', e.target.value)}
/>
<FormField
type="select"
name="category"
label="Category"
options={categoryOptions}
value={form.values.category}
onChange={(e) => form.setFieldValue('category', e.target.value)}
/>
<div className="flex gap-2 items-end">
<Button type="submit" variant="primary" size="sm">
Search
</Button>
<Button type="button" variant="outline" size="sm" onClick={form.reset}>
Reset
</Button>
</div>
</div>
</form>
);
}
Best Practices
1. Always Use FormField for Consistency
// ✅ Good
<FormField name="email" type="email" label="Email" ... />
// ❌ Avoid
<div>
<label>Email</label>
<input type="email" ... />
</div>
2. Validate Before Submit
const form = useForm({
validationRules: {
email: { required: true, email: true },
},
onSubmit: async (values) => {
// Validation happens automatically
// Only called if isValid is true
},
});
3. Show Errors Only After Touch
{form.touched.email && form.errors.email && (
<FormError errors={form.errors.email} />
)}
4. Disable Submit When Invalid
<Button
type="submit"
disabled={!form.isValid || form.isSubmitting}
loading={form.isSubmitting}
>
Submit
</Button>
5. Reset After Success
onSubmit: async (values) => {
await submit(values);
form.reset();
}
Accessibility
All components include:
- Proper ARIA attributes
- Keyboard navigation support
- Focus management
- Screen reader support
- Required field indicators
Styling
Components use the design system:
- Colors:
--color-primary,--color-danger,--color-success - Spacing:
--spacing-sm,--spacing-md, etc. - Typography:
--font-size-sm,--font-size-base - Borders:
--radius-md - Transitions:
--transition-fast
TypeScript Support
Full TypeScript support with proper interfaces:
import type {
FormFieldProps,
FormInputProps,
ValidationRules,
FormErrors
} from '@/components/forms';
Testing
Example test setup:
import { render, screen, fireEvent } from '@testing-library/react';
import { useForm } from '@/components/forms';
test('form validation', () => {
const TestComponent = () => {
const form = useForm({
initialValues: { email: '' },
validationRules: { email: { required: true, email: true } },
onSubmit: jest.fn(),
});
return (
<form {...form.getFormProps()}>
<input
value={form.values.email}
onChange={(e) => form.setFieldValue('email', e.target.value)}
/>
<button type="submit">Submit</button>
</form>
);
};
render(<TestComponent />);
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'invalid' } });
// Validation should trigger
});
Performance Tips
- Memoize validation rules if they depend on external values
- Use useCallback for event handlers
- Avoid unnecessary re-renders by splitting large forms
- Lazy load form examples for better initial load
Migration from Legacy Forms
If migrating from old form components:
// Old
<input
value={email}
onChange={e => setEmail(e.target.value)}
className={error ? 'error' : ''}
/>
// New
<FormField
type="email"
name="email"
value={form.values.email}
error={form.errors.email?.[0]}
onChange={(e) => form.setFieldValue('email', e.target.value)}
/>
Troubleshooting
Common Issues
- Validation not working: Ensure
validationRulesmatchinitialValueskeys - Form not submitting: Check
isValidstate andrequiredfields - Type errors: Import proper types from the forms module
- Styling issues: Ensure design system CSS is imported
Getting Help
Check the examples in FormExamples.tsx for complete implementations.
License
Internal KLZ Cables component system