Forms

UrlInput

A web address box that shows the protocol fixed at the start and lets the browser check the shape, with a tick when it looks right.

#forms#input#link#no-js

Demo

Preview
https://

No address yet.

https://github.com/

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

Example

The code of the preview above.

demos/url_input.qwe
component UrlInputDemoLive() {    state url = null    <div.demo-stack>        <UrlInput name="ui-demo-site" label="Website" protocol="https://" placeholder="example.com" on_change={(u) => { url = u }}/>        <p.demo-note>{url == null ? "No address yet." : `The form sends ${url}.`}</p>    </div>}view UrlInputDemo() {    <div.demo-stack.demo-narrow>        <UrlInputDemoLive/>        <UrlInput name="ui-demo-link" label="Link" value="https://quillolang.org" hint="The whole address, scheme first."/>        <UrlInput name="ui-demo-repo" label="Repository" protocol="https://github.com/" placeholder="owner/repo" size="sm"                  error="That repository does not exist."/>    </div>}

Source

ui/url_input.qwe
// UrlInput: a box for a web address, with the protocol shown and the shape// checked by the browser itself (a tick once it looks right; a red border and// a message once it is left wrong). It is a real <input>, so a plain <form>// sends it, and the browser holds the form until the address is a plausible one.////   <UrlInput name="site" label="Website" protocol="https://" placeholder="example.com"/>//   <UrlInput name="link" label="Link" value="https://quillolang.org" hint="Paste the whole address."/>//// protocol: shown fixed at the start of the box ("https://"); the box takes the rest// ("example.com/about"), and that is what the form sends: the route puts the// protocol back (`"https://" + form.site`). Without it the box is <input type="url">// and takes the whole address, scheme first.// value, placeholder ("example.com"), autocomplete ("url"), name ("url"), label,// hint, error, required, disabled, field (the box's id, "url-input-" + name by// default) and size ("sm" or "md") as for every field.// on_change(url): inside one of your components, given the whole address (the// protocol put back) when the box is left changed.import ui/theme.qwe use ui_clslet url_input_css = css`.ui-url-input { display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-url-input-label { font-size: 13px; font-weight: 500; }.ui-url-input-req { margin-left: 2px; color: var(--ui-danger); }.ui-url-input-box {  display: flex; align-items: center; gap: 8px; height: 38px; padding: 0 12px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface);  transition: border-color .15s, box-shadow .15s;}.ui-url-input-box:hover { border-color: var(--ui-strong); }.ui-url-input-box:focus-within { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-url-input-box:has(input:user-invalid) { border-color: var(--ui-danger); }.ui-url-input-box input {  flex: 1; min-width: 0; height: 100%; padding: 0; border: 0; outline: none; background: transparent;  color: var(--ui-fg); font: 14px var(--ui-font);}.ui-url-input-box input::placeholder { color: var(--ui-muted); opacity: 1; }.ui-url-input-protocol { flex: none; color: var(--ui-muted); user-select: none; font-variant-numeric: tabular-nums; }.ui-url-input-globe { flex: none; width: 16px; height: 16px; color: var(--ui-muted); }.ui-url-input-ok { flex: none; width: 16px; height: 16px; color: var(--ui-success); opacity: 0; transition: opacity .15s; }.ui-url-input-box:has(input:valid:not(:placeholder-shown)) .ui-url-input-ok { opacity: 1; }.ui-url-input-sm .ui-url-input-box { height: 30px; padding: 0 10px; }.ui-url-input-sm .ui-url-input-box input { font-size: 13px; }.ui-url-input-invalid .ui-url-input-box, .ui-url-input-invalid .ui-url-input-box:hover { border-color: var(--ui-danger); }.ui-url-input-hint, .ui-url-input-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-url-input-error { color: var(--ui-danger); }.ui-url-input-disabled { opacity: .55; }.ui-url-input-disabled input { cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-url-input-box, .ui-url-input-ok { transition: none; } }`view UrlInput(name: str = "url", label: str? = null, value: str? = null, protocol: str? = null, placeholder: str = "example.com",              autocomplete: str = "url", hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false,              field: str? = null, size: str = "md", on_change = null, class: str? = null) {    style global url_input_css    let id = field ?? `url-input-${name}`    let described = ui_cls([protocol != null && `${id}-protocol`, hint != null && `${id}-hint`, error != null && `${id}-error`])    // With the protocol fixed, the rest must be a host with a dot and a path with no spaces.    let shape = protocol != null ? "[^\\s\\/:]+\\.[^\\s\\/:]{2,}(\\/\\S*)?" : "https?:\\/\\/[^\\s\\/]+\\.[^\\s\\/]{2,}(\\/\\S*)?"    let shown = protocol != null && value != null && starts_with(lower(value), lower(protocol)) ? slice(value, len(protocol)) : value    <div class={ui_cls(["ui-url-input", size == "sm" && "ui-url-input-sm", error != null && "ui-url-input-invalid",                        disabled && "ui-url-input-disabled", class])}>        {label != null && <label.ui-url-input-label for={id}>            {label}{required && <span.ui-url-input-req aria-hidden="true">*</span>}        </label>}        <div.ui-url-input-box>            {protocol != null                ? <span.ui-url-input-protocol id={`${id}-protocol`}>{protocol}</span>                : <svg.ui-url-input-globe viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">                      <circle cx="10" cy="10" r="7.5"/><path d="M2.5 10h15M10 2.5c2.5 2.5 2.5 12.5 0 15M10 2.5c-2.5 2.5-2.5 12.5 0 15"/>                  </svg>}            <input id={id} type={protocol != null ? "text" : "url"} inputmode="url" name={name} placeholder={placeholder}                   {...(shown != null ? { value: shown } : {})}                   pattern={shape} autocomplete={autocomplete} autocapitalize="off" spellcheck="false" required={required}                   disabled={disabled} title={protocol != null ? `An address such as ${placeholder}` : "A whole address, such as https://example.com"}                   aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null}                   @change={(e) => { on_change && on_change((protocol ?? "") + trim(e.value)) }}/>            <svg.ui-url-input-ok viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                 stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>        </div>        {hint != null && <p.ui-url-input-hint id={`${id}-hint`}>{hint}</p>}        {error != null && <p.ui-url-input-error id={`${id}-error`} role="alert">{error}</p>}    </div>}
Thread
54Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.