@nestledjs/forms
v0.6.4
Published
A flexible React form library supporting both declarative and imperative usage patterns with TypeScript support
Maintainers
Readme
@nestledjs/forms
A flexible React form library that supports both declarative and imperative usage patterns with full TypeScript support.
🚀 Features
- Dual API: Use declaratively with field arrays or imperatively with individual components
- TypeScript First: Full type safety and IntelliSense support
- Flexible: Mix and match declarative and imperative patterns
- Conditional Logic: Dynamic show/hide, required, and disabled field behavior based on form values
- Themeable: Customizable styling system
- Validation: Built-in validation with react-hook-form
- Read-only Support: Toggle between editable and read-only modes
- Rich Field Types: 20+ field types including text, email, select, date pickers, markdown editor, and more
📦 Installation
npm install @nestledjs/forms
# or
yarn add @nestledjs/forms
# or
pnpm add @nestledjs/forms📋 Requirements
For MarkdownEditor Component
If you plan to use the MarkdownEditor field type, you'll need to:
Install the peer dependency:
npm install @mdxeditor/editor # or yarn add @mdxeditor/editor # or pnpm add @mdxeditor/editorImport the required CSS in your application entry point (e.g.,
main.tsx,App.tsx):import '@mdxeditor/editor/style.css'
🎯 Quick Start
Declarative Usage (Recommended)
Perfect for forms where you can define all fields upfront:
import { Form, FormFieldClass } from '@nestledjs/forms'
function UserRegistrationForm() {
const fields = [
FormFieldClass.text('firstName', {
label: 'First Name',
required: true
}),
FormFieldClass.text('lastName', {
label: 'Last Name',
required: true
}),
FormFieldClass.email('email', {
label: 'Email Address',
required: true,
placeholder: '[email protected]'
}),
FormFieldClass.password('password', {
label: 'Password',
required: true,
validate: (value) => value.length >= 8 || 'Password must be at least 8 characters'
}),
FormFieldClass.select('role', {
label: 'Role',
options: [
{ value: 'user', label: 'User' },
{ value: 'admin', label: 'Admin' }
]
})
]
return (
<Form
id="registration-form"
fields={fields}
submit={(values) => {
console.log('Form submitted:', values)
// Handle form submission
}}
>
<button type="submit">Register</button>
</Form>
)
}Imperative Usage
Perfect for dynamic forms or when you need fine-grained control. Supports all the same features including conditional logic:
import { Form, RenderFormField, FormFieldClass } from '@nestledjs/forms'
function DynamicContactForm() {
return (
<Form
id="contact-form"
submit={(values) => console.log('Submitted:', values)}
>
<RenderFormField
field={FormFieldClass.text('name', { label: 'Name', required: true })}
/>
<RenderFormField
field={FormFieldClass.email('email', { label: 'Email', required: true })}
/>
<RenderFormField
field={FormFieldClass.checkbox('includePhone', {
label: 'Include phone number'
})}
/>
<RenderFormField
field={FormFieldClass.phone('phone', {
label: 'Phone Number',
showWhen: (values) => values.includePhone === true,
requiredWhen: (values) => values.includePhone === true
})}
/>
<RenderFormField
field={FormFieldClass.textArea('message', {
label: 'Message',
rows: 4,
placeholder: 'Tell us how we can help...'
})}
/>
<button type="submit">Send Message</button>
</Form>
)
}Mixed Usage
Combine both approaches for maximum flexibility:
import { Form, RenderFormField, FormFieldClass } from '@nestledjs/forms'
function MixedForm() {
const staticFields = [
FormFieldClass.text('username', { label: 'Username', required: true }),
FormFieldClass.email('email', { label: 'Email', required: true }),
]
return (
<Form
id="mixed-form"
fields={staticFields}
submit={(values) => console.log('Submitted:', values)}
>
{/* Static fields are rendered automatically from the fields prop */}
{/* Add dynamic fields as children */}
<RenderFormField
field={FormFieldClass.password('password', {
label: 'Password',
required: true
})}
/>
<RenderFormField
field={FormFieldClass.checkbox('terms', {
label: 'I agree to the terms and conditions',
required: true
})}
/>
<button type="submit">Sign Up</button>
</Form>
)
}Rich Text Editing with Markdown
Create forms with rich text editing capabilities:
import { Form, RenderFormField, FormFieldClass } from '@nestledjs/forms'
function BlogPostForm() {
const handleImageUpload = async (file: File): Promise<string> => {
// Upload image to your server/CDN
const formData = new FormData()
formData.append('image', file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
const { url } = await response.json()
return url
}
return (
<Form
id="blog-post-form"
submit={(values) => console.log('Blog post:', values)}
>
<RenderFormField
field={FormFieldClass.text('title', {
label: 'Post Title',
required: true,
placeholder: 'Enter your blog post title...'
})}
/>
<RenderFormField
field={FormFieldClass.text('slug', {
label: 'URL Slug',
placeholder: 'my-blog-post'
})}
/>
<RenderFormField
field={FormFieldClass.markdownEditor('content', {
label: 'Post Content',
required: true,
height: 400,
placeholder: 'Write your blog post content...\n\n**Use markdown** for formatting!\n\n- Bullet lists\n1. Numbered lists\n- [x] Checkboxes\n\n```javascript\nconsole.log("Code blocks work too!")\n```',
enableImageUpload: true,
imageUploadHandler: handleImageUpload,
maxImageSize: 5 * 1024 * 1024, // 5MB
allowedImageTypes: ['image/png', 'image/jpeg', 'image/gif'],
maxLength: 10000,
helpText: 'Supports markdown formatting, images, and code blocks'
})}
/>
<RenderFormField
field={FormFieldClass.select('category', {
label: 'Category',
options: [
{ value: 'tech', label: 'Technology' },
{ value: 'design', label: 'Design' },
{ value: 'business', label: 'Business' }
]
})}
/>
<RenderFormField
field={FormFieldClass.switch('published', {
label: 'Publish immediately'
})}
/>
<button type="submit">Save Post</button>
</Form>
)
}🛠️ Available Field Types
The FormFieldClass provides methods for creating all field types:
// Text inputs
FormFieldClass.text('field', { label: 'Text Field' })
FormFieldClass.textArea('field', { label: 'Text Area', rows: 4 })
FormFieldClass.email('field', { label: 'Email Field' })
FormFieldClass.password('field', { label: 'Password Field' })
FormFieldClass.url('field', { label: 'URL Field' })
FormFieldClass.phone('field', { label: 'Phone Field' })
// Rich text editing
FormFieldClass.markdownEditor('field', {
label: 'Content',
height: 300,
placeholder: 'Enter your markdown content...',
enableImageUpload: true,
maxLength: 5000
})
// Numbers and currency
FormFieldClass.number('field', { label: 'Number', min: 0, max: 100 })
FormFieldClass.currency('field', { label: 'Price', currency: 'USD' })
// Selections
FormFieldClass.select('field', {
label: 'Select',
options: [{ value: 'a', label: 'Option A' }]
})
FormFieldClass.multiSelect('field', {
label: 'Multi Select',
options: [{ value: 'a', label: 'Option A' }]
})
FormFieldClass.radio('field', {
label: 'Radio',
radioOptions: [{ value: 'a', label: 'Option A' }]
})
// Checkboxes and switches
FormFieldClass.checkbox('field', { label: 'Checkbox' })
FormFieldClass.switch('field', { label: 'Switch' })
// Date and time
FormFieldClass.datePicker('field', { label: 'Date' })
FormFieldClass.dateTimePicker('field', { label: 'Date & Time' })
FormFieldClass.timePicker('field', { label: 'Time' })
// Search and select fields (v0.4.17+ with enhanced clear functionality)
FormFieldClass.searchSelect('field', {
label: 'Search Select',
options: [{ value: 'a', label: 'Option A' }],
placeholder: 'Search or select...'
})
FormFieldClass.searchSelectApollo('field', {
label: 'Apollo Search Select',
document: MY_GRAPHQL_QUERY,
dataType: 'users',
searchFields: ['name', 'email', 'firstName'],
selectOptionsFunction: (items) => items.map(item => ({
value: item.id,
label: item.name
}))
})
FormFieldClass.searchSelectMulti('field', {
label: 'Multi Search Select',
options: [{ value: 'a', label: 'Option A' }]
})
FormFieldClass.searchSelectMultiApollo('field', {
label: 'Apollo Multi Search Select',
document: MY_GRAPHQL_QUERY,
dataType: 'users',
searchFields: ['name', 'email', 'firstName'],
selectOptionsFunction: (items) => items.map(item => ({
value: item.id,
label: item.name
}))
})
FormFieldClass.custom('field', {
label: 'Custom Field',
customField: ({ value, onChange }) => (
<MyCustomComponent value={value} onChange={onChange} />
)
})🚀 Apollo GraphQL Integration
For applications using Apollo Client, the forms library provides specialized search components that integrate with your GraphQL API:
SearchSelectApollo
Single-select dropdown with server-side search:
import { gql } from '@apollo/client'
const SEARCH_USERS_QUERY = gql`
query SearchUsers($input: SearchInput) {
users(input: $input) {
id
name
firstName
lastName
email
}
}
`
FormFieldClass.searchSelectApollo('selectedUser', {
label: 'Select User',
document: SEARCH_USERS_QUERY,
dataType: 'users',
searchFields: ['name', 'firstName', 'lastName', 'email'], // Configure which fields to search
selectOptionsFunction: (users) => users.map(user => ({
value: user.id,
label: `${user.firstName} ${user.lastName}`
})),
filter: (users) => users.slice(0, 10) // Optional client-side filtering
})SearchSelectMultiApollo
Multi-select dropdown with server-side search:
FormFieldClass.searchSelectMultiApollo('selectedUsers', {
label: 'Select Team Members',
document: SEARCH_USERS_QUERY,
dataType: 'users',
searchFields: ['name', 'firstName', 'lastName', 'email'],
selectOptionsFunction: (users) => users.map(user => ({
value: user.id,
label: `${user.firstName} ${user.lastName}`
}))
})Key Features
- Dynamic Search Fields: Use
searchFieldsto specify which backend fields should be searched - Server-side Search: Efficient handling of large datasets
- Custom Data Mapping: Transform API responses with
selectOptionsFunction - Debounced Search: 500ms delay to reduce API calls
- Loading States: Built-in loading indicators
- Type Safety: Full TypeScript support with generic data types
- Enhanced Clear Functionality (v0.4.17+): Multiple ways to clear selections
GraphQL Requirements
Your GraphQL queries must accept an input parameter:
type SearchInput {
search: String
searchFields: [String!] # Frontend specifies which fields to search
limit: Int
offset: Int
}
query SearchUsers($input: SearchInput) {
users(input: $input) {
id
name
firstName
lastName
email
}
}🔄 Search Select Clear Functionality (v0.4.17+)
All search select components now include comprehensive clear functionality with three intuitive methods:
1. Clear Button
- Visual X icon appears when a value is selected
- Located between the input field and dropdown arrow
- Single click to instantly clear the selection
2. Manual Text Deletion
- Focus the field and delete all text
- When you blur (click away), the selection automatically clears
- Works naturally like users expect from modern forms
3. Backspace on Empty Input
- Press backspace when the input is already empty to clear the selection
- Backspacing through search text doesn't affect the selection until the input is empty
Apollo Search Improvements
The Apollo-powered search selects now properly refresh data when cleared:
- Clearing the search input refetches with empty search parameters
- Shows the full unfiltered dataset instead of stale results
- No need for page refresh to see all options
Theme Customization
Customize the clear button appearance with new theme properties:
const customTheme = {
searchSelectField: {
input: 'your-input-classes',
inputWithClear: 'pr-20', // Extra padding when clear button is shown
clearButton: 'absolute inset-y-0 right-10 flex items-center pr-2',
clearIcon: 'h-5 w-5 text-gray-400 hover:text-gray-600',
// ... other properties
}
}🐛 Dropdown Positioning Issues
If the dropdown options render at the bottom of the page with excessive blank space instead of next to the input field, this is a CSS positioning context issue. This commonly happens when parent elements have CSS properties that create new stacking contexts.
Common Causes:
- Parent elements with
transform,filter, orperspectiveCSS properties - Containers with
position: relative/absolute/fixed - CSS frameworks that apply transforms for animations
- Modal dialogs or overlay components
Solutions:
1. Remove problematic CSS properties from parent containers:
/* Instead of this: */
.container {
transform: translateX(0); /* Creates positioning context */
}
/* Use this: */
.container {
/* Remove or replace transform */
}2. Override dropdown positioning with higher z-index:
/* Target the dropdown specifically */
[data-headlessui-state="open"] [role="listbox"] {
position: fixed !important;
z-index: 9999 !important;
}3. Isolate the form field:
/* Wrap your form field in a container with isolation */
.form-field-container {
isolation: isolate;
position: relative;
}Debugging Steps:
- Inspect the dropdown element in browser dev tools when mispositioned
- Check what element it's positioned relative to
- Look for CSS transforms/filters on parent elements
- Try the CSS solutions above to fix positioning context issues
🎨 Theming
Customize the appearance of your forms:
import { Form, FormFieldClass, tailwindTheme } from '@nestledjs/forms'
const customTheme = {
textField: {
input: 'border-2 border-blue-500 rounded-lg px-3 py-2',
error: 'border-red-500',
disabled: 'bg-gray-100'
},
button: {
primary: 'bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded'
}
}
function ThemedForm() {
return (
<Form
id="themed-form"
theme={customTheme}
fields={[
FormFieldClass.text('name', { label: 'Name' }),
FormFieldClass.email('email', { label: 'Email' })
]}
submit={(values) => console.log(values)}
>
<button type="submit" className={customTheme.button.primary}>
Submit
</button>
</Form>
)
}📐 Multi-Column Layouts
Create responsive multi-column layouts using CSS Grid or Flexbox:
CSS Grid Approach
import { Form, RenderFormField, FormFieldClass } from '@nestledjs/forms'
function MultiColumnForm() {
return (
<Form id="multi-column-form" submit={(values) => console.log(values)}>
{/* Two-column grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderFormField
field={FormFieldClass.text('firstName', { label: 'First Name' })}
className="col-span-1"
/>
<RenderFormField
field={FormFieldClass.text('lastName', { label: 'Last Name' })}
className="col-span-1"
/>
</div>
{/* Full-width field */}
<RenderFormField
field={FormFieldClass.email('email', { label: 'Email Address' })}
/>
{/* Three-column grid for address */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<RenderFormField
field={FormFieldClass.text('street', { label: 'Street' })}
className="col-span-2"
/>
<RenderFormField
field={FormFieldClass.text('zipCode', { label: 'ZIP' })}
className="col-span-1"
/>
</div>
<button type="submit">Submit</button>
</Form>
)
}Using wrapperClassName in Field Options
function GridFormWithFieldOptions() {
const fields = [
FormFieldClass.text('firstName', {
label: 'First Name',
wrapperClassName: 'col-span-1'
}),
FormFieldClass.text('lastName', {
label: 'Last Name',
wrapperClassName: 'col-span-1'
}),
FormFieldClass.email('email', {
label: 'Email Address',
wrapperClassName: 'col-span-2'
}),
]
return (
<Form id="grid-form" submit={(values) => console.log(values)}>
<div className="grid grid-cols-2 gap-4">
{fields.map(field => (
<RenderFormField key={field.key} field={field} />
))}
</div>
<button type="submit">Submit</button>
</Form>
)
}Horizontal Field Layout
function HorizontalLayoutForm() {
return (
<Form id="horizontal-form" submit={(values) => console.log(values)}>
<RenderFormField
field={FormFieldClass.checkbox('newsletter', {
label: 'Subscribe to newsletter',
layout: 'horizontal' // Label and input on same line
})}
/>
<RenderFormField
field={FormFieldClass.checkbox('terms', {
label: 'I agree to the terms and conditions',
layout: 'horizontal'
})}
/>
<button type="submit">Submit</button>
</Form>
)
}Custom Wrapper Functions
function CustomWrapperForm() {
return (
<Form id="custom-wrapper-form" submit={(values) => console.log(values)}>
<RenderFormField
field={FormFieldClass.text('amount', {
label: 'Amount',
customWrapper: (children) => (
<div className="flex items-end space-x-2">
<div className="flex-1">{children}</div>
<span className="text-gray-500 pb-2">USD</span>
</div>
)
})}
/>
<button type="submit">Submit</button>
</Form>
)
}🔒 Read-only Mode
Toggle forms between editable and read-only modes:
function ReadOnlyForm() {
const [isReadOnly, setIsReadOnly] = useState(false)
return (
<div>
<button onClick={() => setIsReadOnly(!isReadOnly)}>
{isReadOnly ? 'Edit' : 'View'}
</button>
<Form
id="readonly-form"
readOnly={isReadOnly}
readOnlyStyle="value" // 'value' or 'disabled'
fields={[
FormFieldClass.text('name', { label: 'Name' }),
FormFieldClass.email('email', { label: 'Email' })
]}
submit={(values) => console.log(values)}
/>
</div>
)
}🧩 Custom Field Components
Create custom field components using the provided hooks:
import { useFormContext, useFormTheme } from '@nestledjs/forms'
function CustomColorPicker({ fieldKey, label }) {
const form = useFormContext()
const theme = useFormTheme()
return (
<div>
<label>{label}</label>
<input
type="color"
{...form.register(fieldKey)}
className={theme.textField.input}
/>
</div>
)
}
// Usage
function FormWithCustomField() {
return (
<Form id="custom-form" submit={(values) => console.log(values)}>
<CustomColorPicker fieldKey="favoriteColor" label="Favorite Color" />
<button type="submit">Submit</button>
</Form>
)
}🔀 Conditional Field Logic
Create dynamic forms where fields show, hide, become required, or get disabled based on other field values:
Basic Conditional Visibility
const fields = [
FormFieldClass.select('contactMethod', {
label: 'Contact Method',
required: true,
options: [
{ value: 'email', label: 'Email' },
{ value: 'phone', label: 'Phone' },
{ value: 'mail', label: 'Mail' }
]
}),
// This field only appears when email is selected
FormFieldClass.email('email', {
label: 'Email Address',
required: true,
showWhen: (formValues) => formValues.contactMethod === 'email'
}),
// This field only appears when phone is selected
FormFieldClass.phone('phone', {
label: 'Phone Number',
required: true,
showWhen: (formValues) => formValues.contactMethod === 'phone'
})
]Conditional Required Fields
const fields = [
FormFieldClass.select('accountType', {
label: 'Account Type',
required: true,
options: [
{ value: 'personal', label: 'Personal' },
{ value: 'business', label: 'Business' }
]
}),
// This field becomes required only for business accounts
FormFieldClass.text('companyName', {
label: 'Company Name',
requiredWhen: (formValues) => formValues.accountType === 'business'
}),
FormFieldClass.text('taxId', {
label: 'Tax ID',
requiredWhen: (formValues) => formValues.accountType === 'business'
})
]Conditional Disabled Fields
const fields = [
FormFieldClass.checkbox('useCompanyEmail', {
label: 'Use company email address'
}),
// This field becomes disabled when checkbox is checked
FormFieldClass.email('personalEmail', {
label: 'Personal Email',
disabledWhen: (formValues) => formValues.useCompanyEmail === true
})
]Complex Conditional Logic
const fields = [
FormFieldClass.select('userType', {
label: 'User Type',
options: [
{ value: 'student', label: 'Student' },
{ value: 'teacher', label: 'Teacher' },
{ value: 'admin', label: 'Administrator' }
]
}),
FormFieldClass.checkbox('isHeadOfDepartment', {
label: 'Head of Department',
showWhen: (values) => values.userType === 'teacher'
}),
// Multiple conditions combined
FormFieldClass.text('officeNumber', {
label: 'Office Number',
showWhen: (values) => values.userType === 'teacher' && values.isHeadOfDepartment,
requiredWhen: (values) => values.userType === 'teacher' && values.isHeadOfDepartment
})
]Available Conditional Properties
showWhen(formValues): boolean- Controls field visibilityrequiredWhen(formValues): boolean- Makes field required dynamicallydisabledWhen(formValues): boolean- Disables field interaction
All conditional functions:
- Receive the current form values as their first parameter
- Are re-evaluated whenever any form value changes
- Work with both declarative and imperative APIs
- Include error handling for robust operation
📝 Validation
Built-in validation support with custom validators:
const fields = [
FormFieldClass.text('username', {
label: 'Username',
required: true,
validate: (value) => {
if (value.length < 3) return 'Username must be at least 3 characters'
if (!/^[a-zA-Z0-9_]+$/.test(value)) return 'Username can only contain letters, numbers, and underscores'
return true
}
}),
FormFieldClass.email('email', {
label: 'Email',
required: true,
validate: async (value) => {
const response = await fetch(`/api/check-email?email=${value}`)
const { exists } = await response.json()
return exists ? 'Email already exists' : true
}
})
]✍️ Markdown Editor Configuration
The markdown editor provides rich text editing with full markdown support:
FormFieldClass.markdownEditor('content', {
label: 'Content',
required: true,
// Editor appearance
height: 400,
placeholder: 'Enter your content...',
// Content validation
maxLength: 5000,
// Output format configuration
outputFormat: 'both', // 'markdown' | 'html' | 'both'
onHtmlChange: (html) => {
// Handle HTML output when outputFormat is 'html' or 'both'
console.log('Generated HTML:', html)
},
// Image upload configuration
enableImageUpload: true,
imageUploadHandler: async (file: File) => {
// Custom upload logic
const formData = new FormData()
formData.append('image', file)
const response = await fetch('/api/upload', { method: 'POST', body: formData })
const { url } = await response.json()
return url
},
imageUploadMode: 'custom', // 'base64' | 'custom' | 'immediate'
maxImageSize: 5 * 1024 * 1024, // 5MB
allowedImageTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
// Help text
helpText: 'Supports markdown formatting, images, and code blocks',
// Read-only configuration
readOnly: false,
readOnlyStyle: 'value', // 'value' | 'disabled'
})Markdown Editor Features
- Rich text toolbar with formatting options
- Live preview of markdown content
- Dual format output - Get both markdown and HTML
- Image upload with drag-and-drop support
- Code blocks with syntax highlighting
- Lists (bullet, numbered, checkbox)
- Links and blockquotes
- Keyboard shortcuts for common actions
- Read-only mode for viewing content
Markdown Editor Usage Tips
- Lists: Use the toolbar button to toggle between bullet and numbered lists
- Images: Drag and drop images directly into the editor
- Code blocks: Use triple backticks (```) for code blocks
- Shortcuts: Press
Ctrl+Bfor bold,Ctrl+Ifor italic, etc. - Checkboxes: Type
- [ ]for unchecked or- [x]for checked items
Dual Format Output
The Markdown Editor can output in three modes:
'markdown'(default): Only outputs markdown content'html': Converts and outputs HTML content'both': Outputs both markdown (in the main field) and HTML (in a_htmlsuffixed field)
// Example: Get both markdown and HTML
FormFieldClass.markdownEditor('content', {
outputFormat: 'both',
onHtmlChange: (html) => {
// Access HTML as it's generated
setGeneratedHtml(html)
}
})
// When outputFormat is 'both', form data will contain:
// - content: "# Hello\n\nThis is **bold**"
// - content_html: "<h1>Hello</h1><p>This is <strong>bold</strong></p>"⚠️ Security Note: The built-in markdown-to-HTML conversion is basic and designed for simple use cases. For production use:
- Use robust, security-tested parsers like
marked,markdown-it, orremark - The built-in converter includes ReDoS protection (input size limits, safe regex patterns)
- Consider server-side conversion for untrusted input to avoid client-side DoS attacks
Modal-on-Modal Conflicts
If your MarkdownEditor is inside a modal and the link/image dialogs don't appear properly, this is due to z-index conflicts. Here's how to fix it:
Option 1: Custom Overlay Container
FormFieldClass.markdownEditor('content', {
label: 'Content',
overlayContainer: document.getElementById('your-modal-container'), // Render popups inside your modal
})Option 2: Higher Z-Index
FormFieldClass.markdownEditor('content', {
label: 'Content',
popupZIndex: 10000, // Set higher than your modal's z-index
})Option 3: CSS Override
/* In your global CSS */
.mdxeditor-popup-container {
z-index: 9999 !important; /* Higher than your modal */
}🎛️ Advanced Configuration
Label Display Control
<Form
id="form"
labelDisplay="none" // 'all' | 'default' | 'none'
fields={fields}
submit={handleSubmit}
/>Field-level Configuration
const field = FormFieldClass.text('name', {
label: 'Name',
required: true,
disabled: false,
readOnly: false,
readOnlyStyle: 'value', // 'value' | 'disabled'
helpText: 'Enter your full name',
placeholder: 'John Doe',
defaultValue: 'Default Name',
validate: (value) => value.length > 0 || 'Name is required',
// Conditional logic properties
showWhen: (formValues) => formValues.shouldShowName,
requiredWhen: (formValues) => formValues.accountType === 'business',
disabledWhen: (formValues) => formValues.useGeneratedName
})📚 API Reference
Core Components
Form: Main form component supporting both declarative and imperative usageRenderFormField: Renders individual form fields (imperative usage)FormFieldClass: Factory class for creating field definitions
Hooks
useFormContext<T>(): Access form state and methodsuseFormConfig(): Access form configuration (label display, etc.)useFormTheme(): Access current theme configuration
Types
FormField: Union type of all possible field definitionsFormFieldType: Enum of available field typesFormProps<T>: Props interface for the Form componentFormTheme: Theme configuration interface
🤝 TypeScript Support
The library is built with TypeScript-first design:
interface UserFormData {
name: string
email: string
age: number
preferences: string[]
}
function TypedForm() {
return (
<Form<UserFormData>
id="typed-form"
fields={[
FormFieldClass.text('name', { label: 'Name' }),
FormFieldClass.email('email', { label: 'Email' }),
FormFieldClass.number('age', { label: 'Age' }),
FormFieldClass.multiSelect('preferences', {
label: 'Preferences',
options: [
{ value: 'music', label: 'Music' },
{ value: 'sports', label: 'Sports' }
]
})
]}
submit={(values: UserFormData) => {
// values is fully typed!
console.log(values.name, values.email, values.age, values.preferences)
}}
/>
)
}🎯 Best Practices
- Use declarative approach when possible - it's more maintainable
- Combine with imperative for dynamic scenarios
- Define field arrays outside render to avoid unnecessary re-renders
- Use TypeScript generics for type safety
- Leverage validation for better user experience
- Consider read-only modes for view/edit patterns
📄 License
MIT License - see LICENSE file for details.
📝 Changelog
v0.4.17 (Latest)
- ✨ Enhanced Search Select Clear Functionality
- Added clear button (X icon) for single select fields
- Implemented manual text deletion to clear selections
- Added backspace-to-clear on empty input
- Fixed Apollo search to properly refetch when cleared
- 🐛 Bug Fixes
- Apollo search selects now refresh data correctly when search is cleared
- Form state properly updates when selections are cleared
- 🎨 Theme Improvements
- Added
inputWithClear,clearButton, andclearIcontheme properties - Improved type safety with proper null handling
- Added
v0.4.16
- Initial clear functionality implementation
v0.4.15
- Bug fixes and performance improvements
