Документація

Атрибути data-*: поведінка вітрини

Головна сторінка для верстальника: повний список атрибутів, за які чіпляється магазин, з прикладами розмітки.

На цій сторінці

Магазин підключає до будь-якої теми готову поведінку: живий пошук, синхронізацію кошика, оформлення замовлення, підрахунок упаковками, конфігуратор комплектів, оплату за посиланням. Тема цей код не пише — вона розмічає місця атрибутами 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>

Цього достатньо, щоб товар клався в кошик, лічильник оновлювався, а позначка «у кошику» переживала перезавантаження сторінки.

Оновлено 28 серпня 2026