Itens em conjunto no carrinho
A plataforma Olist Ecommerce permite que na sua loja tenha a opção de adicionar itens em conjunto no carrinho. Isso significa que o cliente pode adicionar diferentes itens de uma única vez no carrinho, sem acarretar erro no Ecommerce.
O uso do recurso itens em conjunto no carrinho é comum em seções de Compre junto, onde a loja oferece produtos similares para os clientes e eles adicionam diferentes itens de uma única vez no carrinho. Veja um exemplo de uso do Compre junto do nosso template 6 na figura a seguir.

Para utilizar essa funcionalidade você precisa habilitá-la em seu template. Habilitamos utilizando um array JavaScript para enviar todos os produtos em uma única requisição, e no array enviamos a quantidade (quantity) e o código identificador do produto (sku). Adicione o código a seguir no seu template para habilitar o recurso.
<script> const addItemsCombo: function (comboItems) { var data = { items: comboItems }
var form_data = JSON.stringify(data);
return $.ajax({ url: '/carrinho/adicionar/kit', type: 'POST', dataType: 'json', contentType: 'application/json; charset=utf-8', xhrFields: { withCredentials: true }, cache: false, data: form_data }); }
$(document).on('submit', '[data-form-kit]', function (event) { event.preventDefault();
var comboItems = [];
$('[data-product-kit]').each(function(index, el){ if($(el).find('[name="quantity"]').val() > 0){ var item = { sku: $(el).find('[name="sku"]').val(), quantity: parseInt($(el).find('[name="quantity"]').val(), 10), } comboItems.push(item); } });
// console.log('comboItems', comboItems)
addItemsCombo(comboItems) });
</script>Produtos personalizados
Seção intitulada “Produtos personalizados”É possível também utilizar a funcionalidade de itens em conjunto no carrinho com produtos personalizados. Nesse caso, a loja permite que o cliente adicione produtos do tipo personalizados em conjunto no carrinho. No entanto, há uma diferença para habilitar a funcionalidade de itens em conjunto no carrinho. O que muda do código anterior é a variável item, pois enviamos a requisição adicionando (junto com quantity e sku) mais um objeto com os campos referentes a personalização. Para o caso de produtos personalizados, utilize o código a seguir.
<script> const addItemsCombo: function (comboItems) { var data = { items: comboItems }
var form_data = JSON.stringify(data);
return $.ajax({ url: '/carrinho/adicionar/kit', type: 'POST', dataType: 'json', contentType: 'application/json; charset=utf-8', xhrFields: { withCredentials: true }, cache: false, data: form_data }); }
$(document).on('submit', '[data-form-kit]', function (event) { event.preventDefault();
var comboItems = [];
$('[data-product-kit]').each(function(index, el){ if($(el).find('[name="quantity"]').val() > 0){ var item = { sku: $(el).find('[name="sku"]').val(), quantity: parseInt($(el).find('[name="quantity"]').val(), 10), }
item[$(el).find('input:not([name="sku"]):not([name="quantity"])').attr('name')] = $(el).find('input:not([name="sku"]):not([name="quantity"])').val();
comboItems.push(item); } });
// console.log('comboItems', comboItems)
addItemsCombo(comboItems) });
</script>🚧 Aplicação da funcionalidade
Note que no exemplo do template 6 o recurso é usado na seção Compre junto que está página de produto. Para utilizar a funcionalidade, lembre-se que:
- Além de habilitar o recurso você precisa ajustar o front-end da vitrine para adicionar a seção onde deseja.
- Utilize tags para agrupar produtos relacionados e assim organizar sua seção e melhorar a experiência para o cliente final.
Como o compre junto costuma ser estruturado
Seção intitulada “Como o compre junto costuma ser estruturado”Uma implementação de compre junto normalmente começa na própria página de produto, a partir de uma tag que identifica quais itens participam do agrupamento. Em vez de depender de uma estrutura fixa, a lógica pode ser resumida em três partes:
- Identificar, no produto atual, a tag que ativa o compre junto.
- Carregar os demais produtos que compartilham essa mesma tag.
- Renderizar uma seção que permita selecionar os itens e enviá-los juntos ao carrinho.
Exemplo de identificação da tag e carregamento dos produtos:
{% assign group_shop = product.tags | where: 'type', 'compre-junto' | first %}
{% if group_shop %} {% load_products tag: group_shop.name per_page: 3 %}{% endif %}Nesse modelo, o nome da tag funciona como vínculo entre o produto principal e os demais produtos que devem aparecer na seção.
Contrato mínimo entre Liquid, HTML e JavaScript
Seção intitulada “Contrato mínimo entre Liquid, HTML e JavaScript”Para que o compre junto funcione de forma previsível, a implementação precisa manter um contrato mínimo entre a camada de renderização e a rotina de compra.
| Elemento | Papel na implementação |
|---|---|
Tag compre-junto | Agrupa os produtos que participam da seção |
Variável de agrupamento, como group_shop | Ativa o carregamento dos produtos relacionados |
| Lista de produtos carregados | Define os itens que poderão ser selecionados |
| Bloco HTML de cada item | Expõe os dados usados na seleção e no cálculo |
Input name="sku" em cada item | Fornece o SKU efetivo que será enviado |
| Botão final de compra | Dispara a adição em conjunto |
| Área de feedback | Exibe sucesso, erro ou indisponibilidade |
Sem esse contrato, o JavaScript consegue até disparar a requisição, mas não consegue montar o payload com segurança.
Dados mínimos necessários
Seção intitulada “Dados mínimos necessários”Para implementar o compre junto de forma consistente, a lógica de frontend precisa ter acesso, no mínimo, aos seguintes dados:
| Dado | Uso |
|---|---|
| Tag de agrupamento | Define quais produtos participam da seção |
| Referência do produto | Evita duplicar o produto principal |
| Preço e preço promocional | Permitem recalcular o resumo |
| Disponibilidade | Impede envio de itens indisponíveis |
| SKU selecionado | Compõe o payload final |
| Dados de personalização | Permitem incluir customizations quando necessário |
Exemplo de HTML da seção
Seção intitulada “Exemplo de HTML da seção”O exemplo abaixo mostra uma referência de implementação do container principal da seção, já alinhada com os elementos lidos pela rotina de compra.
{% if group_shop %} {% load_products tag: group_shop.name per_page: 3 %}
{% if products.size >= 2 %} <section class="section group-shop -side container" data-group-shop-section> <div class='section-wrapper'> <h3 class="title"> Compre Junto </h3>
<div class="products"> <div class='swiper' data-swiper-group> <div class='swiper-wrapper'> <div class='swiper-slide -plus'> {% render 'partials/components/product_block_group_shop', product: current_product, showAttributes: true, showDelete: false, mobile: mobile %} </div>
{% for product in products %} {% if product.reference != current_product.reference %} {% assign customs_tag = product.tags | where: 'type', 'personalizacao' | first %}
{% unless customs_tag %} <div class='swiper-slide -plus'> {% render 'partials/components/product_block_group_shop', product: product, showAttributes: true, showDelete: true, mobile: mobile %} </div> {% endunless %} {% endif %} {% endfor %} </div>
<div class="swiper-pagination"></div> </div>
<div class="purchase -hidden"> <form class="form-product" data-form-product-group-shop style="display: none;"></form>
<div class="price-wrapper"> <h4 class="text">Compre os produtos por</h4> <div class="discount-wrapper -hidden"> <span class="discount-percent"></span> <span class="original-price"></span> </div> <span class="price-group"></span> <span class="warning" data-warning></span> </div>
<button class="button-default" data-group-shop-add type="button"> <span>Compre junto</span> </button>
<div class="msg-response"> <span class="msg-error"></span> </div> </div> </div> </div> </section> {% endif %}{% endif %}Esse exemplo mostra o container principal da seção com os elementos que a lógica de compra espera encontrar, como data-group-shop-section, [data-swiper-group], .purchase, .price-group, .warning, .discount-wrapper, [data-form-product-group-shop] e [data-group-shop-add].
Exemplo do bloco HTML do item do compre junto
Seção intitulada “Exemplo do bloco HTML do item do compre junto”Cada item precisa expor, no mínimo, referência, preços, disponibilidade e um name="sku" que a rotina de compra consiga ler no momento do envio.
<div class="product-block -group-shop-block {{ class_sold_out }}" data-product-box="{{ product.id }}" data-quickview-type="group-shop" data-prod-group-shop data-prod-ref="{{ product.reference }}" data-price="{{ product.price }}" data-sale-price="{{ product.sale_price }}" data-available="{{ product.available }}" data-discount-percent="{{ discount_amount | default: 0 }}"> {% if showDelete %} <button aria-label="Adicionar ou remover produto do compre junto" class="add-to-group"> <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10.4773 3.43628L4.82044 9.09313L1.84315 6.11584" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> </svg> </button> {% endif %}
<div class="images"> <a aria-label="Ir para o produto {{ product.name }}" href="{{ product.url }}"> {% render 'partials/components/flag', product: product %} </a> </div>
<div class="description"> <h3 class="name"> <a aria-label="Ir para o produto {{ product.name }}" href="{{ product.url }}"> {{ product.name }} </a> </h3>
<div class="price-wrapper"> <div class="product-price" data-update-price="{{ product.id }}"></div> </div> </div></div>Quando esse bloco usa quickview ou outro carregamento de formulário, o ponto obrigatório continua sendo o mesmo: no momento da compra, o item precisa expor um campo name="sku" válido dentro do bloco selecionado.
Regras funcionais do compre junto
Seção intitulada “Regras funcionais do compre junto”Além do payload final, o compre junto costuma seguir algumas regras funcionais importantes:
- O produto atual define o contexto da seção.
- Os demais itens são carregados a partir da mesma tag de agrupamento.
- Produtos equivalentes ao principal não devem ser duplicados na lista.
- O total exibido depende apenas dos itens selecionados.
- Produtos indisponíveis não devem entrar no envio final.
- O botão final só deve enviar os itens válidos e selecionados.
- O endpoint usado nesse fluxo é
POST /carrinho/adicionar/kit.
Em implementações mais completas, a interface também pode recalcular preço total, preço promocional, desconto percentual e mensagens de indisponibilidade conforme a seleção do cliente.
Exemplo prático da lógica de seleção no JavaScript
Seção intitulada “Exemplo prático da lógica de seleção no JavaScript”Uma forma consistente de implementar esse comportamento é controlar a seleção por instância da seção. Assim, cada seção mantém sua própria lista de itens selecionados e recalcula o resumo visual sempre que o cliente adiciona ou remove um produto.
O trecho abaixo mostra métodos parciais da rotina de compre junto. Eles devem existir dentro do mesmo objeto ou módulo responsável pela seção.
createInstance: function (section) { return { section, products: section.querySelectorAll('[data-prod-group-shop]'), selectedProducts: [], swiper: null, };},
getElements: function (instance) { const section = instance.section;
return { purchase: section.querySelector('.purchase'), buyButton: section.querySelector('[data-group-shop-add]'), form: section.querySelector('[data-form-product-group-shop]'), swiper: section.querySelector('[data-swiper-group]'), pagination: section.querySelector('.swiper-pagination'), };},
handleSelection: function (instance, currentProduct) { const match = instance.selectedProducts.filter((product) => { return product.ref == currentProduct.getAttribute('data-prod-ref'); });
if (match.length == 0) { this.addItemToBuy(instance, currentProduct); } else { this.removeItem(instance, currentProduct); }},
addItemToBuy: function (instance, product) { const ref = product.getAttribute('data-prod-ref');
const price = Number(product.getAttribute('data-price')); const salePrice = Number(product.getAttribute('data-sale-price')); const available = product.getAttribute('data-available'); const discountPercent = product.getAttribute('data-discount-percent');
const button = product.querySelector('.add-to-group'); if (button) button.classList.add('-group-selected');
instance.selectedProducts.push({ productEl: product, ref, price, salePrice, available, discountPercent, });
this.updateFinalPrice(instance);},
removeItem: function (instance, product) { for (let index = 0; index < instance.selectedProducts.length; index++) { const selectedProduct = instance.selectedProducts[index];
if (selectedProduct.ref == product.getAttribute('data-prod-ref')) { const button = product.querySelector('.add-to-group'); if (button) button.classList.remove('-group-selected');
instance.selectedProducts.splice(index, 1); break; } }
this.updateFinalPrice(instance);},
updateFinalPrice: function (instance) { const selectedProducts = instance.selectedProducts; const purchase = this.getElements(instance).purchase;
if (selectedProducts.length == 0) { if (purchase) purchase.classList.add('-hidden'); return; }
if (!purchase) return;
purchase.classList.remove('-hidden');
let priceTotal = 0.0; let salePriceTotal = 0.0; let discountTotal = 0; let unavailableProducts = 0;
selectedProducts.forEach((product) => { const prodPrice = parseFloat(product.productEl.getAttribute('data-price')); const salePrice = parseFloat(product.productEl.getAttribute('data-sale-price')); const available = product.productEl.getAttribute('data-available');
if (available === 'false') { unavailableProducts++; } else if (prodPrice != salePrice) { priceTotal = priceTotal + prodPrice; salePriceTotal = salePriceTotal + salePrice; } else { priceTotal = priceTotal + prodPrice; salePriceTotal = salePriceTotal + prodPrice; } });
if (priceTotal !== salePriceTotal) { discountTotal = 100 - (salePriceTotal * 100) / priceTotal; }
const textEl = purchase.querySelector('.text'); const priceEl = purchase.querySelector('.price-group'); const totalProducts = selectedProducts.length - unavailableProducts;
if (textEl) textEl.innerText = 'Compre os ' + totalProducts + ' produtos por'; if (priceEl) priceEl.innerText = formatMoney(salePriceTotal);}Nesse fluxo, os pontos principais são:
- A seleção é feita por
data-prod-ref. - O estado fica isolado em
selectedProductsdentro da seção atual. - O resumo só aparece quando há itens selecionados.
- O valor final considera indisponibilidade, preço cheio e preço promocional.
Exemplo prático da rotina de compra no JavaScript
Seção intitulada “Exemplo prático da rotina de compra no JavaScript”Depois que a seleção já foi definida, a rotina de compra pode montar o payload a partir de selectedProducts, ler o sku de cada item, coletar personalizações quando existirem e enviar tudo de uma vez para o carrinho.
O trecho abaixo mostra o método responsável pelo envio da compra. Ele depende do mesmo contexto de instance, getElements(...), formatMoney(...) e Store.addItemResult(...) usados na implementação da seção.
addProducts: async function (instance) { const elements = this.getElements(instance); const buyButton = elements.buyButton; const form = elements.form; const purchase = elements.purchase; if (!buyButton || !form || !purchase) return;
let data = { items: [], };
for (let index = 0; index < instance.selectedProducts.length; index++) { const product = instance.selectedProducts[index].productEl; const sku = product.querySelector('[name=sku]').value; const available = product.getAttribute('data-available'); let customizations = [];
if (available === 'true') { const customsEl = product.querySelectorAll('[data-customization]'); if (customsEl.length > 0) { let hasCustoms = false; let customs = {};
customsEl.forEach((custom) => { const type = custom.getAttribute('type');
if (type !== 'checkbox' || type !== 'radio') { if (custom.value === '') { custom.setAttribute('disabled', true); } }
const disabled = custom.getAttribute('disabled'); let checked = true; if (typeof custom.checked != 'undefined') checked = custom.checked;
if (checked && disabled !== 'true') { hasCustoms = true; customs[custom.getAttribute('data-customization-name')] = custom.getAttribute('value'); } });
if (hasCustoms) { customizations.push(customs); } }
data.items.push({ sku: sku, quantity: 1, customizations, }); } }
const json_data = JSON.stringify(data); const boxResponse = purchase.querySelector('.msg-response');
if (!buyButton.classList.contains('-adding')) { buyButton.classList.add('-adding');
try { const response = await fetch('/carrinho/adicionar/kit', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: json_data, });
if (response.ok) { console.log('success'); const html = await response.text(); Store.addItemResult('produto-adicionado', html, boxResponse, form); } else { console.log('error'); console.error('Erro ao adicionar os produtos do compre junto. Status ' + response.status); Store.addItemResult('erro-adicionar', null, boxResponse, form); } } catch (error) { console.error('Erro ao enviar os produtos do compre junto.'); console.error(error); Store.addItemResult('erro-adicionar', null, boxResponse, form); }
buyButton.classList.remove('-adding'); }}Nesse fluxo, os pontos principais são:
- O payload é montado a partir de
instance.selectedProducts. - O
skué lido diretamente do campo[name=sku]. - Itens indisponíveis não entram em
data.items. - Personalizações são adicionadas em
customizations. - O envio é feito em JSON para
POST /carrinho/adicionar/kit. - O retorno da compra alimenta a interface usando
Store.addItemResult(...).
Exemplo do payload do compre junto
Seção intitulada “Exemplo do payload do compre junto”No fluxo de compre junto, o carrinho recebe um objeto com items, em que cada item representa um produto selecionado pelo cliente.
Exemplo:
{ "items": [ { "sku": "SKU-LOOK-1", "quantity": 1, "customizations": [] }, { "sku": "SKU-LOOK-2", "quantity": 1, "customizations": [ { "Nome": "Maria", "Numero": "10" } ] } ]}Esse payload é enviado em:
POST /carrinho/adicionar/kitcom Content-Type: application/json.
Estados de interface esperados
Seção intitulada “Estados de interface esperados”Uma boa implementação de compre junto deve prever, no mínimo, estes estados:
- Sem Seleção: o resumo do compre junto pode começar oculto.
- Com Itens Selecionados: o resumo de preço e o botão final ficam visíveis.
- Com Desconto: a interface pode exibir preço original, preço final e percentual de desconto.
- Com Itens Indisponíveis: a interface deve informar que há produtos indisponíveis na seleção.
- Em Erro de Requisição: a seção deve exibir uma mensagem de falha.
- Em Sucesso: a resposta do carrinho deve atualizar o feedback visual da compra.
Esses estados ajudam a manter o comportamento do compre junto previsível para o cliente final.
Diferença entre compre junto e kit nativo
Seção intitulada “Diferença entre compre junto e kit nativo”Embora ambos agrupem produtos, compre junto e kit nativo são fluxos diferentes.
| Recurso | Como funciona | Endpoint principal |
|---|---|---|
kit nativo da plataforma (bundle) | produto principal com compra consolidada por SKU principal + bundle_skus[] | POST /carrinho/adicionar |
| compre junto | seleção opcional de produtos relacionados enviada como lista de items[] | POST /carrinho/adicionar/kit |
Se a implementação depende de um produto principal do tipo bundle, o fluxo correto é o de kit nativo. Se a implementação permite que o cliente selecione produtos relacionados manualmente, o fluxo correto é o de compre junto.