defmodule Corex.Checkbox do @moduledoc """ Phoenix implementation of [Zag.js Checkbox](https://zagjs.com/components/react/checkbox). ## Examples ### Minimal ```heex <.checkbox id="my-checkbox"> <:label>Accept terms ``` ### Custom Control This example assumes the import of `.icon` from `Core Components`, you are free to replace it ```heex <.checkbox class="checkbox"> <:label> Accept the terms <:control> <.icon name="hero-check" class="data-checked" /> ``` ### Custom Error This example assumes the import of `.icon` from `Core Components`, you are free to replace it ```heex <.checkbox class="checkbox"> <:label> Accept the terms <:error :let={msg}> <.icon name="hero-exclamation-circle" class="icon" /> {msg} ``` ## 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 ```heex <.form :let={f} for={@changeset} id={get_form_id(@changeset)}> <.checkbox field={f[:terms]} class="checkbox"> <:label>I accept the terms ``` ### Live View When using Phoenix form in a Live view you must also add controlled mode. This allows the Live view to be the source of truth and the component to be in sync accordingly ```heex <.form for={@form} id={get_form_id(@form)} phx-change="validate" phx-submit="save"> <.checkbox field={@form[:terms_accepted]} controlled> <:label>I accept the terms and conditions <:error :let={msg}>{msg} ``` The `field` attribute automatically handles: - Setting the `id` from the form field - Setting the `name` for form submission - Mapping the form value to the `checked` state - Displaying validation errors - Integration with Phoenix changesets ## API Control ```heex # Client-side # Server-side def handle_event("check", _, socket) do {:noreply, Corex.Checkbox.set_checked(socket, "my-checkbox", true)} end def handle_event("toggle", _, socket) do {:noreply, Corex.Checkbox.toggle_checked(socket, "my-checkbox")} end ``` ## Styling Use data attributes to target elements: - `[data-scope="checkbox"][data-part="root"]` - Label wrapper - `[data-scope="checkbox"][data-part="control"]` - Checkbox control - `[data-scope="checkbox"][data-part="label"]` - Label text - `[data-scope="checkbox"][data-part="input"]` - Hidden input - `[data-scope="checkbox"][data-part="error"]` - Error message State-specific styling: - `[data-state="checked"]` - When checkbox is checked - `[data-state="unchecked"]` - When checkbox is unchecked - `[data-disabled]` - When checkbox is disabled - `[data-readonly]` - When checkbox is read-only - `[data-invalid]` - When checkbox has validation errors """ @doc type: :component use Phoenix.Component alias Corex.Checkbox.Anatomy.{Props, Root, HiddenInput, Control, Label, Indicator} alias Corex.Checkbox.Connect @doc """ Renders a checkbox component. """ attr(:id, :string, required: false, doc: "The id of the checkbox, useful for API to identify the checkbox" ) attr(:checked, :boolean, default: false, doc: "The initial checked state or the controlled checked state" ) attr(:controlled, :boolean, default: false, doc: "Whether the checkbox is controlled" ) attr(:name, :string, doc: "The name of the checkbox input for form submission") attr(:form, :string, doc: "The form id to associate the checkbox with") attr(:aria_label, :string, default: "Label", doc: "The accessible label for the checkbox" ) attr(:disabled, :boolean, default: false, doc: "Whether the checkbox is disabled" ) attr(:value, :string, default: "true", doc: "The value of the checkbox when checked" ) attr(:dir, :string, default: "ltr", values: ["ltr", "rtl"], doc: "The direction of the checkbox" ) attr(:read_only, :boolean, default: false, doc: "Whether the checkbox is read-only" ) attr(:invalid, :boolean, default: false, doc: "Whether the checkbox has validation errors" ) attr(:required, :boolean, default: false, doc: "Whether the checkbox 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 :control, required: false do attr(:class, :string, required: false) end slot :error, required: false do attr(:class, :string, required: false) end def checkbox(%{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, Phoenix.HTML.Form.normalize_value("checkbox", field.value)) |> assign_new(:form, fn -> field.form.id end) checkbox(assigns) end def checkbox(assigns) do assigns = assigns |> assign_new(:id, fn -> "checkbox-#{System.unique_integer([:positive])}" end) |> assign_new(:name, fn -> "name-#{System.unique_integer([:positive])}" end) |> assign_new(:form, fn -> nil end) ~H"""
{render_slot(@error, msg)}
""" end @doc type: :api @doc """ Sets the checkbox checked state from client-side. Returns a `Phoenix.LiveView.JS` command. ## Examples """ def set_checked(checkbox_id, checked) when is_binary(checkbox_id) and is_boolean(checked) do Phoenix.LiveView.JS.dispatch("phx:checkbox:set-checked", to: "##{checkbox_id}", detail: %{checked: checked}, bubbles: false ) end @doc type: :api @doc """ Sets the checkbox checked state from server-side. Pushes a LiveView event. ## Examples def handle_event("check", _params, socket) do socket = Corex.Checkbox.set_checked(socket, "my-checkbox", true) {:noreply, socket} end """ def set_checked(socket, checkbox_id, checked) when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(checkbox_id) and is_boolean(checked) do Phoenix.LiveView.push_event(socket, "checkbox_set_checked", %{ checkbox_id: checkbox_id, checked: checked }) end @doc type: :api @doc """ Toggles the checkbox checked state from client-side. Returns a `Phoenix.LiveView.JS` command. ## Examples """ def toggle_checked(checkbox_id) when is_binary(checkbox_id) do Phoenix.LiveView.JS.dispatch("phx:checkbox:toggle-checked", to: "##{checkbox_id}", bubbles: false ) end @doc type: :api @doc """ Toggles the checkbox checked state from server-side. Pushes a LiveView event. ## Examples def handle_event("toggle", _params, socket) do socket = Corex.Checkbox.toggle_checked(socket, "my-checkbox") {:noreply, socket} end """ def toggle_checked(socket, checkbox_id) when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(checkbox_id) do Phoenix.LiveView.push_event(socket, "checkbox_toggle_checked", %{ checkbox_id: checkbox_id }) end end