A number field with a minus and a plus, for a quantity that is changed a step at a time (hours, seats, a rate):
the buttons are disabled at the limits, the field is a spinbutton that says its value (and its unit) and its
limits, and typing in it works as well. The arrows, PageUp and PageDown (ten steps), Home and End (the
limits) are the hook DevactionUI.NumberInput, and what is typed is held inside min and max when the field
is left.
The numbers are whole or decimal, as a number or as text (1, 0.25, "0.25", "0,25"): a fractional step
is fine, and precision is how many decimals the field shows (it defaults to the decimals of step and of the
value). The arithmetic is done on integers scaled by a power of ten, so 0.1 + 0.2 is 0.3 and a step never
drifts, in step/4 and clamp/3 on the server and in the hook in the browser. decimal is the separator the
field shows and reads ("," for pt-BR); the wire (phx-value-step, aria-valuenow, data-*) always uses a dot.
The LiveView keeps the value: it learns of a press, or of a key, in event (with phx-value-step, the signed
amount to move, as text), and of typing in the field's own phx-change. step/4, clamp/3 and parse/1 are
the arithmetic, so the server applies the same rule the buttons suggest:
def handle_event("seats_step", %{"step" => step}, socket) do
seats = NumberInput.step(socket.assigns.seats, step, 1, 30)
{:noreply, assign(socket, :seats, seats)}
end
def handle_event("validate", %{"seats" => text}, socket) do
seats =
case NumberInput.parse(text) do
{:ok, n} -> NumberInput.clamp(n, 1, 30)
:error -> socket.assigns.seats
end
{:noreply, assign(socket, :seats, seats)}
endThe label and the help text (Between 1 and 30) are described to assistive technology; the words of the
buttons are the caller's (decrease_label, increase_label). The unit is text read with the value; a unit_mark
slot only changes how it is drawn (m<sup>2</sup>).
Summary
Types
A number, or text that reads as one (0.25, or 0,25 between quotes); a Decimal works too.
Functions
value inside min and max; a limit that is nil does not apply.
value as text with exactly precision decimals, rounded half away from zero, and decimal as the separator.
The number a typed text says, with a dot or a comma as the decimal separator; :error when it is not one.
value moved by delta, and kept inside min and max. The sum is exact: the result is an integer when it
is whole and the nearest float otherwise.
Types
Functions
value inside min and max; a limit that is nil does not apply.
@spec format(amount(), non_neg_integer(), String.t()) :: String.t()
value as text with exactly precision decimals, rounded half away from zero, and decimal as the separator.
iex> DevactionUI.NumberInput.format(2.5, 2)
"2.50"
iex> DevactionUI.NumberInput.format("1.005", 2, ",")
"1,01"
Attributes
id(:string) (required)name(:string) (required)value(:any) (required) - a number or text that reads as one:3,0.25,"1,5".label(:string) (required) - what is counted, for assistive technology.unit(:string) - shown beside the value and read with it:h,seats. Defaults tonil.min(:any) - a number or text; the minus stops here. Defaults tonil.max(:any) - a number or text; the plus stops here. Defaults tonil.step(:any) - how far a press moves, whole or fractional:1,0.25. Defaults to1.precision(:integer) - decimals shown; defaults to the decimals ofstepand of the value. Defaults tonil.decimal(:string) - the decimal separator shown and read:"."or",". Defaults to".".event(:string) (required) - sent by the buttons and the keys, withphx-value-step(the signed amount, as text).decrease_label(:string) (required)increase_label(:string) (required)disabled(:boolean) - Defaults tofalse.class(:any) - Defaults tonil.- Global attributes are accepted.
Slots
minus_icon(required)plus_icon(required)unit_mark- how the unit is drawn (m<sup>2</sup>);unitstays the text read out.help- a line under the field, e.g.Between 1 and 30.
The number a typed text says, with a dot or a comma as the decimal separator; :error when it is not one.
iex> DevactionUI.NumberInput.parse("1,5")
{:ok, 1.5}
iex> DevactionUI.NumberInput.parse("abc")
:error
value moved by delta, and kept inside min and max. The sum is exact: the result is an integer when it
is whole and the nearest float otherwise.
iex> DevactionUI.NumberInput.step(0.1, 0.2, nil, nil)
0.3
iex> DevactionUI.NumberInput.step("2.5", "-1", 1, 5)
1.5