Атрибуты data-*: поведение витрины
Главная страница для верстальщика: полный список атрибутов, за которые цепляется магазин, с примерами разметки.
На этой странице
- Что подключает магазин, а что делает тема
- Адреса действий: объект window.CartiX
- Корзина в шапке и пометки «в корзине»
- Шапка
- Вес корзины
- Карточка товара
- Добавление в корзину — задача темы
- Выезжающая корзина
- Позиции — обычным шаблоном
- Как показать корзину после добавления
- Фотография позиции
- Корзина окном по центру
- События и общий доступ
- Живой поиск
- Покупка упаковками
- Конфигуратор комплектов
- Окно с аксессуарами в каталоге
- Оформление заказа
- Корень формы
- Места, которые заполняет магазин
- Контакт и получатель
- Город и отделение
- Сохранённые адреса
- Промокод и баллы
- Итоги
- Позиции корзины при оформлении
- Вход по ходу оформления
- Что запускает пересчёт
- Оплата по ссылке
- Классы состояния
- Минимальная рабочая разметка
Магазин подключает к любой теме готовое поведение: живой поиск, синхронизацию
корзины, оформление заказа, счёт упаковками, конфигуратор комплектов, оплату по
ссылке. Тема этот код не пишет — она размечает места атрибутами data-*, за
которые магазин цепляется.
Отсюда правило, из-за которого всё так устроено: новая тема получает рабочий магазин, а обновление магазина приходит во все темы сразу. Новый перевозчик, новый способ оплаты, новое поле в оформлении появятся и у вашей темы — если она не переписала это своим скриптом.
Что подключает магазин, а что делает тема
| Возможность | Кто делает |
|---|---|
| Живой поиск с подсказками | магазин |
| Счётчик корзины и пометки «в корзине» | магазин |
| Оформление заказа целиком: пересчёт, способы, карта отделений | магазин |
| Счёт упаковками | магазин |
| Конфигуратор комплектов (аксессуары) | магазин |
| Оплата по ссылке из письма | магазин |
| Добавление товара в корзину | тема |
| Избранное, сравнение | тема |
| Отзывы, вопросы | тема |
| Галерея, выезжающее меню, вкладки | тема |
Магазин подключает свои скрипты сам, по типу страницы: cart-sync.js и живой
поиск — везде, checkout.js — на страницах оформления, packaging.js и
accessories.js — на страницах товара, pay.js — на странице оплаты. Вам не
нужно их подключать и нельзя дублировать: свой счётчик количества над блоком
упаковок будет драться с магазинным.
⚠️ Без JavaScript оформление заказа не работает. И пересчёт, и создание
заказа отвечают данными, а не страницей. Заглушки <noscript> в базовой теме
нет — если она вам нужна, добавьте её сами.
Адреса действий: объект window.CartiX
Адреса печатает магазин — теме их знать не нужно. Первой строкой <head>
появляется объект, доступный и скриптам магазина, и вашим:
window.CartiX = {
action: 'category', // раздел страницы: default, category, product, cart…
locale: 'ru',
currency: 'UAH',
urls: { … }, // адреса действий, уже с языковым префиксом витрины
accessories: { popupRepeat: 'product' },
runtime: { }, // что вы объявили своим в theme.json
};
Ключ в urls |
Для чего |
|---|---|
cartAdd, cartUpdate, cartRemove |
положить, изменить количество, убрать |
cartState |
счётчик в шапке и пометки «в корзине» |
favoritesToggle, favoritesState |
избранное |
compareToggle, compareState |
сравнение |
reviewSubmit, reviewReply, reviewVote |
отзывы |
questionSubmit, questionAnswer, questionVote |
вопросы о товаре |
configuratorAdd |
кнопка «Добавить комплект» |
configuratorAccessories, configuratorAttach |
окно аксессуаров в каталоге |
Пользоваться так:
fetch(CartiX.urls.cartAdd, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': CartiX.csrf,
},
credentials: 'same-origin',
body: JSON.stringify({ product_id: id, qty: 1 }),
});
Адреса приходят готовыми к вызову: на витрине со вторым языком в них уже стоит
префикс (/uk/cart/add), а у читающих адресов — завершающий слеш, иначе витрина
ответила бы переадресацией и каждое обновление корзины стоило бы лишнего рейса.
Раньше всё это писали атрибутами на <body> — пятнадцать data-*, которые
верстальщик обязан был помнить наизусть, а забытый ломал корзину молча. Темы
поставки их больше не печатают. Если вы делаете тему по старой инструкции, она
продолжит работать: скрипты магазина читают атрибуты запасным путём. Новую тему
пишите на window.CartiX.
Ключ страницы (csrf) ставить в разметку не нужно — магазин сам печатает
<meta name="csrf-token"> в <head> и кладёт тот же ключ в конфиг. Без ключа не
проходит ни один запрос из браузера, а забыть его в теге было делом одной строки,
поэтому теперь это забота магазина. Если ваша тема печатает тег по старой
инструкции, ничего страшного: значение то же самое.
Корзина в шапке и пометки «в корзине»
Страницы каталога лежат в общем кэше: одна и та же страница уходит всем покупателям. Впечатать в неё корзину нельзя — следующий увидит чужую. Поэтому магазин добирает личное отдельным запросом и проставляет числа в браузере.
Шапка
<a href="{$shop->cartPath()}">
Корзина
<span data-badge="cart">0</span>
<span data-cart-total-header></span>
</a>
| Атрибут | Что делает магазин |
|---|---|
data-badge="cart" |
ставит число позиций; при нуле прячет элемент |
data-badge-always |
не прятать даже при нуле |
data-cart-total-header |
ставит сумму корзины без копеек |
Вес корзины
Магазин может показывать покупателю, сколько весит покупка. Владелец включает это в настройках оформления заказа, а тема размечает место — и вес будет меняться вместе с количеством, без перезагрузки страницы.
{if $cart.weight_text}
<div class="cart-summary__row" data-cart-weight-row>
<span>{trans key="storefront.cart_weight"}</span>
<span data-cart-weight>{$cart.weight_text}</span>
</div>
{/if}
| Атрибут | Что делает магазин |
|---|---|
data-cart-weight |
ставит вес корзины: «2,4 кг», «450 г» |
data-cart-weight-row |
прячет строку целиком, когда взвешивать нечего |
Вес считает и оформляет магазин: до килограмма он показывается в граммах, а
разделитель дробной части ставится по языку витрины. Собирать запись самим не
нужно — в переменной $cart.weight_text она уже готова, а число в килограммах
лежит рядом в $cart.weight.
Вес берётся из характеристики товара «Вес» — товарной или заданной варианту покупки. У товаров, где она не заполнена, веса нет, и строка не появляется: «0 кг» покупателю не показывается.
Карточка товара
<article class="card" data-in-cart="{$product.id}">
<button data-add="{$product.id}" data-sku="{$product.sku_id}">В корзину</button>
<span class="card__in-cart">
В корзине <b data-in-cart-qty></b> <span data-in-cart-unit></span>
<span data-in-cart-packs></span>
</span>
</article>
| Атрибут | Что делает магазин |
|---|---|
data-in-cart="<id товара>" |
корень карточки; сюда добавится класс и состояние |
data-in-cart-qty |
количество: «2», «8,4» |
data-in-cart-unit |
единица товара: «шт», «м²» |
data-in-cart-packs |
число упаковок, если товар берут ими |
data-in-cart-sku="<id варианта>" |
необязательно: спрашивать про конкретный вариант |
Магазин ставит на карточку класс is-in-cart и атрибут
data-in-cart-state="yes" или "no", а когда позиция одна — ещё и
data-in-cart-item с её номером. Дальше дело оформления: тема прячет «Купить» и
показывает «В корзине».
.card__in-cart { display: none; }
.card.is-in-cart .card__in-cart { display: block; }
.card.is-in-cart .card__buy { display: none; }
Если data-in-cart-sku не задан, вариант берётся из кнопки покупки
([data-add][data-sku]) — и пометка сама переезжает вслед за выбором цвета или
размера.
Добавление в корзину — задача темы
Магазин даёт адрес и синхронизацию, а сам клик обрабатывает тема. В базовой теме это выглядит так:
<div class="product" data-product data-in-cart="{$product.id}">
<input type="hidden" data-packaging value="">
<input type="number" data-qty value="1" data-step="{$product.qty_step}" data-min="{$product.qty_min}">
<button data-add="{$product.id}" data-sku="{$product.sku_id}">В корзину</button>
</div>
| Атрибут | Что означает |
|---|---|
data-add="<id товара>" |
кнопка покупки |
data-sku="<id варианта>" |
какой вариант класть |
data-product |
корень блока покупки: внутри него ищутся количество и упаковка |
data-qty |
поле количества |
data-packaging |
скрытое поле с выбранной упаковкой — его заполняет магазин |
Скрипт темы собирает product_id, sku_id, qty, packaging_id, отправляет
их на data-cart-add и обязательно сообщает магазину о результате:
window.dispatchEvent(new CustomEvent('cartix:cart-updated', { detail: response }));
Без этого события счётчик в шапке и пометки на карточках не обновятся.
Выезжающая корзина
Корзина, которая выезжает сбоку после «В корзину», — вёрстка темы целиком.
В базовой теме она лежит отдельным шаблоном templates/partials/cart-drawer.html
и подключается из шапки одной строкой:
{include file="partials/cart-drawer.html"}
Вот он весь — можно взять за основу и переверстать под свою тему:
<div class="cart-drawer" data-cart-drawer hidden>
<div class="cart-drawer__backdrop" data-cart-drawer-close></div>
<aside class="cart-drawer__panel">
<div class="cart-drawer__head">
<span class="cart-drawer__title">{trans key="storefront.cart"}</span>
<button class="cart-drawer__close" type="button" data-cart-drawer-close
aria-label="{trans key='storefront.close'}">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
{* Сюда встанут позиции — их рисует partials/cart-lines.html *}
<div class="cart-drawer__body" data-cart-drawer-body></div>
{* Показывается, когда класть нечего *}
<div class="cart-drawer__empty" data-cart-drawer-empty hidden>
<i class="fa-solid fa-bag-shopping"></i>
<p>{trans key="storefront.cart_empty"}</p>
</div>
{* Подвал: итог и куда идти дальше *}
<div class="cart-drawer__foot" data-cart-drawer-foot hidden>
<div class="cart-drawer__total">
<span>{trans key="storefront.order_total"}</span>
<b data-cart-drawer-total></b>
</div>
<a class="cart-drawer__checkout" href="{url path='/checkout'}">{trans key="storefront.checkout"}</a>
<button class="cart-drawer__continue" type="button" data-cart-drawer-close>{trans key="storefront.cart_continue"}</button>
</div>
</aside>
</div>
Обязательны только места, за которые цепляется скрипт темы:
| Место | Зачем |
|---|---|
data-cart-drawer |
корень корзины, скрыт атрибутом hidden |
data-cart-drawer-close |
всё, что закрывает: подложка, крестик, «продолжить покупки» |
data-cart-drawer-body |
сюда встанут позиции |
data-cart-drawer-empty |
что показать, когда корзина пуста |
data-cart-drawer-foot |
подвал с итогом, прячется у пустой корзины |
data-cart-drawer-total |
сюда встанет сумма |
Позиции — обычным шаблоном
Строки корзины вы верстаете шаблоном templates/partials/cart-lines.html, а не
собираете в скрипте. Магазин рисует его сам и кладёт готовую разметку в ответ
корзины полем html — скрипту темы остаётся поставить её на место:
{foreach $cart.items as $i}
<div class="cd-item" data-cd-item="{$i.id}">
<div class="cd-item__photo">
{if $i.image}<img src="{$i.image}" alt="{$i.name}" loading="lazy" onerror="this.hidden=true">{/if}
</div>
<div class="cd-item__name">
{$i.name}
{if $i.sku}<span class="cd-item__sku">{$i.sku}</span>{/if}
{if $i.pack}
<span class="cd-item__pack">
{if $i.pack.rest > 0}
{trans key="storefront.pack_in_cart" packs=$i.pack.packs rest=$i.pack.rest unit=$i.unit}
{else}
{trans key="storefront.pack_in_cart_whole" packs=$i.pack.packs}
{/if}
</span>
{/if}
</div>
<button class="cd-item__remove" type="button" data-cd-remove="{$i.id}"
title="{trans key='storefront.remove'}" aria-label="{trans key='storefront.remove'}">
<i class="fa-solid fa-trash-can"></i>
</button>
<div class="cd-item__controls">
<div class="cd-qty">
<button class="cd-qty__btn" type="button" data-cd-dec="{$i.id}">−</button>
<span class="cd-qty__val" data-cd-qty="{$i.id}" data-qty="{$i.qty}">{qty value=$i.qty}{if $i.unit} {$i.unit}{/if}</span>
<button class="cd-qty__btn" type="button" data-cd-inc="{$i.id}">+</button>
</div>
<span class="cd-item__total">{money amount=$i.total currency=$cart.currency}</span>
</div>
</div>
{/foreach}
В шаблон приходит $cart — состояние корзины: items, total, currency.
Отсюда же берутся цена и количество, оформленные магазином: {money} покажет
сумму в валюте витрины, {qty} — количество по-местному, а {trans} соберёт
подпись «1 уп. + 2 шт» для товаров, которые продаются упаковками. То же
количество числом лежит в data-qty — по нему скрипт считает следующее, не
разбирая надпись «8,4 м» обратно.
Готовые строки приезжают в каждом ответе о корзине — на добавление, на изменение количества, на удаление и на запрос состояния. Скрипт темы только вставляет их:
if (typeof state.html === 'string') bodyEl.innerHTML = state.html;
Нет такого шаблона в теме — поля html в ответе не будет, и тема разбирается
сама. Ошибка в шаблоне тоже не отнимает у покупателя товар: он положен, ответ
пришёл, а вёрстка подождёт до починки.
Как показать корзину после добавления
Корзина открывается не сама: её показывает тема, когда магазин подтвердил, что товар положен. В базовой теме разбор корзины отдаёт наружу две ручки — «наполнить списком» и «открыть»:
/** API выезжающей корзины (устанавливается initCartDrawer). */
let cartDrawerApi = null;
function initCartDrawer() {
// …сбор разметки и обработчики…
cartDrawerApi = { open, render };
}
А обработчик кнопки «В корзину» зовёт их, получив ответ:
fetch(CartiX.urls.cartAdd, { … })
.then((r) => r.json())
.then((d) => {
if (!d || !d.ok) return;
// Счётчик в шапке и пометки «в корзине» обновит магазин
window.dispatchEvent(new CustomEvent('cartix:cart-updated', { detail: d }));
// А корзину показываем сами — иначе покупатель не понимает,
// положился ли товар
if (cartDrawerApi) {
cartDrawerApi.render(d);
cartDrawerApi.open();
}
});
Порядок важен: сначала render(d) — в ответе на добавление уже лежит полный
состав корзины, — и только потом open(). Наоборот покупатель на мгновение
увидит прошлый список.
Открывать корзину при каждом добавлении не обязательно: это решение темы.
Магазину достаточно события cartix:cart-updated — счётчик в шапке и пометки
на карточках обновятся в любом случае.
Фотография позиции
В состоянии корзины у каждой позиции есть поле image — ссылка на уменьшенную
копию (200 px по большей стороне). Какую именно, магазин выбирает сам: сначала
снимки выбранного варианта, затем главное фото варианта и только потом общее
фото товара. Поэтому покупатель, положивший синий, видит в корзине синий, а не
первый снимок карточки.
Фотографии может не быть, и заглушку рисует тема, а не скрипт — фоном клетки. Снимок ложится поверх и закрывает её, а без снимка покупатель видит опрятную рамку:
.cd-item__photo {
width: 56px; height: 56px;
border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
background: #fff center / 22px no-repeat url("data:image/svg+xml,…");
}
.cd-item__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
Шаблону остаётся показать снимок, когда он есть:
<div class="cd-item__photo">
{if $i.image}<img src="{$i.image}" alt="{$i.name}" loading="lazy" onerror="this.hidden=true">{/if}
</div>
onerror прячет картинку, если файл пропал: без него поверх аккуратной
заглушки встанет значок «битое изображение».
Это же поле показывает и страница корзины — снимок там и в выезжающей корзине обязан быть один и тот же.
Корзина окном по центру
Ту же корзину можно показывать иначе — не панелью у правого края, а окном
посреди экрана. В базовой теме для этого лежит второй шаблон
templates/partials/cart-modal.html, а владелец магазина выбирает подачу
в настройках темы: поле «Как показывать корзину после добавления» со значениями
«Сбоку, выезжает» и «По центру, всплывает».
Настройка описывается в theme.json:
{
"key": "cart_popup",
"type": "select",
"label": "Как показывать корзину после добавления",
"placeholder": "Сбоку, выезжает",
"options": [
{ "value": "drawer", "label": "Сбоку, выезжает" },
{ "value": "modal", "label": "По центру, всплывает" }
],
"default": "drawer"
}
А шапка подключает тот шаблон, который выбрали:
{if $theme.cart_popup == 'modal'}
{include file="partials/cart-modal.html"}
{else}
{include file="partials/cart-drawer.html"}
{/if}
Разметка отличается только классами. Места, за которые цепляется скрипт темы, те
же самые — data-cart-drawer, data-cart-drawer-body, data-cart-drawer-foot
и остальные из таблицы выше. Поэтому initCartDrawer работает с обоими
шаблонами без единой правки, а строки позиций достаются даром: их рисует общий
partials/cart-lines.html:
<div class="cart-modal" data-cart-drawer hidden>
<div class="cart-modal__backdrop" data-cart-drawer-close></div>
<div class="cart-modal__window" role="dialog" aria-modal="true"
aria-label="{trans key='storefront.cart'}">
<div class="cart-modal__head">
<span class="cart-modal__title">{trans key="storefront.cart"}</span>
<button class="cart-modal__close" type="button" data-cart-drawer-close
aria-label="{trans key='storefront.close'}">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
{* Сюда встанут позиции — их рисует partials/cart-lines.html *}
<div class="cart-modal__body" data-cart-drawer-body></div>
{* Показывается, когда класть нечего *}
<div class="cart-modal__empty" data-cart-drawer-empty hidden>
<i class="fa-solid fa-bag-shopping"></i>
<p>{trans key="storefront.cart_empty"}</p>
</div>
{* Подвал: итог и куда идти дальше *}
<div class="cart-modal__foot" data-cart-drawer-foot hidden>
<div class="cart-modal__total">
<span>{trans key="storefront.order_total"}</span>
<b data-cart-drawer-total></b>
</div>
<a class="cart-modal__checkout" href="{url path='/checkout'}">{trans key="storefront.checkout"}</a>
<button class="cart-modal__continue" type="button" data-cart-drawer-close>{trans key="storefront.cart_continue"}</button>
</div>
</div>
</div>
Оформление — обычное окно поверх страницы: подложка на весь экран, само окно шириной 560 px по центру, список позиций прокручивается внутри, а шапка и подвал с итогом остаются на месте. На телефоне окно занимает всю ширину и прижимается к нижнему краю — так до кнопок ближе:
.cart-modal { position: fixed; inset: 0; z-index: 320;
display: flex; align-items: center; justify-content: center; padding: 20px; }
.cart-modal[hidden] { display: none; }
.cart-modal__window { width: 560px; max-width: 100%; max-height: 86vh;
display: flex; flex-direction: column; border-radius: 16px; }
.cart-modal__body { flex: 1; overflow-y: auto; }
@media (max-width: 560px) {
.cart-modal { padding: 0; align-items: flex-end; }
.cart-modal__window { width: 100%; max-height: 92vh; border-radius: 16px 16px 0 0; }
}
Показывается окно ровно так же, как выезжающая корзина: сначала render(d),
потом open(). Закрывают его крестик, подложка, кнопка «Продолжить покупки»
и клавиша Esc — всё это уже делает скрипт темы.
Если в вашей теме подача всего одна, условие в шапке не нужно: включайте свой
шаблон напрямую и настройку в theme.json не заводите. Обратное неверно —
пока разметка обращается к $theme.cart_popup, ключ обязан быть в манифесте,
иначе витрина ответит ошибкой.
События и общий доступ
| Событие | Когда | Что внутри |
|---|---|---|
cartix:cart-updated |
корзина изменилась | состояние корзины |
cartix:sku-changed |
покупатель выбрал другой вариант | выбранный вариант |
Слушать их может кто угодно — и тема, и дополнения. Поэтому любой участок, меняющий корзину (кнопка покупки, чат на сайте, дополнение), обязан сообщить об этом событием, а не обновлять шапку сам.
Состояние корзины доступно и напрямую: window.cartix.cart — последний
известный ответ магазина.
Живой поиск
Тема даёт форму, всё остальное — магазин: дописывание запроса, разделы со счётчиками, бренды, товары группами, недавние запросы, управление с клавиатуры.
<form action="{url path='/search'}" data-live-search
data-suggest-url="{url path='/search/suggest'}"
data-text-empty="{trans key='storefront.no_products'}"
data-text-categories="{trans key='storefront.categories'}"
data-text-brands="{trans key='storefront.brands'}"
data-text-all-results="{trans key='storefront.search_all_results'}"
data-text-more="{trans key='storefront.search_more'}"
data-text-recent="{trans key='storefront.search_recent'}"
data-text-popular="{trans key='storefront.search_popular'}"
data-text-clear="{trans key='storefront.search_clear'}"
data-text-corrected="{trans key='storefront.search_corrected'}"
data-text-all="{trans key='storefront.view_all'}">
<input type="search" name="q" autocomplete="off" data-search-input>
<input type="hidden" name="cat" value="" data-search-cat-input>
<div data-search-results hidden></div>
</form>
| Атрибут | Обязателен | Что задаёт |
|---|---|---|
data-live-search |
да | корень формы |
data-search-input |
да | поле запроса |
data-search-results |
да | пустое место под выпадающий список |
data-suggest-url |
да | адрес подсказок |
data-search-cat-input |
нет | скрытое поле выбранной категории |
data-search-cat |
нет | сам выбор категории |
data-search-clear |
нет | кнопка «очистить» |
data-text-* |
да | надписи выпадающего списка |
Надписи передаёт тема, а не магазин. Они принадлежат теме и переводятся в её файлах — поэтому подсказки поиска говорят на языке вашей темы. Не передали надпись — соответствующая строка в выпадающем списке будет пустой.
Подсказки появляются от двух символов.
Покупка упаковками
Тема даёт только место. Магазин рисует внутри него переключатель «упаковками / поштучно», список упаковок, счётчик пачек и подпись «6 пачек — это 8,4 м²».
{if $product.packagings}
<div class="cx-pack" data-pack
data-packagings='{$product.packagings|@json_encode:1}'
data-pack-only="{if $product.pack_only}1{else}0{/if}"
data-currency="{$storefront.currency}">
</div>
<input type="hidden" data-packaging value="">
{/if}
| Атрибут | Что задаёт |
|---|---|
data-pack |
корень блока |
data-packagings |
список упаковок товара |
data-pack-only |
товар отпускают только упаковками |
data-currency |
валюта для подписей цены |
data-packaging |
скрытое поле, куда магазин положит выбранную упаковку |
data-pack-area |
необязательно: поле «сколько нужно» — покупатель вводит метры, магазин считает пачки |
⚠️ Свой счётчик количества над этим блоком не заводите. В режиме упаковок счётчик считает пачки, а не метры, и два счётчика начнут спорить. Товар, продающийся только упаковками, поштучного режима не получает.
На корень магазин ставит класс cx-pack--single, когда упаковка одна.
Конфигуратор комплектов
<div class="product-accessories" data-accessories
data-main-id="{$product.id}"
data-main-price="{$product.price}"
data-currency="{$storefront.currency}">
{foreach $product.accessories as $group}
<div data-acc-group="{$group.id}" data-multiple="{if $group.multiple}1{else}0{/if}">
{foreach $group.items as $acc}
<label>
<input type="checkbox" data-acc-item
data-product-id="{$acc.product_id}"
data-sku-id="{$acc.sku_id}"
data-price="{$acc.final_price}"
data-qty="{$acc.default_quantity}"
{if $acc.is_default}checked{/if}>
{$acc.name}
</label>
{/foreach}
</div>
{/foreach}
<span data-acc-total></span>
<button type="button" data-acc-add>Добавить комплект</button>
<div data-acc-done hidden>Комплект добавлен</div>
</div>
| Атрибут | Что задаёт |
|---|---|
data-accessories |
корень блока |
data-main-id, data-main-price |
сам товар: он входит в сумму комплекта |
data-currency |
валюта для пересчёта |
data-acc-item |
флажок аксессуара |
data-acc-total |
сюда встанет сумма комплекта |
data-acc-add |
кнопка «Добавить комплект» |
data-acc-done |
сообщение об успехе |
Магазин пересчитывает сумму при каждом изменении и добавляет товар вместе с выбранными аксессуарами отдельными позициями.
У каждого аксессуара есть своя кнопка покупки — взять одну крышку, не
собирая комплект. Ставит её магазин прямо в строку аксессуара, теме делать
ничего не нужно. Хотите свою — нарисуйте в строке кнопку с data-acc-buy, и
магазин возьмёт её вместо своей; подпись кнопки магазина задаётся атрибутом
data-l-buy на корне блока.
Группу, где выбор одиночный, покупатель может и снять: повторное нажатие на отмеченное убирает отметку. Иначе предложенное по умолчанию было бы обязательным.
Окно с аксессуарами в каталоге
Владелец может включить в настройках аксессуаров предложение после покупки из списка товаров: покупатель нажал «В корзину» на карточке в каталоге — магазин показывает окно с аксессуарами к этому товару.
Ничего делать не обязательно. Нет у темы шаблона окна — магазин рисует его
сам, и оно работает в любой теме: достаточно того, что кнопка покупки размечена
как [data-add]. Товар кладёт по-прежнему тема, своим обработчиком, а магазин
лишь слушает, что корзина изменилась, и предлагает дополнить покупку.
Забрать окно себе можно на двух уровнях, и второй нужен редко.
Своя вёрстка окна
Положите в тему templates/partials/accessories-popup.html — магазин нарисует
окно по нему. Поведение остаётся за магазином: выбор аксессуаров, живой итог,
покупка одной строки, добавление всего отмеченного. Цепляется оно за опоры
разметки, и их надо сохранить.
| Опора | Где стоит |
|---|---|
.cxap__item |
строка одного аксессуара |
.cxap__check |
флажок или переключатель в строке |
[data-cxap-buy] |
кнопка «взять только это» внутри строки |
[data-cxap-add] |
кнопка «взять всё отмеченное» |
[data-cxap-total] |
сюда магазин пишет сумму отмеченного |
[data-cxap-close] |
всё, что закрывает окно |
На флажке нужны данные аксессуара: data-product-id, data-sku-id,
data-qty, data-price. Итог и кнопку «взять всё» можно не рисовать вовсе —
магазин это переживёт; убрав опоры у строк, вы получите окно, в котором ничего
не покупается.
В шаблон приходят:
| Переменная | Что в ней |
|---|---|
$accessories |
группы с товарами — те же поля, что у $product.accessories |
$main_product |
id и name товара, который только что купили |
$texts |
готовые надписи окна на языке витрины |
Надписи лучше брать из словаря темы — ключи storefront.accessories_popup_title,
_subtitle (в неё подставляется :product), _total, _add, _skip,
_close. Так владелец найдёт их там же, где остальные строки витрины. Ключа в
словаре нет — подставится строка магазина.
Шаблон окна есть во всех темах поставки — берите его за образец.
Своё поведение целиком
Положите assets/js/accessories-popup.js — магазин подключит его вместо своего.
Если же окно ведёт ваш общий код, объявите это в theme.json:
"runtime": {"accessories_popup": "theme"} — тогда магазин не подключит ничего.
Дальше всё на теме: слушать cartix:cart-updated, спрашивать аксессуары у
CartiX.urls.configuratorAccessories и класть выбранное через
CartiX.urls.configuratorAttach. Отвечает первый адрес теми же данными, что
приходят в шаблон, плюс html с разметкой вашего же шаблона, если он есть.
Что стоит знать при вёрстке
- окно перекрывает страницу целиком, поэтому у него
z-index: 9000— плавающие кнопки темы (чат, «наверх») должны быть ниже, иначе они окажутся поверх; - пока окно открыто, на
<body>висит классcxap-open— им же выключается прокрутка страницы под окном; - если у темы есть выезжающая корзина, магазин закрывает её при открытии окна,
нажимая её же
[data-cart-drawer-close]: два окна разом читаются как ошибка; - оформление приходит из магазина — меняется переопределением селекторов
.cxap*в стилях темы. Когда разметку дала тема, на корне окна стоит ещё и классcxap--theme: за него удобно цеплять свои правила.
На странице товара окно для самого товара страницы не показывается — там конфигуратор уже стоит в разметке.
Оформление заказа
Самая большая часть. Тема даёт разметку, магазин делает всё остальное: пересчёт при каждом изменении, список способов доставки и оплаты, дополнительные поля перевозчика, карту отделений, промокод, баллы, вход по ходу оформления и само создание заказа.
Корень формы
<form data-checkout
data-calculate="/checkout/calculate"
data-identify="/checkout/identify"
data-capture="/checkout/capture"
data-saved-delete="/checkout/address/delete"
data-currency="{$cart.currency}"
data-min-order="{$min_order}"
data-free-threshold="{$free_threshold}"
data-l-error="{trans key='storefront.checkout_error_generic'}"
data-l-free="{trans key='storefront.checkout_free'}"
data-l-free-progress="{trans key='storefront.checkout_free_progress'}"
data-l-login="{trans key='storefront.login'}"
data-l-min-order="{trans key='storefront.checkout_min_order'}"
data-l-no-shipping="{trans key='storefront.checkout_no_shipping'}"
data-l-no-payment="{trans key='storefront.checkout_no_payment'}"
data-l-phone-exists="{trans key='storefront.checkout_phone_exists'}"
data-l-point-none="{trans key='storefront.checkout_point_none'}"
data-l-point-search="{trans key='storefront.checkout_point_search'}">
Все десять надписей data-l-* магазин действительно использует — это тексты,
которые он подставляет в браузере. Не передали — покупатель увидит пустоту на
месте сообщения.
Места, которые заполняет магазин
| Атрибут | Что туда встанет |
|---|---|
data-shipping-options |
список способов доставки |
data-payment-methods |
список способов оплаты |
data-shipping-fields |
дополнительные поля выбранного способа |
data-checkout-cart |
редактируемая корзина |
data-checkout-error |
сообщение об ошибке |
data-checkout-submit |
кнопка «Оформить» |
Контакт и получатель
| Атрибут | Что делает |
|---|---|
data-contact-type |
скрытое поле: вид покупателя |
data-contact-radio |
переключатель вида покупателя |
data-contact-fields="<вид>" |
блок полей этого вида |
data-recipient-toggle |
флажок «другой получатель» |
data-recipient |
блок полей получателя |
data-recipient-company, data-recipient-company-fields, data-recipient-company-row |
поля получателя-юрлица |
data-phone-check, data-phone-warn |
проверка «такой телефон уже есть» |
Поля неактивного блока магазин отключает: иначе их пустые значения ушли бы в отправку и перетёрли активный блок.
Город и отделение
| Атрибут | Что делает |
|---|---|
data-city |
поле города |
data-point-input |
поиск отделения |
data-point-list |
список отделений |
data-point-map |
карта |
data-point-value |
скрытое поле выбранного отделения |
Пока город не введён, магазин ставит на форму класс co-no-city и прячет им
всё, что дальше: без города считать доставку нечем.
.co-no-city .co-delivery, .co-no-city .co-payment { display: none; }
Сохранённые адреса
data-saved-addresses — блок, data-saved-item — один адрес, data-saved-pick
— выбрать, data-saved-del — удалить. Удалил последний — магазин прячет блок.
Промокод и баллы
| Атрибут | Что делает |
|---|---|
data-coupon-note |
сюда встанет ответ про промокод: принят, истёк, не подходит |
data-bonus-box |
блок баллов |
data-bonus-input |
поле «сколько списать» |
data-bonus-all |
кнопка «списать все» |
data-bonus-available |
сколько баллов есть |
data-bonus-max |
сколько можно списать на этот заказ |
Итоги
| Атрибут | Что туда встанет |
|---|---|
data-sum-items |
сумма товаров |
data-sum-discount, data-sum-discount-row |
скидка и её строка |
data-sum-bonus, data-sum-bonus-row |
баллы и их строка |
data-sum-shipping |
доставка |
data-sum-total |
итог |
data-free-progress, data-free-text |
прогресс до бесплатной доставки |
data-min-order |
минимальная сумма заказа |
Строки скидки и баллов магазин прячет и показывает сам — верстайте их видимыми, он разберётся.
Позиции корзины при оформлении
| Атрибут | Что делает |
|---|---|
data-cart-item="<id позиции>" |
корень позиции |
data-cart-qty |
поле количества |
data-cart-inc, data-cart-dec |
плюс и минус |
data-cart-remove |
удалить позицию |
data-cart-line-total |
сумма позиции |
Вход по ходу оформления
data-login-open открывает шторку data-login-drawer, data-login-close
закрывает. Внутри: data-login-identifier, data-login-password,
data-login-submit, data-login-error.
Что запускает пересчёт
Пересчёт запускается по именам полей, а не по атрибутам:
shipping, payment, coupon, bonus и всё, что начинается с address[.
⚠️ Атрибуты data-recalc и data-required магазин проставляет сам в той
разметке, которую рисует, и ни у кого их не читает. Ставить их в шаблоне темы
бессмысленно — на пересчёт это не влияет. Единственное исключение —
data-recalc-type: его магазин действительно слушает, но и его он ставит сам.
Поэтому главное правило разметки оформления: имена полей менять нельзя.
Переименуете coupon в promo — промокод перестанет пересчитывать заказ.
Оплата по ссылке
<div data-order-pay>
<button data-pay-submit>Оплатить</button>
<div data-pay-error hidden></div>
</div>
Платёж начинается только по кнопке: на такую страницу заходят превью-боты мессенджеров, и по одному открытию ссылки в шлюзе появлялись бы счета, которых покупатель не создавал.
Классы состояния
Их ставит магазин, оформляет тема.
| Класс | Где | Когда |
|---|---|---|
is-in-cart |
карточка товара | товар уже в корзине |
co-no-city |
форма оформления | город не введён |
is-recalc |
форма оформления | идёт пересчёт |
is-selected |
вариант способа | способ выбран |
is-active |
переключатель | текущий вариант |
is-hidden |
что угодно | элемент скрыт |
is-invalid |
поле | поле не прошло проверку |
is-busy |
кнопка | запрос в работе |
cx-pack--single |
блок упаковок | упаковка одна |
Ни один из них не оформлен за вас — стили пишет тема.
Минимальная рабочая разметка
Если нужно быстро проверить, что тема «завелась», хватит этого:
{* в <head> *}
{* на <body> *}
data-cart-add="/cart/add" data-cart-state="/cart/state/"
{* в шапке *}
<span data-badge="cart">0</span>
{* на карточке *}
<article data-in-cart="{$product.id}">
<button data-add="{$product.id}" data-sku="{$product.sku_id}">Купить</button>
<span>В корзине <b data-in-cart-qty></b></span>
</article>
Этого достаточно, чтобы товар клался в корзину, счётчик обновлялся, а пометка «в корзине» переживала перезагрузку страницы.