Packages
corex
0.1.0-beta.1
0.1.2
0.1.1
0.1.0
0.1.0-rc.1
0.1.0-rc.0
0.1.0-beta.5
0.1.0-beta.4
0.1.0-beta.3
0.1.0-beta.2
0.1.0-beta.1
0.1.0-alpha.33
0.1.0-alpha.32
0.1.0-alpha.31
0.1.0-alpha.30
0.1.0-alpha.29
0.1.0-alpha.28
0.1.0-alpha.27
0.1.0-alpha.26
0.1.0-alpha.25
0.1.0-alpha.24
0.1.0-alpha.23
0.1.0-alpha.22
0.1.0-alpha.21
0.1.0-alpha.20
0.1.0-alpha.19
0.1.0-alpha.18
0.1.0-alpha.17
0.1.0-alpha.16
0.1.0-alpha.15
0.1.0-alpha.14
0.1.0-alpha.13
0.1.0-alpha.12
0.1.0-alpha.11
0.1.0-alpha.10
0.1.0-alpha.9
0.1.0-alpha.8
0.1.0-alpha.7
0.1.0-alpha.6
0.1.0-alpha.5
0.1.0-alpha.4
0.1.0-alpha.3
0.1.0-alpha.2
0.1.0-alpha.1
Accessible and unstyled UI components library written in Elixir and TypeScript that integrates Zag.js state machines into the Phoenix Framework.
Current section
Files
Jump to
Current section
Files
lib/components/checkbox.ex
defmodule Corex.Checkbox do
@moduledoc ~S'''
Phoenix implementation of [Zag.js Checkbox](https://zagjs.com/components/react/checkbox).
## API and events
Client DOM dispatches (handled by the `Checkbox` hook on the component root):
- `corex:checkbox:set-checked` — `detail.checked` must be a boolean; clears indeterminate.
- `corex:checkbox:toggle-checked` — toggles checked.
Server pushes (from `set_checked/3`, `toggle_checked/2`):
- `checkbox_set_checked` — `%{"id" => id, "checked" => boolean}`
- `checkbox_toggle_checked` — `%{"id" => id}`
Declarative `checked` may be `true`, `false`, or `:indeterminate` (Zag `CheckedState`). Imperative `set_checked/2` remains boolean-only.
## Examples
<!-- tabs-open -->
### Minimal
```heex
<.checkbox class="checkbox">
<:label>Accept terms</:label>
</.checkbox>
```
### Custom Control
```heex
<.checkbox class="checkbox">
<:label>
Accept the terms
</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>
```
### Custom Error
```heex
<.checkbox class="checkbox">
<:label>
Accept the terms
</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>
```
<!-- tabs-close -->
## Phoenix Form Integration
When using with Phoenix forms, set the form `id` in `to_form/2` and use `id={@form.id}` on `<.form>`.
### Controller
Build the form from an Ecto changeset and pass it to the template. Pass `id` into `to_form/2` so the template can use `id={@form.id}`:
```elixir
def checkbox_form_page(conn, _params) do
form =
%MyApp.Form.Terms{}
|> MyApp.Form.Terms.changeset(%{})
|> Phoenix.Component.to_form(as: :terms, id: "checkbox-form")
render(conn, :checkbox_form_page, form: form)
end
```
```heex
<.form :let={f} for={@form} id={@form.id} action={@action} method="post">
<.checkbox field={f[:terms]} class="checkbox">
<:label>Accept terms</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>
<button type="submit">Submit</button>
</.form>
```
### 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 (LiveView)
When using an Ecto changeset for validation in a LiveView, enable the `controlled` attribute on the checkbox so the LiveView remains the source of truth.
Schema and changeset:
```elixir
defmodule MyApp.Form.Terms do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :terms, :boolean, default: false
end
def changeset(terms, attrs \\ %{}) do
terms
|> cast(attrs, [:terms])
|> validate_required([:terms])
|> validate_acceptance(:terms)
end
end
```
LiveView with validate and submit:
```elixir
defmodule MyAppWeb.CheckboxFormLive do
use MyAppWeb, :live_view
alias MyApp.Form.Terms
def mount(_params, _session, socket) do
form = %Terms{} |> Terms.changeset(%{}) |> to_form(as: :terms, id: "checkbox-form-terms")
{:ok, assign(socket, :form, form)}
end
def handle_event("validate", %{"terms" => params}, socket) do
changeset = Terms.changeset(%Terms{}, params)
{:noreply, assign(socket, :form, to_form(changeset, action: :validate, as: :terms, id: "checkbox-form-terms"))}
end
def handle_event("save", %{"terms" => params}, socket) do
case Terms.changeset(%Terms{}, params) do
%Ecto.Changeset{valid?: true} = _ ->
{:noreply, assign(socket, :form, to_form(Terms.changeset(%Terms{}, %{}), as: :terms, id: "checkbox-form-terms"))}
changeset ->
{:noreply, assign(socket, :form, to_form(changeset, action: :insert, as: :terms, id: "checkbox-form-terms"))}
end
end
def render(assigns) do
~H"""
<.form for={@form} id={@form.id} phx-change="validate" phx-submit="save">
<.checkbox field={@form[:terms]} class="checkbox" controlled>
<:label>Accept terms</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>
<button type="submit">Submit</button>
</.form>
"""
end
end
```
## API Control
```heex
# Client-side
<button phx-click={Corex.Checkbox.set_checked("my-checkbox", true)}>
Check
</button>
<button phx-click={Corex.Checkbox.toggle_checked("my-checkbox")}>
Toggle
</button>
# 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:
```css
[data-scope="checkbox"][data-part="root"] {}
[data-scope="checkbox"][data-part="control"] {}
[data-scope="checkbox"][data-part="label"] {}
[data-scope="checkbox"][data-part="hidden-input"] {}
[data-scope="checkbox"][data-part="error"] {}
```
If you wish to use the default Corex styling, you can use the class `checkbox` 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/checkbox.css";
```
You can then use modifiers
```heex
<.checkbox class="checkbox checkbox--accent checkbox--lg">
```
components/checkbox#modifiers)
'''
@doc type: :component
use Phoenix.Component
alias Corex.Checkbox.Anatomy.{
Control,
HiddenInput,
Indeterminate,
Indicator,
Label,
Props,
Root
}
alias Corex.Checkbox.Connect
alias Corex.Helpers
alias Phoenix.HTML.Form
alias Phoenix.LiveView
alias Phoenix.LiveView.JS
@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, :any,
default: false,
doc:
"Checked state: true, false, or :indeterminate (Zag CheckedState). Form fields still use boolean."
)
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. 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 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 :indicator, required: false do
attr(:class, :string, required: false)
end
slot :indeterminate, 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, 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)
|> assign(:checked, Helpers.normalize_checkbox_checked(assigns.checked))
~H"""
<div
id={@id}
phx-hook="Checkbox"
data-loading
phx-mounted={Phoenix.LiveView.JS.ignore_attributes(["data-loading"])}
{@rest}
{Connect.props(%Props{
id: @id,
controlled: @controlled,
checked: @checked,
name: @name,
form: @form,
dir: @dir,
orientation: @orientation,
read_only: @read_only,
invalid: @invalid,
required: @required,
on_checked_change: @on_checked_change,
on_checked_change_client: @on_checked_change_client,
label: @aria_label,
disabled: @disabled,
value: @value
})}
>
<label phx-mounted={Connect.ignore_root(%Root{id: @id, dir: @dir, checked: @checked, orientation: @orientation})} {Connect.root(%Root{id: @id, dir: @dir, checked: @checked, orientation: @orientation})}>
<input type="hidden" name={@name} value="false" form={@form} disabled={@disabled}/>
<input
phx-mounted={Connect.ignore_hidden_input(%HiddenInput{id: @id, name: @name, checked: @checked, disabled: @disabled, required: @required, invalid: @invalid, value: @value, controlled: @controlled})}
{Connect.hidden_input(%HiddenInput{id: @id, name: @name, checked: @checked, disabled: @disabled, required: @required, invalid: @invalid, value: @value, controlled: @controlled})}
/>
<div phx-mounted={Connect.ignore_control(%Control{id: @id, dir: @dir, checked: @checked, orientation: @orientation})} {Connect.control(%Control{id: @id, dir: @dir, checked: @checked, orientation: @orientation})}>
<span
:if={@indicator != []}
phx-mounted={Connect.ignore_indicator(%Indicator{id: @id, dir: @dir, checked: @checked, orientation: @orientation})}
{Connect.indicator(%Indicator{id: @id, dir: @dir, checked: @checked, orientation: @orientation})}
>
{render_slot(@indicator)}
</span>
<span
:if={@indeterminate != []}
phx-mounted={Connect.ignore_indeterminate(%Indeterminate{id: @id, dir: @dir, checked: @checked, orientation: @orientation})}
{Connect.indeterminate(%Indeterminate{id: @id, dir: @dir, checked: @checked, orientation: @orientation})}
>
{render_slot(@indeterminate)}
</span>
</div>
<span
:if={@label != []}
phx-mounted={Connect.ignore_label(%Label{id: @id, dir: @dir, checked: @checked, orientation: @orientation})}
{Connect.label(%Label{id: @id, dir: @dir, checked: @checked, orientation: @orientation})}
>
{render_slot(@label)}
</span>
<span
:if={@label == [] && @aria_label}
class="sr-only"
phx-mounted={Connect.ignore_label(%Label{id: @id, dir: @dir, checked: @checked, orientation: @orientation})}
{Connect.label(%Label{id: @id, dir: @dir, checked: @checked, orientation: @orientation})}
>
{@aria_label}
</span>
</label>
<div :if={@error} :for={msg <- @errors} data-scope="checkbox" data-part="error">
{render_slot(@error, msg)}
</div>
</div>
"""
end
attr(:skeleton_label, :boolean,
default: true,
doc:
"When true, renders a compact label-line placeholder (same line height band as the real checkbox label)."
)
attr(:dir, :string,
default: "ltr",
values: ["ltr", "rtl"],
doc: "Same as checkbox: logical direction for layout."
)
attr(:orientation, :string,
default: "horizontal",
values: ["vertical", "horizontal"],
doc: "Same as checkbox: layout orientation for the skeleton root."
)
attr(:rest, :global)
def checkbox_skeleton(assigns) do
~H"""
<div data-dir={@dir} data-orientation={@orientation} {@rest}>
<div
data-scope="checkbox"
data-part="root"
data-loading
dir={@dir}
data-orientation={@orientation}
>
<div data-scope="checkbox" data-part="control" aria-hidden="true">
<span data-scope="checkbox" data-part="indicator">
</span>
</div>
<div
:if={@skeleton_label}
data-scope="checkbox"
data-part="label"
aria-hidden="true"
>
</div>
</div>
</div>
"""
end
@doc type: :api
@doc """
Sets the checkbox checked state from client-side. Returns a `Phoenix.LiveView.JS` command.
## Examples
<button phx-click={Corex.Checkbox.set_checked("my-checkbox", true)}>
Check
</button>
<button phx-click={Corex.Checkbox.set_checked("my-checkbox", false)}>
Uncheck
</button>
"""
def set_checked(checkbox_id, checked) when is_binary(checkbox_id) and is_boolean(checked) do
JS.dispatch("corex: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
LiveView.push_event(socket, "checkbox_set_checked", %{
"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
<button phx-click={Corex.Checkbox.toggle_checked("my-checkbox")}>
Toggle
</button>
"""
def toggle_checked(checkbox_id) when is_binary(checkbox_id) do
JS.dispatch("corex: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
LiveView.push_event(socket, "checkbox_toggle_checked", %{"id" => checkbox_id})
end
end