Corex.PinInput (Corex v0.2.0)

View Source

PIN / OTP input for Phoenix LiveView forms. Behavior follows Zag.js Pin Input.

Anatomy

Basic

<.pin_input count={4} class="pin-input">
  <:label>Code</:label>
</.pin_input>

API

Set a stable id when using imperative API helpers or on_* events. With field={@form[:code]}, the field id is used automatically.

FunctionActionReturns
set_value/2Set cell values (client)%Phoenix.LiveView.JS{}
set_value/3Set cell values (server)socket
clear/1Clear all cells (client)%Phoenix.LiveView.JS{}
clear/2Clear all cells (server)socket
value/1Read values (client)%Phoenix.LiveView.JS{}
value/2Read values (client, opts)%Phoenix.LiveView.JS{}
value/3Read values (server)socket

Events

Pick an event name and pass it to on_* on <.pin_input>.

Server events

EventWhenPayload
on_value_change="pin_changed"Any cell changes%{"id" => id, "value" => list}
on_value_complete="pin_complete"All cells filled%{"id" => id, "value" => list}

on_value_change

<.pin_input count={4} class="pin-input" on_value_change="pin_changed">
  <:label>Code</:label>
</.pin_input>
def handle_event("pin_changed", %{"id" => _id, "value" => value}, socket) do
  {:noreply, assign(socket, :pin, value)}
end

Client events

EventWhenevent.detail
on_value_change_client="pin-changed"Any cell changesid, value
on_value_complete_client="pin-complete"All cells filledid, value

Form

Use field={f[:code]} inside <.form> so the hidden input name and validation align with Phoenix forms.

For cross-cutting invalid styling and error presentation, see the Forms guide. With field={@form[:…]}, pass auto_invalid for alert borders from visible errors, or invalid={true} to force the alert state.

<.form for={@form} phx-change="validate">
  <.pin_input field={@form[:code]} count={4} class="pin-input">
    <:label>Verification code</:label>
  </.pin_input>
</.form>

Style

Use data attributes to target elements:

[data-scope="pin-input"][data-part="root"] {}
[data-scope="pin-input"][data-part="label"] {}
[data-scope="pin-input"][data-part="control"] {}
[data-scope="pin-input"][data-part="input"] {}
@import "../corex/corex.css";

Stack modifiers on the host (class on <.pin_input>). Combine axes, for example pin-input ui-accent ui-size-lg.

Axes: Semantic (ui-accent, ui-brand, ui-alert, ui-info, ui-success), Size (ui-size-smui-size-xl), Radius (ui-rounded-*). No variant axis. See the modifier guide.

Semantic modifiers set palette variables for focus and accent ink on each cell. The cell surface stays the shared ui-input treatment. Pin input has no variant axis. Complete and invalid states keep their dedicated styling.

Semantic

Palette for focus and accent ink on each cell.

ModifierClasses
Defaultpin-input
Accentpin-input ui-accent
Brandpin-input ui-brand
Alertpin-input ui-alert
Infopin-input ui-info
Successpin-input ui-success

Size

ModifierClasses
SMpin-input ui-size-sm
MDpin-input ui-size-md
LGpin-input ui-size-lg
XLpin-input ui-size-xl

Radius

ModifierClasses
Nonepin-input ui-rounded-none
SMpin-input ui-rounded-sm
MDpin-input ui-rounded-md
LGpin-input ui-rounded-lg
XLpin-input ui-rounded-xl
Fullpin-input ui-rounded-full

The value assign is the initial cell contents via data-default-value.

Summary

API

Clear all cells from phx-click. Dispatches corex:pin-input:clear.

Clear all cells from handle_event (pin_input_clear).

Replace cell values from phx-click. Dispatches corex:pin-input:set-value; value accepts a nonempty string list, a comma string, graphemes string, etc. (normalized like the form helper).

Replace cell values from handle_event (pin_input_set_value).

Same as value/2 with default respond_to:.

Read the current value from phx-click. Dispatches corex:pin-input:value. Optional respond_to: :server, :client, or :both.

Read the current value from handle_event (pin_input_value). Same replies as value/2.

Components

pin_input(assigns)

Attributes

  • id (:string) - Stable id for the component. Defaults to nil.
  • field (Phoenix.HTML.FormField) - A form field struct from the form, for example: @form[:email]. Defaults to nil.
  • name (:string) - Name attribute for form submission. Defaults to nil.
  • form (:string) - Form id to associate the control with. Defaults to nil.
  • invalid (:boolean) - Whether the control has validation errors. Defaults to nil.
  • auto_invalid (:boolean) - When true with field, set invalid from visible changeset errors (default false). Defaults to false.
  • controlled (:boolean) - Whether the control is controlled. Defaults to false.
  • disabled (:boolean) - Whether the control is disabled. Defaults to false.
  • read_only (:boolean) - Whether the control is read-only. Defaults to false.
  • required (:boolean) - Whether the control is required. Defaults to false.
  • value (:list) - Initial value (list of single-character strings). Padded to count for the hook. Defaults to [].
  • count (:integer) - Number of input boxes. Defaults to 4.
  • mask (:boolean) - Defaults to false.
  • otp (:boolean) - Defaults to false.
  • blur_on_complete (:boolean) - Defaults to false.
  • select_on_focus (:boolean) - Defaults to false.
  • dir (:string) - Defaults to nil.Must be one of nil, "ltr", or "rtl".
  • orientation (:string) - Defaults to "vertical". Must be one of "horizontal", or "vertical".
  • type (:string) - Defaults to "numeric". Must be one of "alphanumeric", "numeric", or "alphabetic".
  • placeholder (:string) - Defaults to "○".
  • on_value_change (:string) - Defaults to nil.
  • on_value_change_client (:string) - Defaults to nil.
  • on_value_complete (:string) - Defaults to nil.
  • on_value_complete_client (:string) - Defaults to nil.
  • translation (Corex.PinInput.Translation) - Override translatable strings. Defaults to nil.
  • errors (:list) - Error messages to display (non-field API). Defaults to [].
  • Global attributes are accepted.

Slots

  • label - Accepts attributes:
    • class (:string)
  • error - Accepts attributes:
    • class (:string)

API

clear(pin_input_id)

@spec clear(String.t()) :: Phoenix.LiveView.JS.t()

Clear all cells from phx-click. Dispatches corex:pin-input:clear.

<.action phx-click={Corex.PinInput.clear("my-pin")}>Clear</.action>
<.pin_input id="my-pin" count={4} class="pin-input" />

clear(socket, pin_input_id)

Clear all cells from handle_event (pin_input_clear).

def handle_event("clear_pin", _, socket) do
  {:noreply, Corex.PinInput.clear(socket, "my-pin")}
end

set_value(pin_input_id, value)

Replace cell values from phx-click. Dispatches corex:pin-input:set-value; value accepts a nonempty string list, a comma string, graphemes string, etc. (normalized like the form helper).

<.action phx-click={Corex.PinInput.set_value("my-pin", ["1", "2", "", ""])}>Prefill</.action>
<.pin_input id="my-pin" count={4} class="pin-input" />
document.getElementById("my-pin")?.dispatchEvent(
  new CustomEvent("corex:pin-input:set-value", {
    bubbles: false,
    detail: { value: ["1", "2", "", ""] },
  })
);

set_value(socket, pin_input_id, value)

Replace cell values from handle_event (pin_input_set_value).

def handle_event("fill_pin", _, socket) do
  {:noreply, Corex.PinInput.set_value(socket, "my-pin", ["0", "0", "0", "0"])}
end

value(pin_input_id)

@spec value(String.t()) :: Phoenix.LiveView.JS.t()

Same as value/2 with default respond_to:.

value(pin_input_id, opts)

@spec value(
  String.t(),
  keyword()
) :: Phoenix.LiveView.JS.t()

Read the current value from phx-click. Dispatches corex:pin-input:value. Optional respond_to: :server, :client, or :both.

ReplyPayload
Serverpin_input_value_response%{"id" => id, "value" => cells, "valueAsString" => str}
Clientpin-input-value on the hostsame fields in detail
<.action phx-click={Corex.PinInput.value("my-pin")}>Read</.action>
<.pin_input id="my-pin" count={4} class="pin-input" />
def handle_event("pin_input_value_response", %{"id" => _, "valueAsString" => s}, socket) do
  {:noreply, assign(socket, :otp, s)}
end

value(socket, pin_input_id, opts \\ [])

Read the current value from handle_event (pin_input_value). Same replies as value/2.

ReplyPayload
pin_input_value_response%{"id" => id, "value" => cells, "valueAsString" => str}
def handle_event("read_pin", _, socket) do
  {:noreply, Corex.PinInput.value(socket, "my-pin", respond_to: :server)}
end