NumberInput
componentA number field with a button on each side.
Preview
Code
<twig:NumberInput label="Guests" name="guests" :value="2" :min="1" :max="8" />
Code
<twig:NumberInput label="Guests" name="guests" :value="2" :min="1" :max="8" />
Try it
Change the props; the example is rendered again by the server.
<twig:NumberInput label="Guests" name="guests" :value="2" :min="1" :max="8" />
<twig:NumberInput label="Guests" name="guests" :value="2" :min="1" :max="8" />
Variants
required
Whether a value has to be given.
disabled
Whether the field cannot be changed for now.
Installation
php bin/console ux:install number-input --kit=github.com/parler-haut-interagir-librement/ux-kit
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string|null |
null |
The identifier of the field, for a label beside it. |
name |
string|null |
null |
The name of the field. |
value |
int|float|null |
null |
The value. |
min |
int|float|null |
null |
The lowest value. |
max |
int|float|null |
null |
The highest value. |
step |
int|float|string |
1 |
The step, or any. |
label |
string|null |
null |
What screen readers call the field, without a label beside it. |
decrementLabel |
string |
'Decrease' |
The name of the button that lowers the value. |
incrementLabel |
string |
'Increase' |
The name of the button that raises the value. |
required |
bool |
false |
Whether a value has to be given. |
disabled |
bool |
false |
Whether the field cannot be changed for now. |
Accessibility
The field keeps its keyboard: the arrows, Page Up and Page Down as the browser gives them. As in the spin button of WAI-ARIA, the buttons are for the pointer and left out of the Tab order; they stop at the limits and stay hidden without a script.
Files to copy
assets/controllers/number_input_controller.js
import { Controller } from '@hotwired/stimulus';
/*
* The two buttons of a number field: each steps it as its arrows would, tells the page, and stops at its limit.
*/
export default class extends Controller {
static targets = ['input', 'decrement', 'increment'];
connect() {
this.decrementTarget.hidden = false;
this.incrementTarget.hidden = false;
this.refresh();
}
decrement() {
this.#step('stepDown');
}
increment() {
this.#step('stepUp');
}
refresh() {
const input = this.inputTarget;
const value = input.valueAsNumber;
const locked = input.disabled || input.readOnly;
const reached = (limit, beyond) => limit !== '' && Number.isFinite(value) && beyond(value, Number(limit));
this.decrementTarget.disabled = locked || reached(input.min, (a, b) => a <= b);
this.incrementTarget.disabled = locked || reached(input.max, (a, b) => a >= b);
}
#step(method) {
const input = this.inputTarget;
input[method]();
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
this.refresh();
}
}
templates/components/NumberInput.html.twig
{#-
# A number field, with a button on each side once a script can drive them. The keyboard stays the field's own: the
# buttons are for the pointer and are left out of the Tab order, as in the spin button of WAI-ARIA.
#}
{%- props
## string|null The identifier of the field, for a `label` beside it.
id = null,
## string|null The name of the field.
name = null,
## int|float|null The value.
value = null,
## int|float|null The lowest value.
min = null,
## int|float|null The highest value.
max = null,
## int|float|string The step, or `any`.
step = 1,
## string|null What screen readers call the field, without a `label` beside it.
label = null,
## string The name of the button that lowers the value.
decrementLabel = 'Decrease',
## string The name of the button that raises the value.
incrementLabel = 'Increase',
## bool Whether a value has to be given.
required = false,
## bool Whether the field cannot be changed for now.
disabled = false
-%}
<div{{ attributes.defaults({class: 'isNumberInput', 'data-controller': 'number-input'}) }}>
<button type="button" class="isNumberInputStep" hidden tabindex="-1" aria-label="{{ decrementLabel }}" data-number-input-target="decrement" data-action="number-input#decrement"><span aria-hidden="true">−</span></button>
<input class="isNumberInputField" type="number" inputmode="decimal" step="{{ step }}"{% if id is not null %} id="{{ id }}"{% endif %}{% if name is not null %} name="{{ name }}"{% endif %}{% if value is not null %} value="{{ value }}"{% endif %}{% if min is not null %} min="{{ min }}"{% endif %}{% if max is not null %} max="{{ max }}"{% endif %}{% if label is not null %} aria-label="{{ label }}"{% endif %}{% if required %} required{% endif %}{% if disabled %} disabled{% endif %} data-number-input-target="input" data-action="input->number-input#refresh change->number-input#refresh">
<button type="button" class="isNumberInputStep" hidden tabindex="-1" aria-label="{{ incrementLabel }}" data-number-input-target="increment" data-action="number-input#increment"><span aria-hidden="true">+</span></button>
</div>