A form control for numbers a person types — quantities, prices, weights, lengths — where a comma and a dot must both work and nothing may be rounded.
Why not <.input type="number">: the browser owns a number control. Its
decimal separator follows the page/element locale, so on a page with
lang="et" a typed 2.5 can be swallowed (the field submits ""), and
on lang="en" a typed 2,5 is; step makes any other precision
invalid and silently blocks a phx-submit; the spinner arrows steal
wheel and arrow-key events. This component renders a plain
<input type="text" inputmode="decimal">: the mobile keyboard still
opens on digits, and the server decides what the text means with
PhoenixKit.Utils.Number.parse_decimal/2 — comma or dot, grouping
spaces, sign, and nothing else.
Otherwise it is <.input>: a Phoenix.HTML.FormField or raw
name/value, label, gettext-translated errors, class on the
control and wrapper_class on the phx-feedback-for wrapper, the
required marker, and daisyUI 5 styling.
A zero clears itself on focus: a field showing 0 (or 0,00, 0.0)
empties when it gains focus, so typing 1 gives 1 and not 10 (the
caret used to land after the zero). Leaving the field still empty puts
the zero back — nothing was entered, nothing changes — and so does
pressing Enter in it, so an implicit submit sends the zero, not "".
Typing anything keeps what was typed; typing and then erasing it all
puts the zero back and fires an input event, so a phx-change form
hears the zero instead of keeping the "" it last saw. A readonly
field is left alone. All of it is inline handlers on the control
(onfocus/onblur/onkeydown), so it needs no hook and works in any
host app. The remembered zero lives in a property on the element, not a
data- attribute: LiveView strips attributes the server did not render
from a focused input on every patch.
A host's own onfocus/onblur/onkeydown are kept: they run right
after the component's, in the same attribute (a second attribute of the
same name would be dropped by the browser). phx-focus fires on
focusin, after the clear, so it sees the emptied field; phx-blur
fires on focusout, after the restore, so it sees the zero again.
bare renders the control alone, for a host that sets it inside a group
of its own — a daisyUI join with a unit button, a table cell: the
wrapper <div> would sit between .join and its .join-item. It keeps
everything that makes it a decimal control (text, decimal keyboard, no
browser autofill, the zero handlers) and drops what the host draws
itself: label, unit suffix, error list and the full width. A label
given to a bare control becomes its aria-label (unless the host passes
one), so the control keeps an accessible name; with neither, pass
aria-label.
Summary
Functions
Renders a free-decimal text control.
Functions
Renders a free-decimal text control.
The value is shown normalized when it is a number (Decimal.new("2.500")
→ 2.5, 1E+3 → 1000) and echoed back unchanged when it is a binary,
so the text a person typed survives a re-render exactly as typed — a
2,5 does not turn into 2.5 under their cursor.
Examples
<%!-- FormField binding: name, id, value, errors derived --%>
<.decimal_input field={@form[:quantity]} label="Quantity" />
<%!-- Unit suffix inside the field --%>
<.decimal_input field={@form[:weight]} label="Weight" unit="kg" />
<%!-- The control alone, as a join item next to the host's own unit --%>
<div class="join">
<.decimal_input bare name="qty" value={@qty} class="join-item w-20 text-center" />
<span class="btn join-item pointer-events-none">kg</span>
</div>
<%!-- Raw name/value for a row in a list, small and right-aligned --%>
<.decimal_input
id={"row-#{row.id}-qty"}
name="qty"
value={row.qty}
class="input-sm text-right"
phx-debounce="400"
phx-blur="commit_qty"
phx-value-row_id={row.id}
/>Parse the submitted text with PhoenixKit.Utils.Number.parse_decimal/2:
case Number.parse_decimal(params["qty"], min: 0) do
{:ok, qty} -> ...
{:error, :empty} -> ...
{:error, _reason} -> ...
endAttributes
id(:any) - Defaults tonil.name(:any)label(:string) - Defaults tonil.value(:any) - Defaults tonil.unit(:string) - a unit suffix rendered inside the field, after the number (kg, m, pcs). Defaults tonil.class(:any) - extra classes merged onto the control — daisyUI modifiers likeinput-sm,input-primary, ortext-right. Defaults tonil.wrapper_class(:any) - extra classes for the outer<div phx-feedback-for>wrapper. Defaults tonil.field(Phoenix.HTML.FormField) - a form field struct retrieved from the form, for example: @form[:quantity].errors(:list) - Defaults to[].bare(:boolean) - render the<input>alone — no wrapper, label, unit or error list, now-full— for a host that places it in a group of its own (a daisyUIjoin, a table cell);errorsstill mark itinput-error, and alabelbecomes the control'saria-labelunless one is passed. Defaults tofalse.- Global attributes are accepted. Supports all globals plus:
["autofocus", "disabled", "form", "maxlength", "placeholder", "readonly", "required", "size"].