Shopping Cart
This recipe demonstrates building a reactive shopping cart with add/remove items, quantity adjustment, and automatically calculated totals using Datastar's computed signals.
What We're Building
A shopping cart with:
- Adjust quantities
- Remove items
- Automatic subtotal, tax, and total calculations
- Empty cart state
Complete Implementation
Blade Template
Create resources/views/cart/index.blade.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shopping Cart</title>
@hyper
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50">
<div class="max-w-6xl mx-auto px-4 py-8" @signals(['total' => $carts->sum(fn($cart) => $cart->price * $cart->quantity)])>
<h1 class="text-3xl font-bold mb-8">Shopping Cart</h1>
@fragment('cart-total')
<div id="cart-total" class="bg-blue-600 text-white p-4 rounded-lg mb-6 text-xl font-bold">
Total: $<span data-text="$total.toFixed(2)"></span>
</div>
@endfragment
@fragment('cart-items')
<div id="cart-items">
@if($carts->isEmpty())
<div class="text-center py-12">
<p class="text-gray-500 text-xl">Your cart is empty!</p>
</div>
@else
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@foreach ($carts as $cart)
<div class="bg-white rounded-lg shadow p-4"
@signals(["editingItem_{$cart->id}" => false, "tempQuantity_{$cart->id}" => $cart->quantity])>
<img src="{{ $cart->image }}"
alt="{{ $cart->name }}"
class="w-full h-48 object-cover rounded mb-3">
<h3 class="font-bold text-lg mb-2">{{ $cart->name }}</h3>
<p class="text-gray-600 text-sm mb-2">{{ $cart->description }}</p>
<p class="text-gray-800 mb-3">
Unit Price: <span class="font-semibold">${{ $cart->price }}</span>
</p>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">Quantity</label>
<div class="flex items-center gap-2">
<button
data-on:click="$editingItem_{{ $cart->id }} = true; $tempQuantity_{{ $cart->id }} = Math.max(1, $tempQuantity_{{ $cart->id }} - 1)"
class="w-8 h-8 bg-red-500 text-white rounded hover:bg-red-600">
−
</button>
<span class="w-16 text-center font-semibold text-lg" data-text="$tempQuantity_{{ $cart->id }}">
{{ $cart->quantity }}
</span>
<button
data-on:click="$editingItem_{{ $cart->id }} = true; $tempQuantity_{{ $cart->id }} = $tempQuantity_{{ $cart->id }} + 1"
class="w-8 h-8 bg-green-500 text-white rounded hover:bg-green-600">
+
</button>
<button
data-show="$editingItem_{{ $cart->id }}"
data-on:click="@putx('/cart/{{ $cart->id }}/update')"
class="w-8 h-8 bg-green-600 text-white rounded hover:bg-green-700 flex items-center justify-center">
✓
</button>
</div>
</div>
<p class="text-lg font-bold text-blue-600">
Total: ${{ number_format($cart->price * $cart->quantity, 2) }}
</p>
<button
data-on:click="@deletex('/cart/{{ $cart->id }}')"
class="mt-3 w-full bg-red-500 text-white py-2 rounded hover:bg-red-600">
Remove Item
</button>
</div>
@endforeach
</div>
@endif
</div>
@endfragment
</div>
</body>
</html>Controller
Create app/Http/Controllers/CartController.php:
<?php
namespace App\Http\Controllers;
use App\Models\CartItem;
use Illuminate\Http\Request;
class CartController extends Controller
{
public function index()
{
$carts = CartItem::all();
return view('cart.index', compact('carts'));
}
public function update(Request $request, $id)
{
$item = CartItem::findOrFail($id);
$quantity = signals("tempQuantity_{$id}", 1);
if ($quantity < 1) {
$quantity = 1;
}
$item->quantity = $quantity;
$item->save();
$carts = CartItem::all();
$total = $carts->sum(fn($cart) => $cart->price * $cart->quantity);
return hyper()
->signals([
'editingItem_' . $id => false,
'tempQuantity_' . $id => $quantity,
'total' => $total
])
->fragment('cart.index', 'cart-items', compact('carts'))
->fragment('cart.index', 'cart-total', compact('carts'));
}
public function decrease($id)
{
$item = CartItem::findOrFail($id);
if ($item->quantity > 1) {
$item->decrement('quantity');
}
$carts = CartItem::all();
$total = $carts->sum(fn($cart) => $cart->price * $cart->quantity);
return hyper()
->signals(['total' => $total])
->fragment('cart.index', 'cart-items', compact('carts'))
->fragment('cart.index', 'cart-total', compact('carts'));
}
public function destroy($id)
{
$item = CartItem::findOrFail($id);
$item->delete();
$carts = CartItem::all();
$total = $carts->sum(fn($cart) => $cart->price * $cart->quantity);
return hyper()
->signals(['total' => $total])
->fragment('cart.index', 'cart-items', compact('carts'))
->fragment('cart.index', 'cart-total', compact('carts'));
}
}Routing
In your web.php
use App\Http\Controllers\CartController;
Route::get('/cart', [CartController::class, 'index'])->name('cart.index');
Route::put('/cart/{id}/update', [CartController::class, 'update'])->name('cart.update');
Route::put('/cart/{id}/decrease', [CartController::class, 'decrease'])->name('cart.decrease');
Route::delete('/cart/{id}', [CartController::class, 'destroy'])->name('cart.destroy');How It Works
data-on:click (Datastar)
Attaches click event listeners that execute JavaScript expressions:
<button data-on:click="$editingItem_{{ $cart->id }} = true; $tempQuantity_{{ $cart->id }} = Math.max(1, $tempQuantity_{{ $cart->id }} - 1)">Breakdown:
data-on:click- Datastar attribute for handling click events$editingItem- Updates signal to show the save button$tempQuantity- Decreases quantity but ensures minimum of 1
data-show (Datastar)
Conditionally shows/hides elements using CSS display property:
<button data-show="$editingItem_{{ $cart->id }}" data-on:click="@putx('/cart/{{ $cart->id }}/update')">
✓
</button>How it works:
- When
$editingItemistrue: Element is visible - When
false: Setsdisplay: noneon the element - Element stays in the DOM - only visibility changes
- Fast to toggle, preserves element state
@fragment Directive (Hyper)
The @fragment directive defines reusable sections of your Blade template:
@fragment('cart-items')
<div id="cart-items">
<!-- content -->
</div>
@endfragment
@fragment('cart-total')
<div id="cart-total">
Total: $<span data-text="$total.toFixed(2)"></span>
</div>
@endfragmentPurpose:
- Marks sections that can be independently updated
- Keeps related HTML in one view file
- Enables surgical DOM updates without re-rendering the entire page
Why multiple fragments?
->fragment('cart.index', 'cart-items', compact('carts'))
->fragment('cart.index', 'cart-total', compact('carts'))This updates both the cart items list AND the total display in a single response. Hyper sends one SSE event with multiple fragment updates, ensuring atomic UI updates.
Key Takeaways
1. Fragment Updates:
- Extract specific sections from Blade views
- Render only what changed with new data
- Send via SSE for intelligent DOM morphing
- Multiple fragments can update atomically in one response
2. Optimistic UI Updates:
- Client-side signals update instantly for immediate feedback
- Server validates and persists changes
- Fragment updates ensure UI stays in sync with database state
This shopping cart demonstrates the power of combining Datastar's reactive frontend with Hyper's Laravel integration. You get instant UI feedback from client-side signal updates while maintaining server-side validation and persistence. The fragment system enables surgical DOM updates—only the cart items and total recalculate when needed—without full page reloads or complex state management. CSRF protection happens automatically, and the entire data flow from button click to database update to UI refresh requires minimal code. This pattern scales to complex e-commerce applications while keeping your codebase maintainable and your UX snappy.

