Out-of-band updates
One answer, two places: the button that was pressed gets its new state, and the cart badge in the header updates too, swapped by its id with hx-swap-oob.
{fragment}padSession
hx-posthx-swap-oob
Live
🎧
Aurora Headphones
€189.00 · 14 in stock
🎶
Pebble Earbuds
€79.00 · 32 in stock
🔊
Boom Speaker
€129.00 · 8 in stock
🎙️
Studio Microphone
€99.00 · sold out
💻
Nimbus Laptop 14
€1,299.00 · 5 in stock
🖥️
Atlas Monitor 27
€349.00 · 11 in stock
🖱️
Glide Mouse
€39.00 · 54 in stock
⌨️
Tactile Keyboard
€119.00 · 21 in stock
Requests htmx made
Nothing yet - use the example above, and every request it makes shows here with the part PAD answered.
How it is built
PAD, on the server htmx, in the browser
<div class="shop">
<div id="shelf" class="grid shelf">
{products}
{fragment 'item'}
<div class="card item" id="item-{$id}">
<div class="item-top"><span class="item-emoji">{$emoji}</span>{if $inCart}<span class="badge is-accent">{$inCart} in cart</span>{/if}</div>
<strong>{$name}</strong>
<div class="muted small">{$price | currency('EUR')} · {if $stock}{$stock} in stock{else}sold out{/if}</div>
<div class="item-buttons">
<button class="btn-sm" hx-post="?examples/cart&id={$id}&op=add&padFragment=item" hx-target="#item-{$id}" hx-swap="outerHTML">+ Add</button>
{if $inCart}<button class="btn-sm btn-ghost" hx-post="?examples/cart&id={$id}&op=remove&padFragment=item" hx-target="#item-{$id}" hx-swap="outerHTML">−</button>{/if}
</div>
</div>
{if $posted}
<span id="cart-count" hx-swap-oob="true">{$cartCount}</span>
{cartSummary}
{/if}
{/fragment}
{/products}
</div>
{cartSummary}
</div><?php
// The cart is the session's: product id => quantity. A button posts its product - op=add
// or op=remove - and the answer is the {fragment 'item'} of that product: the card with its
// new count, then the parts elsewhere on the page that changed too, each marked
// hx-swap-oob - htmx puts the card back where the button was and the rest where their ids
// are: the count in the header, the summary beside the shelf. Emptying the cart is an
// ordinary {form} post that ends in a redirect, the page coming back whole.
$counts = padSession ( 'htmxCart', [] );
$products = array_slice ( htmxData ( 'products' ), 0, 8 );
$posted = padRequestIs ( 'POST' );
if ( $posted ) {
$id = (int) padRequest ( 'id', 0 );
$op = htmxText ( 'op' );
$stock = array_column ( $products, 'stock', 'id' );
if ( $op == 'clear' ) {
padSessionPut ( 'htmxCart', [] );
padRedirect ();
}
if ( $op == 'add' and ( $stock [$id] ?? 0 ) > ( $counts [$id] ?? 0 ) )
$counts [$id] = ( $counts [$id] ?? 0 ) + 1;
elseif ( $op == 'remove' and isset ( $counts [$id] ) and --$counts [$id] <= 0 )
unset ( $counts [$id] );
padSessionPut ( 'htmxCart', $counts );
$products = array_values ( array_filter ( $products, fn ( $product ) => $product ['id'] == $id ) );
// A product the shelf does not have - a stale or made-up request - has no card to
// answer: a 404, which htmx swaps nothing for, where the empty loop rendered no
// {fragment 'item'} and the answer was a 500, "there is no fragment named 'item'".
if ( ! $products )
padAbort ( 404, 'There is no such product' );
}
foreach ( $products as $at => $product )
$products [$at] ['inCart'] = $counts [ $product ['id'] ] ?? 0;
$cart = htmxCart ( $counts );
$cartCount = $cart ['count'];
$lines = $cart ['lines'];
$total = $cart ['total'];
?>