MinMaxInput
Two number boxes, a minimum and a maximum, kept in order and inside their bounds, posted as name_min and name_max.
Demo
From 18 to 65.
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.qwemain.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>}Related
AddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloadsAutocompleteA free-text box that offers matches as one types, with the arrow keys and Enter; whatever is typed may stay.5 views · 7 downloadsCardInputA payment card as it is typed: the number grouped, the brand, MM/YY and the code, with typos said.6 views · 6 downloadsCharacterCounterA text box or textarea with a live 120 / 280 count that warns near the limit, as a hard stop or a soft one that blocks sending.5 views · 6 downloadsCheckboxA checkbox drawn by CSS on a real input, with an indeterminate state, and a group of them in a fieldset.5 views · 6 downloadsCheckboxCardsAny number of choices as cards with a title, a line of detail and a check, each a real checkbox a form sends.5 views · 6 downloads
Thread
Thread 0 posts
On MinMaxInput: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.