DateTimeInput
A date and a time side by side on the browser's own fields, giving one ISO value, in a time zone when given one.
Demo
Pick a date and a time.
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/date_time_input.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/date_time_input.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/date_time_input.qwe
component DateTimeInputDemoLive() { state at = null <div.demo-stack> <DateTimeInput name="dti-demo-call" label="Call" zone="Europe/Lisbon" step={15} on_change={(t) => { at = t }}/> <p.demo-note>{at == null ? "Pick a date and a time." : `The form sends ${at}.`}</p> </div>}view DateTimeInputDemo() { <div.demo-stack> <DateTimeInput name="dti-demo-starts" label="Starts" value="2026-11-02T09:00" earliest="2026-11-01" latest="2027-12-31" hint="Local time, as written."/> <DateTimeInputDemoLive/> <DateTimeInput name="dti-demo-utc" label="Deploy at (UTC)" zone="UTC" value="2026-11-02T14:30:00+01:00" required={true} error="That is in the past."/> </div>}Source
ui/date_time_input.qwe
// DateTimeInput: a moment picked as a date and a time side by side, on the// browser's own date and time fields (each phone's own pickers), giving one ISO// value. In a time zone it is the instant there, with its offset// ("2026-11-02T14:30:00Z", "2026-07-02T14:30:00+01:00": summer time included);// without one, the local date and time as written ("2026-11-02T14:30"). The value// goes in a hidden input named `name`, so a plain <form> sends it.//// <DateTimeInput name="starts" label="Starts" value="2026-11-02T09:00" earliest="2026-11-01"/>// <DateTimeInput name="call" label="Call" zone="Europe/Lisbon" on_change={(t) => { at = t }}/>//// value: where it starts: "YYYY-MM-DDTHH:MM", or a date-time with an offset (shown// in `zone` when there is one). zone: an IANA zone ("Europe/Lisbon"), "UTC" or an// offset ("+05:30"); it is said under the fields. earliest, latest: the first and// last dates that can be chosen ("YYYY-MM-DD"). step: the minutes between times// the browser offers (1). name ("datetime"), label (the legend), hint, error,// required, disabled and field (the ids' start, "date-time-input-" + name by// default) as for every field. The value is empty until both parts are given.// on_change(value): inside one of your components, given the ISO value, or null.import ui/theme.qwe use ui_clslet date_time_input_css = css`.ui-date-time-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-date-time-input-label { padding: 0; font-size: 13px; font-weight: 500; }.ui-date-time-input-req { margin-left: 2px; color: var(--ui-danger); }.ui-date-time-input-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }.ui-date-time-input-row input { box-sizing: border-box; height: 38px; margin: 0; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font); font-variant-numeric: tabular-nums; transition: border-color .15s, box-shadow .15s;}.ui-date-time-input-row input:hover { border-color: var(--ui-strong); }.ui-date-time-input-row input:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-date-time-input-row input:disabled { opacity: .55; cursor: not-allowed; }.ui-date-time-input-date { width: 160px; }.ui-date-time-input-time { width: 120px; }.ui-date-time-input-at { color: var(--ui-muted); font-size: 13px; }.ui-date-time-input-zone { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; background: var(--ui-subtle); color: var(--ui-muted); font-size: 12px; }.ui-date-time-input-zone svg { width: 12px; height: 12px; }.ui-date-time-input-invalid .ui-date-time-input-row input { border-color: var(--ui-danger); }.ui-date-time-input-hint, .ui-date-time-input-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-date-time-input-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-date-time-input-row input { transition: none; } }`// The date and time parts ("YYYY-MM-DD", "HH:MM") of a value, shown in `zone` when// it has an offset and a zone is given; empty parts for what is not there.fn date_time_input_parts(value: str?, zone: str?) { let v = trim(value ?? "") if !re.test(v, "^\\d{4}-\\d{2}-\\d{2}") { return { d: "", t: "" } } let with_offset = re.test(v, "(Z|[+-]\\d{2}:?\\d{2})$") let local = with_offset && zone != null ? date.to_zone(v, zone) : v let d = slice(local, 0, 10) let t = re.test(local, "^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}") ? slice(local, 11, 16) : "" { d: date.valid(d) ? d : "", t }}// The value for a date and a time: the instant in `zone`, or the local text.fn date_time_input_value(d: str, t: str, zone: str?) -> str = d == "" || t == "" || !date.valid(d) ? "" : zone != null ? date.at(d, t, zone) : `${d}T${t}`component DateTimeInput(name: str = "datetime", label: str? = null, value: str? = null, zone: str? = null, earliest: str? = null, latest: str? = null, step: int = 1, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, on_change = null, class: str? = null) { style global date_time_input_css state d = date_time_input_parts(value, zone).d state t = date_time_input_parts(value, zone).t let id = field ?? `date-time-input-${name}` let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`, zone != null && `${id}-zone`]) let iso = date_time_input_value(d, t, zone) let set_parts = (nd: str, nt: str) => { let before = date_time_input_value(d, t, zone) d = nd t = nt let after = date_time_input_value(nd, nt, zone) if after != before { on_change && on_change(after == "" ? null : after) } } <fieldset class={ui_cls(["ui-date-time-input", error != null && "ui-date-time-input-invalid", class])} id={id} disabled={disabled} aria-describedby={described != "" ? described : null}> {label != null && <legend.ui-date-time-input-label> {label}{required && <span.ui-date-time-input-req aria-hidden="true">*</span>} </legend>} <div.ui-date-time-input-row> <input.ui-date-time-input-date type="date" id={`${id}-date`} value={d} min={earliest} max={latest} required={required} aria-label="Date" aria-invalid={error != null ? "true" : null} @input={(e) => set_parts(e.value, t)} @change={(e) => set_parts(e.value, t)}/> <span.ui-date-time-input-at aria-hidden="true">at</span> <input.ui-date-time-input-time type="time" id={`${id}-time`} value={t} step={step * 60} required={required} aria-label="Time" aria-invalid={error != null ? "true" : null} @input={(e) => set_parts(d, slice(e.value, 0, 5))} @change={(e) => set_parts(d, slice(e.value, 0, 5))}/> {zone != null && <span.ui-date-time-input-zone id={`${id}-zone`}> <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.2" aria-hidden="true"> <circle cx="6" cy="6" r="4.8"/><path d="M1.2 6h9.6M6 1.2c1.6 1.6 1.6 8 0 9.6M6 1.2c-1.6 1.6-1.6 8 0 9.6"/> </svg> <span><span.ui-sr>Time zone: </span>{zone}{iso != "" ? ` (${date.format(iso, "Z")})` : ""}</span> </span>} </div> <input type="hidden" name={name} value={iso}/> {hint != null && <p.ui-date-time-input-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-date-time-input-error id={`${id}-error`} role="alert">{error}</p>} </fieldset>}Related
DurationInputHours, minutes and seconds typed side by side, giving one value in seconds, with the arrow keys and carrying past 59.5 views · 7 downloadsTimeInputA time in parts (hours, minutes, AM/PM) changed with the arrow keys or by typing.5 views · 6 downloadsAddressFormStreet, 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 downloadsBirthdayInputDay, month and year drop-downs that give one date in a hidden input, the days following the month and the years the age allowed.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 downloads
Thread
Thread 0 posts
On DateTimeInput: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.