Главная/Статьи/Битрикс: отправка формы через BX.ajax

Битрикс: отправка формы через BX.ajax

AJAX-отправка данных формы без перезагрузки страницы с использованием Bitrix JavaScript API. FormData, валидация, обработка ответа.

ДМ
Дмитрий Мещеряков
📅 19 апреля 2021 г.📖 7 мин чтения

AJAX-форма обратной связи — задача, которую на Битриксе решают в первый месяц работы с системой и почти всегда одинаково: находят пример в интернете, копируют, оно работает. Проблемы появляются позже — когда в инфоблоке заявок оказывается тысяча спам-записей, а сообщение об ошибке в форме однажды выводит то, что прислал пользователь.

Разберём рабочую реализацию и то, чего в скопированных примерах обычно нет.

Зачем BX.ajax, если есть fetch

Честный ответ: если вы пишете фронтенд с нуля и не трогаете компоненты Битрикса, fetch вам подойдёт лучше — он проще, у него нормальные промисы и он не тянет за собой ядро BX.

BX.ajax выигрывает в другом сценарии: когда ответ сервера содержит вёрстку с компонентами Битрикса. Он умеет выполнять пришедшие в ответе скрипты и подгружать объявленные расширения — то, что при работе через fetch придётся делать руками. Плюс на большинстве проектов ядро BX на странице уже есть, так что дополнительной загрузки не возникает.

Что важно понимать сразу: BX.ajax не добавляет CSRF-защиту автоматически. Токен нужно прикладывать самому, а на сервере — проверять. Это делается в две строки, но пропускается в девяти самописных обработчиках из десяти.

Базовый пример

HTML-форма

html
<form id="callback-form" class="callback-form">
    <input type="text" name="name" placeholder="Ваше имя" required />
    <input type="tel" name="phone" placeholder="Телефон" required />
    <textarea name="message" placeholder="Сообщение"></textarea>
    <button type="submit">Отправить</button>
    <div class="form-message"></div>
</form>

JavaScript (BX.ajax)

javascript
BX.ready(function() {
    var form = BX('callback-form');
    
    BX.bind(form, 'submit', function(e) {
        e.preventDefault();
        
        var formData = new FormData(form);
        var messageEl = form.querySelector('.form-message');
        var submitBtn = form.querySelector('button[type="submit"]');
        
        // Блокируем кнопку
        submitBtn.disabled = true;
        submitBtn.textContent = 'Отправка...';
        
        BX.ajax({
            url: '/local/ajax/callback.php',
            data: formData,
            method: 'POST',
            dataType: 'json',
            processData: false,  // Важно для FormData!
            preparePost: false,  // Важно для FormData!
            onsuccess: function(response) {
                if (response.success) {
                    messageEl.innerHTML = '<span class="success">Заявка отправлена!</span>';
                    form.reset();
                } else {
                    messageEl.innerHTML = '<span class="error">' + response.error + '</span>';
                }
            },
            onfailure: function() {
                messageEl.innerHTML = '<span class="error">Ошибка соединения</span>';
            },
            onfinish: function() {
                submitBtn.disabled = false;
                submitBtn.textContent = 'Отправить';
            }
        });
    });
});
⚠️ Важно

Важно: При использовании FormData обязательно установите processData: false и preparePost: false. Иначе данные будут повреждены.

PHP-обработчик

⚠️ Важно

Обработчик ниже — учебный минимум, в прод его в таком виде выкладывать нельзя. В нём нет ни проверки CSRF-токена, ни защиты от автоматических отправок. Публичный эндпоинт, который пишет в инфоблок и шлёт почту, спамботы находят за считанные дни — дальше в статье оба недостающих куска.

php
<?php
// /local/ajax/callback.php

require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';

use Bitrix\Main\Application;
use Bitrix\Main\Web\Json;

header('Content-Type: application/json; charset=UTF-8');

// Проверка метода
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    echo Json::encode(['success' => false, 'error' => 'Method not allowed']);
    die();
}

// Получение данных
$request = Application::getInstance()->getContext()->getRequest();
$name = trim($request->getPost('name') ?? '');
$phone = trim($request->getPost('phone') ?? '');
$message = trim($request->getPost('message') ?? '');

// Валидация
$errors = [];

if (empty($name)) {
    $errors[] = 'Укажите имя';
}

if (empty($phone)) {
    $errors[] = 'Укажите телефон';
} elseif (!preg_match('/^[\d\s\(\)\-\+]{10,18}$/', $phone)) {
    $errors[] = 'Некорректный формат телефона';
}

if (!empty($errors)) {
    echo Json::encode([
        'success' => false,
        'error' => implode(', ', $errors),
    ]);
    die();
}

// Обработка заявки
try {
    // Сохранение в инфоблок
    \Bitrix\Main\Loader::includeModule('iblock');
    
    $el = new \CIBlockElement();
    $elementId = $el->Add([
        'IBLOCK_ID' => CALLBACK_IBLOCK_ID,
        'NAME' => "Заявка от {$name}",
        'ACTIVE' => 'Y',
        'PROPERTY_VALUES' => [
            'PHONE' => $phone,
            'MESSAGE' => $message,
        ],
    ]);
    
    if (!$elementId) {
        throw new \RuntimeException($el->LAST_ERROR);
    }
    
    // Отправка email
    \CEvent::Send('CALLBACK_FORM', SITE_ID, [
        'NAME' => $name,
        'PHONE' => $phone,
        'MESSAGE' => $message,
    ]);
    
    echo Json::encode(['success' => true]);
    
} catch (\Throwable $e) {
    echo Json::encode([
        'success' => false,
        'error' => 'Ошибка сохранения. Попробуйте позже.',
    ]);
    
    // Логируем реальную ошибку
    AddMessage2Log($e->getMessage(), 'callback_form');
}

Отдельно стоит отметить обработку ошибок в catch: пользователю уходит нейтральное «Ошибка сохранения», а реальный текст исключения пишется в лог. Отдавать наружу $e->getMessage() — распространённая привычка, которая на боевом сервере выдаёт структуру инфоблоков, пути к файлам, а иногда и фрагменты SQL.

Расширенный класс для работы с формами

javascript
/**
 * Класс для AJAX-форм в Битрикс
 */
class BxAjaxForm {
    constructor(formSelector, options = {}) {
        this.form = typeof formSelector === 'string' 
            ? document.querySelector(formSelector) 
            : formSelector;
            
        if (!this.form) {
            console.error('Form not found:', formSelector);
            return;
        }

        this.options = Object.assign({
            url: this.form.action || '/local/ajax/form.php',
            method: 'POST',
            submitButton: this.form.querySelector('[type="submit"]'),
            messageContainer: this.form.querySelector('.form-message'),
            loadingClass: 'is-loading',
            successClass: 'is-success',
            errorClass: 'is-error',
            resetOnSuccess: true,
            onBeforeSubmit: null,
            onSuccess: null,
            onError: null,
            onComplete: null,
        }, options);

        this.init();
    }

    init() {
        BX.bind(this.form, 'submit', this.handleSubmit.bind(this));
    }

    handleSubmit(e) {
        e.preventDefault();
        
        // Callback перед отправкой
        if (typeof this.options.onBeforeSubmit === 'function') {
            const shouldContinue = this.options.onBeforeSubmit(this.form);
            if (shouldContinue === false) return;
        }

        this.setLoading(true);
        this.clearMessage();

        const formData = new FormData(this.form);
        
        // Добавляем CSRF-токен если есть
        const sessid = BX.bitrix_sessid();
        if (sessid) {
            formData.append('sessid', sessid);
        }

        BX.ajax({
            url: this.options.url,
            data: formData,
            method: this.options.method,
            dataType: 'json',
            processData: false,
            preparePost: false,
            onsuccess: this.handleSuccess.bind(this),
            onfailure: this.handleError.bind(this),
        });
    }

    handleSuccess(response) {
        this.setLoading(false);

        if (response.success) {
            this.showMessage(response.message || 'Успешно отправлено', 'success');
            
            if (this.options.resetOnSuccess) {
                this.form.reset();
            }

            if (typeof this.options.onSuccess === 'function') {
                this.options.onSuccess(response, this.form);
            }
        } else {
            this.showMessage(response.error || 'Произошла ошибка', 'error');
            
            // Подсветка ошибочных полей
            if (response.fields) {
                this.highlightErrors(response.fields);
            }

            if (typeof this.options.onError === 'function') {
                this.options.onError(response, this.form);
            }
        }

        if (typeof this.options.onComplete === 'function') {
            this.options.onComplete(response, this.form);
        }
    }

    handleError(error) {
        this.setLoading(false);
        this.showMessage('Ошибка соединения с сервером', 'error');
        
        if (typeof this.options.onError === 'function') {
            this.options.onError({ error: 'Network error' }, this.form);
        }
    }

    setLoading(isLoading) {
        if (this.options.submitButton) {
            this.options.submitButton.disabled = isLoading;
        }
        
        this.form.classList.toggle(this.options.loadingClass, isLoading);
    }

    showMessage(text, type = 'info') {
        if (!this.options.messageContainer) return;

        // textContent, а не innerHTML: текст приходит с сервера и может
        // содержать пользовательский ввод (например, эхо введённого email)
        this.options.messageContainer.textContent = text;
        this.options.messageContainer.className = 'form-message ' + 
            (type === 'success' ? this.options.successClass : 
             type === 'error' ? this.options.errorClass : '');
    }

    clearMessage() {
        if (this.options.messageContainer) {
            this.options.messageContainer.innerHTML = '';
            this.options.messageContainer.className = 'form-message';
        }
        
        // Убираем подсветку ошибок
        this.form.querySelectorAll('.field-error').forEach(el => {
            el.classList.remove('field-error');
        });
    }

    highlightErrors(fields) {
        Object.keys(fields).forEach(fieldName => {
            const field = this.form.querySelector(`[name="${fieldName}"]`);
            if (field) {
                field.classList.add('field-error');
            }
        });
    }
}

// Использование
BX.ready(function() {
    new BxAjaxForm('#callback-form', {
        url: '/local/ajax/callback.php',
        onSuccess: function(response) {
            console.log('Form submitted:', response);
            // Можно закрыть модалку, показать thank-you page и т.д.
        }
    });

    new BxAjaxForm('#subscribe-form', {
        url: '/local/ajax/subscribe.php',
        resetOnSuccess: true,
        onSuccess: function(response) {
            // Отправка цели в метрику
            if (window.ym) {
                ym(YANDEX_METRIKA_ID, 'reachGoal', 'subscribe');
            }
        }
    });
});
💡 Совет

Про textContent вместо innerHTML. Сообщение об ошибке часто собирается из пользовательского ввода («Некорректный адрес: значение из поля»). Стоит один раз вставить такое через innerHTML — и форма превращается в готовый вектор XSS. Если разметка в сообщении действительно нужна, экранируйте текст на сервере, а не полагайтесь на то, что «туда всё равно приходит только наш текст».

Обработчик с CSRF-защитой

php
<?php
// /local/ajax/secure_form.php

require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';

use Bitrix\Main\Application;
use Bitrix\Main\Web\Json;

header('Content-Type: application/json; charset=UTF-8');

$request = Application::getInstance()->getContext()->getRequest();

// Проверка CSRF
if (!check_bitrix_sessid()) {
    echo Json::encode([
        'success' => false,
        'error' => 'Сессия истекла. Обновите страницу.',
    ]);
    die();
}

// ... остальная обработка

Отправка файлов

html
<form id="resume-form" enctype="multipart/form-data">
    <input type="text" name="name" required />
    <input type="file" name="resume" accept=".pdf,.doc,.docx" />
    <button type="submit">Отправить резюме</button>
</form>
javascript
BX.ready(function() {
    new BxAjaxForm('#resume-form', {
        url: '/local/ajax/resume.php',
        onBeforeSubmit: function(form) {
            var fileInput = form.querySelector('[name="resume"]');
            if (fileInput.files.length === 0) {
                alert('Прикрепите файл резюме');
                return false;
            }
            
            // Проверка размера (5 MB)
            if (fileInput.files[0].size > 5 * 1024 * 1024) {
                alert('Файл слишком большой. Максимум 5 MB.');
                return false;
            }
            
            return true;
        }
    });
});
⚠️ Важно

Проверка файла на клиенте — это про удобство, а не про безопасность. Ограничения по расширению и размеру в JavaScript отсекают честные ошибки пользователя и экономят ему время на загрузке. Атакующий отправит запрос напрямую, минуя вашу форму. Проверять тип и размер на сервере обязательно, причём тип — по реальному содержимому (finfo), а не по расширению и не по присланному Content-Type.

Чек-лист публичной формы

Список, по которому я прохожусь перед выкладкой любой формы, доступной без авторизации:

  1. CSRF-токен прикладывается и проверяется (bitrix_sessid() на клиенте, check_bitrix_sessid() на сервере).
  2. Вся валидация продублирована на сервере. Клиентская — только для UX.
  3. Есть защита от автоматических отправок: honeypot-поле, ограничение частоты по IP или капча на подозрительных попытках. Без этого инфоблок заявок за месяц превращается в свалку.
  4. Файлы проверяются по содержимому, сохраняются вне DOCUMENT_ROOT или в директории с запретом на выполнение PHP.
  5. Текст ошибок наружу — нейтральный, детали — в лог.
  6. Ответ выводится через textContent, если это не заведомо доверенная разметка.
  7. Кнопка блокируется на время отправки — иначе двойной клик даст две заявки.
💡 Совет

И самый практичный совет: прежде чем писать своё, посмотрите на bitrix:main.feedback и веб-формы. В них уже есть валидация, капча и сохранение результатов, а свой обработчик оправдан тогда, когда нужна нестандартная логика — интеграция с CRM, сложные зависимые поля, специфический формат ответа.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.