Corex.NumberInput (Corex v0.2.0)

View Source

Number input for Phoenix LiveView forms. Behavior follows Zag.js Number Input.

Anatomy

Minimal

<.number_input class="number-input">
  <:label>Quantity</:label>
  <:decrement_trigger><.heroicon name="hero-chevron-down" class="icon" /></:decrement_trigger>
  <:increment_trigger><.heroicon name="hero-chevron-up" class="icon" /></:increment_trigger>
</.number_input>

Min, max, step

<.number_input
  class="number-input"
  min={0.0}
  max={100.0}
  step={5.0}
  value="10"
>
  <:label>Amount</:label>
  <:decrement_trigger><.heroicon name="hero-chevron-down" class="icon" /></:decrement_trigger>
  <:increment_trigger><.heroicon name="hero-chevron-up" class="icon" /></:increment_trigger>
</.number_input>

min, max, and step are floats. Use step for granularity: step={1} for whole-number steps (display omits .0), step={0.01} for two decimal places. The visible value is formatted on the server to match Zag after hydration.

Slots :decrement_trigger and :increment_trigger are optional (provide both or neither). Prefer a :label slot; without one the input uses a default aria-label from translation.input.

API

Requires a stable id on <.number_input>. For forms, use field; use the API for imperative updates and reading machine state.

FunctionActionReturns
set_value/2Set value (client)%Phoenix.LiveView.JS{}
set_value/3Set value (server)socket
clear_value/1Clear value (client)%Phoenix.LiveView.JS{}
clear_value/2Clear value (server)socket
increment/1Increment by step (client)%Phoenix.LiveView.JS{}
increment/2Increment by step (server)socket
decrement/1Decrement by step (client)%Phoenix.LiveView.JS{}
decrement/2Decrement by step (server)socket
set_to_min/1Set to min (client)%Phoenix.LiveView.JS{}
set_to_min/2Set to min (server)socket
set_to_max/1Set to max (client)%Phoenix.LiveView.JS{}
set_to_max/2Set to max (server)socket
focus/1Focus input (client)%Phoenix.LiveView.JS{}
focus/2Focus input (server)socket
state/1Read machine state (client)%Phoenix.LiveView.JS{}
state/2Read machine state (client, opts)%Phoenix.LiveView.JS{}
state/3Read machine state (server)socket

Machine state (state/2, state/3)

Replies with number_input_state_response (server) or number-input-state on the host (client). Payload includes Zag machine fields:

FieldTypeMeaning
focusedbooleanInput is focused
invalidbooleanInput is invalid
emptybooleanValue is empty
valuestringFormatted value
valueAsNumbernumberNumeric value

Events

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

Server events

EventWhenPayload
on_value_change="number_input_changed"Value changes%{"id" => id, "value" => string, "valueAsNumber" => number}

on_value_change

<.number_input
  class="number-input"
  on_value_change="number_input_changed"
>
  <:label>Quantity</:label>
  <:decrement_trigger><.heroicon name="hero-chevron-down" class="icon" /></:decrement_trigger>
  <:increment_trigger><.heroicon name="hero-chevron-up" class="icon" /></:increment_trigger>
</.number_input>
def handle_event("number_input_changed", %{"id" => _id, "value" => value}, socket) do
  {:noreply, assign(socket, :quantity, value)}
end

Client events

EventWhenevent.detail
on_value_change_client="number-input-changed"Value changesid, value, valueAsNumber

Patterns

Pass value for the initial number on mount. The machine owns updates after that unless you use set_value/2 or form field.

Form

Use field={f[:value]} inside <.form>. With a form field, increment and decrement stay local; the hidden input updates for submit.

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">
  <.number_input field={@form[:value]} class="number-input">
    <:label>Quantity</:label>
    <:decrement_trigger><.heroicon name="hero-chevron-down" class="icon" /></:decrement_trigger>
    <:increment_trigger><.heroicon name="hero-chevron-up" class="icon" /></:increment_trigger>
    <:error :let={msg}>
      <.heroicon name="hero-exclamation-circle" class="icon" />
      {msg}
    </:error>
  </.number_input>
</.form>

Style

Target parts with data-scope and data-part, or use Corex Design: import tokens and number-input.css, then set class="number-input" on <.number_input>.

[data-scope="number-input"][data-part="root"] {}
[data-scope="number-input"][data-part="control"] {}
[data-scope="number-input"][data-part="input"] {}
[data-scope="number-input"][data-part="trigger-group"] {}
[data-scope="number-input"][data-part="decrement-trigger"] {}
[data-scope="number-input"][data-part="increment-trigger"] {}
@import "../corex/corex.css";

Stack modifiers on the host (class on <.number_input>). Combine axes, for example number-input ui-accent ui-size-lg or number-input ui-info.

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 the field and stepper triggers. The input surface stays the shared ui-input treatment. Number input has no variant axis.

Semantic

Palette for focus and accent ink on the field and stepper triggers.

ModifierClasses
Defaultnumber-input
Accentnumber-input ui-accent
Brandnumber-input ui-brand
Alertnumber-input ui-alert
Infonumber-input ui-info
Successnumber-input ui-success

Size

ModifierClasses
SMnumber-input ui-size-sm
MDnumber-input ui-size-md
LGnumber-input ui-size-lg
XLnumber-input ui-size-xl

Summary

API

Clear the value from phx-click. Dispatches corex:number-input:clear-value.

Clear the value from handle_event (number_input_clear_value).

Decrement from phx-click. Dispatches corex:number-input:decrement.

Decrement from handle_event (number_input_decrement).

Focus the value field from phx-click. Dispatches corex:number-input:focus.

Focus the value field from handle_event (number_input_focus).

Increment from phx-click. Dispatches corex:number-input:increment.

Increment from handle_event (number_input_increment).

Snap to maximum from phx-click. Dispatches corex:number-input:set-to-max.

Snap to maximum from handle_event (number_input_set_to_max).

Snap to minimum from phx-click. Dispatches corex:number-input:set-to-min.

Snap to minimum from handle_event (number_input_set_to_min).

Replace the formatted value from phx-click. Dispatches corex:number-input:set-value with a numeric value.

Replace the formatted value from handle_event (number_input_set_value).

Same as state/2 with default respond_to:.

Read machine state from phx-click. Dispatches corex:number-input:state. Optional respond_to: :server, :client, or :both.

Read machine state from handle_event (number_input_state). Same replies as state/2; server-only unless you also use state/2 for a DOM reply.

Components

number_input(assigns)

Attributes

  • id (:string) - Stable id for the component. Defaults to nil.
  • field (Phoenix.HTML.FormField) - A form field struct, e.g. f[:age] or @form[:age]. 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.
  • 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 (:string) - Defaults to nil.
  • min (:float) - Defaults to nil.
  • max (:float) - Defaults to nil.
  • step (:float) - Defaults to 1.0.
  • allow_mouse_wheel (:boolean) - Defaults to false.
  • on_value_change (:string) - Defaults to nil.
  • on_value_change_client (:string) - Defaults to nil.
  • 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".
  • translation (Corex.NumberInput.Translation) - Override translatable strings. Defaults to nil.
  • errors (:list) - List of error messages to display. Defaults to [].
  • Global attributes are accepted.

Slots

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

API

clear_value(number_input_id)

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

Clear the value from phx-click. Dispatches corex:number-input:clear-value.

<.action phx-click={Corex.NumberInput.clear_value("my-num")}>Clear</.action>
<.number_input id="my-num" class="number-input">
  <:increment_trigger><span>+</span></:increment_trigger>
  <:decrement_trigger><span>-</span></:decrement_trigger>
</.number_input>

clear_value(socket, number_input_id)

Clear the value from handle_event (number_input_clear_value).

def handle_event("clear", _, socket) do
  {:noreply, Corex.NumberInput.clear_value(socket, "my-num")}
end

decrement(number_input_id)

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

Decrement from phx-click. Dispatches corex:number-input:decrement.

<.action phx-click={Corex.NumberInput.decrement("my-num")}>Decrement</.action>
<.number_input id="my-num" class="number-input">
  <:increment_trigger><span>+</span></:increment_trigger>
  <:decrement_trigger><span>-</span></:decrement_trigger>
</.number_input>

decrement(socket, number_input_id)

Decrement from handle_event (number_input_decrement).

def handle_event("dec", _, socket) do
  {:noreply, Corex.NumberInput.decrement(socket, "my-num")}
end

focus(number_input_id)

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

Focus the value field from phx-click. Dispatches corex:number-input:focus.

<.action phx-click={Corex.NumberInput.focus("my-num")}>Focus</.action>
<.number_input id="my-num" class="number-input">
  <:increment_trigger><span>+</span></:increment_trigger>
  <:decrement_trigger><span>-</span></:decrement_trigger>
</.number_input>

focus(socket, number_input_id)

Focus the value field from handle_event (number_input_focus).

def handle_event("focus", _, socket) do
  {:noreply, Corex.NumberInput.focus(socket, "my-num")}
end

increment(number_input_id)

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

Increment from phx-click. Dispatches corex:number-input:increment.

<.action phx-click={Corex.NumberInput.increment("my-num")}>Increment</.action>
<.number_input id="my-num" class="number-input">
  <:increment_trigger><span>+</span></:increment_trigger>
  <:decrement_trigger><span>-</span></:decrement_trigger>
</.number_input>

increment(socket, number_input_id)

Increment from handle_event (number_input_increment).

def handle_event("inc", _, socket) do
  {:noreply, Corex.NumberInput.increment(socket, "my-num")}
end

set_to_max(number_input_id)

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

Snap to maximum from phx-click. Dispatches corex:number-input:set-to-max.

<.action phx-click={Corex.NumberInput.set_to_max("my-num")}>Max</.action>
<.number_input id="my-num" max={100} class="number-input">
  <:increment_trigger><span>+</span></:increment_trigger>
  <:decrement_trigger><span>-</span></:decrement_trigger>
</.number_input>

set_to_max(socket, number_input_id)

Snap to maximum from handle_event (number_input_set_to_max).

def handle_event("max", _, socket) do
  {:noreply, Corex.NumberInput.set_to_max(socket, "my-num")}
end

set_to_min(number_input_id)

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

Snap to minimum from phx-click. Dispatches corex:number-input:set-to-min.

<.action phx-click={Corex.NumberInput.set_to_min("my-num")}>Min</.action>
<.number_input id="my-num" min={0} class="number-input">
  <:increment_trigger><span>+</span></:increment_trigger>
  <:decrement_trigger><span>-</span></:decrement_trigger>
</.number_input>

set_to_min(socket, number_input_id)

Snap to minimum from handle_event (number_input_set_to_min).

def handle_event("min", _, socket) do
  {:noreply, Corex.NumberInput.set_to_min(socket, "my-num")}
end

set_value(number_input_id, value)

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

Replace the formatted value from phx-click. Dispatches corex:number-input:set-value with a numeric value.

<.action phx-click={Corex.NumberInput.set_value("my-num", 42)}>42</.action>
<.number_input id="my-num" value={40} step={1} class="number-input">
  <:increment_trigger><span>+</span></:increment_trigger>
  <:decrement_trigger><span>-</span></:decrement_trigger>
</.number_input>
document.getElementById("my-num")?.dispatchEvent(
  new CustomEvent("corex:number-input:set-value", {
    bubbles: false,
    detail: { value: 42 },
  })
);

set_value(socket, number_input_id, value)

Replace the formatted value from handle_event (number_input_set_value).

def handle_event("set_answer", _, socket) do
  {:noreply, Corex.NumberInput.set_value(socket, "my-num", 7)}
end

state(number_input_id)

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

Same as state/2 with default respond_to:.

state(number_input_id, opts)

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

Read machine state from phx-click. Dispatches corex:number-input:state. Optional respond_to: :server, :client, or :both.

ReplyPayload
Servernumber_input_state_response%{"id" => id, "focused" => bool, "invalid" => bool, "empty" => bool, "value" => str, "valueAsNumber" => num}
Clientnumber-input-state on the input rootsame fields in detail
<.action phx-click={Corex.NumberInput.state("my-num")}>Snapshot</.action>
<.number_input id="my-num" class="number-input">
  <:increment_trigger><span>+</span></:increment_trigger>
  <:decrement_trigger><span>-</span></:decrement_trigger>
</.number_input>
def handle_event("number_input_state_response", %{"id" => _, "valueAsNumber" => n}, socket) do
  {:noreply, assign(socket, :n, n)}
end

state(socket, number_input_id, opts \\ [])

Read machine state from handle_event (number_input_state). Same replies as state/2; server-only unless you also use state/2 for a DOM reply.

ReplyPayload
number_input_state_response%{"id" => id} plus focused/invalid/value fields
def handle_event("snapshot", _, socket) do
  {:noreply, Corex.NumberInput.state(socket, "my-num", respond_to: :server)}
end