Drafter.Widget.Checkbox (drafter v0.3.1)

Copy Markdown View Source

A boolean toggle widget that renders an X mark inside a box next to an optional label.

The checked state is toggled by pressing Space, Enter, or clicking the widget. The :on_change callback receives the new boolean value after each toggle.

Component tag

Tag :checkbox, built by Drafter.App as {:checkbox, label, opts}:

checkbox(label, opts)

The positional argument becomes :label. The checked state goes through the binding layer: passing bind: :some_key reads it from that app-state key and writes the new value back on every toggle, and :on_change is built from the same binding.

Options

  • :label - String.t/0 displayed to the right of the box. Default "". Supplied positionally through the checkbox/2 element
  • :checked - boolean/0 initial checked state. Default false
  • :bind - app-state key atom for two-way binding of the checked state. Default nil
  • :on_change - (boolean() -> term()) called with the new value after each toggle. Default nil. An exception raised inside it is caught and ignored
  • :style - map/0 of style overrides. Default %{}. An :app_module key inside this map selects the theme used for the box and label colours
  • :focused - boolean/0 initial focus flag, read by mount/1. Default false
  • :class - accepted by the element and normalised into a :classes prop, but mount/1 and update/2 both ignore it, so it has no effect

update/2 accepts :label, :checked, :focused, :style and :on_change and silently drops every other key. Through the component tree only :on_change and, when :bind is set, :checked are re-applied on a re-render — :label and :style are effectively mount-only there.

Widget value

Drafter.get_widget_value/1 returns the checked boolean/0.

Key bindings

Enter and Space with no modifiers toggle the checkbox, as does a mouse release. The same keys with modifiers are ignored.

Usage

checkbox("Remember me", checked: false, on_change: fn checked -> IO.inspect(checked) end)

Summary

Functions

The registry tag for this widget.

Turns the {:checkbox, label, opts} element into a props map for mount/1.

Handles events directly instead of going through Drafter.Widget.EventRouter.

Builds the checkbox state from props.

Always 1: the checkbox occupies a single row.

Draws the indicator and the label into rect.

Callback implementation for Drafter.Widget.unmount/1.

Folds fresh props into state.

Types

t()

@type t() :: %Drafter.Widget.Checkbox{
  checked: boolean(),
  focused: boolean(),
  hovered: boolean(),
  label: String.t(),
  on_change: (boolean() -> term()) | nil,
  style: map()
}

Functions

component_tag()

@spec component_tag() :: :checkbox

The registry tag for this widget.

iex> Drafter.Widget.Checkbox.component_tag()
:checkbox

focused(state)

from_component_opts(label, opts)

@spec from_component_opts(
  term(),
  keyword()
) :: Drafter.Widget.props()

Turns the {:checkbox, label, opts} element into a props map for mount/1.

label is the positional argument. The checked state comes from :bind read against opts[:__app_state__], falling back to opts[:checked] and then false, and :on_change is the binding's writer. The emitted :classes key is not read by mount/1.

iex> props = Drafter.Widget.Checkbox.from_component_opts("Agree", checked: true)
iex> {props.label, props.checked, props.style, props.classes}
{"Agree", true, %{}, []}

handle_event(event, state)

@spec handle_event(Drafter.Event.t() | atom(), t()) :: {:ok, t()} | {:noreply, t()}

Handles events directly instead of going through Drafter.Widget.EventRouter.

:activate, {:key, :enter}, {:key, :" "} and {:mouse, %{type: :mouse_up}} flip :checked and call :on_change with the new value. :hover and :unhover set and clear :hovered; {:focus} sets both :focused and :hovered, and {:blur} clears both. Everything else, including a key event carrying modifiers, returns {:noreply, state}.

iex> cb = Drafter.Widget.Checkbox.mount(%{label: "Agree"})
iex> {:ok, toggled} = Drafter.Widget.Checkbox.handle_event({:key, :" "}, cb)
iex> toggled.checked
true

iex> cb = Drafter.Widget.Checkbox.mount(%{label: "Agree"})
iex> {:ok, focused} = Drafter.Widget.Checkbox.handle_event({:focus}, cb)
iex> {focused.focused, focused.hovered}
{true, true}

iex> cb = Drafter.Widget.Checkbox.mount(%{label: "Agree"})
iex> Drafter.Widget.Checkbox.handle_event({:key, :enter, [:shift]}, cb) |> elem(0)
:noreply

mount(props)

@spec mount(Drafter.Widget.props()) :: t()

Builds the checkbox state from props.

:hovered always starts at false.

iex> cb = Drafter.Widget.Checkbox.mount(%{label: "Remember me", checked: true})
iex> {cb.label, cb.checked, cb.focused, cb.hovered}
{"Remember me", true, false, false}

iex> cb = Drafter.Widget.Checkbox.mount(%{})
iex> {cb.label, cb.checked, cb.style, cb.on_change}
{"", false, %{}, nil}

preferred_height(args, opts)

@spec preferred_height(
  term(),
  keyword()
) :: pos_integer()

Always 1: the checkbox occupies a single row.

render(state, rect)

Draws the indicator and the label into rect.

Accepts either a t/0 or a raw props map, which is mounted first. The indicator takes the first three columns and the label the rest; a rect narrower than four columns leaves no room for the label. Returns one strip per row of rect.height, with only the first carrying content.

unmount(state)

Callback implementation for Drafter.Widget.unmount/1.

update(props, state)

@spec update(Drafter.Widget.props(), t()) :: t()

Folds fresh props into state.

Only :label, :checked, :focused, :style and :on_change are applied; any other key in props is dropped without error.

iex> cb = Drafter.Widget.Checkbox.mount(%{label: "Agree"})
iex> updated = Drafter.Widget.Checkbox.update(%{checked: true, nonsense: 1}, cb)
iex> {updated.checked, updated.label}
{true, "Agree"}

update_props_from_mount(mount_props, existing_state, opts)

@spec update_props_from_mount(Drafter.Widget.props(), t(), keyword()) ::
  Drafter.Widget.props()

Narrows the props a re-render feeds to update/2.

Always passes :on_change and :classes, and adds :checked only when opts carries a :bind. :label and :style are deliberately left out, so a re-render does not overwrite them.