Elements
The props of an intrinsic element are generated from lib.dom.d.ts, so your editor completes them and checks their types. Each writable primitive DOM property of the element (id, disabled, value, tabIndex, …) takes a Bindable of its type, aria-* takes a Bindable of a string, a number or a boolean (written as "true" / "false"), data-* takes a Bindable<string>, and each event gets its own handler.
Attributes and properties
Section titled “Attributes and properties”import { createBooleanState, createState, map } from 'synstate';import { constant, type JSX } from 'synstate-dom';
const [name$, setName] = createState('');
const [agreed$, { toggle: toggleAgreed }] = createBooleanState(false);
export const SignUp = (): JSX.Element => ( <div id={constant('sign-up')} data-step={constant('1')}> {/* `value` and `checked` are written as properties, which is what stays in step with the user's input. */} <input value={name$} placeholder={constant('Your name')} aria-label={constant('Name')} onInput={(event) => { setName(event.currentTarget.value); }} /> <input type={constant('checkbox')} checked={agreed$} onChange={() => { toggleAgreed(); }} /> {/* A boolean attribute is removed while its value is `false`. */} <button type={constant('submit')} disabled={agreed$.pipe(map((agreed) => !agreed))} > {constant('Sign up')} </button> </div>);- Most props are written with
setAttribute. A few, such asvalue,checked,selectedandindeterminate, have an attribute that drifts apart from the live value as the user interacts; those are written as DOM properties instead (element.value = v). - A bound
checkedorindeterminateof an<input>is controlled. After achange, the input is drawn as its bound value again before the next frame is painted, so a change youronChangedoes not take undoes itself, as in the checkbox above iftoggleAgreedwere not called. To let the user toggle a checkbox freely, give itdefaultCheckedinstead. In a radio group, bind every button. - A boolean attribute such as
disabledtakes aBindable<boolean>and is removed while its value isfalse.
class and style
Section titled “class and style”import { createBooleanState, createState, map } from 'synstate';import { constant, type JSX } from 'synstate-dom';
const [isActive$] = createBooleanState(false);
const [progress$] = createState(0.3);
export const Item = (): JSX.Element => ( // One `Bindable<boolean>` per class name: each is toggled on its own. <li class={{ item: constant(true), active: isActive$ }}> {/* Or the whole class as one string. Never both on one element. */} <span class={isActive$.pipe(map((active) => (active ? 'on' : 'off')))}> {constant('status')} </span> {/* One `Bindable<string>` per hyphenated CSS property name. */} <div style={{ width: progress$.pipe(map((p) => `${p * 100}%`)), 'background-color': constant('teal'), }} /> </li>);classtakes either oneBindable<string>, or an object with oneBindable<boolean>per class name, where each class is toggled on its own withclassList.toggle. Do not use both on one element: writing the whole class string would wipe out the classes the object toggled.styletakes either acssTextstring, or an object with oneBindable<string>per CSS property. Each property is written withstyle.setProperty, so the keys are the hyphenated names ('background-color','--accent').
Events and ref
Section titled “Events and ref”import { createState, map } from 'synstate';import { constant, type JSX } from 'synstate-dom';
const [position$, setPosition] = createState('');
const [width$, setWidth] = createState(0);
const resizeObserver = new ResizeObserver((entries) => { setWidth(entries[0]?.contentRect.width ?? 0);});
export const Pad = (): JSX.Element => ( <div ref={(element) => { // Called once, right after the element is created and before anything is // bound or its children are mounted. resizeObserver.observe(element); }} onPointerMove={(event) => { // `currentTarget` is narrowed to the element: HTMLDivElement here. const rect = event.currentTarget.getBoundingClientRect();
setPosition( `${Math.round(event.clientX - rect.left)}, ${Math.round(event.clientY - rect.top)}`, ); }} > {constant('Pointer at ')} {position$} {width$.pipe(map((w) => ` in a pad ${Math.round(w)}px wide`))} </div>);- A handler’s name is
onfollowed by the event name in PascalCase:onClick,onInput,onPointerDown.currentTargetis narrowed to the element’s type. A handler is always a function, never an observable, and it is removed when the scope it was bound in is disposed. refis called once with the element, right after it is created and before anything is bound or its children are mounted.
import { createState, map } from 'synstate';import { constant, type JSX } from 'synstate-dom';
const [radius$] = createState(10);
// `svg` and `circle` are created in the SVG namespace.export const Dot = (): JSX.Element => ( <svg viewBox={constant('0 0 100 100')} width={constant(100)}> <circle cx={constant(50)} cy={constant(50)} r={radius$} fill={radius$.pipe(map((r) => (r > 30 ? 'tomato' : 'teal')))} /> </svg>);Tags from SVGElementTagNameMap that HTML does not have are created in the SVG namespace. Their props cannot be read off lib.dom.d.ts the way HTML ones are, because SVG properties are SVGAnimatedLength and the like. Instead, SVG elements take a fixed list of attributes, each a Bindable<number | string>. a, script, style and title exist in HTML too, so they are always created as HTML elements.