AddressForm
Street, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.
Demo
Nothing changed yet.
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/address_form.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/address_form.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/address_form.qwe
component AddressFormDemoLive() { state changed = null <div.demo-stack> <AddressForm id="address-form-demo-live" prefix="billing_" section="billing" legend="Billing address" countries={["US", "CA", "GB", ["XK", "Kosovo"]]} values={{ country: "US" }} on_change={(f, v) => { changed = `${f} = ${v}` }}/> <p.demo-note>{changed == null ? "Nothing changed yet." : `on_change: ${changed}`}</p> </div>}view AddressFormDemo() = <div.demo-stack> <AddressForm action="/account/address" id="address-form-demo-plain" section="shipping" legend="Shipping address" values={{ line1: "1 Rue de Rivoli", city: "Paris", postcode: "75001", country: "FR" }} errors={{ postcode: "A French postcode has five digits." }}/> <AddressFormDemoLive/> </div>Source
ui/address_form.qwe
// AddressForm: a postal address in the usual layout: street (two lines), city,// region and postcode side by side, and a country drop-down. Real fields with the// autocomplete names browsers fill from, so a plain <form> sends them.//// <form method="post" action="/checkout"><AddressForm/> ... </form>// <AddressForm action="/account/address" values={{ line1: "1 Rue de Rivoli", city: "Paris", postcode: "75001", country: "FR" }}/>// <AddressForm prefix="shipping_" section="shipping" legend="Shipping address" errors={{ postcode: "Enter a postcode." }}/>//// It is a <fieldset> to put in your form; with `action` it is a form of its own// with a Save button (button: its text). prefix: the start of the field names// (none: line1, line2, city, region, postcode, country). section: "shipping" or// "billing", which autocomplete uses to tell addresses apart. legend: a heading// (none by default). values: what to fill in, { line1, line2, city, region,// postcode, country (a two-letter code) }. errors: a message per field, shown// under it. countries: the countries offered, [code, name] pairs or codes of the// built-in list of forty (`address_form_countries`); the region's label follows// the country chosen on the server (State, Province, County, Region). required// (true): line1, city, postcode and country must be filled.// id: the fields' ids start with it, unique on the page ("address-form").// on_change(field, value): inside one of your components, called with a field's// name (without the prefix) and its value when one is changed.import ui/theme.qwe use ui_clslet address_form_css = css`.ui-address-form-set { display: grid; gap: 14px; min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-address-form-legend { padding: 0; margin-bottom: 4px; font-size: 16px; font-weight: 600; }.ui-address-form-form { display: grid; gap: 14px; margin: 0; }.ui-address-form-row { display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 14px; }.ui-address-form-field { display: grid; gap: 6px; min-width: 0; }.ui-address-form-label { font-size: 13px; font-weight: 500; }.ui-address-form-opt { margin-left: 4px; color: var(--ui-muted); font-weight: 400; }.ui-address-form-input, .ui-address-form-select { box-sizing: border-box; width: 100%; height: 38px; margin: 0; padding: 0 12px; 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); transition: border-color .15s, box-shadow .15s;}.ui-address-form-select { appearance: none; padding-right: 36px; cursor: pointer; }.ui-address-form-select-box { position: relative; }.ui-address-form-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-address-form-input::placeholder { color: var(--ui-muted); opacity: 1; }.ui-address-form-input:hover, .ui-address-form-select:hover { border-color: var(--ui-strong); }.ui-address-form-input:focus, .ui-address-form-select:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-address-form-set [aria-invalid="true"] { border-color: var(--ui-danger); }.ui-address-form-error { margin: 0; font-size: 13px; color: var(--ui-danger); }.ui-address-form-actions { display: flex; gap: 12px; padding-top: 2px; }.ui-address-form-button { display: inline-flex; align-items: center; justify-content: center; height: 38px; padding: 0 18px; border: 0; border-radius: var(--ui-radius-sm); background: var(--ui-accent); color: var(--ui-accent-fg); font: 500 14px var(--ui-font); cursor: pointer; transition: background .15s, box-shadow .15s;}.ui-address-form-button:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-address-form-button:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (max-width: 560px) { .ui-address-form-row { grid-template-columns: 1fr 1fr; } .ui-address-form-row > :first-child { grid-column: 1 / -1; } }@media (prefers-reduced-motion: reduce) { .ui-address-form-input, .ui-address-form-select, .ui-address-form-button { transition: none; } }`// The countries offered by default, as [code, name].let address_form_countries = [ ["AR", "Argentina"], ["AU", "Australia"], ["AT", "Austria"], ["BE", "Belgium"], ["BR", "Brazil"], ["CA", "Canada"], ["CL", "Chile"], ["CN", "China"], ["CZ", "Czechia"], ["DK", "Denmark"], ["FI", "Finland"], ["FR", "France"], ["DE", "Germany"], ["GR", "Greece"], ["HU", "Hungary"], ["IN", "India"], ["ID", "Indonesia"], ["IE", "Ireland"], ["IL", "Israel"], ["IT", "Italy"], ["JP", "Japan"], ["MX", "Mexico"], ["NL", "Netherlands"], ["NZ", "New Zealand"], ["NO", "Norway"], ["PL", "Poland"], ["PT", "Portugal"], ["RO", "Romania"], ["SG", "Singapore"], ["ZA", "South Africa"], ["KR", "South Korea"], ["ES", "Spain"], ["SE", "Sweden"], ["CH", "Switzerland"], ["TR", "Türkiye"], ["AE", "United Arab Emirates"], ["GB", "United Kingdom"], ["US", "United States"], ["UY", "Uruguay"], ["VN", "Vietnam"],]// What the region is called where the address is.fn address_form_region_label(country: str?) -> str = match upper(country ?? "") { "US" | "AU" | "BR" | "IN" | "MX" | "DE" | "AT" => "State" "CA" | "CN" | "ES" | "IT" | "NL" | "BE" | "AR" | "CL" | "ID" | "KR" | "PL" | "TR" | "VN" | "ZA" => "Province" "GB" | "IE" | "NZ" | "NO" | "SE" => "County" "JP" => "Prefecture" _ => "Region"}// And the postcode.fn address_form_postcode_label(country: str?) -> str = match upper(country ?? "") { "US" | "PH" => "ZIP code" "IE" => "Eircode" "IN" => "PIN code" "GB" | "CA" | "AU" | "NZ" => "Postcode" _ => "Postal code"}view AddressFormField(field: str, label: str, error: str? = null, optional: bool = false, children) { style global address_form_css <div.ui-address-form-field> <label.ui-address-form-label for={field}>{label}{optional && <span.ui-address-form-opt>(optional)</span>}</label> {children} {error != null && <p.ui-address-form-error id={`${field}-error`} role="alert">{error}</p>} </div>}view AddressForm(action: str? = null, prefix: str = "", section: str? = null, legend: str? = null, values: map = {}, errors: map = {}, countries: list? = null, required: bool = true, button: str = "Save address", id: str = "address-form", on_change = null, class: str? = null) { style global address_form_css let fid = (f: str) => `${id}-${f}` let bad = (f: str) => errors[f] != null ? "true" : null let described = (f: str) => errors[f] != null ? `${fid(f)}-error` : null let auto = (part: str) => section != null ? `${section} ${part}` : part let country = values.country != null ? upper(values.country) : null let list = countries == null ? address_form_countries : countries | map((c) => type_of(c) == "list" ? c : (find(address_form_countries, (p) => p[0] == upper(str(c))) ?? [upper(str(c)), str(c)])) let change = (f: str) => (e) => { on_change && on_change(f, e.value) } let text = (f: str, label: str, part: str, optional: bool, placeholder: str? = null) => <AddressFormField field={fid(f)} label={label} error={errors[f]} optional={optional}> <input.ui-address-form-input type="text" id={fid(f)} name={prefix + f} value={values[f]} required={!optional && required} autocomplete={auto(part)} placeholder={placeholder} aria-invalid={bad(f)} aria-describedby={described(f)} @change={change(f)}/> </AddressFormField> let body = [ text("line1", "Street address", "address-line1", false, "House number and street"), text("line2", "Apartment, suite, unit", "address-line2", true, null), <div.ui-address-form-row> {text("city", "City", "address-level2", false, null)} {text("region", address_form_region_label(country), "address-level1", true, null)} {text("postcode", address_form_postcode_label(country), "postal-code", false, null)} </div>, <AddressFormField field={fid("country")} label="Country" error={errors.country}> <div.ui-address-form-select-box> <select.ui-address-form-select id={fid("country")} name={`${prefix}country`} required={required} autocomplete={auto("country")} aria-invalid={bad("country")} aria-describedby={described("country")} @change={change("country")}> <option value="" disabled selected={country == null}>Choose a country</option> {for c in list => <option value={c[0]} selected={c[0] == country}>{c[1]}</option>} </select> </div> </AddressFormField>, ] action != null ? <form class={ui_cls(["ui-address-form-form", class])} method="post" action={action}> <fieldset.ui-address-form-set id={id}> {legend != null && <legend.ui-address-form-legend>{legend}</legend>} {body} </fieldset> <div.ui-address-form-actions><button.ui-address-form-button type="submit">{button}</button></div> </form> : <fieldset class={ui_cls(["ui-address-form-set", class])} id={id}> {legend != null && <legend.ui-address-form-legend>{legend}</legend>} {body} </fieldset>}Related
CardInputA payment card as it is typed: the number grouped, the brand, MM/YY and the code, with typos said.6 views · 6 downloadsCurrencyInputAn amount with its currency's symbol, thousands grouped as it is typed and the decimals the currency has, sent as plain digits.5 views · 5 downloadsQuantityInputA compact count with − and + either side, kept to its limits and step, with the arrow keys, PageUp/PageDown, Home and End.5 views · 6 downloadsCouponInputA promo code box with Apply: on_apply(code) says whether it took, and an applied code shows with Remove.6 views · 5 downloadsAutocompleteA free-text box that offers matches as one types, with the arrow keys and Enter; whatever is typed may stay.5 views · 7 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 downloads
Thread
Thread 0 posts
On AddressForm: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.