finform-react-builder
Version:
A powerful, flexible React form builder with dynamic field rendering, custom validation, multi-step forms, Material-UI integration, image component support, toggle/radio buttons, switches, autocomplete, and advanced button positioning
904 lines (790 loc) • 21.4 kB
Markdown
with dynamic field rendering, custom validation, and Material-UI integration.
[](https://www.npmjs.com/package/finform-react-builder)
[](https://opensource.org/licenses/MIT)
**If you're currently using this package from GitHub Packages**, you'll need to update your installation:
```bash
npm uninstall @finflow-analytics/finform-react-builder
npm install finform-react-builder
```
If you have a `.npmrc` file with GitHub Packages configuration, remove the line:
```
@finflow-analytics:registry=https://npm.pkg.github.com
```
- 🎨 **Material-UI Integration** - Beautiful, responsive form components
- 🔧 **Dynamic Field Rendering** - text, email, password (with eye toggle), number, select, checkbox, radio, switch, autocomplete (single/multiple with checkboxes and +N more), date, textarea, image
- ✅ **Advanced Validation** - Built-in validation with custom regex patterns and validation functions
- 📱 **Responsive Grid System** - Flexible column-based layout system
- 🎯 **TypeScript Support** - Full type safety and IntelliSense support
- 🚀 **Easy Integration** - Simple API with sensible defaults
- 🔄 **Real-time Validation** - Instant feedback with react-hook-form
- 🎨 **Customizable** - Highly configurable with custom validation rules
- 🔄 **Multi-Step Forms** - Support for step-by-step form completion with smart navigation
- 🧩 **Custom Components** - Inject any React node via `type: 'component'`
- 💾 **Stateless Design** - Works with data from backend/config without local storage dependencies
- 📎 **Document Upload** - Drag & drop uploader with validation and preview
```bash
npm install finform-react-builder
```
Make sure you have the required peer dependencies installed:
```bash
npm install react react-dom @mui/material @emotion/react @emotion/styled
```
```tsx
import React from 'react';
import { FinForm, FieldConfig } from 'finform-react-builder';
const fields: FieldConfig[] = [
{
name: 'firstName',
label: 'First Name',
type: 'text',
placeholder: 'Enter your first name',
required: true,
col: 6
},
{
name: 'lastName',
label: 'Last Name',
type: 'text',
placeholder: 'Enter your last name',
required: true,
col: 6
},
{
name: 'email',
label: 'Email',
type: 'email',
placeholder: 'Enter your email',
required: true,
col: 12
}
];
function App() {
const handleSubmit = (data: any) => {
console.log('Form submitted:', data);
};
return (
<FinForm
fields={fields}
onSubmit={handleSubmit}
submitButtonText="Submit Form"
/>
);
}
export default App;
```
```tsx
{ name: 'password', label: 'Password', type: 'password', placeholder: 'Enter password', required: true, helperText: 'Use at least 8 characters.' }
```
Behavior: An eye icon toggles visibility between •••• and plain text.
```tsx
{
name: 'username',
label: 'Username',
type: 'text',
placeholder: 'Enter username',
validation: {
pattern: /^[a-zA-Z0-9_]+$/,
message: 'Username can only contain letters, numbers, and underscores'
}
}
```
```tsx
{
name: 'email',
label: 'Email Address',
type: 'email',
placeholder: 'Enter your email',
required: true
}
```
```tsx
{
name: 'age',
label: 'Age',
type: 'number',
placeholder: 'Enter your age',
validation: {
min: 18,
max: 120,
message: 'Age must be between 18 and 120'
}
}
```
```tsx
{
name: 'country',
label: 'Country',
type: 'select',
options: [
{ label: 'United States', value: 'us' },
{ label: 'Canada', value: 'ca' },
{ label: 'United Kingdom', value: 'uk' }
],
required: true
}
```
```tsx
{
name: 'birthDate',
label: 'Birth Date',
type: 'date',
validation: {
custom: (value) => {
const today = new Date();
const birthDate = new Date(value);
return birthDate < today;
},
message: 'Birth date must be in the past'
}
}
```
```tsx
{
name: 'profileImage',
label: 'Profile Image',
type: 'image',
src: 'https://example.com/image.jpg',
alt: 'User profile image',
width: 300,
height: 200,
style: {
border: '2px solid #e0e0e0',
borderRadius: '8px'
},
onClick: () => {
console.log('Image clicked!');
}
}
```
```tsx
{
name: 'bio',
label: 'Biography',
type: 'textarea',
placeholder: 'Tell us about yourself...',
validation: {
maxLength: 500,
message: 'Biography must be less than 500 characters'
}
}
```
```tsx
{ name: 'agree', label: 'Agree to terms', type: 'checkbox', required: true }
```
```tsx
{
name: 'country',
label: 'Country',
type: 'autocomplete',
placeholder: 'Search country',
options: [
{ label: 'United States', value: 'us' },
{ label: 'Canada', value: 'ca' },
],
// For API-driven:
// api_endpoint: '/common/countries?format=dropdown',
// api_method: 'GET',
// value_field: 'id',
// label_field: 'label',
}
```
```tsx
{
name: 'skills',
label: 'Skills',
type: 'autocomplete',
multiple: true,
options: [
{ label: 'JavaScript', value: 'javascript' },
{ label: 'React', value: 'react' },
{ label: 'TypeScript', value: 'typescript' },
],
// Stored value is an array of primitive values: ['javascript', 'react']
}
```
Behavior:
- Renders checkboxes in the dropdown when `multiple: true`.
- Only the first 2 selections are rendered as chips; if more, shows a `+N more` chip.
- Works with API-driven data using `api_endpoint`, `value_field`, `label_field`.
For any disabled field, a small copy icon appears to the right, allowing users to copy the field value to clipboard.
```tsx
{ name: 'readonly_id', label: 'User ID', type: 'text', value: 'USR-7842-AB', disabled: true }
```
```tsx
{
name: 'gender',
label: 'Gender',
type: 'toggle',
options: [
{ label: 'Male', value: 'male' },
{ label: 'Female', value: 'female' },
{ label: 'Other', value: 'other' },
],
required: true,
}
```
```tsx
{
name: 'contactPreference',
label: 'Contact Preference',
type: 'radio',
options: [
{ label: 'Email', value: 'email' },
{ label: 'Phone', value: 'phone' },
],
}
```
```tsx
{
name: 'notifications',
label: 'Enable Notifications',
type: 'switch',
}
```
Render arbitrary React content inside the form layout.
```tsx
{ name: 'custom_block', type: 'component', col: 12, step: 2, content: <div>Any JSX here</div> }
```
```tsx
{ name: 'formTitle', type: 'title', label: 'Create User', variant: 'h4', col: 12 }
{ name: 'detailsSection', type: 'section', label: 'Details', variant: 'h6', col: 12 }
```
```tsx
{
name: 'country_id',
label: 'Country',
type: 'select',
api_endpoint: '/common/countries?format=dropdown',
api_method: 'GET',
value_field: 'id',
label_field: 'label',
}
```
```tsx
{
name: 'org_type_id',
label: 'Entity Type',
type: 'autocomplete',
api_endpoint: '/company/org-types/active/list',
api_method: 'GET',
value_field: 'id',
label_field: 'name',
},
{
name: 'parent_id',
label: 'Parent Entity',
type: 'autocomplete',
api_endpoint: '/company/offices/potential-parents/{org_type_id}?format=dropdown',
api_method: 'GET',
depends_on: 'org_type_id',
conditional: true,
value_field: 'id',
label_field: 'label',
}
```
Behavior:
- The dependent field (`parent_id`) is disabled until `org_type_id` has a value.
- When the dependency value changes, the dependent field is reset (clears previous selection).
## ✅ Validation and Submit Button Behavior
- Required fields must be filled; for arrays (e.g., multi-select), value must be non-empty.
- Required checkboxes must be true.
- Any validation error (including on non-required fields) disables submit.
- Single-step forms: built-in Submit button is disabled until valid.
- Multi-step forms: Next/Submit are disabled until the current step is valid.
- Custom buttons passed via `buttons` or `buttonGroup` are also auto-disabled when they are flow actions (Next/Submit/Save/Finish/Complete) and the step/form is invalid.
Number validation example (number type):
```tsx
{
name: 'longitude',
label: 'Longitude',
type: 'number',
validation: { min: -180, max: 180, message: 'Longitude must be between -180 and 180' },
}
```
For `type: 'text'` number-like validation, use `pattern` or `custom` in `validation`.
```tsx
<FinForm
fields={fields}
buttons={[
{ text: 'Cancel', type: 'button', onClick: () => navigate(-1) },
{ text: 'Submit', type: 'submit', color: 'primary' },
]}
/>
```
- Submit (and Next) buttons are auto-disabled when the form (or current step) is invalid.
- Non-submit buttons (e.g., Cancel) remain clickable.
### Button Group
```tsx
<FinForm
fields={fields}
buttonGroup={{
position: 'right',
buttons: [
{ text: 'Previous', type: 'button' },
{ text: 'Next', type: 'button' },
{ text: 'Submit', type: 'submit' },
],
}}
/>
```
- Buttons with text containing "Next", "Submit", "Save", "Finish", "Complete" are treated as flow actions and auto-disabled appropriately.
```tsx
<FinForm
theme={{
primaryColor: '#2196f3',
secondaryColor: '#f50057',
backgroundColor: '#fafafa',
textColor: '#333',
borderRadius: 8,
spacing: 16,
typography: { fontFamily: 'Roboto, Arial, sans-serif', fontSize: 16 },
}}
fields={fields}
onSubmit={...}
/>
```
```tsx
{
name: 'profileImage',
label: 'Profile Image',
type: 'image',
defaultValue: 'https://via.placeholder.com/300x200',
alt: 'User profile image',
width: 300,
height: 200,
onClick: () => console.log('Image clicked')
}
```
`onChange` is fired with current form values on every change, without creating render loops, and accounts for dependent resets.
```tsx
{
name: 'newsletter',
label: 'Subscribe to Newsletter',
type: 'checkbox',
required: true
}
```
```tsx
{
name: 'phoneNumber',
label: 'Phone Number',
type: 'text',
placeholder: '+1 (555) 123-4567',
validation: {
pattern: /^\+?[1-9]\d{1,14}$/,
message: 'Please enter a valid phone number'
}
}
```
```tsx
{
name: 'password',
label: 'Password',
type: 'password',
placeholder: 'Enter password',
validation: {
minLength: 8,
custom: (value) => {
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasNumbers = /\d/.test(value);
const hasSpecialChar = /[!@
return hasUpperCase && hasLowerCase && hasNumbers && hasSpecialChar;
},
message: 'Password must contain uppercase, lowercase, number and special character'
}
}
```
Use the responsive grid system to control field layout:
```tsx
// Full width field
{ name: 'description', type: 'textarea', col: 12 }
// Half width fields
{ name: 'firstName', type: 'text', col: 6 }
{ name: 'lastName', type: 'text', col: 6 }
// Responsive breakpoints
{
name: 'address',
type: 'text',
xs: 12, // Full width on extra small screens
md: 8, // 8/12 width on medium screens
lg: 6 // Half width on large screens
}
```
FinForm supports multi-step forms with smart navigation and completion tracking. Perfect for complex forms that need to be broken down into manageable steps.
```tsx
const multiStepFields: FieldConfig[] = [
// Step 1: Personal Information
{
name: 'firstName',
label: 'First Name',
type: 'text',
required: true,
step: 1,
col: 6,
},
{
name: 'lastName',
label: 'Last Name',
type: 'text',
required: true,
step: 1,
col: 6,
},
{
name: 'email',
label: 'Email Address',
type: 'email',
required: true,
step: 1,
col: 12,
},
// Step 2: Address Information
{
name: 'street',
label: 'Street Address',
type: 'text',
required: true,
step: 2,
col: 12,
},
{
name: 'city',
label: 'City',
type: 'text',
required: true,
step: 2,
col: 6,
},
{
name: 'state',
label: 'State',
type: 'select',
required: true,
step: 2,
col: 3,
options: [
{ label: 'California', value: 'CA' },
{ label: 'New York', value: 'NY' },
{ label: 'Texas', value: 'TX' },
],
},
{
name: 'zipCode',
label: 'ZIP Code',
type: 'text',
required: true,
step: 2,
col: 3,
validation: {
pattern: /^\d{5}(-\d{4})?$/,
message: 'Please enter a valid ZIP code',
},
},
// Step 3: Preferences
{
name: 'newsletter',
label: 'Subscribe to Newsletter',
type: 'checkbox',
step: 3,
col: 12,
},
{
name: 'preferences',
label: 'Communication Preferences',
type: 'select',
step: 3,
col: 6,
options: [
{ label: 'Email', value: 'email' },
{ label: 'Phone', value: 'phone' },
{ label: 'Both', value: 'both' },
],
},
];
function MultiStepForm() {
const [currentStep, setCurrentStep] = useState(1);
const handleStepChange = (step: number, totalSteps: number) => {
console.log(`Moving to step ${step} of ${totalSteps}`);
setCurrentStep(step);
};
return (
<FinForm
fields={multiStepFields}
onSubmit={(data) => console.log('Form submitted:', data)}
isMultiStep={true}
currentStep={currentStep}
onStepChange={handleStepChange}
submitButtonText="Complete Registration"
stepNavigationProps={{
showStepNumbers: true,
showStepTitles: true,
stepTitles: ['Personal Info', 'Address', 'Preferences'],
}}
/>
);
}
```
The form automatically detects completed steps and starts from the first incomplete step when data is provided:
```tsx
// Data from backend showing user has completed step 1
const backendData = {
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
// Step 2 and 3 are empty
};
function SmartMultiStepForm() {
return (
<FinForm
fields={multiStepFields}
onSubmit={(data) => console.log('Form submitted:', data)}
defaultValues={backendData}
isMultiStep={true}
// Form will automatically start at step 2 since step 1 is complete
submitButtonText="Complete Registration"
/>
);
}
```
| Prop | Type | Description | Default |
|------|------|-------------|---------|
| `isMultiStep` | `boolean` | Enable multi-step functionality | `false` |
| `currentStep` | `number` | Current step (controlled by parent) | `1` |
| `onStepChange` | `(step: number, totalSteps: number) => void` | Step change callback | - |
| `showStepNavigation` | `boolean` | Show step navigation | `true` |
| `stepNavigationProps` | `object` | Step navigation configuration | `{}` |
```tsx
stepNavigationProps={{
showStepNumbers: true, // Show step numbers
showStepTitles: true, // Show step titles
stepTitles: ['Step 1', 'Step 2', 'Step 3'], // Custom step titles
}}
```
Add a `step` property to any field to assign it to a specific step:
```tsx
{
name: 'fieldName',
label: 'Field Label',
type: 'text',
step: 1, // This field will appear in step 1
required: true,
}
```
| Prop | Type | Description | Required |
|------|------|-------------|----------|
| `fields` | `FieldConfig[]` | Array of field configurations | ✅ |
| `onSubmit` | `(data: any) => void` | Form submission handler | ✅ |
| `onChange` | `(data: any) => void` | Form change handler | ❌ |
| `submitButtonText` | `string` | Submit button text | ❌ |
| `defaultValues` | `Record<string, any>` | Default form values | ❌ |
| `isMultiStep` | `boolean` | Enable multi-step functionality | ❌ |
| `currentStep` | `number` | Current step (controlled by parent) | ❌ |
| `onStepChange` | `(step: number, totalSteps: number) => void` | Step change callback | ❌ |
| `showStepNavigation` | `boolean` | Show step navigation | ❌ |
| `stepNavigationProps` | `object` | Step navigation configuration | ❌ |
```tsx
interface BaseField {
name: string;
label: string;
type: 'text' | 'email' | 'password' | 'number' | 'select' | 'checkbox' | 'date' | 'textarea' | 'image';
placeholder?: string;
required?: boolean;
disabled?: boolean;
validation?: ValidationRule;
step?: number; // Step number for multi-step forms (1-based)
col?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
xs?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
sm?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
md?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
lg?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
xl?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
}
interface ImageField extends BaseField {
type: 'image';
src?: string; // Image source URL
alt?: string; // Alt text for accessibility
width?: number | string; // Image width
height?: number | string; // Image height
style?: React.CSSProperties; // Custom styles
className?: string; // Custom CSS class
onClick?: () => void; // Click handler
}
```
```tsx
interface ValidationRule {
pattern?: string | RegExp;
message?: string;
required?: boolean;
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
custom?: (value: any) => boolean | string;
}
```
```tsx
const registrationFields: FieldConfig[] = [
{
name: 'firstName',
label: 'First Name',
type: 'text',
placeholder: 'Enter first name',
required: true,
col: 6
},
{
name: 'lastName',
label: 'Last Name',
type: 'text',
placeholder: 'Enter last name',
required: true,
col: 6
},
{
name: 'email',
label: 'Email',
type: 'email',
placeholder: 'Enter email address',
required: true,
col: 12
},
{
name: 'password',
label: 'Password',
type: 'password',
placeholder: 'Enter password',
required: true,
col: 6,
validation: {
minLength: 8,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
message: 'Password must be at least 8 characters with uppercase, lowercase, number and special character'
}
},
{
name: 'confirmPassword',
label: 'Confirm Password',
type: 'password',
placeholder: 'Confirm password',
required: true,
col: 6
},
{
name: 'agreeToTerms',
label: 'I agree to the Terms and Conditions',
type: 'checkbox',
required: true,
col: 12
}
];
```
```bash
npm run build:lib
npm run build:types
```
```bash
npm run dev
```
MIT © [Ritik](https://github.com/finflow-analytics/finform)
Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/finflow-analytics/finform/issues).
If you like this project, please ⭐ star it on [GitHub](https://github.com/finflow-analytics/finform)!
Drag & drop upload with validation, preview, and optional submit button.
```tsx
import { DocumentUpload } from 'finform-react-builder';
<DocumentUpload
title="Drag & Drop Files"
subtitle="Upload images or PDFs"
buttonText="Select Files"
fileSupportedText=".jpg, .jpeg, .png, .pdf up to 10MB"
config={{
id: 'kyc-docs',
fileTypes: ['image/*', 'application/pdf'],
maxSize: 10 * 1024 * 1024,
multiple: true,
required: true,
submitButton: { text: 'Upload', position: 'right' },
}}
value={files}
onChange={setFiles}
onSubmit={(files) => console.log('Submitting', files)}
/>
```
Card-style stepper with titles, connectors, and progress bar.
```tsx
<FinForm
fields={fields}
stepNavigationProps={{
stepTitles: ['Basic Information', 'Permissions Matrix', 'Review'],
}}
onSubmit={...}
/>
```
Provide guidance beneath inputs via `helperText` on any field.
```tsx
{ name: 'first_name', label: 'First Name', type: 'text', helperText: 'Only alphabets, min 2 characters.' }
```
All inputs default to MUI size "small" for compact UI.
A powerful, flexible React form builder