Lesson 40 of 45 · Web pages

Keyboard and forms

Mouse events start things. Keyboard and form events finish them: a search that filters as you type, a shortcut that saves, a message that says what is wrong with a field. This lesson goes through the events of keys, focus, text fields, forms, the clipboard and the elements that open and close, with an example you can try under each.

Keys: down, up and press

Pressing a key sends key down. If you keep it pressed the browser sends key down again and again, with a property repeat set to true on the repeats. Letting go sends key up. An older third event, key press, fires only for keys that produce a character and is deprecated, so write new code with key down and key up and leave key press to old pages.

Key events go to the element that has focus: a text field the user clicked, a button reached with Tab, or an element with tab index = 0. They then bubble up to its ancestors, as clicks do, so a handler on a wrapper box hears the keys typed into everything inside it. You cannot put a handler on the page itself, so a shortcut works only while the focus is somewhere inside the element that carries the handler.

A handler that wants the event is a named function with one i64 parameter, exactly as in the lesson on mouse events. dom.EventKey(e) returns the key as text. Other properties are read from the event with dom.GetStr and dom.GetBool. The example shows the two most useful: code and repeat. Click the field and press different keys, hold one down, and watch the lines change.

web-keys-echo.salam
import dom

func pressed(e: i64):
    dom.SetText(dom.ById("down"), "key down: " + dom.EventKey(e) + ", code " + dom.GetStr(e, "code"))
    if dom.GetBool(e, "repeat"):
        dom.SetText(dom.ById("note"), "The key is held, so the browser repeats it.")
    else:
        dom.SetText(dom.ById("note"), "")
    end
end

func released(e: i64):
    dom.SetText(dom.ById("up"), "key up: " + dom.EventKey(e))
    dom.SetText(dom.ById("note"), "")
end

page:
    title = "Which key?"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    input: id = "field" key down = pressed key up = released placeholder = "Click here, then press keys" width = "100%" padding = 10 font size = "1rem" box sizing = "border-box" end
    paragraph: id = "down" content = "key down: nothing yet" font family = monospace end
    paragraph: id = "up" content = "key up: nothing yet" font family = monospace end
    paragraph: id = "note" content = "" color = "#b45309" end
end

key is what the key means right now: a, A with Shift held, Enter, Escape, Tab, Backspace, a space is the one-character string " ", and the arrows are ArrowUp, ArrowDown, ArrowLeft and ArrowRight. code is which physical key it is, such as KeyA, whatever the keyboard layout. Use key when the character matters and code when the position matters, as in games. Modifiers are the booleans shiftKey, ctrlKey, altKey and metaKey (Command on a Mac), read with dom.GetBool(e, "ctrlKey").

With a Persian or any other non-Latin layout, key for the key labelled S is a Persian letter, not s, so Ctrl+S written as key == "s" never matches. For shortcuts compare code (KeyS), which does not change with the layout.

Enter, Escape, arrows and shortcuts

Most uses of key events are a small table: this key does that. The first one is a task list. Enter adds the text of the field as a new item, made with dom.Create, and empties the field. Escape only empties it. The handler checks that the field is not empty before it adds an item, with dom.GetValue(field).len() > 0.

web-keys-enter.salam
import dom

func typing(e: i64):
    field := dom.ById("task")
    key := dom.EventKey(e)
    if key == "Enter" and dom.GetValue(field).len() > 0:
        item := dom.Create("li")
        dom.SetText(item, dom.GetValue(field))
        dom.Append(dom.ById("list"), item)
        dom.SetValue(field, "")
    else key == "Escape":
        dom.SetValue(field, "")
    end
end

page:
    title = "Task list"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" max width = "28rem" end
    input: id = "task" key down = typing placeholder = "New task, then Enter" width = "100%" padding = 10 font size = "1rem" box sizing = "border-box" end
    paragraph: content = "Enter adds the task. Escape clears the box." color = "#64748b" font size = "0.9rem" end
    list: id = "list" end
end

The second moves a ball with the arrow keys. The box has tab index = 0, which makes it reachable with Tab and lets it have focus, so it receives the key events. Without that attribute a box never gets them. The handler ignores every other key by returning early, and for arrows it calls dom.PreventDefault(e), because the browser would otherwise scroll the page. The position lives in two mut variables and is written into left and top through dom.SetStyle.

web-keys-arrows.salam
import dom

mut x := 120
mut y := 60

func steer(e: i64):
    key := dom.EventKey(e)
    if key == "ArrowLeft":
        x -= 12
    else key == "ArrowRight":
        x += 12
    else key == "ArrowUp":
        y -= 12
    else key == "ArrowDown":
        y += 12
    else:
        ret
    end
    dom.PreventDefault(e)
    x = x < 0 ? 0 : (x > 270 ? 270 : x)
    y = y < 0 ? 0 : (y > 120 ? 120 : y)
    dom.SetStyle(dom.ById("ball"), "left", x + "px")
    dom.SetStyle(dom.ById("ball"), "top", y + "px")
end

page:
    title = "Arrow keys"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    box:
        id = "arena"
        tab index = 0
        key down = steer
        position = relative
        width = 300
        height = 150
        background = "#ecfeff"
        border = "2px solid #67e8f9"
        border radius = 10
        box: id = "ball" position = absolute left = 120 top = 60 width = 30 height = 30 border radius = "50%" background = "#0891b2" end
    end
    paragraph: content = "Click the box, then use the arrow keys." end
end

The third reads modifier keys. A wrapper box around a text area listens to key down for everything typed inside. When Ctrl (or Command) is held and the key is S, the handler prevents the default, which would open the browser's own save dialog, and runs its own action. Always prevent the default of a shortcut you take over, and never take over one the user depends on, such as Ctrl+C or Ctrl+W.

web-keys-shortcut.salam
import dom

func shortcut(e: i64):
    held := dom.GetBool(e, "ctrlKey") or dom.GetBool(e, "metaKey")
    key := dom.EventKey(e).lower()
    if held and key == "s":
        dom.PreventDefault(e)
        dom.SetText(dom.ById("status"), "Saved with Ctrl+S")
    else held and key == "enter":
        dom.PreventDefault(e)
        dom.SetText(dom.ById("status"), "Sent with Ctrl+Enter")
    end
end

page:
    title = "Shortcuts"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    box:
        id = "editor"
        key down = shortcut
        text area: id = "notes" rows = 5 placeholder = "Write here. Try Ctrl+S and Ctrl+Enter." width = "100%" padding = 10 font size = "1rem" box sizing = "border-box" end
    end
    paragraph: id = "status" content = "Nothing yet." color = "#047857" end
end

Focus and blur

An element has focus when it is the one that receives the keys. focus runs when an element gains focus, by a click, by Tab or from a script, and blur when it loses it. Together they are the natural place for a hint that appears next to the field being filled, and for checking a field when the user leaves it. The first example does both: the hint changes with the field, and leaving the name field empty paints it red.

web-keys-focus.salam
import dom

func hint(text: str):
    dom.SetText(dom.ById("hint"), text)
end

func check_name:
    if dom.GetValue(dom.ById("name")).len() == 0:
        hint("The name is required.")
        dom.SetStyle(dom.ById("name"), "border-color", "#dc2626")
    else:
        hint("Thanks.")
        dom.SetStyle(dom.ById("name"), "border-color", "")
    end
end

page:
    title = "Focus and blur"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    global: selector = "input" display = block width = "100%" padding = 10 margin = "0 0 10px 0" font size = "1rem" border = "2px solid #cbd5e1" border radius = 6 box sizing = "border-box" end
    input: id = "name" placeholder = "Full name" focus = (): hint("Your name as it is on your card.") end blur = check_name end
    input: id = "email" placeholder = "Email" focus = (): hint("We never share it.") end blur = (): hint("") end end
    paragraph: id = "hint" content = "Click a field." color = "#475569" end
end

focus and blur do not bubble, so a handler on a wrapper never hears them from the fields inside. Their two siblings focus in and focus out do bubble, so one handler on a form section or a card can follow the focus of everything in it. They are also the way to style a whole group by what is inside it. CSS can do that too, with the :focus-within selector, and where it can you should use it.

web-keys-focusin.salam
import dom

page:
    title = "A group that follows focus"
    style: content = "#group.active { border-color: #2563eb; background: #eff6ff }" end
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    global: selector = "input" display = block width = "100%" padding = 8 margin = "6px 0" font size = "1rem" box sizing = "border-box" end
    box:
        id = "group"
        focus in = (): dom.AddClass(dom.ById("group"), "active") end
        focus out = (): dom.RemoveClass(dom.ById("group"), "active") end
        padding = 14
        border = "2px solid #cbd5e1"
        border radius = 10
        transition = "background 0.2s"
        paragraph: content = "Delivery address" font weight = bold margin = 0 end
        input: id = "street" placeholder = "Street" end
        input: id = "city" placeholder = "City" end
    end
end

The group has one handler for each event. focus in adds the class that draws the blue border and focus out removes it. When you press Tab from one field to the next, focus out and focus in arrive one right after the other, before the browser draws anything, so the border does not flash. Inside a focus out handler, event.relatedTarget is the element that will receive the focus next, if you ever need to tell leaving the group from moving inside it.

Typing in a field: input, change and before input

A text field has several events for the same act, and the difference is the moment. input fires after every change to the value: a typed letter, a deletion, a paste, a drop, an undo. change fires when the user is finished: when Enter is pressed in a one line field, or when the field loses focus after its value was changed. before input fires first, before the value changes, and its handler may call preventDefault to refuse the edit. Type into the field below and press Enter, then type some more and click away, and compare the two counters.

web-keys-input.salam
import dom

mut inputs := 0
mut changes := 0

func show:
    dom.SetText(dom.ById("counts"), "input fired " + inputs + " times, change fired " + changes + " times")
end

page:
    title = "Input and change"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    input: id = "field" input = (): inputs += 1 show() end change = (): changes += 1 show() end placeholder = "Type, then press Enter or click away" width = "100%" padding = 10 font size = "1rem" box sizing = "border-box" end
    paragraph: id = "counts" content = "input fired 0 times, change fired 0 times" end
    paragraph: content = "input follows every keystroke. change waits until you are done." color = "#64748b" end
end

A handler for input is what live features are made of. A character counter shows how much is left. Setting maxlength on the text area makes the browser stop typing at the limit, and the counter only has to show the length. It turns red near the end.

web-keys-counter.salam
import dom

func count:
    n := dom.GetValue(dom.ById("msg")).len()
    left := dom.ById("left")
    dom.SetText(left, n + " / 80")
    if n >= 70:
        dom.SetStyle(left, "color", "#b91c1c")
    else:
        dom.SetStyle(left, "color", "#475569")
    end
end

page:
    title = "Character counter"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    text area: id = "msg" input = count maxlength = 80 rows = 3 placeholder = "A short message" width = "100%" padding = 10 font size = "1rem" box sizing = "border-box" end
    paragraph: id = "left" content = "0 / 80" text align = right margin = "4px 0" end
end

A search box filters a list on every keystroke. The handler reads the field, makes it lower case and goes through the items found by dom.QueryAll: a matching item gets display set to an empty string, which puts back its normal look, and the others get none. Empty text matches everything, so clearing the box brings all items back with no extra code.

web-keys-filter.salam
import dom

func filter:
    needle := dom.GetValue(dom.ById("q")).lower()
    items := dom.QueryAll("#fruits li")
    repeat dom.CountOf(items) in i:
        item := dom.ItemAt(items, i)
        if dom.GetText(item).lower().includes(needle):
            dom.SetStyle(item, "display", "")
        else:
            dom.SetStyle(item, "display", "none")
        end
    end
end

page:
    title = "Live search"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    input: id = "q" input = filter placeholder = "Search fruit" width = "100%" padding = 10 font size = "1rem" box sizing = "border-box" end
    list: id = "fruits"
        item: content = "Apple" end
        item: content = "Banana" end
        item: content = "Cherry" end
        item: content = "Mango" end
        item: content = "Orange" end
        item: content = "Pear" end
    end
end

The loop uses dom.CountOf for the number of items and dom.ItemAt for each one. QueryAll returns the elements that exist at that moment, so call it inside the handler, as here, and not once at the start, if items may be added later.

Selecting text

Three events report selection. select fires on a text field when the user selects some of its text, and not for a plain caret move. selection change fires whenever the selection or the caret in the field changes, which is also when the caret moves. select start fires when a new selection begins in any element, even one that is not editable, and a handler can call preventDefault to forbid the selection. The positions are the properties selectionStart and selectionEnd of the field, read with dom.GetNum.

web-keys-select.salam
import dom

func chosen:
    field := dom.ById("text")
    a := dom.GetNum(field, "selectionStart") as i64
    b := dom.GetNum(field, "selectionEnd") as i64
    dom.SetText(dom.ById("sel"), "select: " + (b - a) + " characters, from " + a + " to " + b)
end

func caret:
    at := dom.GetNum(dom.ById("text"), "selectionStart") as i64
    dom.SetText(dom.ById("caret"), "selection change: caret at " + at)
end

func started:
    dom.SetText(dom.ById("start"), "select start: a new selection began in the paragraph.")
end

page:
    title = "Selecting text"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    text area: id = "text" select = chosen selection change = caret rows = 3 content = "Select some of these words with the mouse or with Shift and the arrow keys." width = "100%" padding = 10 font size = "1rem" box sizing = "border-box" end
    paragraph: id = "sel" content = "select: nothing selected" end
    paragraph: id = "caret" content = "selection change: no caret yet" end
    paragraph: id = "para" select start = started content = "You can also select this paragraph." background = "#fef9c3" padding = 8 end
    paragraph: id = "start" content = "select start: nothing yet" end
end

Select with the mouse, or hold Shift and use the arrow keys. The lines under the text area show a count of characters and the caret position, and the yellow paragraph reports that a selection began in it. Support for selection change on a text field is newer than for the other two. A browser may send it only to the document, so test it in the browsers you care about.

Submitting, resetting and form data

A form sends submit when the user presses a submit button, or presses Enter in a field, and the browser's own checks have passed. Left alone, the browser then sends the fields to the server and loads the answer. A page that handles the form itself calls dom.PreventDefault(e) first and then reads the fields with dom.GetValue. Put the handler on the form, not on the button, because Enter in a field submits without clicking the button. The reset event fires when a reset button is pressed or the form is reset by script, just before the fields go back to their starting values.

web-keys-submit.salam
import dom

func send(e: i64):
    dom.PreventDefault(e)
    who := dom.GetValue(dom.ById("who"))
    mail := dom.GetValue(dom.ById("mail"))
    dom.SetText(dom.ById("result"), "Thank you, " + who + ". We will write to " + mail + ".")
end

func cleared:
    dom.SetText(dom.ById("result"), "The form was cleared.")
end

page:
    title = "Sign up"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    global: selector = "input" display = block width = "100%" padding = 10 margin = "0 0 10px 0" font size = "1rem" box sizing = "border-box" end
    global: selector = "button" padding = "8px 16px" margin = "0 8px 0 0" font size = "1rem" end
    form:
        id = "signup"
        submit = send
        reset = cleared
        input: id = "who" placeholder = "Your name" end
        input: id = "mail" type = email placeholder = "Your email" end
        button: id = "go" type = submit content = "Sign up" end
        button: id = "clear" type = reset content = "Clear" end
    end
    paragraph: id = "result" content = "" color = "#047857" end
end

Give your fields and buttons ids that differ from the names of your functions. Inside a form, the browser looks up a name among the form's fields and buttons first, so a button with id = "send" hides a function called send and the handler reports "is not a function".

The third form event is form data. It fires every time the browser builds the list of fields of a form, which a submit does, and so does a script that writes new FormData(form). A handler gets event.formData and may add or change entries before they are used. The example adds a hidden field when the button builds the data.

web-keys-formdata.salam
import dom

func extra(e: i64):
    data := dom.GetProp(e, "formData")
    _r := dom.CallS2(data, "append", "source", "newsletter")
    dom.SetText(dom.ById("out"), "form data: the form added a hidden field named source.")
end

page:
    title = "Form data"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    form:
        id = "news"
        form data = extra
        input: name = "email" value = "sara@example.com" padding = 10 font size = "1rem" end
        button: id = "build" type = button click = "new FormData(document.getElementById('news'))" content = "Build the form data" padding = "8px 16px" margin = "0 0 0 8px" end
    end
    paragraph: id = "out" content = "Nothing built yet." end
end

The button uses a short JavaScript string, because the dom helpers have no way to write new. The handler is a normal Salam function: dom.GetProp(e, "formData") gives the FormData object and dom.CallS2 calls its append method with a name and a value.

Checking fields: invalid

HTML can check fields without a line of script: required, type = email, min, max, pattern and maxlength all take part. When the browser checks a field and it fails, the field gets an invalid event. A check runs when the form is submitted, and also when a script calls checkValidity() or reportValidity() on the form or on a field. The second one also shows the browser's bubble with the message. invalid does not bubble, so the handler goes on the field itself.

The handler below replaces the browser's message with its own by calling setCustomValidity on the field. It looks at the field's validity object to tell an empty field from a wrong one. The message stays on the field until you remove it, so another handler, on input, sets it back to an empty string every time the user types. If you forget that, the field stays invalid forever, even after the user fixes it.

web-keys-validate.salam
import dom

func explain(e: i64):
    field := dom.EventTarget(e)
    if dom.GetBool(dom.GetProp(field, "validity"), "valueMissing"):
        _a := dom.CallS1(field, "setCustomValidity", "Please enter your email.")
    else:
        _b := dom.CallS1(field, "setCustomValidity", "That does not look like an email address.")
    end
    dom.SetText(dom.ById("why"), "invalid: " + dom.GetStr(field, "validationMessage"))
end

func retry:
    _c := dom.CallS1(dom.ById("mail"), "setCustomValidity", "")
end

func check_mail:
    _d := dom.Call0(dom.ById("form"), "reportValidity")
    if dom.GetBool(dom.GetProp(dom.ById("mail"), "validity"), "valid"):
        dom.SetText(dom.ById("why"), "The address is valid.")
    end
end

page:
    title = "Custom messages"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    form:
        id = "form"
        input: id = "mail" type = email required = true invalid = explain input = retry placeholder = "Your email" width = "100%" padding = 10 font size = "1rem" box sizing = "border-box" end
        button: id = "recheck" type = button click = check_mail content = "Check" margin = "10px 0 0 0" padding = "8px 16px" end
    end
    paragraph: id = "why" content = "Press Check." end
end

Checks in the browser are help for the user, not protection. Anybody can send a request with any values, so the server must check everything again.

Copy, cut and paste

The three clipboard events fire when the user copies, cuts or pastes by keyboard or menu. They fire on the element that has the selection or the focus, and they bubble. copy and cut run before the browser does its part, so a handler can call preventDefault and put its own text on the clipboard with clipboardData.setData. paste gives the pasted content in event.clipboardData, and getData("text") is the plain text. A handler may prevent the default and insert a cleaned up version instead. A page cannot read the clipboard at any other time without the user's permission, and that is intended.

web-keys-clipboard.salam
import dom

mut copies := 0
mut cuts := 0

func counts:
    dom.SetText(dom.ById("stats"), "copied " + copies + " times, cut " + cuts + " times")
end

func copied:
    copies += 1
    counts()
end

func snipped:
    cuts += 1
    counts()
end

func pasted(e: i64):
    text := dom.CallS1(dom.GetProp(e, "clipboardData"), "getData", "text")
    dom.SetProp(dom.ById("last"), "textContent", text)
end

page:
    title = "Clipboard"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    input: id = "field" copy = copied cut = snipped paste = pasted value = "Copy, cut or paste in this box" width = "100%" padding = 10 font size = "1rem" box sizing = "border-box" end
    paragraph: id = "stats" content = "copied 0 times, cut 0 times" end
    paragraph:
        content = "Last pasted text: "
        span: id = "last" content = "none" font weight = bold end
    end
end

The paste handler passes the string to the page without touching it: dom.CallS1 returns it as a JavaScript value, and dom.SetProp sets the textContent of the span with it. Select all in the box with Ctrl+A, then copy, cut and paste, and watch the counters and the last line.

Details, popovers, dialogs and find in page

Some elements open and close by themselves, and they have events for it. toggle fires on a details element after it opens or closes, and on an element with the popover attribute after it is shown or hidden. A popover event carries newState, which is the text open or closed. The event fires a moment after the change, not at the same time. before toggle fires first, can be cancelled, and exists for popovers (and, in newer browsers, for dialogs). The example below shows the state that is about to start, newState, before the popover changes, and the state it reached after.

web-keys-toggle.salam
import dom

func opened:
    if dom.GetBool(dom.ById("more"), "open"):
        dom.SetText(dom.ById("dstate"), "The details are open.")
    else:
        dom.SetText(dom.ById("dstate"), "The details are closed.")
    end
end

func popped(e: i64):
    dom.SetText(dom.ById("pstate"), "The popover is now " + dom.GetStr(e, "newState") + ".")
end

func about_to(e: i64):
    dom.SetText(dom.ById("before"), "before toggle: about to be " + dom.GetStr(e, "newState") + ".")
end

page:
    title = "Toggle"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    details:
        id = "more"
        toggle = opened
        summary: content = "More about Salam" end
        paragraph: content = "Salam is a language for the web and for programs." end
    end
    paragraph: id = "dstate" content = "The details are closed." end
    button: popover target = "tip" content = "Show a tip" padding = "8px 16px" end
    box: id = "tip" popover = auto before toggle = about_to toggle = popped content = "Tips close when you press Escape." padding = 14 border radius = 8 end
    paragraph: id = "before" content = "No popover yet." end
    paragraph: id = "pstate" content = "" end
end

A dialog opened with showModal() is shown over the page and blocks everything behind it until it closes. dom.Call0(dialog, "showModal") opens it. When the user presses Escape the dialog sends cancel, which can be prevented to keep it open, and then it closes and sends close. A dialog closed by code, with dom.CallS1(dialog, "close", "deleted"), sends only close, and the text you passed is its returnValue.

web-keys-dialog.salam
import dom

func open_dialog:
    dom.SetText(dom.ById("log"), "Dialog is open.")
    _r := dom.Call0(dom.ById("dlg"), "showModal")
end

func canceled:
    dom.SetText(dom.ById("log"), "cancel fired (Escape was pressed).")
end

func finished:
    dom.SetText(dom.ById("end"), "close fired, return value: " + dom.GetStr(dom.ById("dlg"), "returnValue"))
end

func confirm_it:
    _r := dom.CallS1(dom.ById("dlg"), "close", "deleted")
end

page:
    title = "Dialog"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    button: id = "open" click = open_dialog content = "Delete the file" padding = "8px 16px" end
    dialog:
        id = "dlg"
        cancel = canceled
        close = finished
        paragraph: content = "Delete this file for good?" end
        button: id = "yes" click = confirm_it content = "Delete" padding = "6px 14px" end
    end
    paragraph: id = "log" content = "Nothing yet." end
    paragraph: id = "end" content = "" end
end

The last event is before match. An element with hidden = "until-found" is not drawn, but its text stays searchable. When the user finds it with the browser's find in page, or opens a link to its id, the browser shows it and sends before match just before. A handler can open the section around it, such as a closed accordion, so the match is visible. In the example, try Ctrl+F and look for pomegranate, or open the page with #secret at the end of its address. Not all browsers support it yet; in those the text simply stays hidden until a script shows it. Chrome does not run an inline onbeforematch attribute even though it fires the event, so this one event is registered from a script element with addEventListener, as the example does. The attribute before match is the spelling the standard defines, and where a browser honours it you can use it instead.

web-keys-match.salam
page:
    title = "Find in page"
    global: selector = "body" font family = "system-ui, sans-serif" margin = "1.5rem" end
    paragraph: id = "state" content = "The text below stays hidden until the browser finds it." end
    box: id = "secret" hidden = "until-found" content = "The secret word is pomegranate." padding = 14 background = "#fef3c7" border radius = 8 end
    script: content = "document.getElementById('secret').addEventListener('beforematch', function () { document.getElementById('state').textContent = 'before match fired: the browser is revealing the text'; });" end
end

Reference: every keyboard and form event

Each row gives the attribute, the HTML attribute it becomes and when it fires. The snippets are checked by the compiler. Events that need the event object itself use a JavaScript string in these small snippets. The snippet for before match uses a script element for the reason given in the last section.

AttributeWhen it firesExample
key down
فشردن کلید
onkeydown
Runs when a key goes down on the focused element, and again for each repeat while the key is held (repeat is then true). dom.EventKey(e) is the key, such as a, Enter or ArrowUp.input: key down = (): dom.SetText(dom.ById("out"), "A key went down") end placeholder = "Press a key here" end paragraph: id = "out" content = "-" end
key up
رها کردن کلید
onkeyup
Runs when the key comes back up. There is exactly one key up for each press, however long the key was held.input: key up = (): dom.SetText(dom.ById("out"), "A key came up") end placeholder = "Press and release a key" end paragraph: id = "out" content = "-" end
key press
فشار کلید
onkeypress
Deprecated. Fires after key down for keys that produce a character, and not for Escape or the arrows. Use key down in new code.input: key press = (): dom.SetText(dom.ById("out"), "A character key was pressed") end placeholder = "Type a letter" end paragraph: id = "out" content = "-" end
focus
فوکوس
onfocus
Runs when the element gains focus, by a click, the Tab key or a script. It does not bubble; focus in is the bubbling version.input: focus = (): dom.SetText(dom.ById("out"), "The field has focus") end placeholder = "Click or Tab into me" end paragraph: id = "out" content = "-" end
blur
از دست رفتن فوکوس
onblur
Runs when the element loses focus. A good moment to check what the user typed. It does not bubble.input: blur = (): dom.SetText(dom.ById("out"), "The field lost focus") end placeholder = "Click in, then click away" end paragraph: id = "out" content = "-" end
focus in
ورود فوکوس
onfocusin
Like focus, but it bubbles, so one handler on a group hears the focus of every field inside.box: focus in = (): dom.SetText(dom.ById("out"), "Focus is inside the group") end input: placeholder = "First" end input: placeholder = "Second" end end paragraph: id = "out" content = "-" end
focus out
خروج فوکوس
onfocusout
Like blur, but it bubbles. event.relatedTarget is the element receiving the focus next, which lets you tell leaving the group from moving inside it.box: focus out = (): dom.SetText(dom.ById("out"), "Focus left a field of the group") end input: placeholder = "First" end input: placeholder = "Second" end end paragraph: id = "out" content = "-" end
input
ورودی
oninput
Runs after every change to the value of a field: typing, deleting, pasting, dropping, undoing. The right event for live features such as counters and filters.input: input = (): dom.SetText(dom.ById("out"), "You typed " + dom.GetValue(dom.ById("box")).len() + " characters") end id = "box" placeholder = "Type here" end paragraph: id = "out" content = "-" end
before input
پیش از ورودی
onbeforeinput
Runs before the value changes and can be cancelled, which refuses the edit. event.data holds the text about to be inserted.input: before input = "if (event.data === ' ') { event.preventDefault(); }" placeholder = "No spaces allowed" end
change
تغییر
onchange
Runs when the user has finished a change: Enter in a one line field, or the field losing focus after its value changed. For checkboxes, radio buttons and lists it runs at once.input: change = (): dom.SetText(dom.ById("out"), "The value was committed") end placeholder = "Type, then click away" end paragraph: id = "out" content = "-" end
select
انتخاب
onselect
Runs on a text field when the user selects some of its text. The range is in the properties selectionStart and selectionEnd.input: select = (): dom.SetText(dom.ById("out"), "Some text was selected") end value = "Select part of this text" end paragraph: id = "out" content = "-" end
selection change
تغییر انتخاب
onselectionchange
Runs when the selection or the caret changes in a text field. Support on the field itself is newer; some browsers send it only to the document.text area: selection change = (): dom.SetText(dom.ById("out"), "The caret moved") end content = "Click inside this text" end paragraph: id = "out" content = "-" end
select start
شروع انتخاب
onselectstart
Runs when the user starts a new selection, in any element, editable or not. It can be cancelled to forbid selecting.paragraph: select start = (): dom.SetText(dom.ById("out"), "A selection began") end content = "Select some of this text." end paragraph: id = "out" content = "-" end
submit
ارسال
onsubmit
Runs when a form is submitted by its submit button or by Enter in a field, after the browser's checks passed. Call dom.PreventDefault(e) to stop the browser from sending the form and loading a new page.form: submit = "event.preventDefault(); document.getElementById('out').textContent = 'Sent: ' + this.elements.name.value" input: name = "name" value = "Sara" end button: content = "Send" end end paragraph: id = "out" content = "-" end
reset
بازنشانی
onreset
Runs when the form is reset, by a reset button or by form.reset(), just before the fields go back to their starting values. It can be cancelled.form: reset = (): dom.SetText(dom.ById("out"), "The form was reset") end input: value = "Change me" end button: type = reset content = "Reset" end end paragraph: id = "out" content = "-" end
invalid
نامعتبر
oninvalid
Runs on a field that fails the browser's checks (required, type = email, pattern, ...) when the form is submitted or when checkValidity() or reportValidity() is called. It does not bubble, so it goes on the field.form: input: required = true invalid = (): dom.SetText(dom.ById("out"), "This field is required") end placeholder = "Required" end button: type = button click = "this.form.reportValidity()" content = "Check" end end paragraph: id = "out" content = "-" end
form data
داده فرم
onformdata
Runs on a form each time the browser builds its field list: on a submit, and on new FormData(form). event.formData may be changed before it is used.form: id = "f" form data = (): dom.SetText(dom.ById("out"), "The form data was built") end input: name = "q" value = "x" end button: type = button click = "new FormData(document.getElementById('f'))" content = "Build" end end paragraph: id = "out" content = "-" end
copy
رونوشت
oncopy
Runs when the user copies. A handler may call preventDefault and set its own text on the clipboard with clipboardData.setData.input: copy = (): dom.SetText(dom.ById("out"), "Copied") end value = "Select me and press Ctrl+C" end paragraph: id = "out" content = "-" end
cut
برش
oncut
Runs when the user cuts the selection, before the text is removed.input: cut = (): dom.SetText(dom.ById("out"), "Cut") end value = "Select me and press Ctrl+X" end paragraph: id = "out" content = "-" end
paste
چسباندن
onpaste
Runs when the user pastes, before the text is inserted. event.clipboardData.getData('text') is the pasted text.input: paste = (): dom.SetText(dom.ById("out"), "Pasted") end placeholder = "Press Ctrl+V here" end paragraph: id = "out" content = "-" end
toggle
تغییر حالت
ontoggle
Runs after a details element opens or closes, and after a popover is shown or hidden (newState is open or closed). It fires a moment after the change.details: toggle = (): dom.SetText(dom.ById("out"), "The details changed") end summary: content = "More" end paragraph: content = "Hidden text" end end paragraph: id = "out" content = "-" end
before toggle
پیش از تغییر حالت
onbeforetoggle
Runs just before a popover (and, in newer browsers, a dialog) opens or closes, and can be cancelled. event.newState is the state about to start.button: popover target = "tip" content = "Show" end box: id = "tip" popover = auto before toggle = (): dom.SetText(dom.ById("out"), "About to change") end content = "A popover" end paragraph: id = "out" content = "-" end
cancel
انصراف
oncancel
Runs on a dialog when the user asks to close it with Escape. It can be cancelled to keep the dialog open. A dialog closed by code does not send it.button: click = "document.getElementById('d').showModal()" content = "Open" end dialog: id = "d" cancel = (): dom.SetText(dom.ById("out"), "Escape was pressed") end paragraph: content = "Press Escape" end end paragraph: id = "out" content = "-" end
close
بسته شدن
onclose
Runs on a dialog after it has closed, by Escape, by close() or by a form with method = dialog. Its returnValue is the text passed to close().button: click = "document.getElementById('d').showModal()" content = "Open" end dialog: id = "d" close = (): dom.SetText(dom.ById("out"), "The dialog closed") end button: click = "this.parentNode.close('ok')" content = "Close" end end paragraph: id = "out" content = "-" end
before match
پیش از تطبیق
onbeforematch
Runs on an element with hidden = "until-found" just before the browser reveals it because find in page or a link to its id matched. A handler can open the section around it. Chrome fires the event but ignores the inline attribute, so register it from a script element with addEventListener('beforematch', ...).paragraph: id = "out" content = "Find the word below with Ctrl+F." end box: id = "s" hidden = "until-found" content = "Hidden word: pomegranate" end script: content = "document.getElementById('s').addEventListener('beforematch', function () { document.getElementById('out').textContent = 'Found it'; });" end

Try it yourself

  • Make the task list refuse an empty task and show a red message under the field instead. Clear the message the next time the user types.
  • Let the ball in the arrow example move with the keys W, A, S and D as well, using code so that it works on every layout.
  • Add to the search box a line that says how many items match. Update it from the same handler.
  • Write a sign up form that needs two passwords. On input in either field, compare them and use setCustomValidity so that the form cannot be sent while they differ.