@kodeme-io/next-core-forms
v0.8.4
Published
Dynamic form builder and validation for Next.js applications
Maintainers
Readme
@kodeme-io/next-core-forms
Dynamic form builder and validation for Next.js applications with TypeScript support and Zod integration.
🚀 Features
- 📝 Dynamic Form Generation - Build forms from JSON definitions
- ✅ Powerful Validation - Zod-based validation with custom rules
- 🔄 Array Fields - Dynamic field lists with add/remove functionality
- 🎯 Cross-Field Validation - Validate relationships between multiple fields
- 🧭 Form Wizard - Multi-step forms with progress tracking
- 🎨 Flexible Components - Pre-built form components and layouts
- 🔄 Conditional Fields - Show/hide fields based on user input
- 📱 Mobile-First - Responsive design optimized for all devices
- ♿ Accessible - WCAG compliant with proper ARIA labels
- 🔧 TypeScript Native - Full type safety and IntelliSense support
- ⚡ Performance Optimized - Cached validation and efficient rendering
📦 Installation
npm install @kodeme-io/next-core-forms
# or
yarn add @kodeme-io/next-core-forms
# or
pnpm add @kodeme-io/next-core-formsPeer Dependencies
npm install react react-hook-form zod🎯 Quick Start
Basic Form Example
'use client'
import { DynamicForm, FormDefinition } from '@kodeme-io/next-core-forms'
import { z } from 'zod'
const registrationForm: FormDefinition = {
id: 'registration',
title: 'User Registration',
description: 'Create your account',
sections: [
{
id: 'personal-info',
title: 'Personal Information',
fields: [
{
name: 'firstName',
label: 'First Name',
type: 'text',
required: true,
placeholder: 'Enter your first name',
validation: z.string().min(2, 'Must be at least 2 characters')
},
{
name: 'email',
label: 'Email Address',
type: 'email',
required: true,
validation: z.string().email('Invalid email address')
},
{
name: 'password',
label: 'Password',
type: 'password',
required: true,
validation: z.string().min(8, 'Password must be at least 8 characters')
}
]
}
],
submitLabel: 'Register',
layout: 'vertical'
}
export default function RegistrationPage() {
const handleSubmit = async (data: any) => {
console.log('Form submitted:', data)
// Handle form submission
}
return (
<div className="max-w-md mx-auto">
<DynamicForm form={registrationForm} onSubmit={handleSubmit} />
</div>
)
}📚 Core Concepts
Form Definition Structure
interface FormDefinition {
id: string // Unique form identifier
title: string // Form title
description?: string // Optional description
sections: FormSection[] // Form sections
submitLabel?: string // Submit button text
cancelLabel?: string // Cancel button text
layout?: 'vertical' | 'horizontal' | 'grid' // Layout style
}Field Types
Supported field types:
text- Single line text inputemail- Email input with validationnumber- Numeric inputtel- Telephone number inputurl- URL inputpassword- Password inputtextarea- Multi-line text inputselect- Single select dropdownmultiselect- Multi-select dropdowncheckbox- Single checkboxradio- Radio button groupdate- Date pickertime- Time pickerdatetime- Date and time pickerfile- File uploadarray- Dynamic field lists with add/remove functionalitycustom- Custom field component
Field Definition
interface FieldDefinition {
name: string // Field name (form data key)
label: string // Display label
type: FieldType // Field type
placeholder?: string // Placeholder text
defaultValue?: any // Default value
required?: boolean // Required field
options?: FieldOption[] // Options for select/radio
validation?: ZodType // Zod validation schema
helperText?: string // Help text
disabled?: boolean // Disabled state
hidden?: boolean // Hidden field
dependsOn?: { // Conditional display
field: string
value: any
condition?: 'equals' | 'not_equals' | 'contains'
}
arrayConfig?: ArrayFieldConfig // Configuration for array fields
}
interface ArrayFieldConfig {
minItems?: number // Minimum number of items
maxItems?: number // Maximum number of items
itemLabel: string // Label for each item
addLabel?: string // Add button label
removeLabel?: string // Remove button label
canReorder?: boolean // Allow reordering items
fields: FieldDefinition[] // Field definitions for array items
}🎨 Components
DynamicForm
Main component for rendering forms from definitions.
<DynamicForm
form={formDefinition}
onSubmit={handleSubmit}
onSubmitSuccess={handleSuccess}
onSubmitError={handleError}
className="custom-form"
disabled={false}
/>Form Components
Lower-level components for custom form building:
import {
FormProvider,
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage
} from '@kodeme-io/next-core-forms'
function CustomForm() {
const form = useForm({
defaultValues: { name: '', email: '' }
})
return (
<FormProvider {...form}>
<FormField
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</FormProvider>
)
}FormSection
Section component with collapsible support:
<FormSection
section={sectionDefinition}
defaultCollapsed={false}
onToggle={(collapsed) => console.log(collapsed)}
>
{/* Custom section content */}
</FormSection>✅ Validation
Built-in Validation
Using Zod for powerful validation:
import { z } from 'zod'
const validationSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain uppercase letter')
.regex(/[0-9]/, 'Must contain number'),
age: z.number().min(18, 'Must be 18 or older'),
terms: z.boolean().refine(val => val === true, 'Must accept terms')
})
const field: FieldDefinition = {
name: 'email',
label: 'Email',
type: 'email',
required: true,
validation: validationSchema.shape.email
}Custom Validation
Create custom validation rules:
const customValidation = z.string().refine(
(val) => !val.includes('spam'),
'Email cannot contain "spam"'
)
const field: FieldDefinition = {
name: 'customEmail',
label: 'Email',
type: 'email',
validation: customValidation
}Async Validation
For server-side validation:
const asyncValidation = z.string().refine(
async (email) => {
const response = await fetch(`/api/check-email?email=${email}`)
const data = await response.json()
return !data.exists
},
'Email already exists'
)🔄 Array Fields
Dynamic field lists with add/remove functionality:
const arrayFieldForm: FormDefinition = {
id: 'array-form',
title: 'Array Field Example',
sections: [
{
id: 'phone-numbers',
title: 'Phone Numbers',
fields: [
{
name: 'phoneNumbers',
label: 'Phone Numbers',
type: 'array',
required: true,
arrayConfig: {
minItems: 1,
maxItems: 5,
itemLabel: 'Phone Number',
addLabel: 'Add Phone',
removeLabel: 'Remove',
canReorder: true,
fields: [
{
name: 'type',
label: 'Type',
type: 'select',
required: true,
options: [
{ label: 'Mobile', value: 'mobile' },
{ label: 'Home', value: 'home' },
{ label: 'Work', value: 'work' }
]
},
{
name: 'number',
label: 'Phone Number',
type: 'tel',
required: true,
validation: z.string().regex(/^[0-9+\-\s()]+$/, 'Invalid phone number')
}
]
},
validation: z.array(z.object({
type: z.enum(['mobile', 'home', 'work']),
number: z.string().regex(/^[0-9+\-\s()]+$/)
})).min(1, 'At least one phone number required')
}
]
}
]
}🎯 Cross-Field Validation
Validate relationships between multiple fields:
import { commonCrossFieldRules } from '@kodeme-io/next-core-forms'
const crossFieldForm: FormDefinition = {
id: 'cross-field-form',
title: 'Cross-Field Validation Example',
sections: [
// ... sections with password and confirmPassword fields
// ... sections with startDate and endDate fields
],
crossFieldRules: [
commonCrossFieldRules.passwordMatch('password', 'confirmPassword'),
commonCrossFieldRules.dateRange('startDate', 'endDate'),
commonCrossFieldRules.minAge('birthDate', 18),
{
name: 'customRule',
message: 'Custom validation message',
validate: (formData) => {
// Return true if valid, false or string if invalid
return formData.field1 !== formData.field2
}
}
]
}🧭 Form Wizard
Multi-step forms with progress tracking:
import { FormWizard } from '@kodeme-io/next-core-forms'
export default function MultiStepForm() {
return (
<FormWizard
form={multiStepFormDefinition}
onStepChange={(stepIndex, stepData) => {
console.log(`Step ${stepIndex + 1}:`, stepData)
}}
onComplete={() => {
console.log('Wizard completed')
}}
/>
)
}🔄 Conditional Fields
Show/hide fields based on other field values:
const formWithConditionalFields: FormDefinition = {
id: 'conditional-form',
title: 'Conditional Form',
sections: [
{
id: 'main',
title: 'Main Section',
fields: [
{
name: 'hasAccount',
label: 'Do you have an account?',
type: 'radio',
options: [
{ label: 'Yes', value: 'yes' },
{ label: 'No', value: 'no' }
],
required: true
},
{
name: 'email',
label: 'Email Address',
type: 'email',
required: true,
validation: z.string().email(),
dependsOn: {
field: 'hasAccount',
value: 'yes',
condition: 'equals'
}
},
{
name: 'newEmail',
label: 'New Email Address',
type: 'email',
required: true,
validation: z.string().email(),
dependsOn: {
field: 'hasAccount',
value: 'no',
condition: 'equals'
}
}
]
}
]
}🎯 Advanced Usage
Multi-Step Forms
const multiStepForm: FormDefinition = {
id: 'multi-step',
title: 'Multi-Step Registration',
sections: [
{
id: 'step1',
title: 'Personal Information',
fields: [
{ name: 'firstName', label: 'First Name', type: 'text', required: true },
{ name: 'lastName', label: 'Last Name', type: 'text', required: true }
],
collapsible: true,
defaultCollapsed: false
},
{
id: 'step2',
title: 'Contact Information',
fields: [
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'phone', label: 'Phone', type: 'tel', required: true }
],
collapsible: true,
defaultCollapsed: true
},
{
id: 'step3',
title: 'Preferences',
fields: [
{ name: 'newsletter', label: 'Subscribe to newsletter', type: 'checkbox' },
{ name: 'notifications', label: 'Enable notifications', type: 'checkbox' }
],
collapsible: true,
defaultCollapsed: true
}
]
}Form with File Upload
const fileUploadForm: FormDefinition = {
id: 'upload-form',
title: 'File Upload',
sections: [
{
id: 'upload',
title: 'Upload Document',
fields: [
{
name: 'document',
label: 'Document (PDF, DOC, DOCX)',
type: 'file',
required: true,
validation: z.instanceof(File).refine(
(file) => file.size <= 10 * 1024 * 1024, // 10MB
'File must be smaller than 10MB'
).refine(
(file) => ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'].includes(file.type),
'Invalid file type'
)
}
]
}
]
}🧪 Testing
The package includes comprehensive test coverage:
# Run tests
npm test
# Run with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watchTesting Custom Forms
import { render, screen, fireEvent } from '@testing-library/react'
import { DynamicForm } from '@kodeme-io/next-core-forms'
test('form submission', async () => {
const handleSubmit = jest.fn()
render(<DynamicForm form={testForm} onSubmit={handleSubmit} />)
fireEvent.change(screen.getByLabelText('Name'), {
target: { value: 'John Doe' }
})
fireEvent.click(screen.getByText('Submit'))
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({ name: 'John Doe' })
})
})🔧 API Reference
Types
// Core types
export type FieldType = 'text' | 'email' | 'number' | 'tel' | 'url' | 'password' | 'textarea' | 'select' | 'multiselect' | 'checkbox' | 'radio' | 'date' | 'time' | 'datetime' | 'file' | 'custom'
export interface FieldOption {
label: string
value: string | number
disabled?: boolean
}
export interface FormState<T = any> {
values: T
errors: Record<string, string>
touched: Record<string, boolean>
isDirty: boolean
isSubmitting: boolean
isValid: boolean
}
// Component props
export interface DynamicFormProps {
form: FormDefinition
onSubmit: (data: any) => void | Promise<void>
onSubmitSuccess?: (data: any) => void
onSubmitError?: (error: Error) => void
className?: string
disabled?: boolean
defaultValues?: Record<string, any>
}Utilities
// Validation utilities
export function validateField(field: FieldDefinition, value: any): ValidationResult
export function validateForm(form: FormDefinition, data: any, partial?: boolean): FormValidationResult
export function createFormValidator(schema: ZodSchema): ZodSchema
// Form utilities
export function getDefaultValues(form: FormDefinition): Record<string, any>
export function getFormData(form: FormDefinition): any
export function resetForm(form: FormDefinition): void🔍 Troubleshooting
Common Issues
Validation Not Working
- Ensure Zod schema is properly configured
- Check that validation is assigned to the correct field
- Verify field names match form data keys
Conditional Fields Not Showing
- Check the
dependsOnconfiguration - Ensure the dependent field has the correct value
- Verify the condition logic
- Check the
Form Not Submitting
- Check if form is valid (
isValidstate) - Ensure onSubmit handler is properly defined
- Check for validation errors
- Check if form is valid (
🤝 Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Submit a pull request
📄 License
MIT License - see LICENSE file for details
🔗 Related Packages
- @kodeme-io/next-core-ui - UI components
- @kodeme-io/next-core-workflow - Workflow management
- @kodeme-io/next-core-analytics - Analytics and charts
📞 Support
- Documentation: Full documentation
- Issues: GitHub Issues
- Discord: Community Discord
