ContactForm
Name, email, subject and message with a Send button: a plain form posting to your route, shown again with errors or as sent.
Demo
Get in touch
We answer within a working day.
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.qwemain.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>}Related
AddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloadsAuthFormSign in, sign up, forgot and reset password in one form that switches in place, each posting to its own route.7 views · 4 downloadsAutocompleteA free-text box that offers matches as one types, with the arrow keys and Enter; whatever is typed may stay.5 views · 7 downloadsCardInputA payment card as it is typed: the number grouped, the brand, MM/YY and the code, with typos said.6 views · 6 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 downloadsCheckboxA checkbox drawn by CSS on a real input, with an indeterminate state, and a group of them in a fieldset.5 views · 6 downloads
Thread
Thread 0 posts
On ContactForm: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.