@onebi_software/form-builder
v0.0.14
Published
Drag-and-drop form builder and renderer for React / Next.js
Maintainers
Readme
@onebi_software/form-builder
A drag-and-drop form builder and form renderer for React / Next.js.
Installation
npm install @onebi_software/form-builderPeer dependencies:
npm install react react-dom @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities zustand lucide-reactTailwind CSS must be set up in your project.
1. Form Builder
Drop this anywhere to get a full drag-and-drop form editor:
import { FormBuilder } from '@onebi_software/form-builder';
<FormBuilder
onSave={(formStructure) => {
// Save formStructure to your database
console.log(formStructure);
}}
showLayout={true} // set false to hide the Table widget tab
/>2. Rendering a Saved Form
Use RenderForm to display a saved form and collect user input:
import { RenderForm } from '@onebi_software/form-builder';
<RenderForm
formStructure={savedFormFromDB}
onSave={(filledForm) => {
// filledForm.fields[n].defaultValue = what the user typed/selected
submitToAPI(filledForm);
}}
/>3. Pre-filling Fields (Edit Mode)
Set defaultValue on each field to pre-fill it:
const form = {
title: 'Customer Form',
fields: [
{ id: 'f1', widget: 'textField', label: 'Name', defaultValue: 'John Doe' },
{ id: 'f2', widget: 'dropdown', label: 'Status', defaultValue: 'active',
options: [{ label: 'Active', value: 'active' }, { label: 'Inactive', value: 'inactive' }] },
{ id: 'f3', widget: 'checkbox-group', label: 'Tags', defaultValue: ['tech'],
options: [{ label: 'Tech', value: 'tech' }, { label: 'Design', value: 'design' }] },
{ id: 'f4', widget: 'date-range', label: 'Duration',
defaultValue: { start: '2024-01-01', end: '2024-12-31' } },
],
};4. Dynamic Dropdowns
You can pass in custom dropdowns that either fetch their options from an API or provide a static list of options. This replaces the legacy ffm_client_dropdown.
First, define your dynamic dropdown configurations:
const customDropdowns = [
// Example 1: Fetching from an API
{
dropdownWidgetName: 'Select Employee', // Human-readable label in the sidebar
widget: 'Dynamic Users', // Unique internal ID for the schema
api: 'https://backend.com/api/users', // The endpoint to fetch options from
// Optional: Pass extra parameters to attach to the API request
queryParams: {
department: 'engineering',
isActive: true
}
},
// Example 2: Providing static local values (no API needed)
{
dropdownWidgetName: 'Select Role',
widget: 'Dynamic Roles',
values: [
{ label: 'Admin', value: 'admin' },
{ label: 'Manager', value: 'manager' }
]
}
];Pass the EXACT same array to both the builder and the renderer so they know how to handle the widget:
In FormBuilder:
<FormBuilder
onSave={handleSave}
dynamicDropdowns={customDropdowns}
/>In RenderForm:
<RenderForm
formStructure={form}
onSave={handleSave}
dynamicDropdowns={customDropdowns}
/>API Requirements
The provided api endpoint should handle GET requests. The renderer will automatically append ?search=&page=1&limit=20 (along with any queryParams) to your URL when the user types or scrolls.
Your endpoint MUST return data grouped as { label, value } objects:
{
"data": [
{ "label": "John Doe", "value": "user_123" },
{ "label": "Jane Doe", "value": "user_456" }
],
"total": 50
}5. File Upload
Basic (single file)
{ id: 'doc', widget: 'fileUpload', label: 'Attachment',
properties: { accept: '.pdf,.docx' },
validation: { maxSize: 5 } } // 5 MB limitMultiple files with a limit
{ id: 'photos', widget: 'fileUpload', label: 'Photos',
properties: { multiple: true, accept: '.jpg,.png', buttonText: 'Add photos' },
validation: { maxFiles: 3, maxSize: 10 } }Only one file upload field is allowed per form.
Edit mode — showing existing files
Pass existing file URLs via fileUrls (not defaultValue):
<RenderForm
formStructure={form}
fileUrls={{ photos: ['https://cdn.example.com/photo1.jpg'] }}
onSave={handleSave}
/>What you get back on save
field.defaultValue = {
existing: string[]; // URLs the user kept
removedFiles: string[]; // URLs to delete from your storage
toBeAdded: File[]; // New files to upload
}onSave={(result) => {
const fileField = result.fields.find(f => f.widget === 'fileUpload');
const { existing, removedFiles, toBeAdded } = fileField.defaultValue;
await Promise.all(toBeAdded.map(uploadFile)); // upload new
await Promise.all(removedFiles.map(deleteFromCDN)); // delete removed
}}6. Validation Rules
Add a validation object to any field:
| Rule | Applies to | Description |
|---|---|---|
| required: true | All | Must not be empty |
| minLength / maxLength | Text fields | Character limits |
| pattern | Text fields | Regex the value must match |
| customMessage | All | Custom error text |
| minSelect / maxSelect | Dropdown, Checkbox | Selection count limits |
| minFiles / maxFiles | File upload | File count limits |
| maxSize | File upload | Max MB per file |
| minRows / maxRows | Table | Row count limits |
// Example: email with regex
{ id: 'email', widget: 'textField', label: 'Email',
validation: {
required: true,
pattern: '^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$',
customMessage: 'Enter a valid email address.',
} }Supported Field Widgets
| Widget | Input type |
|---|---|
| textField | Single-line text |
| textArea | Multi-line text |
| number | Number |
| dropdown | Select (single or multi) |
| radio-group | Radio buttons |
| checkbox-group | Checkboxes |
| date-picker | Date |
| time-picker | Time |
| date-range | Date range |
| date-range-time | Date + time range |
| fileUpload | File upload |
| ffm_client_dropdown | Client picker |
| table | Editable table |
Publishing
npm run build:lib
npm version patch
npm publish --access public