make PDP variant selection work without JS
All checks were successful
deploy / deploy (push) Successful in 1m3s

Variant options (colour, size) are now URL params handled via
handle_params instead of phx-click events. Swatches and size buttons
render as patch links in shop mode, so changing variants works as
plain navigation without JS. Quantity is now a number input that
submits with the form. Unavailable variants render as disabled spans.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-25 01:08:36 +00:00
parent a61adf4939
commit 23e95a3de6
6 changed files with 238 additions and 75 deletions

View File

@@ -32,7 +32,7 @@
name="variant_id"
value={@selected_variant && @selected_variant.id}
/>
<input type="hidden" name="quantity" value={@quantity} />
<%!-- quantity is provided by the quantity_selector input below --%>
<%!-- Dynamic variant selectors --%>
<%= for option_type <- @option_types do %>
@@ -41,6 +41,7 @@
selected={@selected_options[option_type.name]}
available={@available_options[option_type.name] || []}
mode={@mode}
option_urls={(@option_urls || %{})[option_type.name] || %{}}
/>
<% end %>

View File

@@ -1339,13 +1339,15 @@ defmodule BerrypodWeb.ShopComponents.Product do
Renders a variant selector for a single option type.
Shows color swatches for color-type options, text buttons for others.
Disables unavailable options and fires `select_option` event on click.
In shop mode, each option is a patch link for progressive enhancement.
In preview mode, options are inert buttons.
## Attributes
* `option_type` - Required. Map with :name, :type, :values keys
* `selected` - Required. Currently selected value (string)
* `available` - Required. List of available values for this option
* `option_urls` - Optional. Map of value title => patch URL
* `mode` - Optional. :shop or :preview (default: :shop)
## Examples
@@ -1354,11 +1356,13 @@ defmodule BerrypodWeb.ShopComponents.Product do
option_type={%{name: "Size", type: :size, values: [%{title: "S"}, ...]}}
selected="M"
available={["S", "M", "L"]}
option_urls={%{"S" => "/products/foo?Size=S", ...}}
/>
"""
attr :option_type, :map, required: true
attr :selected, :string, required: true
attr :available, :list, required: true
attr :option_urls, :map, default: %{}
attr :mode, :atom, default: :shop
def variant_selector(assigns) do
@@ -1378,8 +1382,8 @@ defmodule BerrypodWeb.ShopComponents.Product do
hex={value[:hex] || "#888888"}
selected={value.title == @selected}
disabled={value.title not in @available}
option_name={@option_type.name}
mode={@mode}
url={@option_urls[value.title]}
/>
<% else %>
<.size_button
@@ -1387,8 +1391,8 @@ defmodule BerrypodWeb.ShopComponents.Product do
title={value.title}
selected={value.title == @selected}
disabled={value.title not in @available}
option_name={@option_type.name}
mode={@mode}
url={@option_urls[value.title]}
/>
<% end %>
</div>
@@ -1400,44 +1404,76 @@ defmodule BerrypodWeb.ShopComponents.Product do
attr :hex, :string, required: true
attr :selected, :boolean, required: true
attr :disabled, :boolean, required: true
attr :option_name, :string, required: true
attr :mode, :atom, default: :shop
attr :url, :string, default: nil
defp color_swatch(assigns) do
~H"""
<button
type="button"
phx-click={if @mode == :shop, do: "select_option"}
phx-value-option={@option_name}
phx-value-selected={@title}
class="color-swatch"
style={"background-color: #{@hex};"}
title={@title}
aria-label={"Select #{@title}"}
aria-pressed={to_string(@selected)}
>
</button>
<%= cond do %>
<% @mode == :shop and not @disabled -> %>
<.link
patch={@url}
class="color-swatch"
style={"background-color: #{@hex};"}
title={@title}
aria-label={"Select #{@title}"}
aria-pressed={to_string(@selected)}
>
</.link>
<% @mode == :shop -> %>
<span
class="color-swatch"
style={"background-color: #{@hex};"}
title={"#{@title} (unavailable)"}
aria-label={"#{@title} (unavailable)"}
aria-disabled="true"
/>
<% true -> %>
<button
type="button"
class="color-swatch"
style={"background-color: #{@hex};"}
title={@title}
aria-label={"Select #{@title}"}
aria-pressed={to_string(@selected)}
/>
<% end %>
"""
end
attr :title, :string, required: true
attr :selected, :boolean, required: true
attr :disabled, :boolean, required: true
attr :option_name, :string, required: true
attr :mode, :atom, default: :shop
attr :url, :string, default: nil
defp size_button(assigns) do
~H"""
<button
type="button"
phx-click={if @mode == :shop, do: "select_option"}
phx-value-option={@option_name}
phx-value-selected={@title}
class="size-btn"
aria-pressed={to_string(@selected)}
>
{@title}
</button>
<%= cond do %>
<% @mode == :shop and not @disabled -> %>
<.link
patch={@url}
class="size-btn"
aria-pressed={to_string(@selected)}
>
{@title}
</.link>
<% @mode == :shop -> %>
<span
class="size-btn"
aria-disabled="true"
>
{@title}
</span>
<% true -> %>
<button
type="button"
class="size-btn"
aria-pressed={to_string(@selected)}
>
{@title}
</button>
<% end %>
"""
end
@@ -1478,9 +1514,15 @@ defmodule BerrypodWeb.ShopComponents.Product do
>
</button>
<span class="qty-display">
{@quantity}
</span>
<input
type="number"
name="quantity"
value={@quantity}
min={@min}
max={@max}
class="qty-display"
aria-label="Quantity"
/>
<button
type="button"
phx-click="increment_quantity"