Forms

ContactForm

Name, email, subject and message with a Send button: a plain form posting to your route, shown again with errors or as sent.

#forms#input#page#no-js

Demo

Preview

Get in touch

We answer within a working day.

Your message was not sent: check the fields below.

Nothing sent 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/contact_form.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/contact_form.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/contact_form.qwe
component ContactFormDemoLive() {    state got = null    <div.demo-stack>        {got == null            ? <ContactForm action="#" id="contact-form-demo-live" subjects={["Sales", "Support", "Press"]} rows={3}                           on_submit={(f) => { got = f }}/>            : <ContactForm sent={true} id="contact-form-demo-sent"/>}        <p.demo-note>{got == null ? "Nothing sent yet." : `on_submit got ${got.name} about ${got.subject}.`}</p>    </div>}view ContactFormDemo() =    <div.demo-stack>        <ContactForm action="/contact" id="contact-form-demo-plain" phone={true} title="Get in touch"                     text="We answer within a working day." values={{ name: "Ada Lovelace", email: "ada@example" }}                     errors={{ email: "Enter an email address such as ada@example.com." }}                     notice="Your message was not sent: check the fields below."/>        <ContactFormDemoLive/>    </div>

Source

ui/contact_form.qwe
// ContactForm: name, email, subject and message with a Send button, as a plain// <form method="post"> to your route, so it works with no JavaScript. The server// shows it again with `errors` for what was wrong and `values` for what was typed,// or `sent` once the message went.////   <ContactForm action="/contact"/>//   <ContactForm action="/contact" subjects={["Sales", "Support", "Press"]} phone={true}/>//   <ContactForm action="/contact" values={values} errors={{ email: "Enter an email address." }}/>//   <ContactForm sent={true}/>////   route POST /contact { form: { name: str, email: str, subject: str = "", message: str } } { ... }//// action: where it posts. subjects: a list of subjects to choose from (a text// box when none). phone: also asks for a phone number, optional. values: what to// fill in again, { name, email, phone, subject, message }. errors: a message per// field, { name, email, phone, subject, message }, each shown under its field and// read to screen readers. notice: a line above the fields (a server's message).// sent: shows sent_text ("Thanks, your message is on its way.") in place of the// form. button ("Send message"), rows (the message's height in lines, 5),// title and text (a heading and a line above the form; none by default).// id: the fields' ids start with it, unique on the page ("contact-form").// on_submit(fields): inside one of your components, called with the fields as a// map when the form is sent, in place of posting it (send them yourself, with a// server fn, then show sent). Without on_submit the form posts to `action`.import ui/theme.qwe use ui_clslet contact_form_css = css`.ui-contact-form { display: grid; gap: 14px; min-width: 0; font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-contact-form-title { margin: 0; font-size: 20px; font-weight: 650; line-height: 1.25; }.ui-contact-form-text { margin: -6px 0 0; color: var(--ui-muted); }.ui-contact-form-fields { display: contents; }.ui-contact-form-form { display: grid; gap: 14px; margin: 0; }.ui-contact-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; }.ui-contact-form-field { display: grid; gap: 6px; min-width: 0; }.ui-contact-form-label { font-size: 13px; font-weight: 500; }.ui-contact-form-opt { margin-left: 4px; color: var(--ui-muted); font-weight: 400; }.ui-contact-form-input, .ui-contact-form-select, .ui-contact-form-area {  box-sizing: border-box; width: 100%; 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/1.5 var(--ui-font); transition: border-color .15s, box-shadow .15s;}.ui-contact-form-input, .ui-contact-form-select { height: 38px; }.ui-contact-form-select { appearance: none; padding-right: 36px; cursor: pointer; }.ui-contact-form-select-box { position: relative; }.ui-contact-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-contact-form-area { padding: 8px 12px; resize: vertical; min-height: 80px; }.ui-contact-form-input::placeholder, .ui-contact-form-area::placeholder { color: var(--ui-muted); opacity: 1; }.ui-contact-form-input:hover, .ui-contact-form-select:hover, .ui-contact-form-area:hover { border-color: var(--ui-strong); }.ui-contact-form-input:focus, .ui-contact-form-select:focus, .ui-contact-form-area:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-contact-form [aria-invalid="true"] { border-color: var(--ui-danger); }.ui-contact-form-error { margin: 0; font-size: 13px; color: var(--ui-danger); }.ui-contact-form-notice { margin: 0; padding: 10px 12px; border-radius: var(--ui-radius-sm); background: var(--ui-info-soft); color: var(--ui-info); font-size: 13px; }.ui-contact-form-actions { display: flex; align-items: center; gap: 12px; }.ui-contact-form-button {  display: inline-flex; align-items: center; justify-content: center; height: 40px; padding: 0 20px; 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-contact-form-button:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-contact-form-button:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-contact-form-sent {  display: flex; align-items: center; gap: 10px; margin: 0; padding: 14px 16px; border-radius: var(--ui-radius-sm);  background: var(--ui-success-soft); color: var(--ui-success); font-weight: 500;}.ui-contact-form-sent svg { flex: none; width: 20px; height: 20px; }@media (prefers-reduced-motion: reduce) { .ui-contact-form-input, .ui-contact-form-select, .ui-contact-form-area, .ui-contact-form-button { transition: none; } }`// A field's label, its control (children) and its error, by the ids the control points to.view ContactFormField(field: str, label: str, error: str? = null, optional: bool = false, children) {    style global contact_form_css    <div.ui-contact-form-field>        <label.ui-contact-form-label for={field}>{label}{optional && <span.ui-contact-form-opt>(optional)</span>}</label>        {children}        {error != null && <p.ui-contact-form-error id={`${field}-error`} role="alert">{error}</p>}    </div>}view ContactForm(action: str = "/contact", subjects: list? = null, phone: bool = false, values: map = {}, errors: map = {},                 notice: str? = null, sent: bool = false, sent_text: str = "Thanks, your message is on its way.",                 button: str = "Send message", rows: int = 5, title: str? = null, text: str? = null,                 id: str = "contact-form", on_submit = null, class: str? = null) {    style global contact_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 fields = <div.ui-contact-form-fields>        {notice != null && <p.ui-contact-form-notice role="status">{notice}</p>}        <div.ui-contact-form-row>            <ContactFormField field={fid("name")} label="Name" error={errors.name}>                <input.ui-contact-form-input type="text" id={fid("name")} name="name" value={values.name} required={true}                       autocomplete="name" aria-invalid={bad("name")} aria-describedby={described("name")}/>            </ContactFormField>            <ContactFormField field={fid("email")} label="Email" error={errors.email}>                <input.ui-contact-form-input type="email" id={fid("email")} name="email" value={values.email} required={true}                       autocomplete="email" autocapitalize="off" spellcheck="false"                       aria-invalid={bad("email")} aria-describedby={described("email")}/>            </ContactFormField>        </div>        <div.ui-contact-form-row>            {phone && <ContactFormField field={fid("phone")} label="Phone" optional={true} error={errors.phone}>                <input.ui-contact-form-input type="tel" id={fid("phone")} name="phone" value={values.phone}                       autocomplete="tel" aria-invalid={bad("phone")} aria-describedby={described("phone")}/>            </ContactFormField>}            <ContactFormField field={fid("subject")} label="Subject" error={errors.subject}>                {subjects != null                    ? <div.ui-contact-form-select-box>                        <select.ui-contact-form-select id={fid("subject")} name="subject" required={true}                                aria-invalid={bad("subject")} aria-describedby={described("subject")}>                            <option value="" disabled selected={values.subject == null}>Choose one</option>                            {for s in subjects => <option value={str(s)} selected={str(s) == values.subject}>{str(s)}</option>}                        </select>                    </div>                    : <input.ui-contact-form-input type="text" id={fid("subject")} name="subject" value={values.subject}                             required={true} aria-invalid={bad("subject")} aria-describedby={described("subject")}/>}            </ContactFormField>        </div>        <ContactFormField field={fid("message")} label="Message" error={errors.message}>            <textarea.ui-contact-form-area id={fid("message")} name="message" rows={rows} required={true}                      aria-invalid={bad("message")} aria-describedby={described("message")}>{values.message ?? ""}</textarea>        </ContactFormField>        <div.ui-contact-form-actions>            <button.ui-contact-form-button type="submit">{button}</button>        </div>    </div>    <div class={ui_cls(["ui-contact-form", class])}>        {title != null && <h3.ui-contact-form-title>{title}</h3>}        {text != null && <p.ui-contact-form-text>{text}</p>}        {sent            ? <p.ui-contact-form-sent role="status">                <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                     stroke-linejoin="round" aria-hidden="true"><circle cx="10" cy="10" r="8"/><path d="M6.5 10.5l2.5 2.5 4.5-5"/></svg>                {sent_text}            </p>            : (on_submit != null                ? <form.ui-contact-form-form method="post" action={action} @submit={(e) => { on_submit && on_submit(e.form) }}>{fields}</form>                : <form.ui-contact-form-form method="post" action={action}>{fields}</form>)}    </div>}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.