DevactionUI.NumberInput (devaction_ui v0.2.3)

Copy Markdown View Source

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)}
end

The 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

amount()

@type amount() :: number() | String.t()

A number, or text that reads as one (0.25, or 0,25 between quotes); a Decimal works too.

Functions

clamp(value, min, max)

@spec clamp(amount(), amount() | nil, amount() | nil) :: number()

value inside min and max; a limit that is nil does not apply.

format(value, precision, decimal \\ ".")

@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"

number_input(assigns)

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 to nil.
  • min (:any) - a number or text; the minus stops here. Defaults to nil.
  • max (:any) - a number or text; the plus stops here. Defaults to nil.
  • step (:any) - how far a press moves, whole or fractional: 1, 0.25. Defaults to 1.
  • precision (:integer) - decimals shown; defaults to the decimals of step and of the value. Defaults to nil.
  • decimal (:string) - the decimal separator shown and read: "." or ",". Defaults to ".".
  • event (:string) (required) - sent by the buttons and the keys, with phx-value-step (the signed amount, as text).
  • decrease_label (:string) (required)
  • increase_label (:string) (required)
  • disabled (:boolean) - Defaults to false.
  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • minus_icon (required)
  • plus_icon (required)
  • unit_mark - how the unit is drawn (m<sup>2</sup>); unit stays the text read out.
  • help - a line under the field, e.g. Between 1 and 30.

parse(text)

@spec parse(String.t() | nil) :: {:ok, number()} | :error

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

step(value, delta, min, max)

@spec step(amount(), amount(), amount() | nil, amount() | nil) :: number()

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