defmodule Corex.Switch do @moduledoc ~S''' Phoenix implementation of [Zag.js Switch](https://zagjs.com/components/react/switch). ## Anatomy ### Minimal ```heex <.switch class="switch" aria_label="Enable notifications" /> ``` ### With label ```heex <.switch class="switch"> <:label>Enable ``` ## API Requires a stable `id` on `<.switch>`. | Function | Action | Returns | | -------- | ------ | ------- | | [`set_checked/2`](#set_checked/2) | Set checked state (client) | `%Phoenix.LiveView.JS{}` | | [`set_checked/3`](#set_checked/3) | Set checked state (server) | `socket` | | [`toggle_checked/2`](#toggle_checked/2) | Toggle checked (client) | `%Phoenix.LiveView.JS{}` | | [`toggle_checked/3`](#toggle_checked/3) | Toggle checked (server) | `socket` | ## Events Pick an event name and pass it to `on_*` on `<.switch>`. ### Server events | Event | When | Payload | | ----- | ---- | ------- | | `on_checked_change="switch_changed"` | Checked state changes | `%{"id" => id, "checked" => boolean}` | ### on_checked_change ```heex <.switch class="switch" on_checked_change="switch_changed"> <:label>Subscribe ``` ```elixir def handle_event("switch_changed", %{"id" => id, "checked" => checked}, socket) do {:noreply, assign(socket, :checked, checked)} end ``` ### Client events | Event | When | `event.detail` | | ----- | ---- | -------------- | | `on_checked_change_client="switch-changed"` | Checked state changes | `id`, `checked` | ### on_checked_change_client ```heex <.switch id="switch-on-checked-change-client" class="switch" on_checked_change_client="switch-changed"> <:label>Subscribe ``` ```javascript const el = document.getElementById("switch-on-checked-change-client"); el?.addEventListener("switch-changed", (event) => console.log(event.detail)); ``` ## Patterns ### Controlled For server-owned checked state, set `controlled`, bind `checked`, and handle `on_checked_change` in LiveView. ```heex <.switch class="switch" controlled checked={@checked} on_checked_change="patterns_checked" > <:label>Enable ``` ```elixir def mount(_params, _session, socket) do {:ok, assign(socket, :checked, false)} end def handle_event("patterns_checked", %{"checked" => checked}, socket) do {:noreply, assign(socket, :checked, checked == true or checked == "true")} end ``` ## Form Set the form `id` in `to_form/2` and use `<.form for={@form}>`. Use `field={@form[:notifications]}` so the switch name matches the form. For Ecto validation in LiveView, add `phx-change` on the form so params stay in sync. For cross-cutting invalid styling and error presentation, see the [Forms](forms.html) guide. Pass `invalid={Corex.FormField.invalid?(@form[:notifications])}` when you want alert borders after validation. ```heex <.form for={@form} phx-change="validate"> <.switch field={@form[:notifications]} class="switch"> <:label>Enable notifications <: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 `switch.css`, then set `class="switch"` on `<.switch>`. ```css [data-scope="switch"][data-part="root"] {} [data-scope="switch"][data-part="control"] {} [data-scope="switch"][data-part="thumb"] {} [data-scope="switch"][data-part="label"] {} [data-scope="switch"][data-part="input"] {} [data-scope="switch"][data-part="error"] {} ``` ```css @import "../corex/main.css"; @import "../corex/tokens/themes/neo/light.css"; @import "../corex/components/switch.css"; ``` Stack modifiers on the host (`class` on `<.switch>`). ### Color | Modifier | Classes | | -------- | ------- | | Default | `switch` | | Accent | `switch switch--accent` | | Brand | `switch switch--brand` | | Alert | `switch switch--alert` | | Info | `switch switch--info` | | Success | `switch switch--success` | ### Size | Modifier | Classes | | -------- | ------- | | SM | `switch switch--sm` | | MD | `switch switch--md` | | LG | `switch switch--lg` | | XL | `switch switch--xl` | ''' @doc type: :component use Phoenix.Component import Corex.Api.Doc alias Corex.Checkable.Helpers, as: CheckableHelpers alias Corex.Switch.Anatomy.{Control, HiddenInput, Label, Props, Root, Thumb} alias Corex.Switch.Connect alias Phoenix.HTML.Form alias Phoenix.LiveView alias Phoenix.LiveView.JS @doc """ Renders a switch component. """ attr(:id, :string, required: false, doc: "The id of the switch, useful for API to identify the switch" ) attr(:checked, :boolean, default: false, doc: "The initial checked state or the controlled checked state" ) attr(:controlled, :boolean, default: false, doc: "Whether the switch is controlled" ) attr(:name, :string, doc: "The name of the switch input for form submission") attr(:form, :string, doc: "The form id to associate the switch with") attr(:aria_label, :string, default: "Label", doc: "The accessible label for the switch" ) attr(:disabled, :boolean, default: false, doc: "Whether the switch is disabled" ) attr(:value, :string, default: "true", doc: "The value of the switch when checked" ) attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"], doc: "The direction of the switch. When nil, derived from document (html lang + config :rtl_locales)" ) attr(:orientation, :string, default: "horizontal", values: ["vertical", "horizontal"], doc: "Layout orientation for CSS (vertical or horizontal)" ) attr(:read_only, :boolean, default: false, doc: "Whether the switch is read-only" ) attr(:invalid, :boolean, default: false, doc: "Whether the switch has validation errors" ) attr(:required, :boolean, default: false, doc: "Whether the switch is required" ) attr(:on_checked_change, :string, default: nil, doc: "The server event name when the checked state changes" ) attr(:on_checked_change_client, :string, default: nil, doc: "The client event name when the checked state changes" ) attr(:errors, :list, default: [], doc: "List of error messages to display" ) attr(:field, Phoenix.HTML.FormField, doc: "A form field struct retrieved from the form, for example: @form[:email]. Automatically sets id, name, checked state, and errors from the form field" ) attr(:rest, :global) slot :label, required: false do attr(:class, :string, required: false) attr(:position, :atom, required: false, values: [:pre, :post], doc: "Place the label before (:pre) or after (:post) the control. Defaults to :post when omitted." ) end slot :error, required: false do attr(:class, :string, required: false) end def switch(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do assigns = assigns |> Corex.FormField.assign_form_field(field) |> assign(:checked, Form.normalize_value("checkbox", field.value)) switch(assigns) end def switch(assigns) do assigns = assigns |> assign_new(:id, fn -> "switch-#{System.unique_integer([:positive])}" end) |> assign_new(:name, fn -> "name-#{System.unique_integer([:positive])}" end) |> assign_new(:form, fn -> nil end) |> assign_new(:form_field, fn -> false end) |> assign(:checked, CheckableHelpers.normalize_checked(assigns.checked)) ~H"""