Forms

Form

A plain <form method="post"> laid out stacked, inline or in a grid, with a summary of what is wrong at the top and a submit row at the end.

#forms#layout#no-js

Demo

Preview
Cancel

Nothing sent yet.

Get it

It needs theme.qwe, as every component does, and button.qwe; 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/button.qwe$ curl -O https://quillolang.org/components/ui/form.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/form.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/form.qwe
component FormDemoLive() {    state sent = null    <div.demo-stack>        <Form id="form-demo-live" layout="inline" submit="Search" on_submit={(f) => { sent = f }}>            <FormField label="Find" field="form-demo-live-q">                <input id="form-demo-live-q" name="q" type="search" placeholder="A name"/>            </FormField>            <FormField label="In" field="form-demo-live-in">                <select id="form-demo-live-in" name="in"><option>People</option><option>Projects</option></select>            </FormField>        </Form>        <p.demo-note>{sent == null ? "Nothing sent yet." : `on_submit got ${json.stringify(sent)}.`}</p>    </div>}view FormDemo() =    <div.demo-stack>        <Form id="form-demo-signup" action="/signup" submit="Create account" cancel_href="#form"              errors={{ "form-demo-email": "Enter an email address such as ada@example.com.", "form-demo-terms": "Accept the terms to go on." }}>            <FormField label="Name" field="form-demo-name">                <input id="form-demo-name" name="name" type="text" autocomplete="name"/>            </FormField>            <FormField label="Email" field="form-demo-email" required={true} error="Enter an email address such as ada@example.com.">                <input id="form-demo-email" name="email" type="email" value="ada@" aria-invalid="true" aria-describedby="form-demo-email-error"/>            </FormField>            <Checkbox name="terms" label="I accept the terms" field="form-demo-terms" error="Accept the terms to go on."/>        </Form>        <Form id="form-demo-grid" layout="grid" columns={2} action="/profile" submit="Save" actions_align="start"              actions={<Button variant="ghost" tone="neutral" type="reset">Reset</Button>}>            <TextField name="first" label="First name" field="form-demo-first"/>            <TextField name="last" label="Last name" field="form-demo-last"/>            <FormField label="Bio" field="form-demo-bio" full={true} optional={true}>                <textarea id="form-demo-bio" name="bio" rows="3"></textarea>            </FormField>        </Form>        <FormDemoLive/>    </div>

Source

ui/form.qwe
// Form: a <form> that lays its fields out (stacked, inline or in a grid), puts a// summary of what is wrong at the top, and ends with a submit row. It is a plain// <form method="post">, so its fields go to a route without any script; the// summary is what the route sends back with the values typed.////   <Form action="/signup" submit="Create account" cancel_href="/">//       <FormField label="Email" field="email"><input id="email" name="email" type="email"/></FormField>//   </Form>//   <Form layout="grid" columns={2} errors={{ email: "Enter an email address." }} submit="Save"> ... </Form>//   <Form layout="inline" submit="Search"> ... </Form>//// method ("post"), action, enctype (set "multipart/form-data" to send files),// id and novalidate go to the <form>. layout: "stacked" (default, one field under// another), "inline" (fields in a row, bottom-aligned, wrapping) or "grid" (columns// of equal width; a FormField with full={true}, the summary and the submit row take// the whole row). columns: how many, for "grid" (2). gap: pixels between fields (16).// errors: what is wrong, shown first in a red box screen readers hear at once: a// map of { field_id: message } makes each a link to that field, a list of strings// a plain list. errors_title: its heading ("There is a problem").// submit: the submit button's text; none hides the row. cancel_href: a Cancel link// beside it (cancel_label, "Cancel"). actions: more buttons for the row. busy: the// submit button shows it is working. The row aligns right in a stacked form; actions_align// ("start", "end", "center" or "between") says otherwise.// Needs button.qwe for the submit button.// on_submit(fields): inside one of your components, given the form's fields as a// map, instead of the browser sending it (a plain form posts as usual).import ui/button.qwe use Buttonimport ui/theme.qwe use ui_clslet form_css = css`.ui-form { display: grid; gap: var(--ui-form-gap, 16px); min-width: 0; margin: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-form-inline { display: flex; flex-wrap: wrap; align-items: flex-end; }.ui-form-inline > * { flex: 1 1 160px; min-width: 0; }.ui-form-inline > .ui-form-submit { flex: 0 0 auto; }.ui-form-grid { grid-template-columns: repeat(var(--ui-form-cols, 2), minmax(0, 1fr)); }.ui-form-grid > .ui-form-errors, .ui-form-grid > .ui-form-submit, .ui-form-grid > .ui-form-field-full,.ui-form-grid > .ui-form-actions, .ui-form-grid > .ui-form-row { grid-column: 1 / -1; }.ui-form-errors {  padding: 12px 14px; border: 1px solid color-mix(in srgb, var(--ui-danger) 35%, transparent);  border-radius: var(--ui-radius-sm); background: var(--ui-danger-soft); color: var(--ui-danger); outline: none;}.ui-form-errors h3 { margin: 0 0 4px; font-size: 14px; font-weight: 600; }.ui-form-errors ul { margin: 0; padding-left: 18px; }.ui-form-errors a { color: inherit; font-weight: 500; }.ui-form-errors a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-form-submit { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; justify-content: flex-end; }.ui-form-submit-start { justify-content: flex-start; }.ui-form-submit-center { justify-content: center; }.ui-form-submit-between { justify-content: space-between; }.ui-form-cancel { color: var(--ui-muted); text-decoration: none; font-weight: 500; padding: 0 6px; border-radius: 3px; }.ui-form-cancel:hover { color: var(--ui-fg); text-decoration: underline; }.ui-form-cancel:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (max-width: 559px) {  .ui-form-grid { grid-template-columns: minmax(0, 1fr); }  .ui-form-inline > * { flex-basis: 100%; }}`// The summary's items as { href, text }: links for a map of field ids, plain for a list.fn form_items(errors) -> list =    type_of(errors) == "map" ? keys(errors) | map((k) => { href: `#${k}`, text: str(errors[k]) })    : type_of(errors) == "list" ? errors | map((e) => { href: null, text: str(e) })    : []view Form(method: str = "post", action: str? = null, enctype: str? = null, id: str? = null, novalidate: bool = false,          layout: str = "stacked", columns: int = 2, gap: int = 16, errors = null, errors_title: str = "There is a problem",          submit: str? = null, cancel_href: str? = null, cancel_label: str = "Cancel", actions = null, busy: bool = false,          actions_align: str? = null, on_submit = null, class: str? = null, children) {    style global form_css    let items = form_items(errors)    let align = actions_align ?? (layout == "inline" ? "start" : "end")    <form class={ui_cls(["ui-form", layout != "stacked" && `ui-form-${layout}`, class])} id={id} method={method} action={action}          enctype={enctype} novalidate={novalidate}          style={`--ui-form-gap: ${gap}px${layout == "grid" ? `; --ui-form-cols: ${columns}` : ""}`}          @submit={on_submit != null ? (e) => { on_submit(e.form) } : null}>        {len(items) > 0 && <div.ui-form-errors role="alert" tabindex="-1" id={id != null ? `${id}-errors` : null}>            <h3>{errors_title}</h3>            <ul>{for it in items => <li>{it.href != null ? <a href={it.href}>{it.text}</a> : it.text}</li>}</ul>        </div>}        {children}        {(submit != null || actions != null) && <div class={ui_cls(["ui-form-submit", align != "end" && `ui-form-submit-${align}`])}>            {cancel_href != null && align == "between" && <a.ui-form-cancel href={cancel_href}>{cancel_label}</a>}            {submit != null && <Button type="submit" loading={busy}>{submit}</Button>}            {actions}            {cancel_href != null && align != "between" && <a.ui-form-cancel href={cancel_href}>{cancel_label}</a>}        </div>}    </form>}
Thread
68Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.