UNPKG

@nestledjs/forms

Version:

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

1,088 lines (905 loc) 29.2 kB
# @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 ```bash 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:** ```bash 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`): ```tsx import '@mdxeditor/editor/style.css' ``` ## 🎯 Quick Start ### Declarative Usage (Recommended) Perfect for forms where you can define all fields upfront: ```tsx 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: 'user@example.com' }), 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: ```tsx 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: ```tsx 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: ```tsx 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: ```tsx // 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 FormFieldClass.searchSelect('field', { label: 'Search Select', options: [{ value: 'a', label: 'Option A' }] }) 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: ```tsx 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: ```tsx 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 ### GraphQL Requirements Your GraphQL queries must accept an `input` parameter: ```graphql 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 } } ``` ### 🐛 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:** ```css /* 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:** ```css /* Target the dropdown specifically */ [data-headlessui-state="open"] [role="listbox"] { position: fixed !important; z-index: 9999 !important; } ``` **3. Isolate the form field:** ```css /* 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: ```tsx 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 ```tsx 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 ```tsx 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 ```tsx 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 ```tsx 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: ```tsx 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: ```tsx 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 ```tsx 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 ```tsx 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 ```tsx 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 ```tsx 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: ```tsx 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: ```tsx 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) ```tsx // 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** ```tsx FormFieldClass.markdownEditor('content', { label: 'Content', overlayContainer: document.getElementById('your-modal-container'), // Render popups inside your modal }) ``` **Option 2: Higher Z-Index** ```tsx FormFieldClass.markdownEditor('content', { label: 'Content', popupZIndex: 10000, // Set higher than your modal's z-index }) ``` **Option 3: CSS Override** ```css /* In your global CSS */ .mdxeditor-popup-container { z-index: 9999 !important; /* Higher than your modal */ } ``` ## 🎛️ Advanced Configuration ### Label Display Control ```tsx <Form id="form" labelDisplay="none" // 'all' | 'default' | 'none' fields={fields} submit={handleSubmit} /> ``` ### Field-level Configuration ```tsx 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: ```tsx 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.