Dynamic Forms and Extrafields
Dolibarr extrafields allow adding custom fields to business objects. SmartMaker provides a complete system to expose them to the React application with automatic form generation.
Overview
The SmartMaker extrafields system works in three steps:
1. Dolibarr Admin: Configure which extrafields to expose (RO/RW)
↓
2. PHP Mapper: Read configuration, expose metadata
↓
3. React: Dynamically generate forms
1. Admin Configuration
Configuration Page
SmartBoot generates an admin page admin/smartmaker.php that allows configuring which extrafields are exposed to the application.
// Define Dolibarr objects to configure
$elementsToConfig = array(
'fichinter' => 'Interventions',
'societe' => 'Third Parties',
'projet_task' => 'Tasks',
);
This page displays all extrafields of each object with two columns:
| Option | Description |
|---|---|
| RO (Read-Only) | The field is visible in the application but not editable |
| RW (Read-Write) | The field is visible AND editable |
Configuration Storage
The configuration is stored in Dolibarr constants:
MONMODULE_SMARTMAKER_EXTRAFIELDS_RO = "field1,field2,field3"
MONMODULE_SMARTMAKER_EXTRAFIELDS_RW = "field4,field5"
2. PHP Mapper (dmGenericObject)
Basic Structure
The mapper inherits from dmBase and uses dmTrait for extrafields management:
<?php
namespace MonModule\Api;
use SmartAuth\DolibarrMapping\dmBase;
use SmartAuth\DolibarrMapping\dmTrait;
class dmItem extends dmBase
{
use dmTrait;
// Dolibarr source class
protected $parentClassName = 'Fichinter';
// Element for extrafields (llx_extrafields table)
protected $parentElementToUseForExtraFields = 'fichinter';
// Mapping of standard fields
protected $listOfPublishedFields = [
'rowid' => 'id',
'ref' => 'ref',
'description' => 'description',
'fk_statut' => 'status',
];
// Editable fields via API
protected $writableFields = [];
}
Automatic Loading of Extrafields
The constructor loads configuration from Dolibarr:
public function __construct()
{
global $db;
$this->db = $db;
// Read-only extrafields
$extRO = getDolGlobalString('MONMODULE_SMARTMAKER_EXTRAFIELDS_RO');
if (!empty($extRO)) {
foreach (explode(',', $extRO) as $field) {
$field = trim($field);
if (!empty($field)) {
$key = 'options_' . $field;
$this->listOfPublishedFields[$key] = $key;
}
}
}
// Read-write extrafields
$extRW = getDolGlobalString('MONMODULE_SMARTMAKER_EXTRAFIELDS_RW');
if (!empty($extRW)) {
foreach (explode(',', $extRW) as $field) {
$field = trim($field);
if (!empty($field)) {
$key = 'options_' . $field;
$this->listOfPublishedFields[$key] = $key;
$this->writableFields[] = $key;
}
}
}
$this->boot();
}
objectDesc() Method
The objectDesc() method (provided by dmTrait) returns field metadata for the frontend:
// In the controller
$mapping = new dmItem();
$config = $mapping->objectDesc();
// Returns an array with info for each field:
// [
// 'options_niveau' => [
// 'type' => 'select',
// 'label' => 'Level',
// 'required' => true,
// 'options' => ['1' => 'Easy', '2' => 'Medium', '3' => 'Hard'],
// 'writable' => true,
// ],
// ...
// ]
3. Controller: Expose Configuration
The controller must return the configuration to the frontend:
class ItemController
{
private $mapping;
public function __construct()
{
$this->mapping = new dmItem();
}
// GET /items (list + config)
public function index($arr = null)
{
// ... load items ...
return [[
'statusCode' => 200,
'items' => $items,
'config' => $this->mapping->objectDesc(), // Field metadata
], 200];
}
// GET /items/123 (detail + config)
public function show($arr = null)
{
$id = $arr['id'] ?? 0;
// ... load item ...
return [[
'statusCode' => 200,
'item' => $this->mapping->map($dolibarrObject),
'config' => $this->mapping->objectDesc(),
], 200];
}
}
4. Frontend: Dynamic Forms
Store Configuration
Store the configuration received from the API in Redux or local state:
// In component or via Redux
const [config, setConfig] = useState({});
useEffect(() => {
api.private.get('items')
.then(response => {
setConfig(response.config || {});
// ... process items ...
});
}, []);
FormComponentsMap
SmartCommon provides FormComponentsMap which maps Dolibarr types to React components:
import { FormComponentsMap } from "@cap-rel/smartcommon";
// FormComponentsMap returns the appropriate component based on type:
// 'varchar' -> Input
// 'text' -> Textarea
// 'int' -> Input type="number"
// 'double' -> Input type="number" step="0.01"
// 'date' -> DatePicker
// 'datetime' -> DateTimePicker
// 'boolean' -> Checkbox
// 'select' -> Select
// 'sellist' -> Select (with options from DB)
// 'radio' -> RadioGroup
// 'checkbox' -> CheckboxGroup
// 'link' -> SearchSelect (link to another Dolibarr object)
Dynamic Form Generation
const DynamicForm = ({ config, values, onChange }) => {
return (
<div className="flex flex-col gap-4">
{Object.entries(config).map(([fieldName, fieldConfig]) => {
// Get component based on type
const Component = FormComponentsMap(fieldConfig.type);
if (!Component) return null;
return (
<Component
key={fieldName}
label={fieldConfig.label}
value={values[fieldName] || ''}
onChange={(val) => onChange(fieldName, val)}
required={fieldConfig.required}
disabled={!fieldConfig.writable}
options={fieldConfig.options} // For select/radio
/>
);
})}
</div>
);
};
Complete Example with useApi
import { useStates, useApi, Page, Button, Block } from "@cap-rel/smartcommon";
import { FormComponentsMap } from "@cap-rel/smartcommon";
export const ItemEditPage = ({ itemId }) => {
const { states, set } = useStates({
item: null,
config: {},
isLoading: true,
isSaving: false,
});
const { item, config, isLoading, isSaving } = states;
const api = useApi();
// Load item and its configuration
useEffect(() => {
api.private.get(`items/${itemId}`)
.then(response => {
set("item", response.item);
set("config", response.config);
})
.finally(() => set("isLoading", false));
}, [itemId]);
// Change a value
const handleFieldChange = (fieldName, value) => {
set("item", { ...item, [fieldName]: value });
};
// Save
const handleSave = () => {
set("isSaving", true);
api.private.put(`items/${itemId}`, item)
.then(() => {
// Success
})
.finally(() => set("isSaving", false));
};
if (isLoading) return <div>Loading...</div>;
return (
<Page>
<Block title="Edit item">
{/* Standard fields */}
<Input
label="Reference"
value={item?.ref || ''}
disabled
/>
{/* Dynamic extrafields */}
{Object.entries(config).map(([fieldName, fieldConfig]) => {
// Only show extrafields (start with options_)
if (!fieldName.startsWith('options_')) return null;
const Component = FormComponentsMap(fieldConfig.type);
if (!Component) return null;
return (
<Component
key={fieldName}
label={fieldConfig.label}
value={item?.[fieldName] || ''}
onChange={(val) => handleFieldChange(fieldName, val)}
disabled={!fieldConfig.writable}
required={fieldConfig.required}
options={fieldConfig.options}
/>
);
})}
<Button
onClick={handleSave}
loading={isSaving}
>
Save
</Button>
</Block>
</Page>
);
};
Supported Extrafield Types
| Dolibarr Type | React Component | Notes |
|---|---|---|
| varchar | Input | Short text |
| text | Textarea | Long text |
| int | Input (number) | Integer |
| double | Input (number) | Decimal with step |
| date | DatePicker | Date only |
| datetime | DateTimePicker | Date and time |
| boolean | Checkbox | Yes/No |
| select | Select | Dropdown |
| sellist | Select | List from SQL |
| radio | RadioGroup | Radio buttons |
| checkbox | CheckboxGroup | Multiple checkboxes |
| link | SearchSelect | Link to Dolibarr object |
| price | Input (number) | Price with formatting |
Best Practices
Security
- Never expose sensitive extrafields (passwords, tokens)
- Verify permissions server-side before modification
- Validate data received from frontend
Performance
- Cache configuration if it doesn't change often
- Only load necessary extrafields
UX
- Group extrafields by category if numerous
- Use clear and translated labels
- Display required fields distinctly
Summary
The SmartMaker extrafields system allows:
- Configure via Dolibarr admin which fields to expose
- Map automatically with dmGenericObject constructor
- Expose metadata via objectDesc()
- Generate dynamic React forms with FormComponentsMap
This system avoids modifying code each time an extrafield is added: admin configuration is sufficient.