Атрибути 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: 'uk',
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>
Цього достатньо, щоб товар клався в кошик, лічильник оновлювався, а позначка «у кошику» переживала перезавантаження сторінки.