simpleshop_theme/lib/simpleshop_theme_web/cart_hook.ex
jamey b3d1019cd4 add quantity controls to cart drawer via shared CartHook
Move increment/decrement handlers from Cart LiveView into CartHook so
they work from any page's drawer. Enable show_quantity_controls on the
drawer's cart_item_row. Scope cart tests to #main-content to avoid
duplicate button matches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 00:15:04 +00:00

138 lines
4.0 KiB
Elixir

defmodule SimpleshopThemeWeb.CartHook do
@moduledoc """
LiveView on_mount hook for cart state and shared event handling.
Mounted in the public_shop live_session to give all shop LiveViews
cart state, PubSub sync, and shared event handlers via attach_hook.
Handles these events so individual LiveViews don't have to:
- `open_cart_drawer` / `close_cart_drawer` - toggle drawer visibility
- `remove_item` - remove item from cart
- `increment` / `decrement` - change item quantity
- `{:cart_updated, cart}` info - cross-tab cart sync via PubSub
LiveViews with custom cart logic (e.g. add_to_cart) can call
`update_cart_assigns/2` and `broadcast_and_update/2` directly.
"""
import Phoenix.Component, only: [assign: 3]
import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1, push_event: 3]
alias SimpleshopTheme.Cart
def on_mount(:mount_cart, _params, session, socket) do
cart_items = Cart.get_from_session(session)
socket =
socket
|> update_cart_assigns(cart_items)
|> assign(:cart_drawer_open, false)
|> assign(:cart_status, nil)
|> attach_hook(:cart_events, :handle_event, &handle_cart_event/3)
|> attach_hook(:cart_info, :handle_info, &handle_cart_info/2)
socket =
if connected?(socket) do
csrf_token = Map.get(session, "_csrf_token", "default")
topic = "cart:#{csrf_token}"
Phoenix.PubSub.subscribe(SimpleshopTheme.PubSub, topic)
assign(socket, :cart_topic, topic)
else
assign(socket, :cart_topic, nil)
end
{:cont, socket}
end
# Shared event handlers
defp handle_cart_event("open_cart_drawer", _params, socket) do
{:halt, assign(socket, :cart_drawer_open, true)}
end
defp handle_cart_event("close_cart_drawer", _params, socket) do
{:halt, assign(socket, :cart_drawer_open, false)}
end
defp handle_cart_event("remove_item", %{"id" => variant_id}, socket) do
cart = Cart.remove_item(socket.assigns.raw_cart, variant_id)
socket =
socket
|> broadcast_and_update(cart)
|> assign(:cart_status, "Item removed from cart")
{:halt, socket}
end
defp handle_cart_event("increment", %{"id" => variant_id}, socket) do
cart = Cart.add_item(socket.assigns.raw_cart, variant_id, 1)
new_qty = Cart.get_quantity(cart, variant_id)
socket =
socket
|> broadcast_and_update(cart)
|> assign(:cart_status, "Quantity updated to #{new_qty}")
{:halt, socket}
end
defp handle_cart_event("decrement", %{"id" => variant_id}, socket) do
current = Cart.get_quantity(socket.assigns.raw_cart, variant_id)
cart = Cart.update_quantity(socket.assigns.raw_cart, variant_id, current - 1)
new_qty = Cart.get_quantity(cart, variant_id)
socket =
socket
|> broadcast_and_update(cart)
|> assign(:cart_status, "Quantity updated to #{new_qty}")
{:halt, socket}
end
defp handle_cart_event(_event, _params, socket), do: {:cont, socket}
# Shared info handlers
defp handle_cart_info({:cart_updated, cart}, socket) do
{:halt, update_cart_assigns(socket, cart)}
end
defp handle_cart_info(_msg, socket), do: {:cont, socket}
# Public helpers for LiveViews with custom cart logic
@doc """
Updates all cart-related assigns from raw cart data.
"""
def update_cart_assigns(socket, cart) do
%{items: items, count: count, subtotal: subtotal} = Cart.build_state(cart)
socket
|> assign(:raw_cart, cart)
|> assign(:cart_items, items)
|> assign(:cart_count, count)
|> assign(:cart_subtotal, subtotal)
end
@doc """
Broadcasts cart update to other tabs and updates local assigns.
Uses broadcast_from to avoid notifying self (prevents double-update).
"""
def broadcast_and_update(socket, cart) do
if socket.assigns.cart_topic do
Phoenix.PubSub.broadcast_from(
SimpleshopTheme.PubSub,
self(),
socket.assigns.cart_topic,
{:cart_updated, cart}
)
end
socket
|> update_cart_assigns(cart)
|> push_event("persist_cart", %{items: Cart.serialize(cart)})
end
end