Agent skill
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.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/forms
SKILL.md
Form Builder
Create forms using this project's centralized FNForm component.
Quick Start
typescript
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 |
|---|---|---|
text |
Input | Single-line text |
email |
Input type="email" | Email addresses |
password |
Input type="password" | Passwords |
number |
Input type="number" | Numeric values |
textarea |
Textarea | Multi-line text |
select |
Select dropdown | Choose from options |
checkbox |
Checkbox | Boolean with inline label |
switch |
Switch | Toggle with inline label |
hidden |
None (hidden) | Hidden values |
custom |
Your component | Anything else |
Field Definition
typescript
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:
typescript
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
typescript
{ name: 'email', type: 'email', label: 'Email', required: true }
// Shows * after label, validates on submit
Custom Validation
typescript
{
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
typescript
// 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
typescript
{
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:
typescript
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
typescript
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
typescript
{
name: 'price',
type: 'number',
label: 'Price',
prefix: '$', // Shows "$" inside the input
required: true,
}
Character Counter
typescript
{
name: 'description',
type: 'textarea',
label: 'Description',
maxLength: 500, // Shows "0 / 500 characters"
}
External Form Control
Use formRef to control the form from outside:
typescript
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
typescript
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:
typescript
<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
typescript
<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:
typescript
<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
typescript
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
typescript
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
typescript
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
typescript
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
src/components/ui/fn-form.tsx- Component sourcesrc/routes/$lang/account/addresses.tsx- Address form examplesrc/routes/admin/login.tsx- Login form exampleadmin-crudskill - Forms in admin context
Didn't find tool you were looking for?