Claude-skill-registry forms
Build forms with FNForm component including validation, grid layouts, custom fields, and external control. Use when creating forms, adding validation, or building complex form UIs.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/forms" ~/.claude/skills/majiayu000-claude-skill-registry-forms-a0bf8f && rm -rf "$T"
manifest:
skills/data/forms/SKILL.mdsource content
Form Builder
Create forms using this project's centralized
FNForm component.
Quick Start
import { FNForm, type FormDefinition } from '@/components/ui/fn-form' const formDefinition: FormDefinition = { fields: [ { name: 'email', type: 'email', label: 'Email', required: true }, { name: 'name', type: 'text', label: 'Name' }, ], } function MyForm() { const handleSubmit = (values: Record<string, unknown>) => { console.log(values) } return ( <FNForm formDefinition={formDefinition} onSubmit={handleSubmit} submitButtonText="Save" /> ) }
Field Types
| Type | Component | Use Case |
|---|---|---|
| Input | Single-line text |
| Input type="email" | Email addresses |
| Input type="password" | Passwords |
| Input type="number" | Numeric values |
| Textarea | Multi-line text |
| Select dropdown | Choose from options |
| Checkbox | Boolean with inline label |
| Switch | Toggle with inline label |
| None (hidden) | Hidden values |
| Your component | Anything else |
Field Definition
interface FieldDefinition { name: string // Form field name (required) type: FieldType // Input type (required) label: string // Display label (required) placeholder?: string // Placeholder text required?: boolean // Shows * and validates optional?: boolean // Shows "(optional)" disabled?: boolean // Disable input options?: SelectOption[] // For select type validate?: (value: unknown) => string | undefined validateOnChange?: boolean // Validate as user types className?: string // Wrapper class inputClassName?: string // Input class labelClassName?: string // Label class prefix?: string // Input prefix (e.g., "$") maxLength?: number // Shows character count render?: (props: CustomFieldRenderProps) => ReactNode }
Grid Layouts
Use
rows with columns for multi-column forms:
const formDefinition: FormDefinition = { rows: [ // Full width row { fields: [ { name: 'email', type: 'email', label: 'Email', required: true }, ], }, // Two column row { columns: 2, fields: [ { name: 'firstName', type: 'text', label: 'First Name', required: true, }, { name: 'lastName', type: 'text', label: 'Last Name', required: true }, ], }, // Three column row { columns: 3, fields: [ { name: 'city', type: 'text', label: 'City', required: true }, { name: 'state', type: 'text', label: 'State' }, { name: 'zip', type: 'text', label: 'ZIP', required: true }, ], }, ], }
Validation
Required Fields
{ name: 'email', type: 'email', label: 'Email', required: true } // Shows * after label, validates on submit
Custom Validation
{ name: 'email', type: 'email', label: 'Email', required: true, validate: (value) => { const email = value as string if (!email.includes('@company.com')) { return 'Must be a company email' } return undefined // No error }, validateOnChange: true, // Validate as user types }
Common Validators
// Email format validate: (value) => { const email = value as string if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return 'Invalid email format' } } // Min length validate: (value) => { if ((value as string).length < 8) { return 'Must be at least 8 characters' } } // Number range validate: (value) => { const num = Number(value) if (num < 0 || num > 100) { return 'Must be between 0 and 100' } } // Match another field (password confirmation) validate: (value, allValues) => { if (value !== allValues.password) { return 'Passwords do not match' } }
Select Dropdown
{ name: 'status', type: 'select', label: 'Status', placeholder: 'Select status', required: true, options: [ { value: 'draft', label: 'Draft' }, { value: 'active', label: 'Active' }, { value: 'archived', label: 'Archived' }, ], }
Custom Fields
For masked inputs, autocomplete, date pickers, or any special component:
import { IMaskInput } from 'react-imask' { name: 'phone', type: 'custom', label: 'Phone', render: (props) => ( <IMaskInput id={props.id} mask="+{1} (000) 000-0000" value={String(props.value ?? '')} onAccept={(value) => props.onChange(value)} onBlur={props.onBlur} disabled={props.disabled} placeholder={props.placeholder} className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" /> ), }
Custom Field Props
interface CustomFieldRenderProps { value: unknown // Current field value onChange: (value: unknown) => void // Update value onBlur: () => void // Trigger blur validation error?: string // Current error message id: string // Field ID for labels disabled?: boolean // Disabled state placeholder?: string // Placeholder text }
Prefix Input
{ name: 'price', type: 'number', label: 'Price', prefix: '$', // Shows "$" inside the input required: true, }
Character Counter
{ name: 'description', type: 'textarea', label: 'Description', maxLength: 500, // Shows "0 / 500 characters" }
External Form Control
Use
formRef to control the form from outside:
import { useRef } from 'react' import { FNForm, type FNFormRef } from '@/components/ui/fn-form' function MyForm() { const formRef = useRef<FNFormRef | null>(null) return ( <div> <FNForm formRef={formRef} hideSubmitButton // We'll use our own button formDefinition={formDefinition} onSubmit={handleSubmit} /> {/* External submit button */} <Button onClick={() => formRef.current?.submit()}> Save </Button> {/* External value access */} <Button onClick={() => { const values = formRef.current?.getValues() console.log(values) }}> Log Values </Button> </div> ) }
FormRef Methods
interface FNFormRef { submit: () => void // Trigger form submission setFieldValue: (name: string, value: unknown) => void // Set a field value getFieldValue: (name: string) => unknown // Get a field value setFieldError: (name: string, error: string) => void // Set a field error getValues: () => Record<string, unknown> // Get all values isSubmitting: boolean // Submission state }
Field Change Callbacks
React to field changes and update other fields:
<FNForm formDefinition={formDefinition} onSubmit={handleSubmit} onFieldChange={(name, value, setFieldValue) => { // When country changes, update country code if (name === 'country') { const countryCode = getCountryCode(value as string) setFieldValue('countryCode', countryCode) } // When "same as billing" is checked, copy address if (name === 'sameAsBilling' && value === true) { setFieldValue('shippingAddress', billingAddress) } }} />
Custom Submit Button
<FNForm formDefinition={formDefinition} onSubmit={handleSubmit} renderSubmitButton={(isSubmitting) => ( <Button type="submit" disabled={isSubmitting} className="w-full"> {isSubmitting ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Saving... </> ) : ( 'Save Changes' )} </Button> )} />
Before Submit Content
Add content above the submit button:
<FNForm formDefinition={formDefinition} onSubmit={handleSubmit} renderBeforeSubmit={(values) => ( <div className="rounded-lg bg-muted p-4"> <p className="text-sm text-muted-foreground"> Total: ${calculateTotal(values)} </p> </div> )} />
Complete Examples
Address Form
const addressForm: FormDefinition = { rows: [ { columns: 2, fields: [ { name: 'firstName', type: 'text', label: 'First Name', required: true, }, { name: 'lastName', type: 'text', label: 'Last Name', required: true }, ], }, { fields: [ { name: 'company', type: 'text', label: 'Company', optional: true }, ], }, { fields: [ { name: 'address1', type: 'text', label: 'Address', required: true }, ], }, { fields: [ { name: 'address2', type: 'text', label: 'Apartment, suite, etc.', optional: true, }, ], }, { columns: 3, fields: [ { name: 'city', type: 'text', label: 'City', required: true }, { name: 'province', type: 'text', label: 'State/Province', required: true, }, { name: 'zip', type: 'text', label: 'ZIP/Postal', required: true }, ], }, { columns: 2, fields: [ { name: 'country', type: 'select', label: 'Country', required: true, options: [ { value: 'US', label: 'United States' }, { value: 'CA', label: 'Canada' }, { value: 'GB', label: 'United Kingdom' }, ], }, { name: 'phone', type: 'text', label: 'Phone', optional: true }, ], }, ], }
Login Form
const loginForm: FormDefinition = { fields: [ { name: 'email', type: 'email', label: 'Email', required: true, placeholder: 'you@example.com', }, { name: 'password', type: 'password', label: 'Password', required: true, }, { name: 'remember', type: 'checkbox', label: 'Remember me', }, ], }
Product Form
const productForm: FormDefinition = { rows: [ { fields: [ { name: 'name', type: 'text', label: 'Product Name', required: true, maxLength: 100, }, ], }, { fields: [ { name: 'description', type: 'textarea', label: 'Description', maxLength: 500, }, ], }, { columns: 2, fields: [ { name: 'price', type: 'number', label: 'Price', prefix: '$', required: true, validate: (v) => Number(v) < 0 ? 'Price cannot be negative' : undefined, }, { name: 'compareAtPrice', type: 'number', label: 'Compare at Price', prefix: '$', optional: true, }, ], }, { columns: 2, fields: [ { name: 'status', type: 'select', label: 'Status', required: true, options: [ { value: 'draft', label: 'Draft' }, { value: 'active', label: 'Active' }, ], }, { name: 'quantity', type: 'number', label: 'Quantity', required: true, }, ], }, ], }
FNForm Props
interface FNFormProps { formDefinition: FormDefinition onSubmit: (values: Record<string, unknown>) => void | Promise<void> defaultValues?: Record<string, unknown> submitButtonText?: string hideSubmitButton?: boolean formRef?: React.RefObject<FNFormRef | null> onFieldChange?: ( name: string, value: unknown, setFieldValue: (name: string, value: unknown) => void, ) => void className?: string renderSubmitButton?: (isSubmitting: boolean) => React.ReactNode renderBeforeSubmit?: (values: Record<string, unknown>) => React.ReactNode }
See Also
- Component sourcesrc/components/ui/fn-form.tsx
- Address form examplesrc/routes/$lang/account/addresses.tsx
- Login form examplesrc/routes/admin/login.tsx
skill - Forms in admin contextadmin-crud