npm package: @onebi_software/form-builder
Description: Drag-and-drop form builder and renderer for React / Next.js
Author: OneBi
Latest version: 0.0.14

# @onebi_software/form-builder

A drag-and-drop **form builder** and **form renderer** for React / Next.js.

## Installation

```bash
npm install @onebi_software/form-builder
```

**Peer dependencies:**
```bash
npm install react react-dom @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities zustand lucide-react
```

> Tailwind CSS must be set up in your project.

---

## 1. Form Builder

Drop this anywhere to get a full drag-and-drop form editor:

```tsx
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:

```tsx
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:

```ts
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:
```ts
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:**
```tsx
<FormBuilder
  onSave={handleSave}
  dynamicDropdowns={customDropdowns}
/>
```

**In RenderForm:**
```tsx
<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:
```json
{
  "data": [
    { "label": "John Doe", "value": "user_123" },
    { "label": "Jane Doe", "value": "user_456" }
  ],
  "total": 50
}
```

---

## 5. File Upload

### Basic (single file)

```ts
{ id: 'doc', widget: 'fileUpload', label: 'Attachment',
  properties: { accept: '.pdf,.docx' },
  validation: { maxSize: 5 } }  // 5 MB limit
```

### Multiple files with a limit

```ts
{ 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`):

```tsx
<RenderForm
  formStructure={form}
  fileUrls={{ photos: ['https://cdn.example.com/photo1.jpg'] }}
  onSave={handleSave}
/>
```

### What you get back on save

```ts
field.defaultValue = {
  existing:     string[];  // URLs the user kept
  removedFiles: string[];  // URLs to delete from your storage
  toBeAdded:    File[];    // New files to upload
}
```

```ts
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 |

```ts
// 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

```bash
npm run build:lib
npm version patch
npm publish --access public
```