Запуск ajax-действий в компоненте. BX.ajax.runComponentAction

Структура компонента:

local/components/vendor/example/

файлы: 1.class.php -одного этого файла достаточно, чтобы компонент работал.

Вызов BX.ajax.runComponentAction

BX.ajax.runComponentAction('vendor:example', 'greet', {
    mode: 'class', //это означает, что мы хотим вызывать действие из class.php, а конкретно метод greet. Может принимать ajax.php - в нем автоСвязывание идет сразу под капотом.
    data: {
        person: 'Hero!' //данные будут автоматически замаплены на параметры метода
    },
    analyticsLabel: {
        viewMode: 'grid',
        filterState: 'closed'
    }
}).then(function (response) {
    console.log(response);
    /**
    {
        "status": "success",
        "data": "Hi Hero!",
        "errors": []
    }
    **/
}, function (response) {
    //сюда будут приходить все ответы, у которых status !== 'success'
    console.log(response);
    /**
    {
        "status": "error",
        "errors": [...]
    }
    **/
});

Получить ответ BX.ajax.runComponentAction в методе PHP

async function basketIsEmpty(productId = null) {
    try {
        return await BX.ajax.runComponentAction('bannikon:warehouse', 'checkBasketForEmpty', {
            mode: 'class',
            data: {
                productId: +productId
            }
        });
    } catch (error) {
        console.error(error);
        return null;
    }
}

async function openModalMapHandler(button) {
    // Проверяем пустая ли корзина?
    let basketIsEmptyResponse = await basketIsEmpty()

    // Если корзина пуста.
    if (basketIsEmptyResponse.data.isEmpty === true) {
        await getDataAndShowMapModal(button)
    } else {
        // ...
    }
}

Работа с файлами и FormData

BX.ajax.runComponentAction('componentName', "actionName", {
    mode: 'ajax',
    data: new FormData(form)
})

// Вариант с class
BX.ajax.runComponentAction('bannikon:order.make', 'createOrder', {
    mode: 'class',
    data: {
        formData: Object.fromEntries(new FormData(form)), // Могут быть проблемы, если есть квадратные скобки (напр. property[CITY]: "Москва"). Тогда можно использовать [...new FormData(form)]
        deliveryData: this.deliveryData
    }
    }).then((response) => {
        console.log(response)
    }, function (response) {
    // сюда будут приходить все ответы, у которых status !== 'success'.
    console.log(response);
});

Если в форме нужно собрать несколько значений в массив, можно воспользоваться методом getAll у FormData

< form class="reg" action="" data-modal-invite>
	< input type="hidden" name="actorId[]" value="1">
	< input type="hidden" name="actorId[]" value="2">
< /form>
form.addEventListener('submit', event => {
                event.preventDefault()
                let formData = new FormData(form);
                let data = {
                    ...Object.fromEntries(formData),
                    'actorId': formData.getAll('actorId[]') // получаем массив значений и заменяем ключ, убрав скобки [].
                };
                delete data['actorId[]'];
                BX.ajax.runComponentAction('kochnev:invite.form', 'inviteActors', {
                    mode: 'class',
                    data: {
                        formData: Object.fromEntries(new FormData(form))
                    }
                }).then((response) => {
                    console.log(response)
                }, function (response) {
                    // сюда будут приходить все ответы, у которых status !== 'success'.
                    console.log(response);
                });
            })

На сервере принимаем

use \Bitrix\Main\Application;
$request = Application::getInstance()->getContext()->getRequest();
$files = $request->getFileList()->toArray();
/* или через $_FILES */

// Получить поле из $request
$element->get('NAME')

// Вариант с class
public function createOrderAction(array $formData, array $deliveryData)
{
    dd($formData, $deliveryData);
}

Пример

Файл class.php

use Bitrix\Main\Engine\Contract\Controllerable; use CBitrixComponent;

<?php

namespace Vendor;

use Bitrix\Main\Diag\Debug;
use \Bitrix\Main\Engine\ActionFilter\Authentication;
use CIBlockElement;
use CModule;
use CSaleBasket;
use Bitrix\Main\Application;
use Bitrix\Main\Web\Cookie;
use Bitrix\Main\Loader;
use Bitrix\Highloadblock\HighloadBlockTable;

class RecipeFind extends \CBitrixComponent implements \Bitrix\Main\Engine\Contract\Controllerable
{
    public function configureActions()
    {
        return [
            'addToBasket' => [
                'prefilters' => [],
                'postfilters' => [],
            ],
            'addOrder' => [
                'prefilters' => [],
                'postfilters' => [],
            ],
        ];
    }

    public function addToBasketAction($id, $quan, $recipe):array
    {
        // ...

        return ['status' => 'success', 'id' => $id, $res];
    }

    public function addOrderAction($name, $phone, $product):array
    {
        // ...

        return [];
    }
}

BX.ajax.runComponentAction

BX.ajax.runComponentAction('vendor:component.name', 'checkRecipeProductsInBasket', {
                    mode: 'class',
                    data: {
                        list: list,
                        arProductIdInBasket: arBasketItemsId
                    }
                }).then(function (response) {
                    console.log(response);
                    if (response.data['can_order']) {

                        loader(false)

                        switch (response.data['can_order']) {
                            case 0: {
                                disableSaveButton()
                                if (statusContainer.classList.contains('_confirmed')) {
                                    statusContainer.classList.remove('_confirmed')
                                }

                                statusContainer.classList.add('_refused')
                                statusContainer.textContent = 'Рецепт не найден.'
                                checkHiddenInput.value = ''

                                break;
                            }
                            case 1: {
                                if (statusContainer.classList.contains('_refused')) {
                                    statusContainer.classList.remove('_refused')
                                }
                                statusContainer.classList.add('_confirmed')
                                statusContainer.textContent = 'Ваш рецепт принят.'

                                checkHiddenInput.value = checkInput.value
                                disableSaveButton(false)

                                break;
                            }
                            case 2: {
                                disableSaveButton()
                                if (statusContainer.classList.contains('_confirmed')) {
                                    statusContainer.classList.remove('_confirmed')
                                }

                                statusContainer.classList.add('_refused')
                                statusContainer.textContent = 'Не все товары из корзины есть в рецепте. Либо недопустимая дозировка.'
                                checkHiddenInput.value = ''

                                break;
                            }
                        }
                    }

                }, function (response) {
                    // сюда будут приходить все ответы, у которых status !== 'success'
                    loader(false)
                    statusContainer.classList.add('_refused')
                    statusContainer.textContent = 'Ошибка запроса.'
                    console.log(response);
                });

Как скачать файл runComponentAction

PHP

/**
 * Экспортирует данные в файл
 * @param int $id
 * @param string $type
 * @param null|array $params
 * @return null|BFile
 */
public function exportAction(int $id, string $type, ?array $params = []): ?BFile
{
   try {
      $file = \Bitrix\Main\Engine\Response\BFile::createByFileId(985);
   } catch (\Throwable $th) {
      $this->addError(new Error($th->getMessage(), $th->getCode()));
   }
   return $file;
}

JS

BX.ajax.runComponentAction('company:sale.basket.detail', 'export', {
   mode: 'ajax',
   data: {id, type},
   method: 'POST',
})
?.then(response => {
   if (response.status === 'success' && response.data?.url) {
     window.location.href = response.data.url;
   }
})
.catch(response => {...})

Как скачать сгенерированный файл runComponentAction

PHP

/**
 * Экспортирует данные в файл
 * @param int $id
 * @param string $type
 * @param null|array $params
 * @return null|BFile
 */
public function exportAction(int $id, string $type, ?array $params = []): ?BFile
{
   try {
      $pFilename = \TEMP_DIR . '/' . 'item.xls';
      \file_put_contents($pFilename, $data);
      $arFile = \CFile::MakeFileArray($pFilename, false, true);
      $file = new BFile($arFile);
   } catch (\Throwable $th) {
      $this->addError(new Error($th->getMessage(), $th->getCode()));
   }
   return $file;
}

JS

BX.ajax.runComponentAction('company:sale.basket.detail', 'export', {
   mode: 'ajax',
   data: {id, type},
   method: 'POST',
})
?.then(response => {
   if (response.status === 'success' && response.data?.url) {
     window.location.href = response.data.url;
   }
})
.catch(response => {...})

Как загрузить файл runComponentAction

PHP

/**
 * Импортируем файл
 * @param array $data
 * @param null|array $params
 * @return array
 */
public function importAction(array $data, ?array $params = []): array
{
   try {
           ...
   } catch (\Throwable $th) {
      $this->addError(new Error($th->getMessage(), $th->getCode()));
   }
   return $file;
}

JS

const data = document.getElementById();
// data =

BX.ajax.runComponentAction('company:sale.basket.detail', 'import', {
   mode: 'ajax',
   data: Default,
})
?.then(response => {...})
.catch(response => {...})

Пример с лоадером и валидацией

JS

document.addEventListener('DOMContentLoaded', () => {
    let formFaq = document.querySelector('form[name="form_faq"]')

    console.log(formFaq)

    if (formFaq) {

        formFaq.addEventListener('submit', sendForm)

        function sendForm(e) {
            e.preventDefault()
            toggleLoader(formFaq, true)
            BX.ajax.runComponentAction('bannikon:form', 'addFaq', {
                mode: 'class',
                data: new FormData(formFaq)
            }).then(function (response) {
                setTimeout(() => {
                    toggleLoader(formFaq)
                    if (response.data.success === true) {
                        toggleAlert(true)
                        formFaq.reset()
                    } else {
                        toggleAlert(false, response.data.error)
                    }
                }, 2000)
            }, function (response) {
                //сюда будут приходить все ответы, у которых status !== 'success'
                console.log(response);
                setTimeout(() => {
                    toggleLoader(formFaq)
                    toggleAlert(false, response.data.error)
                }, 2000)

            });
        }

        function toggleAlert(success, error = null) {
            const alertSuccess = formFaq.querySelector('.alert-success');
            const alertDanger = formFaq.querySelector('.alert-danger');

            if (success) {
                if (!alertDanger.classList.contains('d-none')) {
                    alertDanger.classList.add('d-none');
                }
                alertSuccess.classList.remove('d-none');
                formFaq.querySelector('[data-btn-submit]').disabled = true;
            } else {
                if (!alertSuccess.classList.contains('d-none')) {
                    alertSuccess.classList.add('d-none');
                }

                if (error !== null) {
                    alertDanger.textContent = error
                } else {
                    alertDanger.textContent = 'Сервис временно недоступен. Попробуйте отправить Ваше сообщение немного позже.'
                }

                alertDanger.classList.remove('d-none');
            }
        }

        function toggleLoader(elem, toggle = false) {
            if (!toggle) {
                elem.classList.remove('_loader')
                return;
            }
            elem.classList.add('_loader')
        }
    }
});

Html

< form action="#" name="form_faq">
                < div class="row">
                    < div class="col">
                        < label for="name" class="form-label">Имя*< /label>
                        < input type="text" class="form-control" id="name" name="name"
                               placeholder="Укажите Ваше имя"/>
                    < /div>
                    < div class="col">
                        < label for="patronymic" class="form-label">Отчество (при
                            наличии)< /label>
                        < input type="text" class="form-control" id="patronymic" name="second_name"
                               placeholder="Введите номер дела"/>
                    < /div>
                    < div class="col">
                        < label for="city" class="form-label">Город< /label>
                        < div class="select__box">
                            < select class="form-select" aria-label="" id="city" name="city">
                                < option selected>
                                    ---
                                < /option>
                                < option value="1">
                                    Арзамас
                                < /option>
                                < option value="2">
                                    Москва
                                < /option>
                                < option value="3">
                                    Комсомольск-на-Амуре
                                < /option>
                            < /select>
                        < /div>
                    < /div>
                < /div>
                < div class="row">
                    < div class="col">
                        < label for="message" class="form-label">Ваш Вопрос*< /label>
                        < textarea class="form-control" id="message" rows="4"
                                  placeholder="Введите текст сообщения" name="message">< /textarea>
                    < /div>
                < /div>
                < button class="col-4 btn blue" data-btn-submit
                        style="border: 1px solid teal;">Отправить
                < /button>
                < div class="alert alert-success col col-5 mt-3 d-none" role="alert">
                    Спасибо! Ваше сообщение отправлено.
                < /div>
                < div class="alert alert-danger col col-5 mt-3 d-none" role="alert">< /div>
            < /form>

CSS

/*=== Loader ===*/
._loader {
    position: relative;
}

._loader:before {
    content: '';
    background: rgba(255, 255, 255, .8);
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    z-index: 10;
}

._loader:after {
    content: '';
    width: 48px;
    height: 48px;
    border: 5px solid var(--blue);
    border-bottom-color: var(--sand-color);
    border-radius: 50%;
    display: inline-block;
    box-sizing: border-box;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    z-index: 20;
    animation: rotation 1s linear infinite;
}

@keyframes rotation {
    0% {
        transform: translate(-50%, -50%) rotate(0deg);
    }
    100% {
        transform: translate(-50%, -50%) rotate(360deg);
    }
}