Forms

MinMaxInput

Two number boxes, a minimum and a maximum, kept in order and inside their bounds, posted as name_min and name_max.

#forms#input#filter#interactive

Demo

Preview
Price

Leaving a box puts the pair in order and inside 0 to 1000.

Age

From 18 to 65.

Weight
Disabled

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/min_max_input.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/min_max_input.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/min_max_input.qwe
component MinMaxInputDemoLive() {    state range = [18, 65]    <div.demo-stack>        <MinMaxInput name="age" label="Age" low={18} high={65} lowest={0} highest={120} field="min-max-input-demo-live"                     on_change={(lo, hi) => { range = [lo, hi] }}/>        <p.demo-note>From {range[0] ?? "any"} to {range[1] ?? "any"}.</p>    </div>}view MinMaxInputDemo() =    <div.demo-stack>        <MinMaxInput name="price" label="Price" low={10} high={200} lowest={0} highest={1000} step={5} unit="€"                     field="min-max-input-demo-price" hint="Leaving a box puts the pair in order and inside 0 to 1000."/>        <MinMaxInputDemoLive/>        <div.demo-row>            <MinMaxInput name="weight" label="Weight" size="sm" unit="kg" field="min-max-input-demo-small" required={true}                         error="Give both weights."/>            <MinMaxInput name="locked" label="Disabled" size="sm" low={1} high={5} disabled={true}/>        </div>    </div>

Source

ui/min_max_input.qwe
// MinMaxInput: two number boxes, a minimum and a maximum, kept in order: a// minimum typed above the maximum raises the maximum to it when the box is left,// and the other way round, and both stay inside the bounds. Real// <input type="number"> fields, so a plain <form> sends them as `name` + "_min"// and `name` + "_max".////   <MinMaxInput name="price" label="Price" low={10} high={200} lowest={0} highest={1000} unit="€"/>//   <MinMaxInput name="age" label="Age" lowest={18} highest={99} on_change={(lo, hi) => { range = [lo, hi] }}/>//// low, high: the two values to start with (null: empty). lowest, highest: the// bounds (none by default). step: what the arrows add (1). unit: short text// after each box ("kg", "€"). low_label, high_label: the boxes' names for screen// readers ("Minimum", "Maximum"). hint, error, required, disabled, field (the ids'// start, default "min-max-input-" + name) and size ("sm" or "md") as in TextField.// on_change(low, high): inside one of your components, given the two numbers// (null for an empty box) once a box is left and the pair put in order.import ui/theme.qwe use ui_clslet min_max_input_css = css`.ui-min-max-input { display: grid; gap: 6px; min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-min-max-input-legend { padding: 0; font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-min-max-input-req { margin-left: 2px; color: var(--ui-danger); }.ui-min-max-input-row { display: flex; align-items: center; gap: 10px; }.ui-min-max-input-box {  display: flex; align-items: center; gap: 6px; width: 140px; height: 38px; padding: 0 12px; background: var(--ui-surface);  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); transition: border-color .15s, box-shadow .15s;}.ui-min-max-input-box:hover { border-color: var(--ui-strong); }.ui-min-max-input-box:focus-within { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-min-max-input-box input {  flex: 1; min-width: 0; width: 100%; height: 100%; padding: 0; border: 0; outline: none; background: transparent;  color: var(--ui-fg); font: 14px var(--ui-font); font-variant-numeric: tabular-nums;}.ui-min-max-input-box input::placeholder { color: var(--ui-muted); opacity: 1; }.ui-min-max-input-unit { flex: none; color: var(--ui-muted); user-select: none; }.ui-min-max-input-dash { color: var(--ui-muted); }.ui-min-max-input-sm .ui-min-max-input-box { height: 30px; width: 110px; padding: 0 10px; }.ui-min-max-input-sm .ui-min-max-input-box input { font-size: 13px; }.ui-min-max-input-invalid .ui-min-max-input-box { border-color: var(--ui-danger); }.ui-min-max-input-hint, .ui-min-max-input-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-min-max-input-error { color: var(--ui-danger); }.ui-min-max-input-disabled { opacity: .55; }.ui-min-max-input-disabled input { cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-min-max-input-box { transition: none; } }`// The number in a box's text, or null for nothing or not a number.fn min_max_input_num(text: str?) {    let t = trim(text ?? "")    if t == "" { return null }    let n = float(t)    is_err(n) ? null : n}// n kept between the bounds (either may be null).fn min_max_input_clamp(n, lowest, highest) =    n == null ? null : lowest != null && n < lowest ? lowest : highest != null && n > highest ? highest : ncomponent MinMaxInput(name: str, label: str? = null, low: num? = null, high: num? = null, lowest: num? = null,                      highest: num? = null, step: num = 1, unit: str? = null, low_label: str = "Minimum",                      high_label: str = "Maximum", hint: str? = null, error: str? = null, required: bool = false,                      disabled: bool = false, field: str? = null, size: str = "md", on_change = null, class: str? = null) {    style global min_max_input_css    state lo = low    state hi = high    state lo_text = low != null ? str(low) : ""    state hi_text = high != null ? str(high) : ""    let id = field ?? `min-max-input-${name}`    let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`])    let text_of = (n) => n == null ? "" : str(n)    // Puts the pair in order after one box is left: the box left wins, the other follows.    let settle = (which: str) => {        let a = min_max_input_clamp(min_max_input_num(lo_text), lowest, highest)        let b = min_max_input_clamp(min_max_input_num(hi_text), lowest, highest)        let next_lo = which == "lo" ? a : (a != null && b != null && a > b ? b : a)        let next_hi = which == "hi" ? b : (a != null && b != null && b < a ? a : b)        lo = next_lo        hi = next_hi        lo_text = text_of(next_lo)        hi_text = text_of(next_hi)        on_change && on_change(next_lo, next_hi)    }    let lo_said = unit != null ? `${low_label} (${unit})` : low_label    let hi_said = unit != null ? `${high_label} (${unit})` : high_label    let invalid = error != null ? "true" : null    let desc = described != "" ? described : null    <fieldset class={ui_cls(["ui-min-max-input", size == "sm" && "ui-min-max-input-sm", error != null && "ui-min-max-input-invalid",                             disabled && "ui-min-max-input-disabled", class])} id={id} aria-describedby={described != "" ? described : null}>        {label != null && <legend.ui-min-max-input-legend>            {label}{required && <span.ui-min-max-input-req aria-hidden="true">*</span>}        </legend>}        <div.ui-min-max-input-row>            <span.ui-min-max-input-box>                <input type="number" id={`${id}-min`} name={`${name}_min`} value={lo_text} min={lowest} max={highest} step={step}                       required={required} disabled={disabled} inputmode="decimal" aria-label={lo_said} placeholder="Min"                       aria-invalid={invalid} aria-describedby={desc}                       @input={(e) => { lo_text = e.value }} @change={(e) => {                           lo_text = e.value                           settle("lo")                       }}/>                {unit != null && <span.ui-min-max-input-unit aria-hidden="true">{unit}</span>}            </span>            <span.ui-min-max-input-dash aria-hidden="true">–</span>            <span.ui-min-max-input-box>                <input type="number" id={`${id}-max`} name={`${name}_max`} value={hi_text} min={lowest} max={highest} step={step}                       required={required} disabled={disabled} inputmode="decimal" aria-label={hi_said} placeholder="Max"                       aria-invalid={invalid} aria-describedby={desc}                       @input={(e) => { hi_text = e.value }} @change={(e) => {                           hi_text = e.value                           settle("hi")                       }}/>                {unit != null && <span.ui-min-max-input-unit aria-hidden="true">{unit}</span>}            </span>        </div>        {hint != null && <p.ui-min-max-input-hint id={`${id}-hint`}>{hint}</p>}        {error != null && <p.ui-min-max-input-error id={`${id}-error`} role="alert">{error}</p>}    </fieldset>}
Thread
56Useful? Like it, or add to its thread below.

Thread 0 posts

On MinMaxInput: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.