Forms

AddressForm

Street, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.

#forms#input#checkout#commerce#no-js

Demo

Preview
Shipping address
Billing address

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

Thread 0 posts

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

No posts yet. The first one opens the thread.