migration wip
This commit is contained in:
632
components/forms/README.md
Normal file
632
components/forms/README.md
Normal file
@@ -0,0 +1,632 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user