Development of a form designer on 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1177
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Form Builder Development on 1C-Bitrix

A form builder is a tool that allows a manager or marketer to independently create arbitrary forms without a developer. Adding a field, changing the order, setting validation, configuring where to send data — all through a web interface. 1C-Bitrix has a built-in form module (Web Forms), but it has serious limitations: an outdated interface, lack of responsive UI, and difficulty integrating with external systems. A custom builder solves these limitations.

What to Store

A form consists of metadata (a set of fields and rules) plus results (submitted data). Storage via HL-blocks:

Forms table (b_hl_form_builder):

class FormBuilderTable extends \Bitrix\Main\ORM\Data\DataManager
{
    public static function getTableName(): string { return 'b_hl_form_builder'; }

    public static function getMap(): array
    {
        return [
            new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
            new StringField('TITLE'),
            new StringField('SLUG'),          // URL identifier
            new TextField('FIELDS_JSON'),     // Field descriptions
            new TextField('SETTINGS_JSON'),   // Form settings
            new BooleanField('IS_ACTIVE', ['values' => [false, true]]),
            new IntegerField('SUBMISSIONS_COUNT'),
            new DatetimeField('CREATED_AT'),
        ];
    }
}

FIELDS_JSON — an array of fields with full configuration:

[
  {
    "id": "field_name",
    "type": "text",
    "label": "Your name",
    "placeholder": "John Smith",
    "required": true,
    "width": "half"
  },
  {
    "id": "field_phone",
    "type": "tel",
    "label": "Phone",
    "required": true,
    "mask": "+7 (000) 000-00-00",
    "width": "half"
  },
  {
    "id": "field_service",
    "type": "select",
    "label": "Service",
    "options": ["Development", "Audit", "Support"],
    "required": true
  },
  {
    "id": "field_message",
    "type": "textarea",
    "label": "Message",
    "rows": 4,
    "required": false
  }
]

SETTINGS_JSON — form behavior:

{
  "submit_text": "Submit application",
  "success_message": "Thank you! We will contact you.",
  "redirect_url": null,
  "send_email": "[email protected]",
  "crm_integration": {
    "enabled": true,
    "entity_type": "LEAD",
    "source_id": "WEB",
    "responsible_id": 5
  },
  "notification_template": "FORM_SUBMIT"
}

Submissions table (b_hl_form_submissions):

class FormSubmissionTable extends \Bitrix\Main\ORM\Data\DataManager
{
    public static function getTableName(): string { return 'b_hl_form_submissions'; }

    public static function getMap(): array
    {
        return [
            new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
            new IntegerField('FORM_ID'),
            new TextField('DATA_JSON'),       // Submitted data
            new StringField('USER_IP'),
            new StringField('USER_AGENT'),
            new IntegerField('USER_ID'),      // NULL if anonymous
            new IntegerField('CRM_ENTITY_ID'),// ID of the created lead/deal
            new DatetimeField('CREATED_AT'),
        ];
    }
}

Builder Interface (Admin Panel)

The interface is a drag-and-drop field editor. Implemented in /local/admin/form_builder.php or as a standalone React/Vue page in /local/assets/form-builder/.

A simplified version — without drag-and-drop, just adding fields via a button and sorting with arrows. This is faster to develop and sufficient for most tasks.

// Simple field builder without drag-and-drop
class FormBuilder {
    constructor(containerId) {
        this.container = document.getElementById(containerId);
        this.fields    = [];
        this.bindEvents();
    }

    addField(type) {
        const id    = 'field_' + Date.now();
        const field = {id, type, label: '', required: false, options: []};
        this.fields.push(field);
        this.renderFields();
        return field;
    }

    removeField(fieldId) {
        this.fields = this.fields.filter(f => f.id !== fieldId);
        this.renderFields();
    }

    moveField(fieldId, direction) {
        const idx = this.fields.findIndex(f => f.id === fieldId);
        if (direction === 'up' && idx > 0) {
            [this.fields[idx-1], this.fields[idx]] = [this.fields[idx], this.fields[idx-1]];
        } else if (direction === 'down' && idx < this.fields.length - 1) {
            [this.fields[idx], this.fields[idx+1]] = [this.fields[idx+1], this.fields[idx]];
        }
        this.renderFields();
    }

    getFieldsJson() {
        // Collect current values from DOM
        this.fields = this.fields.map(field => {
            const row = this.container.querySelector(`[data-field-id="${field.id}"]`);
            if (!row) return field;
            return {
                ...field,
                label:    row.querySelector('.field-label').value,
                required: row.querySelector('.field-required').checked,
                placeholder: row.querySelector('.field-placeholder')?.value || '',
            };
        });
        return JSON.stringify(this.fields, null, 2);
    }
}

Frontend Form Rendering

A renderer component reads the JSON configuration and builds the form HTML:

// /local/components/local/form.builder.render/class.php
class FormBuilderRenderComponent extends \CBitrixComponent
{
    public function executeComponent(): void
    {
        $formSlug = $this->arParams['FORM_SLUG'] ?? '';
        $form     = FormBuilderTable::getList([
            'filter' => ['SLUG' => $formSlug, 'IS_ACTIVE' => true],
        ])->fetch();

        if (!$form) {
            $this->arResult['NOT_FOUND'] = true;
            $this->includeComponentTemplate();
            return;
        }

        $form['FIELDS']   = json_decode($form['FIELDS_JSON'], true) ?? [];
        $form['SETTINGS'] = json_decode($form['SETTINGS_JSON'], true) ?? [];

        $this->arResult = [
            'FORM'   => $form,
            'SESSID' => bitrix_sessid(),
        ];

        $this->includeComponentTemplate();
    }
}

In template.php — iterate over $arResult['FORM']['FIELDS'] with a switch on the field type.

Submit Handler

A universal handler works with any form via FORM_ID:

// /local/ajax/form_builder_submit.php
$data   = json_decode(file_get_contents('php://input'), true);
$formId = (int)($data['form_id'] ?? 0);

$form = FormBuilderTable::getByPrimary($formId)->fetch();
if (!$form || !$form['IS_ACTIVE']) {
    echo json_encode(['error' => 'Form not found']);
    exit;
}

$fields   = json_decode($form['FIELDS_JSON'], true) ?? [];
$settings = json_decode($form['SETTINGS_JSON'], true) ?? [];

// Validate required fields
$errors = [];
foreach ($fields as $field) {
    if ($field['required'] && empty($data[$field['id']])) {
        $errors[$field['id']] = 'Field is required';
    }
}

if ($errors) {
    echo json_encode(['errors' => $errors]);
    exit;
}

// Collect data for saving
$submissionData = [];
foreach ($fields as $field) {
    $submissionData[$field['label']] = htmlspecialchars($data[$field['id']] ?? '');
}

// Save submission
$subId = FormSubmissionTable::add([
    'FORM_ID'    => $formId,
    'DATA_JSON'  => json_encode($submissionData, JSON_UNESCAPED_UNICODE),
    'USER_IP'    => $_SERVER['REMOTE_ADDR'],
    'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
])->getId();

// CRM integration
if ($settings['crm_integration']['enabled'] ?? false) {
    // Find name/phone/email fields by type
    $nameField  = $this->findFieldByType($fields, 'text');
    $phoneField = $this->findFieldByType($fields, 'tel');
    $emailField = $this->findFieldByType($fields, 'email');

    \Bitrix\Main\Loader::includeModule('crm');
    $lead = new \CCrmLead(false);
    $lead->Add([
        'TITLE'     => $form['TITLE'] . ': ' . ($data[$nameField['id']] ?? 'New lead'),
        'NAME'      => $data[$nameField['id']] ?? '',
        'PHONE'     => [['VALUE' => $data[$phoneField['id']] ?? '', 'VALUE_TYPE' => 'WORK']],
        'EMAIL'     => [['VALUE' => $data[$emailField['id']] ?? '', 'VALUE_TYPE' => 'WORK']],
        'SOURCE_ID' => 'WEB',
        'COMMENTS'  => json_encode($submissionData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
    ]);
}

echo json_encode(['success' => true]);

Development Timeline

Option Scope Timeline
Basic builder Field management, saving, rendering 8–12 days
With CRM integration + Leads, field mapping, notifications 12–18 days
Drag-and-drop + analytics + Visual editor, form statistics 18–28 days