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:

  1. Configure via Dolibarr admin which fields to expose
  2. Map automatically with dmGenericObject constructor
  3. Expose metadata via objectDesc()
  4. Generate dynamic React forms with FormComponentsMap

This system avoids modifying code each time an extrafield is added: admin configuration is sufficient.

Mappers | Module 9: Integration ->