Skip to content

Template Cart Items

Each line owns its quantity through an sv-ui-state nested in the template, so the stepper works without any per-line JavaScript. The instance script only does what the bindings cannot: it keeps the shared subtotal in sync, and drops the line from the subtotal again when it is removed.

Subtotal

Code

The following example assumes that you have imported the component and set up the theme.

Cart Items
<sv-template id="cart-line">
126 collapsed lines
<template>
<sv-ui-state>
<!-- Per-instance state -->
<sv-store type="number" name="$qty" value="1"></sv-store>
<!-- Runs once per instance -->
<script type="module">
const { props, refs, onDisconnect } = TemplateHost;
const { $qty } = TemplateHost.querySelector("sv-ui-state").stores;
const $total = TemplateHost.closest("sv-ui-state").getStore("$total");
const unsubscribe = $qty.subscribe((qty, previousQty = 0) => {
$total.set($total.get() + (qty - previousQty) * props.price);
});
const removeLine = () => TemplateHost.remove();
refs.remove.addEventListener("click", removeLine);
onDisconnect(() => {
unsubscribe();
$total.set($total.get() - $qty.get() * props.price);
refs.remove.removeEventListener("click", removeLine);
});
</script>
<div class="line">
<div class="line-info">
<span class="line-name">[[ name ]]</span>
<span class="line-price">$[[ price ]] each</span>
</div>
<div class="line-qty">
<button type="button" aria-label="Less" bind-attr-toggle="disabled:$qty == 1" bind-event="click:$qty($qty - 1)">
−
</button>
<span bind-text="$qty"></span>
<button
type="button"
aria-label="More"
bind-attr-toggle="disabled:$qty == [[ stock ]]"
bind-event="click:$qty($qty + 1)"
>
+
</button>
</div>
<span class="line-total" bind-text="'$' + $qty * [[ price ]]"></span>
<button type="button" class="line-remove" aria-label="Remove [[ name ]]" template-ref="remove">✕</button>
</div>
</sv-ui-state>
</template>
<style>
.line {
display: grid;
grid-template-areas: "info qty total remove";
grid-template-columns: 1fr auto auto auto;
gap: 0.75em 1em;
align-items: center;
padding: 0.75em 1em;
background-color: var(--sv-gray-7);
border: var(--sv-bdr-sz-sm) solid var(--sv-gray-5);
border-radius: var(--sv-bdr-rad-md);
@container (inline-size < 24rem) {
grid-template-areas:
"info remove"
"qty total";
grid-template-columns: 1fr auto;
}
}
.line-info {
display: grid;
grid-area: info;
gap: 0.15em;
}
.line-price {
font-size: 0.75em;
color: var(--sv-gray-3);
}
.line-qty {
display: flex;
grid-area: qty;
gap: 0.5em;
align-items: center;
justify-self: start;
padding: 0.2em;
background-color: var(--sv-gray-6);
border-radius: var(--sv-bdr-rad-sm);
button {
inline-size: 1.6em;
font: inherit;
color: var(--sv-gray-1);
cursor: pointer;
background-color: var(--sv-gray-5);
border: none;
border-radius: var(--sv-bdr-rad-sm);
&:disabled {
cursor: not-allowed;
opacity: 0.4;
}
}
}
.line-total {
grid-area: total;
min-inline-size: 3.5em;
font-weight: 600;
text-align: end;
}
.line-remove {
grid-area: remove;
font: inherit;
color: var(--sv-gray-3);
cursor: pointer;
background-color: transparent;
border: none;
}
</style>
</sv-template>
<sv-ui-state>
14 collapsed lines
<sv-store type="number" name="$total" value="0"></sv-store>
<div class="cart">
<div class="cart-lines">
<sv-template prop-name="Desk lamp" prop-price:json="45" prop-stock:json="4" use="cart-line"></sv-template>
<sv-template prop-name="Notebook" prop-price:json="12" prop-stock:json="10" use="cart-line"></sv-template>
<sv-template prop-name="Coffee beans" prop-price:json="18" prop-stock:json="6" use="cart-line"></sv-template>
</div>
<p class="cart-total">
Subtotal
<strong bind-text="'$' + $total"></strong>
</p>
</div>
</sv-ui-state>
<style>
21 collapsed lines
.cart {
display: grid;
gap: 0.6em;
max-inline-size: 32rem;
padding: clamp(1rem, 4vw, 2rem);
margin-inline: auto;
.cart-lines {
display: grid;
gap: 0.6em;
container-type: inline-size;
}
.cart-total {
display: flex;
justify-content: space-between;
padding-block-start: 0.6em;
margin: 0;
border-block-start: var(--sv-bdr-sz-sm) solid var(--sv-gray-5);
}
}
</style>