npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nestledjs/forms

v0.6.4

Published

A flexible React form library supporting both declarative and imperative usage patterns with TypeScript support

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:

  1. Install the peer dependency:

    npm install @mdxeditor/editor
    # or
    yarn add @mdxeditor/editor
    # or
    pnpm add @mdxeditor/editor
  2. Import 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 searchFields to 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, or perspective CSS 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:

  1. Inspect the dropdown element in browser dev tools when mispositioned
  2. Check what element it's positioned relative to
  3. Look for CSS transforms/filters on parent elements
  4. 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 visibility
  • requiredWhen(formValues): boolean - Makes field required dynamically
  • disabledWhen(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

  1. Lists: Use the toolbar button to toggle between bullet and numbered lists
  2. Images: Drag and drop images directly into the editor
  3. Code blocks: Use triple backticks (```) for code blocks
  4. Shortcuts: Press Ctrl+B for bold, Ctrl+I for italic, etc.
  5. 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 _html suffixed 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, or remark
  • 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 usage
  • RenderFormField: Renders individual form fields (imperative usage)
  • FormFieldClass: Factory class for creating field definitions

Hooks

  • useFormContext<T>(): Access form state and methods
  • useFormConfig(): Access form configuration (label display, etc.)
  • useFormTheme(): Access current theme configuration

Types

  • FormField: Union type of all possible field definitions
  • FormFieldType: Enum of available field types
  • FormProps<T>: Props interface for the Form component
  • FormTheme: 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

  1. Use declarative approach when possible - it's more maintainable
  2. Combine with imperative for dynamic scenarios
  3. Define field arrays outside render to avoid unnecessary re-renders
  4. Use TypeScript generics for type safety
  5. Leverage validation for better user experience
  6. 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, and clearIcon theme properties
    • Improved type safety with proper null handling

v0.4.16

  • Initial clear functionality implementation

v0.4.15

  • Bug fixes and performance improvements