Forms

DependentSelect

Two drop-downs where the second's options depend on the first, both real selects a form sends.

#forms#select#input#interactive

Demo

Preview
Where you live

Changing the country empties the region.

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.qwe
main.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>}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.