defmodule Corex.Toggle do
@moduledoc ~S'''
Phoenix implementation of [Zag.js Toggle](https://zagjs.com/components/react/toggle).
## Anatomy
### Minimal
```heex
<.toggle class="toggle">
lorem
```
### With indicator
```heex
<.toggle class="toggle">
<:indicator><.heroicon name="hero-bold" />
Bold
<.toggle class="toggle">
<:indicator><.heroicon name="hero-bold" />
Bold
```
### Dual label
Set `data-toggle-dual-label` on the host to swap visible label text when pressed. Use a second `` for the on-state label.
```heex
<.toggle class="toggle" data-toggle-dual-label>
loremdonec
```
## API
Requires a stable `id` on `<.toggle>`.
| Function | Action | Returns |
| -------- | ------ | ------- |
| [`set_pressed/2`](#set_pressed/2) | Set pressed state (client) | `%Phoenix.LiveView.JS{}` |
| [`set_pressed/3`](#set_pressed/3) | Set pressed state (server) | `socket` |
| [`toggle_pressed/2`](#toggle_pressed/2) | Toggle pressed (client) | `%Phoenix.LiveView.JS{}` |
| [`toggle_pressed/3`](#toggle_pressed/3) | Toggle pressed (server) | `socket` |
## Events
Pick an event name and pass it to `on_*` on `<.toggle>`.
### Server events
| Event | When | Payload |
| ----- | ---- | ------- |
| `on_pressed_change="toggle_pressed_changed"` | Pressed state changes | `%{"id" => id, "pressed" => boolean}` |
### on_pressed_change
```heex
<.toggle
class="toggle"
controlled
pressed={false}
on_pressed_change="toggle_pressed_changed"
>
lorem
```
```elixir
def handle_event("toggle_pressed_changed", %{"pressed" => pressed}, socket) do
p = pressed == true or pressed == "true"
{:noreply, assign(socket, :pressed, p)}
end
```
### Client events
| Event | When | `event.detail` |
| ----- | ---- | -------------- |
| `on_pressed_change_client="toggle-client-changed"` | Pressed state changes | `id`, `pressed` |
### on_pressed_change_client
```heex
<.toggle
id="toggle-on-pressed-change-client"
class="toggle"
on_pressed_change_client="toggle-client-changed"
>
lorem
```
```javascript
const el = document.getElementById("toggle-on-pressed-change-client");
el?.addEventListener("toggle-client-changed", (e) => console.log(e.detail));
```
## Patterns
### Controlled
For server-owned pressed state, set `controlled`, bind `pressed`, and handle `on_pressed_change` in LiveView.
```heex
<.toggle
class="toggle"
controlled
pressed={@pressed}
on_pressed_change="toggle_patterns_pressed"
>
lorem
```
```elixir
def mount(_params, _session, socket) do
{:ok, assign(socket, :pressed, false)}
end
def handle_event("toggle_patterns_pressed", %{"pressed" => pressed}, socket) do
{:noreply, assign(socket, :pressed, pressed == true or pressed == "true")}
end
```
## Style
Target parts with `data-scope` and `data-part`, or use Corex Design: import tokens and `toggle.css`, then set `class="toggle"` on `<.toggle>`.
```css
[data-scope="toggle"][data-part="root"] {}
[data-scope="toggle"][data-part="indicator"] {}
```
```css
@import "../corex/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/toggle.css";
```
Stack modifiers on the host (`class` on `<.toggle>`).
### Color
| Modifier | Classes |
| -------- | ------- |
| Default | `toggle` |
| Accent | `toggle toggle--accent` |
| Brand | `toggle toggle--brand` |
| Alert | `toggle toggle--alert` |
| Info | `toggle toggle--info` |
| Success | `toggle toggle--success` |
### Size
| Modifier | Classes |
| -------- | ------- |
| SM | `toggle toggle--sm` |
| MD | `toggle toggle--md` |
| LG | `toggle toggle--lg` |
| XL | `toggle toggle--xl` |
### Rounded
| Modifier | Classes |
| -------- | ------- |
| None | `toggle toggle--rounded-none` |
| SM | `toggle toggle--rounded-sm` |
| MD | `toggle toggle--rounded-md` |
| LG | `toggle toggle--rounded-lg` |
| XL | `toggle toggle--rounded-xl` |
| Full | `toggle toggle--rounded-full` |
'''
@doc type: :component
use Phoenix.Component
import Corex.Api.Doc
alias Corex.Toggle.Anatomy.{Indicator, Props, Root}
alias Corex.Toggle.Connect
alias Phoenix.LiveView
alias Phoenix.LiveView.JS
attr(:id, :string, required: false)
attr(:pressed, :boolean,
default: false,
doc: "Initial or controlled pressed state"
)
attr(:controlled, :boolean, default: false)
attr(:disabled, :boolean, default: false)
attr(:dir, :string,
default: nil,
values: [nil, "ltr", "rtl"],
doc: "When nil, derived from document"
)
attr(:on_pressed_change, :string, default: nil)
attr(:on_pressed_change_client, :string, default: nil)
attr(:rest, :global)
slot(:inner_block, required: false)
slot :indicator, required: false do
attr(:class, :string, required: false)
end
def toggle(assigns) do
assigns =
assigns
|> assign_new(:id, fn -> "toggle-#{System.unique_integer([:positive])}" end)
|> assign_new(:dir, fn -> "ltr" end)
~H"""
"""
end
api_doc(~S"""
Set pressed state from a control (`phx-click`).
```heex
<.action phx-click={Corex.Toggle.set_pressed("my-toggle", true)}>On
<.toggle id="my-toggle" class="toggle">Label
```
```javascript
document.getElementById("my-toggle")?.dispatchEvent(
new CustomEvent("corex:toggle:set-pressed", {
bubbles: false,
detail: { pressed: true },
})
);
```
""")
def set_pressed(toggle_id, pressed) when is_binary(toggle_id) and is_boolean(pressed) do
JS.dispatch("corex:toggle:set-pressed",
to: "##{toggle_id}",
detail: %{pressed: pressed},
bubbles: false
)
end
api_doc(~S"""
Set pressed state from `handle_event`.
```heex
<.action phx-click="press_toggle">On
<.toggle id="my-toggle" class="toggle">Label
```
```elixir
def handle_event("press_toggle", _, socket) do
{:noreply, Corex.Toggle.set_pressed(socket, "my-toggle", true)}
end
```
""")
def set_pressed(socket, toggle_id, pressed)
when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(toggle_id) and
is_boolean(pressed) do
LiveView.push_event(socket, "toggle_set_pressed", %{
id: toggle_id,
pressed: pressed
})
end
api_doc(~S"""
Flip pressed state from a control (`phx-click`).
```heex
<.action phx-click={Corex.Toggle.toggle_pressed("my-toggle")}>Toggle
<.toggle id="my-toggle" class="toggle">Label
```
```javascript
document.getElementById("my-toggle")?.dispatchEvent(
new CustomEvent("corex:toggle:toggle-pressed", { bubbles: false })
);
```
""")
def toggle_pressed(toggle_id) when is_binary(toggle_id) do
JS.dispatch("corex:toggle:toggle-pressed",
to: "##{toggle_id}",
bubbles: false
)
end
api_doc(~S"""
Flip pressed state from `handle_event`.
```heex
<.action phx-click="flip_toggle">Toggle
<.toggle id="my-toggle" class="toggle">Label
```
```elixir
def handle_event("flip_toggle", _, socket) do
{:noreply, Corex.Toggle.toggle_pressed(socket, "my-toggle")}
end
```
""")
def toggle_pressed(socket, toggle_id)
when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(toggle_id) do
LiveView.push_event(socket, "toggle_toggle_pressed", %{id: toggle_id})
end
end