Events and forms
Events are native DOM events, not synthetic ones, so event.persist() is gone and event.target is typed as an Element.
Events are native DOM events, not synthetic ones, so event.persist() is gone and event.target is typed as an
Element. These events are delegated to one listener per event name on the document: click, dblclick, input, change,
keydown, keyup, keypress, pointerdown, pointerup, pointermove, mousedown, mouseup, mousemove,
focusin, focusout, beforeinput, touchstart, touchend, touchmove and contextmenu. Every other event
(onSubmit, onFocus, onBlur, onScroll, onMouseEnter, ...) is a native listener on the element.
Handlers run in a batch: effects flush when the event finishes, so inside the handler the DOM still shows the old state.
React's event names keep React's meaning, so a React form ports unchanged:
onChangefollows React. On a text input or a textarea it fires on every edit (theinputevent). On a select, a checkbox, a radio or a file input it fires when the choice is committed (thechangeevent).onchange(lower case) is the nativechangeevent. On a text input it fires when the value is committed (on blur or Enter). Use the React spelling for React code and the lower-case one for DOM code.onFocusandonBlurbubble, as in React: they are thefocusinandfocusoutevents, so a wrapper sees its inputs get and lose focus.onfocusandonblurare the native, non-bubbling events.onWheelis passive, so a handler cannot cancel scrolling, as in React. Every...Capturespelling (onClickCapture,onFocusCapture) binds the capture phase.- The handler expression is evaluated once.
onClick={cond() ? a : b}binds the handler that was chosen when the element was created. Branch inside the handler instead. - Two handlers for one event on one element both run.
onInputandonChangeon the same text input run in the order they are written.
The [handler, data] tuple form calls handler(data, event) and avoids a closure per row.
import { useStore } from "@shadow-js/core";
export function Signup(props: { onSave: (value: { name: string; subscribed: boolean; plan: string }) => void }) {
const [name, setName] = useStore("");
const [subscribed, setSubscribed] = useStore(false);
const [plan, setPlan] = useStore("free");
return (
<form
onSubmit={(event) => {
event.preventDefault();
props.onSave({ name: name(), subscribed: subscribed(), plan: plan() });
setName("");
}}
>
<label>
Name
<input value={name()} onInput={(event) => setName(event.currentTarget.value)} />
</label>
<label>
<input
type="checkbox"
checked={subscribed()}
onChange={(event) => setSubscribed(event.currentTarget.checked)}
/>
Subscribe
</label>
<select value={plan()} onChange={(event) => setPlan(event.currentTarget.value)} aria-label="Plan">
<option value="free">Free</option>
<option value="pro">Pro</option>
</select>
<button type="submit">Save</button>
</form>
);
}
A controlled input is value={signal()} plus onInput: writing the signal from anywhere updates the field,
and typing updates the signal.
class, className, for and htmlFor#
The compiler accepts the React names as aliases, so existing markup compiles unchanged. In the type definitions,
class and className are both props of every element, and for and htmlFor are props of label and
output. Both spellings write the same attribute. class also takes arrays and objects (clsx style), so
clsx and cn helpers are often unnecessary; classList toggles by key and style takes a string or an object
in camelCase or kebab-case.
export function Attributes() {
return (
<div
class={["a", { b: true, c: 1, d: false }]}
style={{ backgroundColor: "red", "margin-top": "4px" }}
>
<label htmlFor="x">X</label>
<label for="y">Y</label>
<input id="x" readOnly tabIndex={2} />
</div>
);
}
Other camelCase spellings, such as tabIndex and readOnly above, compile to the lower-case attribute. HTML
attribute names are always the lower-case name (colSpan is colspan, httpEquiv is http-equiv), in the markup
on the server as in the DOM. SVG follows the SVG names: a presentation attribute takes its hyphenated name
(strokeWidth is stroke-width, fillOpacity is fill-opacity, stopColor is stop-color), xlinkHref is
xlink:href, xmlLang is xml:lang, and names that SVG spells in camelCase keep their case (viewBox,
gradientUnits). The React spelling and the SVG spelling both type-check.
dangerouslySetInnerHTML={{ __html }} is innerHTML: a property write of the markup, so use it for trusted HTML
only. suppressHydrationWarning and suppressContentEditableWarning are accepted and ignored.
key on an element is accepted and ignored: list identity belongs to <For key={...}>. On a component it is passed
as the component's key prop, which For and the head components (Style, Meta) read:
import { For } from "@shadow-js/web";
export function List(props: { items: { id: string; label: string }[] }) {
return (
<ul>
<For each={props.items} key={(item) => item.id}>
{(item) => <li key={item().id}>{item().label}</li>}
</For>
</ul>
);
}
export function Icon() {
return (
<svg viewBox="0 0 2 2">
<path strokeWidth="2" fillOpacity={0.5} stopColor="red" />
<use xlinkHref="#a" />
</svg>
);
}