mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
- Moved 104 production-ready hooks from frontends/nextjs and redux packages to root /hooks - Removed duplicate hooks from frontends/nextjs/src/hooks directory - Updated frontends/nextjs to import from @metabuilder/hooks instead of local hooks - Added comprehensive documentation: EXPORT_GUIDE.md, FORM_VALIDATION_HOOKS.md, QUICK_REFERENCE.md - Removed stale index.d.ts from hooks root (TypeScript auto-generates declarations) - All 104 hooks now consolidated as single source of truth at root /hooks - Package @metabuilder/hooks v2.0.0 ready for use across all MetaBuilder projects Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
12 KiB
12 KiB
Form Hooks - Quick Reference
Import All Hooks
import {
useValidation,
useInput,
useCheckbox,
useSelect,
useFieldArray
} from '@/hooks'
Quick Start Examples
1. Simple Text Input
const email = useInput('', {
onValidate: (v) => /^[^@]+@[^@]+$/.test(v) ? '' : 'Invalid email'
})
return (
<div>
<input value={email.value} onChange={email.handlers.onChange} />
{email.error && <span className="error">{email.error}</span>}
</div>
)
2. Checkbox with Validation
const agree = useCheckbox(false, {
onValidate: (v) => v ? '' : 'You must agree'
})
return (
<label>
<input
type="checkbox"
checked={agree.checked}
onChange={agree.handlers.onChange}
/>
I agree to terms
{agree.error && <span>{agree.error}</span>}
</label>
)
3. Select Dropdown
const country = useSelect(null, {
options: [
{ value: 'us', label: 'USA' },
{ value: 'uk', label: 'UK' },
{ value: 'ca', label: 'Canada' }
],
clearable: true
})
return (
<select value={country.value || ''} onChange={country.handlers.onChange}>
<option value="">Select country...</option>
{country.filteredOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
)
4. Multiple Checkboxes
const permissions = useCheckbox({
read: false,
write: false,
delete: false,
admin: false
})
return (
<div>
{Object.keys(permissions.values).map(key => (
<label key={key}>
<input
type="checkbox"
checked={permissions.values[key]}
onChange={permissions.handlers.onChange}
name={key}
/>
{key.charAt(0).toUpperCase() + key.slice(1)}
</label>
))}
<p>Selected: {permissions.count} / {Object.keys(permissions.values).length}</p>
</div>
)
5. Dynamic Field Array
const emails = useFieldArray(
[{ value: '' }],
{
minFields: 1,
maxFields: 5,
validateField: (v) =>
/^[^@]+@[^@]+$/.test(v.value) ? '' : 'Invalid email'
}
)
return (
<div>
{emails.fields.map((field, idx) => (
<div key={field.id}>
<input
value={field.value.value}
onChange={(e) => emails.handlers.updateField(idx, { value: e.target.value })}
/>
{emails.errors[idx] && <span>{emails.errors[idx]}</span>}
<button
onClick={() => emails.handlers.remove(idx)}
disabled={!emails.canRemove}
>
Remove
</button>
</div>
))}
<button
onClick={() => emails.handlers.append({ value: '' })}
disabled={!emails.canAdd}
>
Add Email ({emails.count}/{emails.fields.length})
</button>
</div>
)
6. Complete Contact Form
export const ContactForm = () => {
// Individual fields
const name = useInput('', {
onValidate: (v) => v.length >= 2 ? '' : 'Min 2 characters'
})
const email = useInput('', {
onValidate: (v) => /^[^@]+@[^@]+$/.test(v) ? '' : 'Invalid email'
})
const subject = useSelect('general', {
options: [
{ value: 'general', label: 'General Inquiry' },
{ value: 'support', label: 'Support' },
{ value: 'sales', label: 'Sales' }
]
})
const agree = useCheckbox(false, {
onValidate: (v) => v ? '' : 'Required'
})
// Form-level validation
const validation = useValidation({
name: (v) => v.length >= 2 ? '' : 'Min 2 characters',
email: (v) => /^[^@]+@[^@]+$/.test(v) ? '' : 'Invalid email',
subject: (v) => v ? '' : 'Required',
agree: (v) => v ? '' : 'Must agree'
})
const handleSubmit = (e) => {
e.preventDefault()
const isValid = validation.validate({
name: name.value,
email: email.value,
subject: subject.value,
agree: agree.checked
})
if (isValid) {
console.log('Submit:', {
name: name.value,
email: email.value,
subject: subject.value,
agree: agree.checked
})
// Reset form
name.handlers.reset()
email.handlers.reset()
subject.handlers.reset()
agree.handlers.reset()
validation.clearErrors()
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name</label>
<input
value={name.value}
onChange={name.handlers.onChange}
onBlur={name.handlers.onBlur}
/>
{validation.getFieldError('name') && (
<span className="error">{validation.getFieldError('name')}</span>
)}
</div>
<div>
<label>Email</label>
<input
type="email"
value={email.value}
onChange={email.handlers.onChange}
onBlur={email.handlers.onBlur}
/>
{validation.getFieldError('email') && (
<span className="error">{validation.getFieldError('email')}</span>
)}
</div>
<div>
<label>Subject</label>
<select value={subject.value || ''} onChange={subject.handlers.onChange}>
<option value="">Select...</option>
{subject.filteredOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
{validation.getFieldError('subject') && (
<span className="error">{validation.getFieldError('subject')}</span>
)}
</div>
<label>
<input
type="checkbox"
checked={agree.checked}
onChange={agree.handlers.onChange}
/>
I agree to terms
</label>
{validation.getFieldError('agree') && (
<span className="error">{validation.getFieldError('agree')}</span>
)}
<button type="submit" disabled={!validation.isValid}>
Submit
</button>
</form>
)
}
Hook Comparison
| Hook | Use Case | Type |
|---|---|---|
useValidation |
Form-level schema validation | Validation |
useInput |
Text inputs, textareas | Single value |
useCheckbox |
Single or group checkboxes | Single/Group value |
useSelect |
Single or multi dropdowns | Single/Group value |
useFieldArray |
Dynamic field lists | Array value |
State Properties
All Hooks Include
{
isDirty: boolean // Changed from initial
isTouched: boolean // User interacted
error: string // Validation error
isValid: boolean // No errors
}
Array-Specific
// useFieldArray additionally has:
{
count: number // Number of fields
canAdd: boolean // Can add more (< maxFields)
canRemove: boolean // Can remove (> minFields)
}
// useCheckbox (multi) additionally has:
{
count: number // Checked count
isAllChecked: boolean // All checked
isIndeterminate: boolean // Some checked
}
// useSelect additionally has:
{
searchTerm: string // Search filter
filteredOptions: [] // Filtered list
}
Handler Methods
useInput
onChange() // Handle input change
onBlur() // Handle blur, validate
setValue() // Set value programmatically
setError() // Set custom error
clearError() // Clear error
reset() // Reset to initial
touch() // Mark as touched
validate() // Manually validate
useCheckbox (Single)
onChange() // Handle checkbox change
setChecked() // Set checked state
toggle() // Toggle state
reset() // Reset to initial
touch() // Mark as touched
validate() // Manually validate
setError() // Set error
clearError() // Clear error
useCheckbox (Multi)
onChange() // Handle checkbox change
setValues() // Set values
isChecked() // Check if specific checkbox is checked
toggle() // Toggle specific checkbox
toggleAll() // Toggle all checkboxes
checkAll() // Check all
uncheckAll() // Uncheck all
reset() // Reset to initial
touch() // Mark as touched
validate() // Manually validate
useSelect (Single)
onChange() // Handle select change
setValue() // Set value
clear() // Clear selection
reset() // Reset to initial
touch() // Mark as touched
validate() // Manually validate
setSearchTerm() // Filter options
getOptionLabel()// Get label for value
useSelect (Multi)
onChange() // Handle select change
setValues() // Set values
isSelected() // Check if value selected
toggleOption() // Toggle a value
addOption() // Add a value
removeOption() // Remove a value
clearAll() // Clear all selections
reset() // Reset to initial
touch() // Mark as touched
validate() // Manually validate
setSearchTerm() // Filter options
useFieldArray
append() // Add field at end
prepend() // Add field at start
insert() // Insert at index
remove() // Remove at index
move() // Move from → to
swap() // Swap two indices
replace() // Replace at index
replaceAll() // Replace all fields
updateField() // Update partial field
getField() // Get field at index
clear() // Clear all
reset() // Reset to initial
push() // Array.push equivalent
pop() // Array.pop equivalent
shift() // Array.shift equivalent
unshift() // Array.unshift equivalent
touch() // Mark as touched
validateField() // Validate single field
validateAll() // Validate all fields
setFieldError() // Set error for field
clearFieldError()// Clear error for field
clearErrors() // Clear all errors
Tips & Tricks
1. Auto-validate on blur
const input = useInput('', {
onValidate: myValidator
})
<input
onBlur={input.handlers.onBlur} // Auto-validates
/>
2. Show errors only after touched
{input.isTouched && input.error && <span>{input.error}</span>}
3. Disable submit while validating
<button disabled={!validation.isValid || !input.isDirty}>
Submit
</button>
4. Display touched state
{input.isTouched && !input.isValid && <span className="warn">Invalid</span>}
5. Dynamic field defaults
const fields = useFieldArray(
Array(3).fill(null).map(() => ({ name: '', value: '' })),
{ maxFields: 10 }
)
6. Combine validations
const isFormValid =
name.isValid &&
email.isValid &&
agree.isValid &&
fields.handlers.validateAll()
7. Dependent field validation
const fields = useFieldArray([], {
validateField: (val, idx) => {
// Check for duplicates in other fields
if (idx > 0) {
const prev = fields.fields[idx - 1]?.value
if (prev && prev.name === val.name) {
return 'Duplicate name'
}
}
return ''
}
})
Best Practices
- Use
useValidationfor form-level schema - Define validation rules centrally - Leverage
isDirty- Only show errors after user changes field - Use
isTouched- Show errors after blur, not on initial render - Reset on success - Clear form after successful submission
- Combine hooks - Use multiple hooks for complex forms
- Type your data - Use TypeScript for form data structures
- Memoize callbacks - Use
useCallbackin form components - Handle async validation - Use custom validators for API calls
Common Patterns
Pattern: Form with Async Validation
const email = useInput('', {
onValidate: async (v) => {
const exists = await checkEmailExists(v)
return exists ? 'Email already registered' : ''
}
})
Pattern: Conditional Fields
const country = useSelect('us', { options: countries })
const province = useSelect(null, {
options: country.value === 'ca' ? caProvinces : [],
onValidate: (v) => country.value === 'ca' && !v ? 'Required' : ''
})
Pattern: Dependent Validation
const password = useInput('')
const confirm = useInput('', {
onValidate: (v) => v !== password.value ? 'Passwords must match' : ''
})
Pattern: Multi-Select with Limit
const tags = useSelect([], {
options: availableTags,
isMulti: true,
onValidate: (values) => {
if (values.length < 2) return 'Select at least 2'
if (values.length > 5) return 'Select max 5'
return ''
}
})