1С Битрикс · 7 мин чтения

Обновление количества товара в корзине Битрикс без перезагрузки

Обновление количества товара в корзине Битрикс без перезагрузки страницы решается одним из двух способов: либо штатным ajax-механизмом компонента sale.basket.basket, либо своим обработчиком на API Bitrix\Sale\Basket, когда корзина живёт вне стандартного шаблона. На практике чаще сталкиваюсь именно со вторым случаем: заказчик просит кастомный виджет корзины в шапке, мобильное приложение или SPA-фронт, и штатная логика компонента до них просто не дотягивается. Ниже разбираю оба варианта и показываю, где обычно ломается синхронизация счётчика и суммы.

Как штатно работает изменение количества в компоненте корзины

Страница корзины на bitrix:sale.basket.basket с шаблоном .default в актуальных версиях платформы не перезагружается целиком при смене количества в поле ввода. Скрипт шаблона перехватывает изменение значения, отправляет запрос через собственный компонентный action и обновляет только строку товара и итоговую сумму заказа. Это встроенное поведение, а не то, что нужно писать с нуля.

Проблема в другом: этот механизм живёт внутри конкретного экземпляра компонента. Если у вас кастомный шаблон корзины, отдельный виджет в шапке сайта (bitrix:sale.basket.basket.small), мобильное приложение через REST или SPA-фронт на отдельном домене, они не в курсе внутренней логики другого экземпляра компонента. Каждый компонент на странице обновляет только себя. Отсюда и берутся типичные жалобы «в корзине сумма поменялась, а счётчик в шапке нет».

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

Свой AJAX-обработчик на Bitrix\Sale\Basket

Для обработчика использую D7 API модуля sale, а не старый CSaleBasket. Он даёт нормальную валидацию количества, скидочные правила пересчитываются автоматически при сохранении, и не нужно вручную дергать пересчёт заказа.

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

use Bitrix\Main\Loader;
use Bitrix\Main\Context;
use Bitrix\Sale\Basket;
use Bitrix\Sale\Fuser;

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

if (!check_bitrix_sessid()) {
    echo json_encode(['success' => false, 'error' => 'Неверный sessid']);
    die();
}

if (!Loader::includeModule('sale')) {
    echo json_encode(['success' => false, 'error' => 'Модуль sale не подключен']);
    die();
}

$itemId = (int)($_POST['id'] ?? 0);
$quantity = (float)($_POST['quantity'] ?? 0);

if ($itemId <= 0 || $quantity <= 0) {
    echo json_encode(['success' => false, 'error' => 'Некорректные параметры']);
    die();
}

$siteId = Context::getCurrent()->getSite();
$basket = Basket::loadItemsForFUser(Fuser::getId(), $siteId);
$item = $basket->getItemById($itemId);

if (!$item) {
    echo json_encode(['success' => false, 'error' => 'Товар не найден в корзине']);
    die();
}

$result = $item->setField('QUANTITY', $quantity);
if (!$result->isSuccess()) {
    echo json_encode(['success' => false, 'error' => implode(', ', $result->getErrorMessages())]);
    die();
}

$basket->save();

$basketSum = 0;
$basketQuantity = 0;
foreach ($basket as $basketItem) {
    $basketSum += $basketItem->getPrice() * $basketItem->getQuantity();
    $basketQuantity += $basketItem->getQuantity();
}

echo json_encode([
    'success' => true,
    'itemSum' => $item->getPrice() * $item->getQuantity(),
    'basketSum' => $basketSum,
    'basketQuantity' => $basketQuantity,
]);

Проверку check_bitrix_sessid() ставлю первой строкой не для галочки. Обработчик меняет данные в БД по POST-запросу, и без проверки токена сессии его можно дергать с чужого сайта поддельной формой. В старом CSaleBasket этот шаг тоже нужен, разница только в наборе методов внутри.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

JS: отправка запроса и обновление DOM без перезагрузки

На фронте вешаю обработчик на изменение поля количества с debounce, чтобы не долбить сервер на каждое нажатие клавиши или клик по стрелочке степпера.

document.querySelectorAll('.basket-item__qty-input').forEach(function (input) {
  let timer = null;

  input.addEventListener('input', function () {
    clearTimeout(timer);
    const itemId = input.dataset.itemId;
    const quantity = parseFloat(input.value);

    if (!quantity || quantity <= 0) {
      return;
    }

    timer = setTimeout(function () {
      fetch('/local/ajax/basket_qty.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: 'id=' + encodeURIComponent(itemId) +
              '&quantity=' + encodeURIComponent(quantity) +
              '&sessid=' + BX.bitrix_sessid()
      })
        .then(function (response) { return response.json(); })
        .then(function (data) {
          if (!data.success) {
            console.error(data.error);
            return;
          }

          document.querySelector('[data-item-sum=\"' + itemId + '\"]').textContent = data.itemSum.toFixed(2);
          document.querySelector('.basket-total-sum').textContent = data.basketSum.toFixed(2);
          document.querySelectorAll('.header-basket-counter').forEach(function (el) {
            el.textContent = data.basketQuantity;
          });
        });
    }, 300);
  });
});

BX.bitrix_sessid() берёт тот же токен, что проверяется на сервере через check_bitrix_sessid(), поэтому отдельно прокидывать его через скрытое поле не нужно, если main.core уже подключен на странице.

Как синхронизировать счётчик в шапке сайта

Вот тут кроется большинство реальных проблем на проектах. Мини-корзина в шапке (bitrix:sale.basket.basket.small) обычно висит отдельным компонентом на всех страницах сайта, а не только на /personal/cart/. Это отдельный экземпляр со своим кэшем и своей отрисовкой, он не знает о том, что где-то на странице count товара только что поменялся через другой обработчик.

Надёжнее всего не полагаться на внутренние события конкретной версии компонента, а просто вернуть нужные значения прямо в ответе своего же ajax-обработчика, как в примере выше (basketSum и basketQuantity), и обновить DOM-элемент счётчика напрямую по селектору. Это то, что я и делаю в JS-коде: document.querySelectorAll(‘.header-basket-counter’) обновляется в том же callback, без ожидания, что сторонний компонент сам заметит изменение.

Если в шапке используется штатный шаблон компонента bitrix:sale.basket.basket.small с собственной ajax-логикой, а не просто статичная цифра, для полной синхронизации проще всего один раз завести на сайте общий счётчик товаров как отдельный span с фиксированным классом, который обновляют все места, меняющие корзину, а не полагаться на то, что штатный виджет сам подхватит чужое изменение.

Гонка запросов при быстрых кликах по степперу

Если пользователь быстро тыкает плюс на степпере пять раз подряд, уходит пять запросов, и они не гарантированно возвращаются в том порядке, в котором отправлены. Последний по времени ответа может оказаться не последним по факту изменения, и в поле окажется не то число, что реально сохранено в корзине.

Что у меня обычно снимает эту проблему:

  • debounce на 250-300 мс перед отправкой запроса, как в примере выше;
  • отключение input на время запроса (disabled), чтобы соседний клик не улетел вторым параллельным запросом;
  • метка времени в каждом запросе и игнорирование в обработчике ответа, если пришёл более старый timestamp, чем уже отрисованный.

Последний пункT нужен не всегда, но на карточках с несколькими товарами в одной строке (например, таблица позиций в B2B-корзине) без него количество иногда «прыгает» обратно на старое значение при нестабильном интернете у клиента.

Когда штатного AJAX недостаточно и нужна доработка

Со штатным поведением компонента справляется большинство обычных интернет-магазинов на стандартном шаблоне. Свой обработчик и ручная синхронизация счётчика реально нужны в конкретных ситуациях:

  • кастомный виджет корзины в шапке или в сайдбаре, отрисованный не через штатный компонент;
  • мобильное приложение или PWA, где фронт дергает Bitrix через REST, а не через рендер компонента;
  • B2B-корзина со списком из десятков позиций и построчным изменением количества без отправки формы целиком;
  • headless-связка, где витрина на отдельном фронтенде, а Битрикс работает только как бэкенд для заказов.

Если задача именно точечная (один обработчик, один виджет), это укладывается в разовую доработку. Если же корзина и весь фронт магазина проектируются заново под такую архитектуру, это уже не патч поверх готового шаблона, а разработка интернет-магазина под ключ, где ajax-логика корзины закладывается сразу в архитектуру, а не прикручивается сбоку.

1С-Битрикс регулярно обновляется, поэтому перед внедрением я сверяюсь с актуальной документацией.

Частые вопросы

Почему при смене количества товара всё равно перезагружается вся страница?

Чаще всего дело в кастомном шаблоне корзины, который переопределяет разметку компонента, но не переносит его JS-логику. Реже - сторонний скрипт или плагин перехватывает submit формы раньше, чем срабатывает штатный обработчик, и браузер уходит в обычную отправку формы. Смотрите вкладку Network в devtools: если при смене количества там вообще нет XHR/fetch-запроса, а сразу идёт полная загрузка документа, значит JS-обработчик на инпут не навешан или падает с ошибкой раньше вызова fetch.

Как обновить счётчик товаров в шапке после смены количества?

Проще и надёжнее всего не полагаться на то, что отдельный компонент мини-корзины сам заметит изменение, а обновлять его DOM-элемент напрямую значениями, которые вернул ваш же ajax-обработчик. Именно так сделано в примере выше: обработчик отдаёт basketQuantity, а JS обновляет все элементы с классом счётчика на странице в том же callback.

Обязательно ли проверять sessid в своём обработчике?

Да. Обработчик меняет данные в базе по POST-запросу от авторизованной сессии, и без проверки check_bitrix_sessid() его может дернуть чужая страница через скрытую форму или скрипт, подменив количество в чужой корзине. Это стандартная защита от CSRF, которую Битрикс даёт из коробки, отключать её ради простоты не стоит.

Что делать, если нужно менять количество сразу в нескольких строках корзины?

Для B2B-таблиц с десятками позиций логичнее принимать в обработчике не один id и quantity, а массив пар id-quantity, в цикле применять setField к каждой найденной позиции корзины и вызывать $basket->save() один раз в конце, а не на каждую строку. Это заметно быстрее, чем слать отдельный запрос на каждое изменённое поле, особенно если пользователь правит сразу пять-шесть позиций перед тем, как уйти со страницы.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно