632 lines
14 KiB
Markdown
632 lines
14 KiB
Markdown
# 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.
|
|
|
|
```tsx
|
|
<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.
|
|
|
|
```tsx
|
|
<FormInput
|
|
type="email"
|
|
name="email"
|
|
label="Email"
|
|
prefix={<EmailIcon />}
|
|
showClear
|
|
value={value}
|
|
onChange={handleChange}
|
|
/>
|
|
```
|
|
|
|
### FormTextarea
|
|
Textarea with auto-resize and character counting.
|
|
|
|
```tsx
|
|
<FormTextarea
|
|
name="message"
|
|
label="Message"
|
|
rows={5}
|
|
showCharCount
|
|
maxLength={500}
|
|
autoResize
|
|
value={value}
|
|
onChange={handleChange}
|
|
/>
|
|
```
|
|
|
|
### FormSelect
|
|
Select dropdown with search and multi-select support.
|
|
|
|
```tsx
|
|
<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.
|
|
|
|
```tsx
|
|
// 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.
|
|
|
|
```tsx
|
|
<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.
|
|
|
|
```tsx
|
|
<FormError
|
|
errors={errors}
|
|
variant="block"
|
|
showIcon
|
|
/>
|
|
```
|
|
|
|
### FormSuccess
|
|
Success message with auto-dismiss option.
|
|
|
|
```tsx
|
|
<FormSuccess
|
|
message="Form submitted successfully!"
|
|
autoDismiss
|
|
onClose={() => setShowSuccess(false)}
|
|
/>
|
|
```
|
|
|
|
## Hooks
|
|
|
|
### useForm
|
|
Main form state management hook with validation and submission handling.
|
|
|
|
```tsx
|
|
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.
|
|
|
|
```tsx
|
|
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.
|
|
|
|
```tsx
|
|
const { validateField, validateForm } = useFormValidation();
|
|
|
|
const errors = validateField(value, {
|
|
required: true,
|
|
email: true,
|
|
}, 'email');
|
|
```
|
|
|
|
## Validation Rules
|
|
|
|
Available validation rules:
|
|
|
|
```typescript
|
|
{
|
|
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
|
|
|
|
```tsx
|
|
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
|
|
|
|
```tsx
|
|
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
|
|
|
|
```tsx
|
|
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
|
|
```tsx
|
|
// ✅ Good
|
|
<FormField name="email" type="email" label="Email" ... />
|
|
|
|
// ❌ Avoid
|
|
<div>
|
|
<label>Email</label>
|
|
<input type="email" ... />
|
|
</div>
|
|
```
|
|
|
|
### 2. Validate Before Submit
|
|
```tsx
|
|
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
|
|
```tsx
|
|
{form.touched.email && form.errors.email && (
|
|
<FormError errors={form.errors.email} />
|
|
)}
|
|
```
|
|
|
|
### 4. Disable Submit When Invalid
|
|
```tsx
|
|
<Button
|
|
type="submit"
|
|
disabled={!form.isValid || form.isSubmitting}
|
|
loading={form.isSubmitting}
|
|
>
|
|
Submit
|
|
</Button>
|
|
```
|
|
|
|
### 5. Reset After Success
|
|
```tsx
|
|
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:
|
|
|
|
```typescript
|
|
import type {
|
|
FormFieldProps,
|
|
FormInputProps,
|
|
ValidationRules,
|
|
FormErrors
|
|
} from '@/components/forms';
|
|
```
|
|
|
|
## Testing
|
|
|
|
Example test setup:
|
|
|
|
```tsx
|
|
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
|
|
|
|
1. **Memoize validation rules** if they depend on external values
|
|
2. **Use useCallback** for event handlers
|
|
3. **Avoid unnecessary re-renders** by splitting large forms
|
|
4. **Lazy load** form examples for better initial load
|
|
|
|
## Migration from Legacy Forms
|
|
|
|
If migrating from old form components:
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
1. **Validation not working**: Ensure `validationRules` match `initialValues` keys
|
|
2. **Form not submitting**: Check `isValid` state and `required` fields
|
|
3. **Type errors**: Import proper types from the forms module
|
|
4. **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 |