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.
[[ name ]] $[[ price ]] each
Subtotal
Code
The following example assumes that you have imported the component and set up the theme.
<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>