
Formedible
- 5 installs
- 47 repo stars
- Updated May 25, 2026
- dimitrigilbert/formedible
Build declarative schema-driven React forms with Formedible using a fields array over TanStack Form.
About
Expert knowledge for the Formedible React form library, a declarative schema-driven wrapper over TanStack Form. A developer uses it to build type-safe multi-page forms from field configuration.
- Declarative schema-driven forms via useFormedible with a fields array, not manual JSX
- Built on TanStack Form with 22+ field types, multi-page forms, and Zod validation
Formedible by the numbers
- 5 all-time installs (skills.sh)
- Ranked #1,791 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dimitrigilbert/formedible --skill formedibleAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 47 |
| Last updated | May 25, 2026 |
| Repository | dimitrigilbert/formedible ↗ |
What it does
Build declarative schema-driven React forms with Formedible using a fields array over TanStack Form.
Files
Formedible Skill
Formedible is a DECLARATIVE and SCHEMA-DRIVEN form library.
The schema is the single source of truth. Define forms via fields array configuration, NOT manual JSX.
Use this skill when working with Formedible forms - creating, debugging, or extending functionality.
Quick Start
⚠️ Formedible is DECLARATIVE and SCHEMA-DRIVEN. Do NOT write manual JSX for fields! import { useFormedible } from "@/hooks/use-formedible"; import { z } from "zod"; import { toast } from "sonner";
const schema = z.object({ name: z.string().min(2), email: z.string().email(), });
const { Form } = useFormedible({ schema, fields: [ { name: "name", type: "text", label: "Name" }, { name: "email", type: "email", label: "Email" }, ], formOptions: { defaultValues: { name: "", email: "" }, onSubmit: async ({ value }) => { toast.success("Form submitted!"); console.log(value); }, }, });
return <Form className="space-y-4" />;
## Field Types Quick Reference
| Type | Key Config |
|------|------------|
| `text` | Basic text input |
| `email` | Email validation |
| `password` | Password field |
| `textarea` | `textareaConfig: { rows, showWordCount, maxLength }` |
| `number` | `min, max, step` |
| `date` | `dateConfig: { disablePastDates, disableFutureDates }` |
| `select` | `options: []` (static or function) |
| `radio` | `options: []` |
| `multiSelect` | `multiSelectConfig: { maxSelections, searchable }` |
| `checkbox` | Boolean checkbox |
| `switch` | Toggle switch |
| `rating` | `ratingConfig: { max, allowHalf, icon }` |
| `phone` | `phoneConfig: { format, defaultCountry }` |
| `array` | `arrayConfig: { itemType, minItems, maxItems, sortable, objectConfig }` |
## Key Examples (Self-Contained)
### Multi-Page Form with Dynamic Text
import { useFormedible } from "@/hooks/use-formedible"; import { z } from "zod"; import { toast } from "sonner";
const schema = z.object({ firstName: z.string().min(1), lastName: z.string().min(1), email: z.string().email(), plan: z.enum(["basic", "pro"]), });
const { Form } = useFormedible({ schema, fields: [ { name: "firstName", type: "text", label: "First Name", page: 1 }, { name: "lastName", type: "text", label: "Last Name", page: 1 }, { name: "email", type: "email", label: "Email", page: 2, description: "We'll contact {{firstName}} at {{email}}", // Dynamic text! }, { name: "plan", type: "radio", label: "Plan", page: 2, options: [ { value: "basic", label: "Basic - Free" }, { value: "pro", label: "Pro - $9/mo" }, ], }, ], pages: [ { page: 1, title: "Personal Info", description: "Tell us about yourself" }, { page: 2, title: "Contact", description: "How can we reach you, {{firstName}}?" }, ], progress: { showSteps: true, showPercentage: true }, formOptions: { defaultValues: { firstName: "", lastName: "", email: "", plan: "basic" as const, }, onSubmit: async ({ value }) => { toast.success("Registered!"); }, }, });
return <Form className="space-y-4" />;
### Conditional Fields AND Pages
const schema = z.object({ applicationType: z.enum(["individual", "business"]), firstName: z.string().optional(), companyName: z.string().optional(), });
const { Form } = useFormedible({ schema, fields: [ { name: "applicationType", type: "radio", label: "Application Type", page: 1, options: [ { value: "individual", label: "Individual" }, { value: "business", label: "Business" }, ], }, { name: "firstName", type: "text", label: "First Name", page: 2, conditional: (values: any) => values.applicationType === "individual", }, { name: "companyName", type: "text", label: "Company Name", page: 3, conditional: (values: any) => values.applicationType === "business", }, ], pages: [ { page: 1, title: "Type" }, { page: 2, title: "Personal Info", conditional: (values: any) => values.applicationType === "individual", }, { page: 3, title: "Business Info", conditional: (values: any) => values.applicationType === "business", }, ], formOptions: { defaultValues: { applicationType: "individual" as const, firstName: "", companyName: "", }, onSubmit: async ({ value }) => { console.log(value); }, }, });
### Tabbed Form
const schema = z.object({ firstName: z.string(), theme: z.enum(["light", "dark"]), notifications: z.boolean(), });
const { Form } = useFormedible({ schema, fields: [ { name: "firstName", type: "text", label: "Name", tab: "personal" }, { name: "theme", type: "select", label: "Theme", tab: "preferences", options: [ { value: "light", label: "Light" }, { value: "dark", label: "Dark" }, ], }, { name: "notifications", type: "switch", label: "Enable Notifications", tab: "preferences", }, ], tabs: [ { id: "personal", label: "Personal Info", description: "About you" }, { id: "preferences", label: "Preferences", description: "Settings" }, ], formOptions: { defaultValues: { firstName: "", theme: "light" as const, notifications: true, }, onSubmit: async ({ value }) => console.log(value), }, });
### Dynamic Options (Dependent Fields)
const schema = z.object({ country: z.string(), state: z.string(), });
const { Form } = useFormedible({ schema, fields: [ { name: "country", type: "select", label: "Country", options: [ { value: "us", label: "United States" }, { value: "ca", label: "Canada" }, ], }, { name: "state", type: "select", label: "State/Province", options: (values: any) => { if (values.country === "us") { return [ { value: "ca", label: "California" }, { value: "ny", label: "New York" }, ]; } if (values.country === "ca") { return [ { value: "on", label: "Ontario" }, { value: "qc", label: "Quebec" }, ]; } return []; }, }, ], formOptions: { defaultValues: { country: "", state: "" }, onSubmit: async ({ value }) => console.log(value), }, });
### Array Fields with Nested Objects
const schema = z.object({ teamMembers: z.array( z.object({ name: z.string().min(1), email: z.string().email(), role: z.enum(["dev", "design", "pm"]), }) ).min(1), });
const { Form } = useFormedible({ schema, fields: [ { name: "teamMembers", type: "array", label: "Team Members", section: { title: "Team Composition", description: "Add your team", }, arrayConfig: { itemType: "object", itemLabel: "Team Member", minItems: 1, maxItems: 10, sortable: true, addButtonLabel: "Add Member", defaultValue: { name: "", email: "", role: "dev", }, objectConfig: { fields: [ { name: "name", type: "text", label: "Name" }, { name: "email", type: "email", label: "Email" }, { name: "role", type: "select", label: "Role", options: [ { value: "dev", label: "Developer" }, { value: "design", label: "Designer" }, { value: "pm", label: "Product Manager" }, ], }, ], }, }, }, ], formOptions: { defaultValues: { teamMembers: [{ name: "", email: "", role: "dev" as const }], }, onSubmit: async ({ value }) => console.log(value), }, });
### Analytics with Proper Memoization
import React from "react";
const schema = z.object({ email: z.string().email(), });
// MUST use useCallback for analytics callbacks! const onFieldFocus = React.useCallback((fieldName: string, timestamp: number) => { console.log(Field "${fieldName}" focused at, timestamp); }, []);
const onFieldBlur = React.useCallback((fieldName: string, timeSpent: number) => { console.log(Field "${fieldName}" completed in ${timeSpent}ms); }, []);
const onFormComplete = React.useCallback((timeSpent: number, data: any) => { console.log(Form completed in ${timeSpent}ms, data); toast.success("Form completed!"); }, []);
// MUST useMemo the analytics config const analyticsConfig = React.useMemo( () => ({ onFieldFocus, onFieldBlur, onFormComplete, }), [onFieldFocus, onFieldBlur, onFormComplete] );
const { Form } = useFormedible({ schema, fields: [ { name: "email", type: "email", label: "Email" }, ], analytics: analyticsConfig, formOptions: { defaultValues: { email: "" }, onSubmit: async ({ value }) => console.log(value), }, });
### Rating Field with Config
const schema = z.object({ satisfaction: z.number().min(1).max(5), improvements: z.string().optional(), });
const { Form } = useFormedible({ schema, fields: [ { name: "satisfaction", type: "rating", label: "How satisfied are you?", ratingConfig: { max: 5, allowHalf: false, showValue: true, }, }, { name: "improvements", type: "textarea", label: "What can we improve?", conditional: (values: any) => values.satisfaction < 4, textareaConfig: { rows: 4, showWordCount: true, maxLength: 500, }, }, ], formOptions: { defaultValues: { satisfaction: 5, improvements: "" }, onSubmit: async ({ value }) => console.log(value), }, });
### Textarea with Configuration
const schema = z.object({ description: z.string().min(20).max(500), });
const { Form } = useFormedible({ schema, fields: [ { name: "description", type: "textarea", label: "Description", textareaConfig: { rows: 4, showWordCount: true, maxLength: 500, }, }, ], formOptions: { defaultValues: { description: "" }, onSubmit: async ({ value }) => console.log(value), }, });
## Critical Patterns
### 0. FORMEDIBLE IS DECLARATIVE AND SCHEMA DRIVEN (MOST IMPORTANT!)
// ❌ WRONG - Never write manual JSX for fields! <form> <input name="name" /> <input name="email" /> </form>
// ✅ CORRECT - Define fields declaratively via configuration! const { Form } = useFormedible({ schema: z.object({ name: z.string(), email: z.string().email(), }), fields: [ { name: "name", type: "text", label: "Name" }, { name: "email", type: "email", label: "Email" }, ], formOptions: { onSubmit: async ({ value }) => console.log(value), }, });
**THE SCHEMA IS THE SINGLE SOURCE OF TRUTH.** Field names in `fields` MUST match schema keys exactly!
### 1. Always Use className on Form
<Form className="space-y-4" />
### 2. Toast Notifications
import { toast } from "sonner";
onSubmit: async ({ value }) => { toast.success("Success!", { description: "Your data was saved", }); }
### 3. Use `as const` for Enums
defaultValues: { plan: "basic" as const, // ✅ role: "admin" as const, // ✅ }
### 4. Dynamic Options = Function
// ❌ Wrong options: [{ value: "a", label: "A" }]
// ✅ Correct options: (values) => { if (values.category === "tech") return techOptions; return []; }
### 5. Conditional Returns Boolean
// ❌ Wrong conditional: (values) => { if (values.type === "business") return true; }
// ✅ Correct conditional: (values) => values.type === "business"
### 6. Analytics Must Be Memoized
const callback = React.useCallback((...) => { ... }, []); const analytics = React.useMemo(() => ({ callback }), [callback]);
## Build Workflow (CRITICAL!)
**PACKAGES ARE SOURCE OF TRUTH**
1. Edit: `packages/formedible/src/...`
2. Build: `npm run build:pkg`
3. Sync: `node scripts/quick-sync.js`
4. Build web: `npm run build:web`
5. Sync components: `npm run sync-components`
6. Build web: `npm run build:web`
**NEVER edit web app files directly!**
## Common Issues
| Issue | Fix |
|-------|-----|
| Field not showing | Check field type in `field-registry.tsx` |
| Dynamic options not updating | Use function: `options: (values) => {...}` |
| Validation not showing | Schema names must match field names exactly |
| Conditional always hidden | Return boolean, never undefined |
| Analytics not firing | Use `React.useCallback` + `React.useMemo` |
| Pages not working | Pages start at 1, must be sequential |
## Type Safety
const schema = z.object({ name: z.string(), age: z.number(), });
type FormValues = z.infer<typeof schema>;
const { Form } = useFormedible<FormValues>({ schema, formOptions: { defaultValues: { name: "", // Type-safe age: 0, // Type-safe }, }, });
## File Structure Reference
packages/formedible/src/ ├── hooks/use-formedible.tsx # Main hook ├── components/formedible/ │ ├── fields/ # All 22 field components │ ├── layout/ # FormGrid, FormTabs, etc. │ └── ui/ # Radix UI primitives ├── lib/formedible/ │ ├── types.ts # TypeScript interfaces │ ├── field-registry.tsx # Field type mapping │ └── template-interpolation.ts # Dynamic text resolution
## Adding New Field Types
1. Create: `packages/formedible/src/components/formedible/fields/my-field.tsx`
2. Use `BaseFieldWrapper` for consistency
3. Add type to `packages/formedible/src/lib/formedible/types.ts`
4. Register in `packages/formedible/src/lib/formedible/field-registry.tsx`
5. Add to `packages/formedible/registry.json`
See [FIELD_TEMPLATES.md](references/FIELD_TEMPLATES.md) for templates.
Formedible Skill - Installation Complete
The Formedible skill has been successfully created and is ready to use.
Skill Structure
formedible/
├── SKILL.md # Main skill instructions
├── README.md # This file
├── references/
│ ├── FIELD_TEMPLATES.md # Templates for creating new field types
│ ├── COMMON_PATTERNS.md # Reusable form patterns
│ └── DEBUGGING.md # Troubleshooting guide
└── templates/
├── form-template.tsx # Quick-start form template
└── multi-page-form-template.tsx # Multi-page wizard templateWhat This Skill Provides
Core Knowledge (SKILL.md)
- Quick reference for all 22 field types
- Dynamic options and dynamic text patterns
- Conditional rendering and cross-field validation
- Multi-page forms, tabs, and analytics
- Architecture patterns and best practices
- Critical workflow rules for development
- Type safety guidelines
Field Templates (references/FIELD_TEMPLATES.md)
- Basic field template
- Selection field template
- Boolean field template
- Complex field template
- Integration steps for adding new fields
Common Patterns (references/COMMON_PATTERNS.md)
- Dependent fields (country/state selection)
- Password confirmation
- Date range validation
- Terms and conditions
- Dynamic field arrays
- Search with debounce
- Auto-save forms
- Multi-page wizards
- Conditional sections
- Analytics tracking
- Nested objects
- Tabbed forms
- Custom ratings
- File uploads
- Slider with value mapping
Debugging Guide (references/DEBUGGING.md)
- Common issues and solutions
- Diagnostics commands
- Debug mode setup
- Performance debugging
- Browser console commands
Quick Templates (templates/)
- Basic form scaffold
- Multi-page wizard scaffold
When to Use This Skill
Invoke this skill when:
- Creating new Formedible forms
- Adding custom field types to Formedible
- Debugging form issues
- Implementing conditional logic or dynamic options
- Setting up multi-page forms or analytics
- Working with form persistence
Usage
The skill will be automatically available when working on Formedible-related tasks. It provides context-aware guidance based on your specific task.
Key Principles Embedded
1. Packages are source of truth - Never edit web app files directly 2. Always use BaseFieldWrapper - For consistent field behavior 3. Type safety first - Always infer types from Zod schemas 4. Analytics-aware - Use eventHandlers for tracking 5. Performance optimized - Use TanStack Form selectors
Development Workflow Reminder
# Work in packages first
# Edit: packages/formedible/src/...
# Then sync
npm run build:pkg
node scripts/quick-sync.js
npm run build:web
npm run sync-components---
Created with expertise from Formedible codebase analysis.
Common Form Patterns
Reusable patterns for common form scenarios in Formedible.
Dependent Fields
Pattern: Field B options depend on Field A value.
const { Form } = useFormedible({
fields: [
{
name: "country",
type: "select",
label: "Country",
options: [
{ value: "us", label: "United States" },
{ value: "ca", label: "Canada" },
{ value: "uk", label: "United Kingdom" },
],
},
{
name: "state",
type: "select",
label: "State/Province",
conditional: (values) => !!values.country,
options: (values) => {
if (values.country === "us") return US_STATES;
if (values.country === "ca") return CA_PROVINCES;
if (values.country === "uk") return UK_REGIONS;
return [];
},
},
],
});Password Confirmation
Pattern: Password field with confirmation and matching validation.
const schema = z.object({
password: z.string().min(8, "Password must be at least 8 characters"),
confirmPassword: z.string(),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "password",
type: "password",
label: "Password",
passwordConfig: {
showStrengthIndicator: true,
},
},
{
name: "confirmPassword",
type: "password",
label: "Confirm Password",
},
],
crossFieldValidation: [
{
fields: ["password", "confirmPassword"],
validator: (values) => {
if (values.password !== values.confirmPassword) {
return "Passwords do not match";
}
return null;
},
},
],
});Date Range Validation
Pattern: Start date must be before end date.
const { Form } = useFormedible({
fields: [
{
name: "startDate",
type: "date",
label: "Start Date",
dateConfig: {
disablePastDates: true,
},
},
{
name: "endDate",
type: "date",
label: "End Date",
dateConfig: {
disablePastDates: true,
disableDate: (date, values) => {
if (values.startDate) {
return date < new Date(values.startDate);
}
return false;
},
},
},
],
crossFieldValidation: [
{
fields: ["startDate", "endDate"],
validator: (values) => {
if (values.startDate && values.endDate && values.startDate > values.endDate) {
return "Start date must be before end date";
}
return null;
},
},
],
});Terms and Conditions
Pattern: Checkbox that must be accepted to submit.
const schema = z.object({
termsAccepted: z.boolean().refine((val) => val === true, {
message: "You must accept the terms and conditions",
}),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "termsAccepted",
type: "checkbox",
label: "I accept the terms and conditions",
required: true,
help: {
link: {
url: "/terms",
text: "Read terms and conditions",
},
},
},
],
});Dynamic Field Arrays
Pattern: Add/remove dynamic items.
const schema = z.object({
teamMembers: z.array(z.object({
name: z.string().min(1),
email: z.string().email(),
role: z.string(),
})).min(1),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "teamMembers",
type: "array",
label: "Team Members",
arrayConfig: {
itemType: "object",
itemLabel: "Team Member",
minItems: 1,
maxItems: 10,
sortable: true,
addButtonLabel: "Add Team Member",
defaultValue: {
name: "",
email: "",
role: "developer",
},
fields: [
{ name: "name", type: "text", label: "Name" },
{ name: "email", type: "email", label: "Email" },
{
name: "role",
type: "select",
label: "Role",
options: [
{ value: "developer", label: "Developer" },
{ value: "designer", label: "Designer" },
{ value: "manager", label: "Manager" },
],
},
],
},
},
],
});Search with Debounce
Pattern: Search field with async validation.
const { Form } = useFormedible({
fields: [
{
name: "username",
type: "text",
label: "Username",
},
],
asyncValidation: {
username: {
validator: async (value) => {
if (!value || value.length < 3) return null;
const response = await fetch(`/api/check-username/${value}`);
const { available } = await response.json();
return available ? null : "Username is already taken";
},
debounceMs: 500,
loadingMessage: "Checking username availability...",
},
},
});Form with Auto-Save
Pattern: Auto-save to localStorage.
const { Form } = useFormedible({
fields: [
{ name: "title", type: "text", label: "Title" },
{ name: "content", type: "textarea", label: "Content" },
],
persistence: {
key: "blog-post-draft",
storage: "localStorage",
debounceMs: 2000,
exclude: [], // Save all fields
restoreOnMount: true,
},
onFormRestore: (restoredData) => {
toast.success("Draft restored from previous session");
},
});Multi-Page Form with Progress
Pattern: Multi-step wizard with progress tracking.
const { Form } = useFormedible({
fields: [
// Page 1
{ name: "firstName", type: "text", label: "First Name", page: 1 },
{ name: "lastName", type: "text", label: "Last Name", page: 1 },
// Page 2
{ name: "email", type: "email", label: "Email", page: 2 },
{ name: "phone", type: "phone", label: "Phone", page: 2 },
// Page 3
{ name: "company", type: "text", label: "Company", page: 3 },
{ name: "role", type: "text", label: "Role", page: 3 },
],
pages: [
{ page: 1, title: "Personal Information", description: "Tell us about yourself" },
{ page: 2, title: "Contact Details", description: "How can we reach you?" },
{ page: 3, title: "Professional Info", description: "Your work details" },
],
progress: {
showSteps: true,
showPercentage: true,
className: "mb-6",
},
analytics: {
onPageChange: (from, to, timeSpent) => {
console.log(`User moved from page ${from} to ${to} in ${timeSpent}ms`);
},
},
});Conditional Sections
Pattern: Show entire sections based on conditions.
const { Form } = useFormedible({
fields: [
{
name: "accountType",
type: "radio",
label: "Account Type",
options: [
{ value: "individual", label: "Individual" },
{ value: "business", label: "Business" },
],
},
{
name: "firstName",
type: "text",
label: "First Name",
conditional: (values) => values.accountType === "individual",
},
{
name: "lastName",
type: "text",
label: "Last Name",
conditional: (values) => values.accountType === "individual",
},
{
name: "companyName",
type: "text",
label: "Company Name",
conditional: (values) => values.accountType === "business",
},
{
name: "taxId",
type: "text",
label: "Tax ID",
conditional: (values) => values.accountType === "business",
},
],
});Form with Analytics
Pattern: Track all user interactions.
const { Form } = useFormedible({
fields: [
{ name: "email", type: "email", label: "Email" },
{ name: "name", type: "text", label: "Name" },
],
analytics: {
onFormStart: (timestamp) => {
gtag("event", "form_start", { timestamp });
},
onFieldFocus: (fieldName, timestamp) => {
gtag("event", "field_focus", { field: fieldName });
},
onFieldBlur: (fieldName, timeSpent) => {
gtag("event", "field_blur", { field: fieldName, duration: timeSpent });
},
onFieldError: (fieldName, errors, timestamp) => {
gtag("event", "field_error", { field: fieldName, errors: errors.length });
},
onFormComplete: (timeSpent, data) => {
gtag("event", "form_complete", { time_spent: timeSpent });
},
onFormAbandon: (completion, context) => {
gtag("event", "form_abandon", {
completion_percent: completion,
current_page: context.currentPage,
});
},
},
});Nested Object Field
Pattern: Group related fields in a collapsible section.
const { Form } = useFormedible({
fields: [
{
name: "address",
type: "object",
objectConfig: {
title: "Address Information",
description: "Enter your complete address",
collapsible: true,
collapsed: false,
fields: [
{ name: "street", type: "text", label: "Street Address" },
{ name: "city", type: "text", label: "City" },
{
name: "state",
type: "select",
label: "State",
options: STATE_OPTIONS,
},
{ name: "zipCode", type: "text", label: "ZIP Code" },
],
},
},
],
});Tabbed Form
Pattern: Organize fields into tabs.
const { Form } = useFormedible({
fields: [
{ name: "name", type: "text", label: "Name", tab: "personal" },
{ name: "email", type: "email", label: "Email", tab: "personal" },
{ name: "company", type: "text", label: "Company", tab: "professional" },
{ name: "role", type: "text", label: "Role", tab: "professional" },
{ name: "bio", type: "textarea", label: "Bio", tab: "additional" },
],
tabs: [
{ id: "personal", label: "Personal Info" },
{ id: "professional", label: "Professional" },
{ id: "additional", label: "Additional" },
],
});Rating with Custom Icons
Pattern: Custom rating visualization.
const { Form } = useFormedible({
fields: [
{
name: "satisfaction",
type: "rating",
label: "How satisfied are you?",
ratingConfig: {
icon: "heart", // or "star", "thumb"
max: 5,
allowHalf: false,
clearable: true,
size: "md",
colors: {
active: "#ef4444",
inactive: "#e5e7eb",
},
},
},
],
});File Upload with Validation
Pattern: File upload with size and type restrictions.
const { Form } = useFormedible({
fields: [
{
name: "documents",
type: "file",
label: "Upload Documents",
fileConfig: {
maxFiles: 5,
maxSize: 5 * 1024 * 1024, // 5MB
accept: ".pdf,.doc,.docx",
multiple: true,
showPreview: true,
},
},
],
});Slider with Value Mapping
Pattern: Map slider values to display values.
const { Form } = useFormedible({
fields: [
{
name: "energyRating",
type: "slider",
label: "Energy Efficiency",
sliderConfig: {
min: 1,
max: 7,
step: 1,
valueMapping: [
{ sliderValue: 1, displayValue: "A", label: "Excellent" },
{ sliderValue: 2, displayValue: "B", label: "Very Good" },
{ sliderValue: 3, displayValue: "C", label: "Good" },
{ sliderValue: 4, displayValue: "D", label: "Fair" },
{ sliderValue: 5, displayValue: "E", label: "Poor" },
{ sliderValue: 6, displayValue: "F", label: "Very Poor" },
{ sliderValue: 7, displayValue: "G", label: "Terrible" },
],
showValue: true,
gradientColors: {
start: "#22c55e",
end: "#ef4444",
},
},
},
],
});Formedible Debugging Guide
Quick reference for debugging common Formedible issues.
Diagnostics Commands
# Check if package is built
ls -la packages/formedible/dist
# Sync components to web app
node scripts/quick-sync.js
# Rebuild everything
npm run build
# Check registry.json
cat packages/formedible/registry.json | grep "your-field-name"Common Issues & Solutions
Issue: Field not rendering
Symptoms: Field doesn't appear in the form
Checks: 1. Field is registered in field-registry.tsx 2. Field type in config matches registry key exactly 3. Field is exported from index 4. Component exists in correct path
Fix:
// Check field-registry.tsx
export const FIELD_COMPONENTS = {
myField: MyField, // Must match type: "myField"
} as const;
// Check usage
{ name: "myField", type: "myField" } // Must match registryIssue: Dynamic options not updating
Symptoms: Select options don't change when dependent field changes
Checks: 1. Options function receives values parameter 2. Function is not memoized incorrectly 3. Dependencies are extracted correctly
Fix:
// ❌ Wrong - static options
options: [{ value: "a", label: "A" }]
// ✅ Correct - dynamic options
options: (values) => {
if (values.category === "tech") return techOptions;
return [];
}Issue: Validation not showing
Symptoms: Errors don't display when validation fails
Checks: 1. Zod schema field names match form field names exactly 2. Validation mode is set (default: onSubmit) 3. Error meta is being accessed correctly
Fix:
// Check schema matches fields
const schema = z.object({
emailAddress: z.string().email(), // ❌ Wrong name
email: z.string().email(), // ✅ Correct - matches field name
});
// Check field config
{ name: "email", type: "email" } // ✅ Matches schemaIssue: Multi-page navigation not working
Symptoms: Next/Previous buttons don't work or show wrong fields
Checks: 1. Page numbers are sequential starting from 1 2. All fields on a page have the same page number 3. Pages array matches page numbers 4. visiblePages is being computed correctly
Fix:
// ✅ Correct - pages start at 1
fields: [
{ name: "name", page: 1 },
{ name: "email", page: 1 },
{ name: "company", page: 2 },
]
pages: [
{ page: 1, title: "Personal" },
{ page: 2, title: "Company" },
]Issue: Analytics not firing
Symptoms: Analytics callbacks not being triggered
Checks: 1. Analytics config is set on useFormedible 2. Event handlers use fieldApi.eventHandlers 3. Analytics callbacks are configured for the events you need
Fix:
// ✅ Use eventHandlers for field events
fieldApi.eventHandlers.onFocus?.()
fieldApi.eventHandlers.onBlur?.()
fieldApi.eventHandlers.onChange?.(value)
// ✅ Configure analytics
analytics: {
onFieldFocus: (fieldName, timestamp) => {
console.log("Focused:", fieldName);
},
}Issue: Persistence not saving
Symptoms: Form data not saved to localStorage
Checks: 1. Storage key is unique 2. Browser has localStorage available 3. No sensitive fields in exclude array 4. restoreOnMount is set if auto-restore needed
Fix:
persistence: {
key: "unique-form-key", // ✅ Unique across forms
storage: "localStorage",
exclude: ["password"], // ✅ Exclude sensitive fields
restoreOnMount: true, // ✅ Auto-restore on load
}Issue: Conditional fields always hidden
Symptoms: Conditional fields never show even when condition is met
Checks: 1. Conditional function returns boolean (not undefined) 2. Function receives values parameter 3. Referenced fields have values when checking
Fix:
// ❌ Wrong - returns undefined
conditional: (values) => {
if (values.type === "premium") return true;
}
// ✅ Correct - returns boolean
conditional: (values) => values.type === "premium"Issue: Template strings not interpolating
Symptoms: {{fieldName}} shows as literal text instead of value
Checks: 1. Template syntax is correct: {{fieldName}} 2. Field being referenced exists and has a value 3. Field is in dependencies list (automatic for most cases)
Fix:
// ✅ Correct syntax
{
name: "email",
description: "Contact {{firstName}} at {{email}}", // ✅ Valid
}Issue: Cross-field validation not running
Symptoms: Cross-field errors never show
Checks: 1. All fields in validation exist 2. Validator function returns string (error) or null 3. Validation is triggered (onSubmit, onChange, etc.)
Fix:
crossFieldValidation: [
{
fields: ["password", "confirmPassword"], // ✅ Both fields exist
validator: (values) => {
if (values.password !== values.confirmPassword) {
return "Passwords do not match"; // ✅ Returns error string
}
return null; // ✅ Returns null when valid
},
},
]Issue: Async validation stuck loading
Symptoms: Loading state never clears
Checks: 1. Validator function returns a value (never undefined) 2. Debounce is not too long 3. No network errors in console
Fix:
asyncValidation: {
username: {
validator: async (value) => {
if (!value) return null; // ✅ Handle empty case
try {
const response = await fetch(`/api/check/${value}`);
const data = await response.json();
return data.available ? null : "Taken"; // ✅ Always return
} catch {
return "Failed to check"; // ✅ Handle errors
}
},
debounceMs: 500, // ✅ Reasonable debounce
},
}Issue: TypeScript errors on form values
Symptoms: Type errors when accessing form values
Checks: 1. Generic type parameter matches schema 2. DefaultValues match schema type 3. Schema is properly inferred
Fix:
const schema = z.object({
name: z.string(),
age: z.number(),
});
type FormValues = z.infer<typeof schema>;
const { Form } = useFormedible<FormValues>({ // ✅ Generic type
schema,
formOptions: {
defaultValues: {
name: "", // ✅ Matches schema
age: 0, // ✅ Matches schema
},
onSubmit: async ({ value }) => {
console.log(value.name); // ✅ Type-safe access
},
},
});Debug Mode
Enable debug logging in useFormedible:
const { Form } = useFormedible({
// ... config
// Add temporary logging
formOptions: {
onSubmit: async ({ value, formApi }) => {
console.log("Form value:", value);
console.log("Form state:", formApi.state);
console.log("Field meta:", formApi.getFieldMeta("fieldName"));
},
},
});Performance Debugging
Check for unnecessary re-renders:
// Add React DevTools Profiler
// Look for fields re-rendering when unrelated fields change
// Common cause: Not using TanStack Form selectors
// ❌ Bad - subscribes to entire state
<form.Subscribe>
{(state) => <div>{state.canSubmit}</div>}
</form.Subscribe>
// ✅ Good - subscribes only to canSubmit
<form.Subscribe selector={(state) => ({ canSubmit: state.canSubmit })}>
{({ canSubmit }) => <div>{canSubmit}</div>}
</form.Subscribe>Getting Field Info
Inspect field state and metadata:
// In your component
const form = useFormedible({ /* config */ });
// Log field info
console.log("Field value:", form.form.getFieldValue("fieldName"));
console.log("Field meta:", form.form.getFieldMeta("fieldName"));
console.log("Field errors:", form.form.getFieldInfo("fieldName").validationMeta.errors);Browser Console Commands
Open browser console and run:
// Check localStorage
console.log(JSON.parse(localStorage.getItem("your-form-key")));
// Check form state (add this temporarily to component)
useEffect(() => {
console.log("Form state:", form.state);
}, [form.state]);Field Component Templates
Templates for creating new Formedible field types.
Basic Field Template
Use this as starting point for simple field types:
// packages/formedible/src/components/formedible/fields/my-field.tsx
import { type FieldApi } from "@tanstack/react-form";
import { BaseFieldWrapper } from "./base-field-wrapper";
export interface MyFieldConfig {
// Add field-specific config here
customOption?: string;
maxLength?: number;
}
export interface MyFieldProps {
fieldApi: FieldApi<any, any>;
label?: string;
description?: string;
required?: boolean;
fieldConfig?: MyFieldConfig;
className?: string;
}
export function MyField({
fieldApi,
label,
description,
required,
fieldConfig,
className,
}: MyFieldProps) {
return (
<BaseFieldWrapper
fieldApi={fieldApi}
label={label}
description={description}
required={required}
className={className}
>
{(field) => (
<input
type="text"
value={field.fieldApi.getValue() ?? ""}
onChange={(e) => field.fieldApi.handleChange(e.target.value)}
onFocus={() => field.fieldApi.eventHandlers.onFocus?.()}
onBlur={() => field.fieldApi.eventHandlers.onBlur?.()}
maxLength={fieldConfig?.maxLength}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2"
/>
)}
</BaseFieldWrapper>
);
}Selection Field Template
For dropdown/select-style fields:
import { normalizeOptions } from "@/lib/utils";
export function MySelectField({ fieldApi, options, ...props }: MySelectFieldProps) {
const normalizedOptions = normalizeOptions(options);
return (
<BaseFieldWrapper {...props}>
{(field) => (
<select
value={field.fieldApi.getValue() ?? ""}
onChange={(e) => field.fieldApi.handleChange(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2"
>
<option value="">Select...</option>
{normalizedOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
)}
</BaseFieldWrapper>
);
}Boolean Field Template
For checkboxes and switches:
export function MyCheckboxField({ fieldApi, label, ...props }: MyCheckboxFieldProps) {
return (
<BaseFieldWrapper {...props}>
{(field) => (
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={field.fieldApi.getValue() ?? false}
onChange={(e) => field.fieldApi.handleChange(e.target.checked)}
onFocus={() => field.fieldApi.eventHandlers.onFocus?.()}
onBlur={() => field.fieldApi.eventHandlers.onBlur?.()}
className="h-4 w-4 rounded border-input"
/>
<span>{label}</span>
</label>
)}
</BaseFieldWrapper>
);
}Complex Field Template
For fields with multiple inputs or complex UI:
import { useState } from "react";
export function MyComplexField({ fieldApi, fieldConfig, ...props }: MyComplexFieldProps) {
const [localState, setLocalState] = useState(false);
const value = fieldApi.getValue() ?? "";
const handleChange = (newValue: string) => {
fieldApi.handleChange(newValue);
fieldApi.eventHandlers.onChange?.(newValue);
};
return (
<BaseFieldWrapper {...props}>
{(field) => (
<div className="space-y-2">
<input
type="text"
value={value}
onChange={(e) => handleChange(e.target.value)}
onFocus={() => field.fieldApi.eventHandlers.onFocus?.()}
onBlur={() => field.fieldApi.eventHandlers.onBlur?.()}
className="flex h-10 w-full rounded-md border border-input"
/>
{fieldConfig?.showExtra && (
<div className="text-sm text-muted-foreground">
Additional UI here
</div>
)}
</div>
)}
</BaseFieldWrapper>
);
}Integration Steps
After creating a new field component:
1. Add Type Definition
// packages/formedible/src/lib/formedible/types.ts
export interface FieldConfig {
// ... existing props
myFieldConfig?: {
customOption?: string;
maxLength?: number;
showExtra?: boolean;
};
}2. Register Field Component
// packages/formedible/src/lib/formedible/field-registry.tsx
import { MyField } from "@/components/formedible/fields/my-field";
export const FIELD_COMPONENTS = {
// ... existing fields
myField: MyField,
} as const;3. Add to Registry
// packages/formedible/registry.json
{
"components": [
{
"name": "my-field",
"registryDependencies": ["base-field-wrapper"],
"files": [
{
"path": "src/components/formedible/fields/my-field.tsx",
"content": "..."
}
],
"type": "components:formedible"
}
]
}4. Use in Form
const { Form } = useFormedible({
fields: [
{
name: "myField",
type: "myField",
label: "My Custom Field",
myFieldConfig: {
customOption: "value",
maxLength: 100,
},
},
],
});Best Practices
1. Always use BaseFieldWrapper for consistent behavior 2. Use eventHandlers for analytics tracking 3. Support dynamic options with function form 4. Handle undefined values with ?? "" or ?? false 5. Include proper TypeScript types 6. Follow existing patterns - copy from similar fields 7. Test with conditional rendering and multi-page forms 8. Support all common props (label, description, required, className)
/**
* Formedible Form Template
* Copy this template to quickly create a new form
*/
import { useFormedible } from "@/hooks/use-formedible";
import { z } from "zod";
// Step 1: Define your Zod schema
const schema = z.object({
// Add your fields here
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Please enter a valid email"),
});
// Step 2: Infer TypeScript types
type FormValues = z.infer<typeof schema>;
export function MyForm() {
// Step 3: Configure your form
const { Form } = useFormedible<FormValues>({
schema,
// Step 4: Define your fields
fields: [
{
name: "name",
type: "text",
label: "Full Name",
placeholder: "John Doe",
description: "Enter your full name",
},
{
name: "email",
type: "email",
label: "Email Address",
placeholder: "john@example.com",
inlineValidation: {
enabled: true,
showSuccess: true,
},
},
],
// Step 5: Configure form options
formOptions: {
defaultValues: {
name: "",
email: "",
},
onSubmit: async ({ value }) => {
console.log("Form submitted:", value);
// Handle submission here
},
},
// Optional: Configure persistence
persistence: {
key: "my-form-draft",
storage: "localStorage",
restoreOnMount: true,
},
// Optional: Configure analytics
analytics: {
onFormStart: (timestamp) => console.log("Form started", timestamp),
onFormComplete: (timeSpent, data) => console.log("Form completed", { timeSpent, data }),
},
});
return (
<div className="container mx-auto max-w-2xl py-8">
<h1 className="text-3xl font-bold mb-6">My Form</h1>
<Form />
</div>
);
}
/**
* Multi-Page Form Template
* For multi-step wizards with progress tracking
*/
import { useFormedible } from "@/hooks/use-formedible";
import { z } from "zod";
// Step 1: Define schema for all pages
const schema = z.object({
// Page 1: Personal Info
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
dateOfBirth: z.string(),
// Page 2: Contact Details
email: z.string().email("Invalid email"),
phone: z.string().min(10, "Valid phone number required"),
address: z.string().min(5, "Address is required"),
// Page 3: Preferences
notifications: z.boolean(),
newsletter: z.boolean(),
interests: z.array(z.string()),
});
type FormValues = z.infer<typeof schema>;
export function MultiPageForm() {
const { Form } = useFormedible<FormValues>({
schema,
// Step 2: Define fields with page numbers
fields: [
// Page 1: Personal Information
{ name: "firstName", type: "text", label: "First Name", page: 1 },
{ name: "lastName", type: "text", label: "Last Name", page: 1 },
{
name: "dateOfBirth",
type: "date",
label: "Date of Birth",
page: 1,
dateConfig: {
disableFutureDates: true,
},
},
// Page 2: Contact Details
{
name: "email",
type: "email",
label: "Email Address",
page: 2,
description: "We'll contact you at {{firstName}}",
},
{
name: "phone",
type: "phone",
label: "Phone Number",
page: 2,
phoneConfig: {
format: "international",
defaultCountry: "US",
},
},
{
name: "address",
type: "textarea",
label: "Address",
page: 2,
textareaConfig: {
rows: 3,
},
},
// Page 3: Preferences
{
name: "notifications",
type: "switch",
label: "Enable Notifications",
page: 3,
},
{
name: "newsletter",
type: "checkbox",
label: "Subscribe to Newsletter",
page: 3,
},
{
name: "interests",
type: "multiSelect",
label: "Areas of Interest",
page: 3,
options: [
{ value: "tech", label: "Technology" },
{ value: "design", label: "Design" },
{ value: "business", label: "Business" },
{ value: "marketing", label: "Marketing" },
],
multiSelectConfig: {
maxSelections: 3,
},
},
],
// Step 3: Define page configuration
pages: [
{
page: 1,
title: "Personal Information",
description: "Tell us about yourself",
},
{
page: 2,
title: "Contact Details",
description: "How can we reach you, {{firstName}}?",
},
{
page: 3,
title: "Preferences",
description: "Customize your experience",
},
],
// Step 4: Configure progress tracking
progress: {
showSteps: true,
showPercentage: true,
className: "mb-8",
},
// Step 5: Configure persistence
persistence: {
key: "multi-page-form",
storage: "localStorage",
restoreOnMount: true,
},
// Step 6: Add analytics
analytics: {
onFormStart: (timestamp) => console.log("Form started at", timestamp),
onPageChange: (from, to, timeSpent) => {
console.log(`Page ${from} → ${to} (${timeSpent}ms)`);
},
onFormComplete: (timeSpent, data) => {
console.log("Form completed", { timeSpent, data });
},
onFormAbandon: (completion, context) => {
console.log("Form abandoned", {
completion,
currentPage: context.currentPage,
});
},
},
// Step 7: Configure form submission
formOptions: {
defaultValues: {
firstName: "",
lastName: "",
dateOfBirth: "",
email: "",
phone: "",
address: "",
notifications: true,
newsletter: false,
interests: [],
},
onSubmit: async ({ value }) => {
console.log("Form submitted:", value);
// Handle submission
},
},
// Customize button labels
nextLabel: "Continue →",
previousLabel: "← Back",
submitLabel: "Complete Registration",
});
return (
<div className="container mx-auto max-w-2xl py-8">
<div className="mb-8 text-center">
<h1 className="text-3xl font-bold mb-2">Registration Wizard</h1>
<p className="text-muted-foreground">
Complete all steps to register your account
</p>
</div>
<Form />
</div>
);
}