DependentSelect
Two drop-downs where the second's options depend on the first, both real selects a form sends.
Demo
Category bikes, product gravel.
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/dependent_select.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/dependent_select.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/dependent_select.qwe
let dependent_select_demo_regions = [ { label: "Canada", value: "CA", options: [{ label: "British Columbia", value: "BC" }, { label: "Ontario", value: "ON" }, { label: "Quebec", value: "QC" }] }, { label: "Spain", value: "ES", options: ["Andalusia", "Catalonia", "Madrid"] }, { label: "Japan", value: "JP", options: ["Hokkaido", "Kyoto", "Tokyo"] },]component DependentSelectDemoLive() { state picked = ["bikes", "gravel"] <div.demo-stack> <DependentSelect name="category" child_name="product" label="Category" child_label="Product" value="bikes" child_value="gravel" field="dependent-select-demo-live" size="sm" options={[{ label: "Bikes", value: "bikes", options: [{ label: "Road", value: "road" }, { label: "Gravel", value: "gravel" }] }, { label: "Helmets", value: "helmets", options: ["Commuter", "Race"] }]} on_change={(a, b) => { picked = [a, b] }}/> <p.demo-note>Category {picked[0] ?? "none"}, product {picked[1] ?? "none"}.</p> </div>}view DependentSelectDemo() = <div.demo-stack> <DependentSelect name="country" child_name="region" label="Country" child_label="Region" legend="Where you live" field="dependent-select-demo-plain" options={dependent_select_demo_regions} required={true} hint="Changing the country empties the region."/> <DependentSelectDemoLive/> </div>Source
ui/dependent_select.qwe
// DependentSelect: two drop-downs where the second's options depend on the first:// a country and its regions, a category and its products, a make and its models.// Both are the browser's own <select> with a name, so a plain <form> sends them;// changing the first empties the second and offers its options.//// <DependentSelect name="country" child_name="region" label="Country" child_label="Region"// value="CA" child_value="QC"// options={[{ label: "Canada", value: "CA", options: [{ label: "Ontario", value: "ON" }, { label: "Quebec", value: "QC" }] },// { label: "Spain", value: "ES", options: ["Andalusia", "Catalonia", "Madrid"] }]}/>// <DependentSelect name="make" child_name="model" options={cars} on_change={(make, model) => { ... }}/>//// options: maps of { label, value, options }: the first drop-down's choices, each// with the second's as strings or { label, value }. value and child_value: what// is chosen to start with. label and child_label: the two labels. placeholder// and child_placeholder: the first lines, which cannot be chosen ("Choose one",// and "Choose a <child_label>" once the first is chosen; before that the second// is disabled and says child_first, "Choose the first one first"). hint, error,// required, disabled, field (the ids' start, default "dependent-select-" + name)// and size ("sm" or "md") as in Select.// on_change(value, child_value): inside one of your components, given the two// values (the second null while nothing is chosen there) after either changes.import ui/theme.qwe use ui_clslet dependent_select_css = css`.ui-dependent-select { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-dependent-select-legend { grid-column: 1 / -1; padding: 0; font-size: 13px; font-weight: 500; }.ui-dependent-select-req { margin-left: 2px; color: var(--ui-danger); }.ui-dependent-select-field { display: grid; gap: 6px; min-width: 0; }.ui-dependent-select-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-dependent-select-box { position: relative; display: block; }.ui-dependent-select-box::after { content: ""; position: absolute; right: 14px; top: 50%; width: 7px; height: 7px; margin-top: -6px; border: solid var(--ui-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg); pointer-events: none;}.ui-dependent-select select { appearance: none; display: block; width: 100%; height: 38px; padding: 0 36px 0 12px; background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); cursor: pointer; text-overflow: ellipsis; transition: border-color .15s, box-shadow .15s;}.ui-dependent-select select:hover { border-color: var(--ui-strong); }.ui-dependent-select select:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-dependent-select select:has(option[value=""]:checked) { color: var(--ui-muted); }.ui-dependent-select select:disabled { opacity: .55; cursor: not-allowed; }.ui-dependent-select option { color: var(--ui-fg); background: var(--ui-surface); }.ui-dependent-select-sm select { height: 30px; padding: 0 30px 0 10px; font-size: 13px; }.ui-dependent-select-sm .ui-dependent-select-box::after { right: 11px; width: 6px; height: 6px; margin-top: -5px; }.ui-dependent-select-invalid select { border-color: var(--ui-danger); }.ui-dependent-select-hint, .ui-dependent-select-error { grid-column: 1 / -1; margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-dependent-select-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-dependent-select select { transition: none; } }`fn dependent_select_option(o) = type_of(o) == "map" ? { label: o.label ?? str(o.value), value: str(o.value ?? o.label) } : { label: str(o), value: str(o) }component DependentSelect(name: str, child_name: str, options: list = [], value: str? = null, child_value: str? = null, label: str? = null, child_label: str? = null, placeholder: str = "Choose one", child_placeholder: str? = null, child_first: str = "Choose the first one first", legend: str? = null, 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 dependent_select_css state parent = value state child = child_value let id = field ?? `dependent-select-${name}` let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`]) let groups = options | map((g) => { label: g.label ?? str(g.value), value: str(g.value ?? g.label), options: (g.options ?? []) | map(dependent_select_option) }) let current = parent != null ? find(groups, (g) => g.value == parent) : null let kids = current != null ? current.options : [] let kid = child != null && any(kids, (k) => k.value == child) ? child : null <fieldset class={ui_cls(["ui-dependent-select", size == "sm" && "ui-dependent-select-sm", error != null && "ui-dependent-select-invalid", class])} id={id} disabled={disabled} aria-describedby={described != "" ? described : null}> {legend != null && <legend.ui-dependent-select-legend> {legend}{required && <span.ui-dependent-select-req aria-hidden="true">*</span>} </legend>} <div.ui-dependent-select-field> {label != null && <label.ui-dependent-select-label for={`${id}-parent`}>{label}</label>} <span.ui-dependent-select-box> <select id={`${id}-parent`} name={name} required={required} aria-invalid={error != null ? "true" : null} aria-controls={`${id}-child`} @change={(e) => { parent = e.value != "" ? e.value : null child = null on_change && on_change(parent, null) }}> <option value="" disabled selected={current == null}>{placeholder}</option> {for g in groups => <option value={g.value} selected={g.value == parent}>{g.label}</option>} </select> </span> </div> <div.ui-dependent-select-field> {child_label != null && <label.ui-dependent-select-label for={`${id}-child`}>{child_label}</label>} <span.ui-dependent-select-box> <select id={`${id}-child`} name={child_name} required={required} disabled={current == null} aria-invalid={error != null ? "true" : null} @change={(e) => { child = e.value != "" ? e.value : null on_change && on_change(parent, child) }}> <option value="" disabled selected={kid == null}> {current == null ? child_first : child_placeholder ?? (child_label != null ? `Choose a ${lower(child_label)}` : placeholder)} </option> {for k in kids => <option key={k.value} value={k.value} selected={k.value == kid}>{k.label}</option>} </select> </span> </div> {hint != null && <p.ui-dependent-select-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-dependent-select-error id={`${id}-error`} role="alert">{error}</p>} </fieldset>}Related
CheckboxCardsAny 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 downloadsComboboxA text box that filters a list of options as you type, or asks for them (a server, a map service), with the arrow keys, Enter and Escape.5 views · 6 downloadsPhoneInputA country code select and a number box, sent together in international form (+351912345678) with the country's ISO code.6 views · 6 downloadsRadioCardsOne choice of several as cards with a title, a line of detail and a dot, each a real radio a form sends.5 views · 7 downloadsRadioGroupOne choice of several: real radio buttons in a fieldset, in a column, a row or as cards.5 views · 6 downloadsAddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloads
Thread
Thread 0 posts
On DependentSelect: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.