defmodule Corex.RadioGroup do @moduledoc ~S''' Phoenix implementation of [Zag.js Radio Group](https://zagjs.com/components/react/radio-group). ## Anatomy ### Minimal ```heex <.radio_group name="rg-minimal" class="radio-group" items={[ %{value: "lorem", label: "Lorem ipsum dolor sit amet"}, %{value: "duis", label: "Duis dictum gravida odio ac pharetra?"}, %{value: "donec", label: "Donec condimentum ex mi"} ]} > <:label>Choose one ``` ### With indicator ```heex <.radio_group name="rg-indicator" class="radio-group" items={[ %{value: "lorem", label: "Lorem ipsum dolor sit amet"}, %{value: "duis", label: "Duis dictum gravida odio ac pharetra?"}, %{value: "donec", label: "Donec condimentum ex mi"} ]} > <:label>Choose one <:item_control><.heroicon name="hero-check" class="data-checked" /> ``` ### Invalid ```heex <.radio_group name="rg-invalid" class="radio-group" invalid errors={["Required"]} items={[ %{value: "lorem", label: "Lorem ipsum dolor sit amet"}, %{value: "duis", label: "Duis dictum gravida odio ac pharetra?"}, %{value: "donec", label: "Donec condimentum ex mi"} ]} > <:label>Choose one <:item_control><.heroicon name="hero-check" class="data-checked" /> <:error :let={msg}> <.heroicon name="hero-exclamation-circle" class="icon" /> {msg} ``` ### Read-only ```heex <.radio_group name="rg-readonly" class="radio-group" read_only value="lorem" items={[ %{value: "lorem", label: "Lorem ipsum dolor sit amet"}, %{value: "duis", label: "Duis dictum gravida odio ac pharetra?"}, %{value: "donec", label: "Donec condimentum ex mi"} ]} > <:label>Choose one <:item_control><.heroicon name="hero-check" class="data-checked" /> ``` Items can be `{value, label}` tuples or maps with `:value`, `:label`, and optional `:disabled`, `:invalid`. ## Events Pick an event name and pass it to `on_*` on `<.radio_group>`. ### Server events | Event | When | Payload | | ----- | ---- | ------- | | `on_value_change="radio_group_changed"` | Selected value changes | `%{"id" => id, "value" => value}` | ### on_value_change ```heex <.radio_group name="rg-events" class="radio-group" on_value_change="radio_group_changed" items={[ %{value: "lorem", label: "Lorem ipsum dolor sit amet"}, %{value: "duis", label: "Duis dictum gravida odio ac pharetra?"}, %{value: "donec", label: "Donec condimentum ex mi"} ]} > <:label>Choose one <:item_control><.heroicon name="hero-check" class="data-checked" /> ``` ```elixir def handle_event("radio_group_changed", %{"id" => _id, "value" => value}, socket) do {:noreply, assign(socket, :choice, value)} end ``` ### Client events | Event | When | `event.detail` | | ----- | ---- | -------------- | | `on_value_change_client="radio-group-changed"` | Selected value changes | `id`, `value` | ### on_value_change_client ```heex <.radio_group id="radio-group-events-client" name="rg-events-client" class="radio-group" on_value_change_client="radio-group-changed" items={[ %{value: "lorem", label: "Lorem ipsum dolor sit amet"}, %{value: "duis", label: "Duis dictum gravida odio ac pharetra?"}, %{value: "donec", label: "Donec condimentum ex mi"} ]} > <:label>Choose one <:item_control><.heroicon name="hero-check" class="data-checked" /> ``` ```javascript const el = document.getElementById("radio-group-events-client"); el?.addEventListener("radio-group-changed", (event) => console.log(event.detail)); ``` ## API Requires a stable `id` on `<.radio_group>`. Imperative helpers set the selected value. | Function | Action | Returns | | -------- | ------ | ------- | | [`set_value/2`](#set_value/2) | Set value (client) | `%Phoenix.LiveView.JS{}` | | [`set_value/3`](#set_value/3) | Set value (server) | `socket` | | [`clear_value/1`](#clear_value/1) | Clear selection (client) | `%Phoenix.LiveView.JS{}` | | [`clear_value/2`](#clear_value/2) | Clear selection (server) | `socket` | | [`focus/1`](#focus/1) | Focus the group (client) | `%Phoenix.LiveView.JS{}` | | [`focus/2`](#focus/2) | Focus the group (server) | `socket` | | [`value/1`](#value/1) | Read current value (client) | `%Phoenix.LiveView.JS{}` | | [`value/2`](#value/2) | Read current value (client, options) | `%Phoenix.LiveView.JS{}` | | [`value/3`](#value/3) | Read current value (server) | `socket` | ### set_value ```heex <.action phx-click={Corex.RadioGroup.set_value("radio-group-api-server", "duis")} class="button button--sm"> Set Duis <.radio_group id="radio-group-api-server" name="rg-api-server" class="radio-group" value="lorem" items={[ %{value: "lorem", label: "Lorem ipsum dolor sit amet"}, %{value: "duis", label: "Duis dictum gravida odio ac pharetra?"}, %{value: "donec", label: "Donec condimentum ex mi"} ]} > <:label>Pick <:item_control><.heroicon name="hero-check" class="data-checked" /> ``` ## Patterns ### Controlled For server-owned selection, set `controlled`, bind `value`, and handle `on_value_change` in LiveView. ```heex <.radio_group id="radio-group-api-controlled" name="rg-controlled" class="radio-group" controlled value={@api_controlled_value} on_value_change="radio_group_api_controlled" items={[ %{value: "lorem", label: "Lorem ipsum dolor sit amet"}, %{value: "duis", label: "Duis dictum gravida odio ac pharetra?"}, %{value: "donec", label: "Donec condimentum ex mi"} ]} > <:label>Pick <:item_control><.heroicon name="hero-check" class="data-checked" /> ``` ```elixir def handle_event("radio_group_api_controlled", %{"value" => v}, socket) do {:noreply, assign(socket, :api_controlled_value, v)} end ``` ### Stream Use `Phoenix.LiveView.stream/3` to add or remove items at runtime. Keep `@items_list` in sync with the stream and pass it as `items`. Configure `dom_id` to match each item (`radio-group:stream-radio-group:item:#{value}`). ```heex <.radio_group name="stream-rg" class="radio-group" items={@items_list} value={@stream_value} controlled on_value_change="patterns_stream_value" > <:label>Choose one <:item_control><.heroicon name="hero-check" class="data-checked" /> ``` ## Form Use `field={@form[:choice]}` inside `<.form>` so the hidden input name and validation align with Phoenix forms. Pass `invalid` only when you want invalid styling. For cross-cutting invalid styling and error presentation, see the [Forms](forms.html) guide. Pass `invalid={Corex.FormField.invalid?(@form[:choice])}` when you want alert borders after validation. ```heex <.form for={@form} phx-change="validate"> <.radio_group field={@form[:choice]} class="radio-group" items={[ %{value: "lorem", label: "Lorem ipsum dolor sit amet"}, %{value: "duis", label: "Duis dictum gravida odio ac pharetra?"}, %{value: "donec", label: "Donec condimentum ex mi"} ]} > <:label>Choose one <:item_control><.heroicon name="hero-check" class="data-checked" /> <:error :let={msg}> <.heroicon name="hero-exclamation-circle" class="icon" /> {msg} ``` ## Style Target parts with `data-scope` and `data-part`, or use Corex Design: import tokens and `radio-group.css`, then set `class="radio-group"` on `<.radio_group>`. ```css [data-scope="radio-group"][data-part="root"] {} [data-scope="radio-group"][data-part="label"] {} [data-scope="radio-group"][data-part="item"] {} [data-scope="radio-group"][data-part="item-text"] {} [data-scope="radio-group"][data-part="item-control"] {} [data-scope="radio-group"][data-part="item-hidden-input"] {} ``` ```css @import "../corex/main.css"; @import "../corex/tokens/themes/neo/light.css"; @import "../corex/components/radio-group.css"; ``` Stack modifiers on the host (`class` on `<.radio_group>`). ### Color | Modifier | Classes | | -------- | ------- | | Default | `radio-group` | | Accent | `radio-group radio-group--accent` | | Brand | `radio-group radio-group--brand` | | Alert | `radio-group radio-group--alert` | | Info | `radio-group radio-group--info` | | Success | `radio-group radio-group--success` | ### Size | Modifier | Classes | | -------- | ------- | | SM | `radio-group radio-group--sm` | | MD | `radio-group radio-group--md` | | LG | `radio-group radio-group--lg` | | XL | `radio-group radio-group--xl` | ''' @doc type: :component use Phoenix.Component import Corex.Api.Doc alias Phoenix.LiveView alias Phoenix.LiveView.JS import Corex.Helpers, only: [respond_to_fields: 1] alias Corex.RadioGroup.Anatomy.{ Indicator, Item, ItemControl, ItemHiddenInput, ItemText, Label, Props, Root, ValueInput } alias Corex.RadioGroup.Connect attr(:id, :string, required: false) attr(:value, :string, default: nil) attr(:controlled, :boolean, default: false) attr(:name, :string, default: nil) attr(:form, :string, default: nil) attr(:disabled, :boolean, default: false) attr(:invalid, :boolean, default: false) attr(:required, :boolean, default: false) attr(:read_only, :boolean, default: false) attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"]) attr(:orientation, :string, default: "vertical", values: ["horizontal", "vertical"]) attr(:on_value_change, :string, default: nil) attr(:on_value_change_client, :string, default: nil) attr(:items, :list, required: true, doc: "List of [value, label] or %{value: ..., label: ..., disabled: ..., invalid: ...}" ) attr(:errors, :list, default: [], doc: "Error messages to display (non-field API)") attr(:field, Phoenix.HTML.FormField, doc: "A form field, e.g. f[:choice] or @form[:choice]") attr(:rest, :global) slot :label, required: false do attr(:class, :string, required: false) end slot :item_control, required: false do attr(:class, :string, required: false) end slot :item, required: false do attr(:class, :string, required: false) end slot :error, required: false do attr(:class, :string, required: false) end def radio_group(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do v = if field.value in [nil, ""], do: nil, else: to_string(field.value) assigns |> Corex.FormField.assign_form_field(field) |> assign(:value, v) |> assign(:form_field, true) |> do_radio_group() end def radio_group(assigns) do assigns |> assign_new(:form_field, fn -> false end) |> do_radio_group() end defp do_radio_group(assigns) do assigns = assigns |> assign_new(:id, fn -> "radio-group-#{System.unique_integer([:positive])}" end) |> assign_new(:errors, fn -> [] end) |> assign_new(:dir, fn -> "ltr" end) |> assign(:items, normalize_radio_items(assigns.items)) ~H"""