要素
組み込み要素の props は lib.dom.d.ts から生成しているので、エディタで補完が効き、型もチェックされます。要素の書き込み可能なプリミティブ型の DOM プロパティ(id、disabled、value、tabIndex など)にはその型の Bindable を、aria-* には文字列・数値・真偽値("true" / "false" と書き込みます)の Bindable を、data-* には Bindable<string> を渡します。イベントにはそれぞれのハンドラを渡します。
属性とプロパティ
Section titled “属性とプロパティ”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>);- ほとんどの props は
setAttributeで書き込みます。ただしvalue・checked・selected・indeterminateのように、ユーザーの操作で属性と実際の値がずれていくものは、DOM のプロパティとして書き込みます(element.value = v)。 <input>のcheckedとindeterminateは、束縛すると制御されたものになります。changeの後、次のフレームが描画される前に束縛中の値で描き直すので、onChangeで受け取らなかった変更は元に戻ります。上のチェックボックスなら、toggleAgreedを呼ばなければ切り替わりません。ユーザーが自由に切り替えるチェックボックスにはdefaultCheckedを渡します。ラジオボタンのグループでは、すべてのボタンを束縛します。disabledなどの真偽属性はBindable<boolean>を受け取り、値がfalseの間は属性を外します。
class と style
Section titled “class と 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>);classには、1 つのBindable<string>か、クラス名ごとにBindable<boolean>を持つオブジェクトのどちらかを渡します。オブジェクトの場合はclassList.toggleでクラスを 1 つずつ切り替えます。1 つの要素で両方を使うことはできません。クラス文字列全体を書き込むと、オブジェクトで切り替えたクラスが消えてしまうからです。styleには、cssTextの文字列か、CSS プロパティごとにBindable<string>を持つオブジェクトのどちらかを渡します。各プロパティはstyle.setPropertyで書き込むので、キーはハイフン区切りの名前('background-color'、'--accent')で書きます。
イベントと ref
Section titled “イベントと 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>);- ハンドラの名前は、
onのあとにイベント名を PascalCase でつなげたものです(onClick、onInput、onPointerDown)。currentTargetは要素の型に絞り込まれています。ハンドラには関数を渡し、Observable は渡せません。束縛したスコープが破棄されると、ハンドラも外れます。 refは、要素を作った直後、何かを束縛したり子をマウントしたりする前に、要素を引数にして 1 回だけ呼ばれます。
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>);SVGElementTagNameMap のうち HTML にないタグは、SVG の名前空間で作ります。SVG のプロパティは SVGAnimatedLength などの型なので、HTML のように lib.dom.d.ts から props の型を読み取ることができません。そこで SVG の要素が受け付ける属性は一覧で決めてあり、どれも Bindable<number | string> を受け取ります。a・script・style・title は HTML にもあるタグなので、常に HTML の要素として作ります。