Markup
The markup of a component is HTML with {…} expressions, attribute values without quotes, event handlers, and # blocks for control flow. Markup that never changes ships as HTML only.
Expressions
{expression} inserts a value. It updates when the values it reads change:
The sample runs when it comes into view.
- A string or a number is inserted as text. Text is always escaped, so
{'<b>'}shows the characters<b>. null,undefined,trueandfalseinsert nothing. This makes{error && error.message}safe.- An array inserts each of its items.
Attributes
An attribute takes a quoted string, a value without quotes, or an expression in parentheses:
<input type="text" value=draft maxlength=(limit - 1) disabled=(!editable)>
<img src=user.avatar alt=(`Photo of ${user.name}`)>
- A quoted value is a plain string.
- A value without quotes is a name, a property path or a call, such as
draft,user.avatarorformat(date). Numbers, objects, arrays and template strings work without parentheses too. - Anything else, such as an operator, needs parentheses:
(count > 0). nullandundefinedleave the attribute out.trueandfalsedepend on the attribute:- An attribute that says its state in its value gets the word:
aria-*anddata-*,contenteditable,draggable,spellcheckandwritingsuggestionssay"true"or"false",translatesays"yes"or"no", andautocompleteandautocorrectsay"on"or"off". Soaria-expanded=openrendersaria-expanded="false"whenopenisfalse. - Any other attribute, such as
disabledoropen, is there fortrue, without a value, and left out forfalse.
- An attribute that says its state in its value gets the word:
- An attribute without a value in the markup, such as
<input disabled>or<div data-ready>, renders as it is written.
Shorthands:
<a {href}>Open</a> <!-- the same as href=href -->
<button {onClick}>Go</button> <!-- the same as onClick=onClick, a handler -->
<input {...rest}> <!-- every property of rest as an attribute -->
<div [name]=value></div> <!-- the attribute name is an expression -->
class and style
class takes a string, an array, or an object whose keys are class names that apply while their values are true. style takes a string, or an object of properties in camel case:
The sample runs when it comes into view.
{ on } is the class on while on is true. In an array, such as class=(['task', on && 'on']), empty and false items are skipped. A style property whose value is null, undefined or false is left out.
Dynamic tags
<{tag}> renders the element or component that tag names. Close it with </>:
The sample runs when it comes into view.
When tag changes in the browser, the element renders again with the new tag.
A tag with no content can close itself, as a void element or a component with no children does:
<script>
import { $props } from 'brumate/signals';
const { as = 'hr' } = $props<{ as?: string }>();
</script>
<{as} class="divider" />
</> is the only closing tag of a dynamic tag. </{tag}> is an error.
HTML strings
Inserted text is escaped. raw from brumate/html inserts a string as HTML, such as the HTML of a Markdown file:
<script>
import { raw } from 'brumate/html';
import { html } from './post.md';
</script>
<article>{raw(html)}</article>
Insert only HTML that you trust. HTML from a user can run scripts in your page.
Events
onEvent=handler runs the handler when the event fires. The name after on is the DOM event, so onClick listens to click and onKeyDown to keydown. The handler gets the event:
The sample runs when it comes into view.
The handler is a name of a function, an arrow function in parentheses, or a call that returns a function. Its code loads with the page. An event that comes before the code has loaded waits for it.
Before the handler loads
A handler runs after the event has finished dispatching, so the browser has acted on the event by then. brumate/events wraps a handler with what has to happen at once:
| Wrapper | Effect |
|---|---|
prevent(handler) |
Calls event.preventDefault(). |
prevent(handler, ...keys) |
On a keyboard event, prevents only these keys. The handler still runs for every key. |
stop(handler) |
Stops the event at this element, so the handlers of the elements around it do not run. |
once(handler) |
Runs the handler for the first event only. |
capture(handler) |
Runs the handler before the handlers of the elements inside, as a listener in the capture phase. |
Handlers run as if each element had its own listener. Those wrapped by capture run first, from the outside in, and then the others, from the target outwards. An event that does not bubble, such as focus, blur, mouseenter or scroll, runs only the handlers of its target and the capture handlers around it. Listeners are passive unless a handler of the event prevents, so there is no passive wrapper.
<script>
import { prevent, stop } from 'brumate/events';
</script>
<ul role="listbox" tabindex="0" onKeyDown=(prevent(move, 'ArrowDown', 'ArrowUp', 'Home', 'End'))>
…
</ul>
<button onClick=(stop(prevent(save)))>Save</button>
- The compiler reads the wrappers in the markup and writes them into the page, so the loader applies them while the event dispatches, even before the code of the page has loaded. Calling
event.preventDefault()in the handler is too late. - Wrappers nest, in any order.
- Keys are strings of their
event.key: a named key such as'Enter','Escape','ArrowDown'or'F1', a letter or a digit, and'Space'for the space bar. TypeScript completes them, and the compiler reports a key that is not one. OnlypreventofonKeyDown,onKeyUpandonKeyPresstakes keys. - Write the wrapper in the attribute, with keys as string literals, so the compiler can read it.
- A key that an input method gets while it composes text, such as the Enter that ends a word in Japanese, is never prevented, because it belongs to the input method.
- A key alone is prevented only while Ctrl, Meta and Alt are up, so the shortcuts of the browser, such as Cmd+Left or Alt+Left to go back, keep working. Shift does not count. To prevent a key with a modifier held, name the modifier:
'Mod+ArrowDown'for Ctrl or Meta, or'Ctrl+…','Meta+…'or'Alt+…'. A list that moves focus with Ctrl and an arrow prevents'Mod+ArrowDown'too.
A modifier after the event name, as in onSubmit:prevent=send, does the same, with a warning that points to the wrapper. It goes in a later version. A modifier on a component is a compile error: the component puts the wrapper on the element that it renders, such as a listbox that prevents the keys it handles.
Animating a change
transition(handler) from brumate/events runs the handler inside a view transition. The browser animates from the page before the handler to the page after it, including elements that #if or #for removes and items that change order. CSS styles the animation.
The sample runs when it comes into view.
- The transition holds the writes that the handler makes before its first
await. Writes after it, such as after a server call, change the page without an animation. - When the code of the page has not loaded yet, the transition waits for it. The browser keeps the old page on screen until then.
transition(handler, ...types)gives the transition types, which CSS matches with:active-view-transition-type(type).- Without view transitions, or with
prefers-reduced-motion, the handler runs as it is. - It nests with the other wrappers, in any order:
transition(prevent(save)). - The browser runs one view transition at a time, and blocks clicks while it runs. Keep them short, and use CSS transitions for changes that repeat fast, such as typing.
Window and document events
<window> and <document> listen to events of the window and the document. They must close with />:
The sample runs when it comes into view.
Their listeners start when the page resumes, and stop when the component is removed.
Accessibility warnings
The compiler warns about common accessibility mistakes. The warnings show in the build, in the editor and in brumate-tsc:
- An
<img>withoutalt. Usealt=""for an image that only decorates. - A click handler on an element that a keyboard cannot use, such as a
<div>or an<li>, without aroleand a key handler. A<button>is usually the better element. - An
<input>,<select>or<textarea>without a label: a<label>around it, a<label for>that names itsid, oraria-label. A placeholder is not a label. - A link or a button without text or
aria-label. An image withalttext counts as text.
An element with a spread or a computed attribute is not checked, because any attribute can be in it. a11y: false in the plugin options turns the warnings off.
$id
$id() returns an ID that is unique in the page. Use it to connect a label or a description to a field:
The sample runs when it comes into view.
- The server and the browser make the same ID, so a component that the browser renders later gets the same IDs as it would on the server.
- Each instance of a component gets its own IDs, so a component can be used many times on a page.
- An ID has only letters, digits,
-and_, such asb3-0, so it also works in a CSS selector, as#b3-0. - In the script of a block, such as the body of a
#for, each item gets its own IDs:
The sample runs when it comes into view.