Packages
corex
0.1.0-beta.5
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/switch.ex
defmodule Corex.Switch do
@moduledoc ~S'''
Phoenix implementation of [Zag.js Switch](https://zagjs.com/components/react/switch).
## Examples
<!-- tabs-open -->
### Basic Usage
```heex
<.switch id="my-switch">
<:label>Enable notifications</:label>
</.switch>
```
### Label before control
```heex
<.switch id="my-switch">
<:label position={:pre}>Enable notifications</:label>
</.switch>
```
### Controlled Mode
```heex
<.switch
id="my-switch"
controlled
checked={@switch_checked}
on_checked_change="switch_changed">
<:label>Enable notifications</:label>
</.switch>
```
```elixir
def handle_event("switch_changed", %{"checked" => checked}, socket) do
{:noreply, assign(socket, :switch_checked, checked)}
end
```
<!-- tabs-close -->
## Phoenix Form Integration
When using with Phoenix forms, set the form `id` in `to_form/2` (for example `to_form(changeset, as: :name, id: "my-form")`) and use `id={@form.id}` on `<.form>`.
### 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={@form.id} action={@action} method="post">
<.switch field={f[:notifications]} class="switch">
<:label>Enable notifications</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.switch>
<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
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={@form.id} phx-change="validate">
<.switch field={@form[:notifications_enabled]} class="switch" controlled>
<:label>Enable notifications</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.switch>
</.form>
"""
end
end
```
## Programmatic Control
```heex
# Client-side
<button phx-click={Corex.Switch.set_checked("my-switch", true)}>
Turn On
</button>
<button phx-click={Corex.Switch.toggle_checked("my-switch")}>
Toggle
</button>
# 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">
```
'''
@doc type: :component
use Phoenix.Component
alias Corex.Helpers
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
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)
|> assign(:checked, Helpers.normalize_checkbox_checked(assigns.checked))
~H"""
<div
id={@id}
phx-hook="Switch"
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})}
/>
<span
:for={label <- @label}
:if={Map.get(label, :position, :post) == :pre}
class={Map.get(label, :class, nil)}
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 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 phx-mounted={Connect.ignore_thumb(%Thumb{id: @id, dir: @dir, checked: @checked, orientation: @orientation})} {Connect.thumb(%Thumb{id: @id, dir: @dir, checked: @checked, orientation: @orientation})}></span>
</span>
<span
:for={label <- @label}
:if={Map.get(label, :position, :post) == :post}
class={Map.get(label, :class, nil)}
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 == [] and @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="switch" data-part="error">
{render_slot(@error, msg)}
</div>
</div>
"""
end
@doc type: :api
@doc """
Sets the switch checked state from client-side. Returns a `Phoenix.LiveView.JS` command.
## Examples
<button phx-click={Corex.Switch.set_checked("my-switch", true)}>
Turn On
</button>
<button phx-click={Corex.Switch.set_checked("my-switch", false)}>
Turn Off
</button>
"""
def set_checked(switch_id, checked) when is_binary(switch_id) and is_boolean(checked) do
JS.dispatch("corex:switch:set-checked",
to: "##{switch_id}",
detail: %{checked: checked},
bubbles: false
)
end
@doc type: :api
@doc """
Sets the switch checked state from server-side. Pushes a LiveView event.
## Examples
def handle_event("turn_on", _params, socket) do
socket = Corex.Switch.set_checked(socket, "my-switch", true)
{:noreply, socket}
end
"""
def set_checked(socket, switch_id, checked)
when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(switch_id) and
is_boolean(checked) do
LiveView.push_event(socket, "switch_set_checked", %{
id: switch_id,
checked: checked
})
end
@doc type: :api
@doc """
Toggles the switch checked state from client-side. Returns a `Phoenix.LiveView.JS` command.
## Examples
<button phx-click={Corex.Switch.toggle_checked("my-switch")}>
Toggle
</button>
"""
def toggle_checked(switch_id) when is_binary(switch_id) do
JS.dispatch("corex:switch:toggle-checked",
to: "##{switch_id}",
bubbles: false
)
end
@doc type: :api
@doc """
Toggles the switch checked state from server-side. Pushes a LiveView event.
## Examples
def handle_event("toggle", _params, socket) do
socket = Corex.Switch.toggle_checked(socket, "my-switch")
{:noreply, socket}
end
"""
def toggle_checked(socket, switch_id)
when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(switch_id) do
LiveView.push_event(socket, "switch_toggle_checked", %{
id: switch_id
})
end
end