Social

ChatInput

A chat message box that grows as you type, sends on Enter and breaks the line on Shift+Enter; a plain form in a page.

#social#chat#forms#input#interactive

Demo

Preview

Nothing sent yet.

Enter sends, Shift+Enter starts a new line. Below, one that posts a form:

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

Example

The code of the preview above.

demos/chat_input.qwe
component ChatInputDemoLive() {    state sent = []    state typed = 0    <div.demo-stack>        <ChatInput id="chat-input-demo-live" on_send={(t) => { sent = sent | push(t) }} on_typing={() => { typed += 1 }}                   before={<button type="button" class="ui-chat-composer-send" aria-label="Attach" style="background: var(--ui-subtle); color: var(--ui-muted)">                       <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.4 11.1l-9.2 9.2a6 6 0 0 1-8.5-8.5l9.2-9.2a4 4 0 0 1 5.7 5.7l-9.2 9.2a2 2 0 0 1-2.8-2.8l8.5-8.5"/></svg>                   </button>}/>        <p.demo-note>{len(sent) == 0 ? "Nothing sent yet." : `Sent: ${join(sent | map((s) => `"${s}"`), ", ")}.`}{typed > 0 ? ` Typing told ${typed} ${typed == 1 ? "time" : "times"}.` : ""}</p>    </div>}view ChatInputDemo() =    <div.demo-stack>        <ChatInputDemoLive/>        <p.demo-note>Enter sends, Shift+Enter starts a new line. Below, one that posts a form:</p>        <ChatInput id="chat-input-demo-form" method="get" action="#chat_input" placeholder="Message support…" send_label="Send to support">            <input type="hidden" name="ticket" value="77"/>        </ChatInput>    </div>

Source

ui/chat_input.qwe
// ChatInput: the box at the bottom of a chat. It grows with what is typed (up to a// few lines, then scrolls), Enter sends, Shift+Enter starts a new line, and the Send// button is off while it is empty. It is a real <textarea> named `name`, so in a page// a plain <form> sends it; inside one of your components, on_send(text) gets the text// and the box is emptied.////   <ChatInput action="/chat/42"><input type="hidden" name="thread" value="42"/></ChatInput>//   <ChatInput on_send={(text) => { send(text) }} on_typing={() => { tell_them() }}/>//// name ("message"), placeholder ("Write a message…"), send_label ("Send"), rows: the// most lines it grows to before scrolling (5). disabled. id: the text box's id// ("ui-chat-composer"). method ("post") and action for the form in a page, with children// inside it (hidden inputs). autofocus: the box takes the focus on the page.// children can also be buttons at the left of the box (attach, emoji): put them in// `before`, a slot of HTML.// on_send(text): inside one of your components, called with the text trimmed.// on_typing(): called on each key while something is written, for a typing indicator.import ui/theme.qwe use ui_clslet chat_input_css = css`.ui-chat-composer { display: flex; align-items: flex-end; gap: 8px; min-width: 0; margin: 0; padding: 8px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-chat-composer-before { display: flex; align-items: center; gap: 2px; align-self: flex-end; height: 38px; }.ui-chat-composer-box {  flex: 1; min-width: 0; min-height: 38px; max-height: calc(var(--ui-chat-composer-rows, 5) * 1.45em + 18px); margin: 0; padding: 8px 12px; resize: none;  field-sizing: content; border: 1px solid transparent; border-radius: 19px; background: var(--ui-subtle); color: var(--ui-fg); font: 14px/1.45 var(--ui-font);}.ui-chat-composer-box::placeholder { color: var(--ui-muted); opacity: 1; }.ui-chat-composer-box:focus { outline: none; border-color: var(--ui-accent); background: var(--ui-surface); box-shadow: var(--ui-focus); }.ui-chat-composer-box:disabled { opacity: .55; cursor: not-allowed; }.ui-chat-composer-send {  display: grid; place-items: center; flex: none; width: 38px; height: 38px; padding: 0; border: 0; border-radius: 50%;  background: var(--ui-accent); color: var(--ui-accent-fg); cursor: pointer; transition: background .15s, opacity .15s;}.ui-chat-composer-send svg { width: 18px; height: 18px; }.ui-chat-composer-send:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-chat-composer-send:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-chat-composer-send:disabled { opacity: .45; cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-chat-composer-send { transition: none; } }`component ChatInput(name: str = "message", placeholder: str = "Write a message…", send_label: str = "Send", rows: int = 5,                    action: str? = null, method: str = "post", disabled: bool = false, autofocus: bool = false,                    id: str = "ui-chat-composer", before = null, on_send = null, on_typing = null, class: str? = null, children) {    style global chat_input_css    state draft = ""    state box = null    let ready = trim(draft) != ""    let send = () => {        if ready && on_send != null {            on_send(trim(draft))            draft = ""            box.value = ""            box.focus()        }    }    <form class={ui_cls(["ui-chat-composer", class])} method={method} action={action} style={`--ui-chat-composer-rows: ${rows}`}          @submit={on_send != null ? send : null}>        {children}        {before != null && <div.ui-chat-composer-before>{before}</div>}        <label.ui-sr for={id}>{placeholder}</label>        <textarea.ui-chat-composer-box id={id} name={name} rows="1" placeholder={placeholder} required={true} disabled={disabled}                  autofocus={autofocus} enterkeyhint="send" autocomplete="off"                  @mount={(el) => { box = el }}                  @input={(e) => {                      draft = e.value                      if trim(e.value) != "" && on_typing != null { on_typing() }                  }}                  @keydown={(e) => {                      if e.key == "Enter" && !e.shiftKey && !e.isComposing {                          e.preventDefault()                          if trim(e.target.value) != "" {                              if on_send != null { send() } else { e.target.form.requestSubmit() }                          }                      }                  }}></textarea>        <button.ui-chat-composer-send type="submit" aria-label={send_label} title={send_label} disabled={disabled || !ready}>            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                 stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>        </button>    </form>}
Thread
66Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.