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.
Demo
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.qwemain.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>}Related
FormActionsThe row of submit and cancel buttons at the end of a form, aligned at the end, start, centre or both sides.5 views · 4 downloadsFormFieldA label, a hint and an error around any control, with a required mark, the label beside or above it.5 views · 5 downloadsFormRowFields side by side, equal or weighted, that stack one under another on a phone.5 views · 8 downloadsActionBarA page's main action and a line about it, fixed to the bottom of the screen on a phone.13 views · 6 downloadsFieldsetA <fieldset> with a legend and a line about it, its fields stacked, in a row or in a grid; disabled as one.6 views · 7 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 Form: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.