defmodule Corex.Switch do @moduledoc ~S''' Phoenix implementation of [Zag.js Switch](https://zagjs.com/components/react/switch). ## Examples ### Basic Usage ```heex <.switch id="my-switch"> <:label>Enable notifications ``` ### Controlled Mode ```heex <.switch id="my-switch" controlled checked={@switch_checked} on_checked_change="switch_changed"> <:label>Enable notifications ``` ```elixir def handle_event("switch_changed", %{"checked" => checked}, socket) do {:noreply, assign(socket, :switch_checked, checked)} end ``` ## Phoenix Form Integration When using with Phoenix forms, you must add an id to the form using the `Corex.Form.get_form_id/1` function. ### Controller Build the form from an Ecto changeset: ```elixir def form_page(conn, _params) do form = %MyApp.Form.Preferences{} |> MyApp.Form.Preferences.changeset(%{}) |> Phoenix.Component.to_form(as: :preferences, id: "preferences-form") render(conn, :form_page, form: form) end ``` ```heex <.form :let={f} for={@form} id={Corex.Form.get_form_id(@form)} action={@action} method="post"> <.switch field={f[:notifications]} class="switch"> <:label>Enable notifications <:error :let={msg}> <.heroicon name="hero-exclamation-circle" class="icon" /> {msg} ``` ### Live View When using Phoenix form in a Live view you must also add controlled mode. Prefer building the form from an Ecto changeset (see "With Ecto changeset" below). ### With Ecto changeset When using Ecto changeset for validation and inside a Live view you must enable the controlled mode. This allows the Live View to be the source of truth and the component to be in sync accordingly. First create your schema and changeset: ```elixir defmodule MyApp.Accounts.User do use Ecto.Schema import Ecto.Changeset embedded_schema do field :notifications_enabled, :boolean, default: false end def changeset(user, attrs) do user |> cast(attrs, [:notifications_enabled]) |> validate_required([:notifications_enabled]) end end ``` ```elixir defmodule MyAppWeb.UserLive do use MyAppWeb, :live_view alias MyApp.Accounts.User def mount(_params, _session, socket) do {:ok, assign(socket, :form, to_form(User.changeset(%User{}, %{})))} end def handle_event("validate", %{"user" => user_params}, socket) do changeset = User.changeset(%User{}, user_params) {:noreply, assign(socket, form: to_form(changeset, action: :validate))} end def render(assigns) do ~H""" <.form for={@form} id={get_form_id(@form)} phx-change="validate"> <.switch field={@form[:notifications_enabled]} class="switch" controlled> <:label>Enable notifications <:error :let={msg}> <.heroicon name="hero-exclamation-circle" class="icon" /> {msg} """ end end ``` ## Programmatic Control ```heex # Client-side # Server-side def handle_event("turn_on", _, socket) do {:noreply, Corex.Switch.set_checked(socket, "my-switch", true)} end def handle_event("toggle", _, socket) do {:noreply, Corex.Switch.toggle_checked(socket, "my-switch")} end ``` ## Styling Use data attributes to target elements: ```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"] {} ``` If you wish to use the default Corex styling, you can use the class `switch` on the component. This requires to install `Mix.Tasks.Corex.Design` first and import the component css file. ```css @import "../corex/main.css"; @import "../corex/tokens/themes/neo/light.css"; @import "../corex/components/switch.css"; ``` You can then use modifiers ```heex <.switch class="switch switch--accent switch--lg"> ``` Learn more about modifiers and [Corex Design](https://corex-ui.com/components/switch#modifiers) ''' @doc type: :component use Phoenix.Component 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) end slot :error, required: false do attr(:class, :string, required: false) end def switch(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do errors = if Phoenix.Component.used_input?(field), do: field.errors, else: [] assigns = assigns |> assign(field: nil) |> assign(:errors, Enum.map(errors, &Corex.Gettext.translate_error(&1))) |> assign_new(:id, fn -> field.id end) |> assign_new(:name, fn -> field.name end) |> assign(:checked, Form.normalize_value("checkbox", field.value)) |> assign_new(:form, fn -> field.form.id end) 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) ~H"""