> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# События

> Все события Aftersell Cart SDK: когда каждое из них срабатывает, что оно вам передаёт, для чего его использовать и какие ошибки вызывают бесконечные циклы.

События позволяют запускать код, **когда что-то происходит** в корзине. Они находятся в `window.aftersell.cart.events`.

Подписка — это вызов настройки, поэтому её безопасно размещать в начале вашего скрипта, без необходимости ждать `ready()`.

<div id="available-events">
  ## Доступные события
</div>

| Событие                                       | Payload                                               | Срабатывает когда                                   |
| --------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------- |
| [`cart_loaded`](#cart_loaded)                 | [`AftersellCart`](/ru/aftersell/cart/sdk-cart-object) | Корзина загружается, один раз на страницу.          |
| [`cart_updated`](#cart_updated)               | [`AftersellCart`](/ru/aftersell/cart/sdk-cart-object) | Содержимое корзины меняется, после первой загрузки. |
| [`item_added`](#item_added)                   | `{ item }`                                            | В корзине появляется новая позиция.                 |
| [`item_removed`](#item_removed)               | `{ item }`                                            | Позиция исчезает из корзины.                        |
| [`cart_opened`](#cart_opened-and-cart_closed) | Нет                                                   | Drawer открывается.                                 |
| [`cart_closed`](#cart_opened-and-cart_closed) | Нет                                                   | Drawer закрывается.                                 |
| [`checkout`](#checkout)                       | Нет                                                   | Нажата кнопка оформления заказа.                    |

<div id="subscribing">
  ## Подписка
</div>

`events.on(event, handler)` регистрирует обработчик и **возвращает функцию, которая отменяет подписку**:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const off = window.aftersell.cart.events.on('cart_updated', (state) => {
  console.log('Cart total is now', state.totalPrice);
});

// later, to stop listening:
off();
```

* `events.once(event, handler)`: срабатывает один раз, затем отписывается сам.
* `events.off(event, handler)`: удаляет конкретный обработчик.

Обработчик, выбросивший исключение, изолируется и логируется в консоль; остальные обработчики продолжают выполняться.

***

<div id="the-two-rules">
  ## Два правила
</div>

Почти каждая ошибка с событиями сводится к одному из них.

<div id="dont-change-the-cart-from-cart_updated-without-a-guard">
  ### Не изменяйте корзину из `cart_updated` без защитного условия
</div>

Изменение корзины внутри обработчика `cart_updated` снова вызывает `cart_updated`. Если этот обработчик снова изменяет корзину, у вас бесконечный цикл. Покупатель наблюдает, как его корзина лихорадочно меняется, пока страница бомбардирует Shopify.

<Warning>
  **Никогда не вызывайте действие безусловно из `cart_updated` или `cart_loaded`.** Защитите его проверкой состояния, которое вы собираетесь создать, чтобы второй проход ничего не делал.
</Warning>

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// ❌ Loops forever: every add triggers an update, which triggers another add.
window.aftersell.cart.events.on('cart_updated', (state) => {
  window.aftersell.cart.actions.addItem(GIFT_VARIANT_ID, 1);
});

// ✅ Guarded: once the gift is present, the condition is false and it stops.
window.aftersell.cart.events.on('cart_updated', (state) => {
  const hasGift = state.items.some((line) => line.variantId === GIFT_VARIANT_ID);
  if (state.totalPrice >= 5000 && !hasGift) {
    window.aftersell.cart.actions.addItem(GIFT_VARIANT_ID, 1);
  }
});
```

Корзина даёт вам одну страховку: обновление, дающее **идентичную** корзину, ничего не излучает, поэтому повторный запрос, который ничего не меняет, не перезапустит цикл. Это защищает от случайных «пустых» циклов. Это **не** защищает от обработчика, который действительно каждый раз изменяет корзину.

<div id="treat-the-payload-as-read-only">
  ### Считайте payload доступным только для чтения
</div>

Каждый обработчик одного события получает *один и тот же* объект. Его мутация меняет то, что видят обработчики после вашего, включая обработчики, принадлежащие другим приложениям магазина.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// ❌ Corrupts the payload for every later handler.
window.aftersell.cart.events.on('cart_updated', (state) => {
  state.items = state.items.filter((line) => line.finalLinePrice > 0);
});

// ✅ Copy first.
window.aftersell.cart.events.on('cart_updated', (state) => {
  const paidItems = state.items.filter((line) => line.finalLinePrice > 0);
});
```

Чтобы действительно изменить корзину, используйте [действие](/ru/aftersell/cart/sdk-actions). Чтобы изменить отрисовку позиций, используйте [`registerLineTransform`](/ru/aftersell/cart/sdk-hooks#registerlinetransform).

***

<div id="cart_loaded">
  ## cart\_loaded
</div>

Срабатывает **один раз**, когда корзина впервые загружается на странице. Payload — полный [объект корзины](/ru/aftersell/cart/sdk-cart-object).

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_loaded', (state) => {
  console.log('Page loaded with', state.itemCount, 'items');
});
```

**Используйте для:** всего, что должно выполняться относительно начального состояния корзины, например сверки бесплатного подарка, инициализации виджета или отправки содержимого корзины в аналитику при загрузке страницы.

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

<Tip>
  Логика, которая должна быть корректной и при загрузке страницы, и при каждом последующем изменении, должна подписываться **и** на `cart_loaded`, **и** на `cart_updated` одной и той же функцией. Это стандартный паттерн для «держать X в синхронизации с корзиной».
</Tip>

<div id="cart_updated">
  ## cart\_updated
</div>

Срабатывает каждый раз, когда содержимое корзины меняется **после** первой загрузки, будь то из drawer, из ваших собственных действий, из темы или из другого приложения. Payload — полный [объект корзины](/ru/aftersell/cart/sdk-cart-object).

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_updated', (state) => {
  document.querySelector('#my-total').textContent =
    window.aftersell.cart.actions.formatMoney(state.totalPrice);
});
```

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

Обновление, дающее идентичную корзину, ничего не излучает. Повторное открытие drawer, возврат к вкладке или повторный запрос, вернувший то же содержимое, его не вызовут.

<Warning>
  Перечитайте [два правила](#the-two-rules) перед вызовом действия отсюда.
</Warning>

<div id="item_added">
  ## item\_added
</div>

Срабатывает, когда в корзине появляется **новая позиция**. Payload — `{ item }`, где `item` — это [позиция корзины](/ru/aftersell/cart/sdk-cart-object#cart-lines).

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('item_added', (payload) => {
  myAnalytics.track('Added to cart', {
    id: payload.item.variantId,
    title: payload.item.title,
    quantity: payload.item.quantity,
  });
});
```

**Используйте для:** отслеживания добавлений в корзину в стороннем инструменте аналитики. Это самое распространённое использование SDK. См. [отслеживание добавления в корзину](/ru/aftersell/cart/sdk-use-case-analytics).

Две вещи, которые нужно знать о том, как оно вычисляется:

<Warning>
  **Изменение количества — это не добавление.** Корзина определяет добавления и удаления, сравнивая *позиции*, а не количества. Покупатель, увеличивший позицию с 1 до 3, вызывает `cart_updated`, а не `item_added`. Если вам нужно ловить и увеличения количества, сравнивайте с предыдущим состоянием в обработчике `cart_updated`.
</Warning>

Оно также не срабатывает для товаров, которые уже были в корзине при загрузке страницы; они приходят через `cart_loaded`. Добавление нескольких разных товаров сразу вызывает событие один раз на каждую позицию.

<div id="item_removed">
  ## item\_removed
</div>

Срабатывает, когда позиция исчезает из корзины. Payload — `{ item }`, позиция в том виде, в каком она была непосредственно перед исчезновением, поэтому вы всё ещё можете прочитать её `key`, `variantId` и `title`.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('item_removed', (payload) => {
  console.log('Removed', payload.item.title);
});
```

**Используйте для:** отмены того, что вы сделали при добавлении, например сброса флага, повторного показа предложения, от которого покупатель отказался, или отправки удалений в аналитику.

То же предостережение, что и для `item_added`: уменьшение количества без достижения нуля не является удалением.

<div id="cart_opened-and-cart_closed">
  ## cart\_opened и cart\_closed
</div>

Срабатывают при открытии и закрытии drawer. Без payload.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_opened', () => {
  myAnalytics.track('Cart viewed');
});

window.aftersell.cart.events.on('cart_closed', () => {
  document.body.classList.remove('cart-is-open');
});
```

**Используйте для:** отслеживания просмотров, приостановки видео или карусели за drawer, переключения класса на странице.

Ни одно из них не срабатывает при первоначальной загрузке страницы, только при фактическом открытии или закрытии.

<div id="checkout">
  ## checkout
</div>

Срабатывает, когда покупатель нажимает кнопку оформления заказа, непосредственно перед навигацией браузера. Без payload.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('checkout', () => {
  myAnalytics.track('Checkout started');
});
```

**Используйте для:** отслеживания намерения оформить заказ.

<Warning>
  **Вы не можете отменить оформление заказа из этого обработчика.** Событие — это уведомление, а не барьер; навигация происходит независимо от того, что делает ваш код. Держите обработчик быстрым и синхронным: `await` или медленный сетевой вызов могут не успеть завершиться до выгрузки страницы. Используйте [`navigator.sendBeacon`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon) для всего, что нужно надёжно отправить.
</Warning>

***

<div id="listening-from-outside-the-sdk">
  ## Прослушивание извне SDK
</div>

Каждое событие также отправляется как DOM `CustomEvent` на `window`, поэтому вы можете слушать, не касаясь `window.aftersell.cart`. Это полезно из файла темы, стороннего приложения или скрипта, загружающегося независимо от корзины.

| Событие шины   | DOM-событие                   |
| -------------- | ----------------------------- |
| `cart_loaded`  | `aftersell:cart:cart-loaded`  |
| `cart_updated` | `aftersell:cart:cart-updated` |
| `item_added`   | `aftersell:cart:item-added`   |
| `item_removed` | `aftersell:cart:item-removed` |
| `cart_opened`  | `aftersell:cart:cart-opened`  |
| `cart_closed`  | `aftersell:cart:cart-closed`  |
| `checkout`     | `aftersell:cart:checkout`     |

Обратите внимание на именование: шина использует `snake_case`, DOM-события используют `kebab-case` с префиксом `aftersell:cart:`.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.addEventListener('aftersell:cart:cart-updated', (event) => {
  console.log('Cart total is now', event.detail.totalPrice);
});
```

Payload приходит в `event.detail` и соответствует [объекту корзины](/ru/aftersell/cart/sdk-cart-object). События отправляются на `window`, поэтому слушатель в любом месте страницы их получает. Корзина отрисовывается в shadow root, но граница shadow никогда не находится на пути события. Каждая отправка клонирует payload, поэтому слушатель, мутирующий `event.detail`, не может повлиять на других, а слушатель, выбросивший исключение, не может нарушить работу SDK.

<Warning>
  **`cart-loaded` не воспроизводится повторно в DOM.** Шина повторно воспроизводит `cart_loaded` для поздних подписчиков, но этот путь обходит DOM-отправку, поэтому `window.addEventListener('aftersell:cart:cart-loaded')`, зарегистрированный после того, как корзина уже загрузилась, никогда не сработает. Если порядок загрузки вашего скрипта не гарантирован, используйте `window.aftersell.cart.events.on('cart_loaded', …)`, который воспроизводится повторно, или также слушайте `aftersell:cart:cart-updated`.
</Warning>

<div id="shopify-standard-cart-events">
  ### Стандартные события корзины Shopify
</div>

Отдельно корзина публикует [стандартные события корзины](https://shopify.dev/docs/storefronts/themes/best-practices/standard-events) Shopify на `document` всякий раз, когда она изменяет корзину, поэтому код темы и другие приложения могут реагировать на мутации Aftersell так же, как они реагируют на мутации темы:

| Событие                        | Payload на экземпляре события                                                    |
| ------------------------------ | -------------------------------------------------------------------------------- |
| `shopify:cart:lines-update`    | `action: 'add' \| 'update' \| 'remove'`, `context: 'cart' \| 'product'`, `lines` |
| `shopify:cart:note-update`     | `context: 'cart'`, `note`                                                        |
| `shopify:cart:discount-update` | `discountCodes: [{ code }]`                                                      |

<Warning>
  **Payload не находится в `event.detail`.** `detail` несёт только `{ source: 'aftersell' }` — метку, которую корзина использует, чтобы игнорировать собственные события вместо зацикливания. Всё из таблицы выше присваивается непосредственно объекту события, поэтому читайте `event.action`, а не `event.detail.action`.
</Warning>

Каждое событие также несёт `promise`, который Aftersell разрешает, когда завершается базовая запись, в соответствии со стандартом Shopify — ожидайте его, не разрешайте сами. Они отправляются на `document` и всплывают, поэтому слушатель на `window` тоже их получает.

<div id="where-to-go-next">
  ## Что дальше
</div>

* **[Объект корзины](/ru/aftersell/cart/sdk-cart-object)**: полная структура payload выше.
* **[Действия](/ru/aftersell/cart/sdk-actions)**: как изменить корзину из обработчика.
* **[Хуки](/ru/aftersell/cart/sdk-hooks)**: для изменения отрисовки корзины, а не реакции на неё.
* **[Сценарии использования](/ru/aftersell/cart/sdk-use-cases)**: отслеживание аналитики, бесплатные подарки и другие полные примеры.
