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 |







