フォーム
コントロールはデフォルトで非制御とし、五種類の書き込み可能な signal アダプターを必要に応じて使います。
デフォルトでは非制御
明示的なモデルを渡さない限り、コントロールはブラウザのネイティブな状態を使います。静的な value と checked の props は初期値を設定します。リアクティブな value と checked は、後述する表示専用の value バインディングを除き、TypeScript(zfb check)でもレンダリング時にも拒否されます。コントロールから signal を読み書きする場合は model prop を使います。
予約済みの props は defaultValue、defaultChecked、modelValue、modelChecked です。HTML 属性にはなりません。モデルと対応する default、value と defaultValue、checked と defaultChecked を同時に指定するとエラーになります。
defaultValue を使えるのは、type を省略した input、type が text、search、email、url、tel、password の input、textarea、単一選択の select です。defaultChecked を使えるのは checkbox と radio の input です。これらは signal のバインディングではなく初期値です。広い string 型の input type は非対応の type になる可能性があるため、どちらの default prop も使えません。対応するリテラル型だけからなる union なら、その default prop を使えます。color、number、range、date、time など、それ以外の input type では、そのコントロールが対応していれば静的な value 属性を使います。この制限はサーバーレンダリング、mount、hydration のいずれにも適用されます。
書き込み可能な signal をバインドする
テキスト入力、textarea、checkbox、単一選択の select、radio グループでモデルを使えます。
| コントロール | モデル | サーバー出力と初期値 |
|---|---|---|
| テキスト入力(type の省略を含む)、search、email、URL、tel、password | 書き込み可能な文字列の signal を modelValue に指定 | エスケープ済みの value 属性 |
| textarea | 書き込み可能な文字列の signal を modelValue に指定 | エスケープ済みのテキスト内容 |
| checkbox 入力 | 書き込み可能な真偽値の signal を modelChecked に指定 | 真偽値の checked 属性 |
| 単一選択の select | 書き込み可能な文字列の signal を modelValue に指定 | 一致する静的な option の selected |
| radio グループ | 共有する書き込み可能な文字列または null の signal を modelValue に指定 | 一致する静的な選択肢の checked。null ならどれも選択しない |
対応するイベントは、テキスト入力と textarea では input、checkbox、select、radio では change です。radio のモデルはグループ内で共有します。textarea のデフォルト内容の先頭にある改行は、HTML の解析後も保たれます。
import { signal } from "@takazudo/zfb/zudo-react";
function Preferences() {
const displayName = signal("Ada");
const updatesEnabled = signal(true);
const delivery = signal<string | null>("email");
return (
<form>
<label>
Display name
<input modelValue={displayName} />
</label>
<label>
<input type="checkbox" modelChecked={updatesEnabled} />
Email me updates
</label>
<label>
<input type="radio" name="delivery" value="email" modelValue={delivery} />
Email
</label>
<label>
<input type="radio" name="delivery" value="post" modelValue={delivery} />
Post
</label>
</form>
);
}モデルを使わない場合、defaultChecked は radio の初期選択状態を設定します。name と value は必須ではありません。checked と併用しないでください。
モデルを使うグループ内の各 radio には静的な value と、同じ空でない name、フォームとの関連付け、ルート、共有する modelValue が必要です。radio グループを checked でバインドせず、modelValue で一致する選択肢を選びます。radio と select の選択肢の値は一意の文字列にします。radio モデルが null ならどれも選択されません。単一選択の select モデルは静的な選択肢に一致する必要がありますが、ハイドレーション時にはブラウザの未選択状態を選択を強制せずに記録できます。
モデルには書き込み可能な signal が必要
すべてのモデルには signal() で作った Signal<T> が必要です。ReadonlySignal<T> や computed 値は、共通のリアクティブなブランドを持っていてもモデルにはできません。Signal は必須の writable ブランドも持ちます。TypeScript に加え、h で作った動的な記述も含めて、サーバーとクライアントのランタイムで検証されます。
import type { ReadonlySignal, Signal } from "@takazudo/zfb/zudo-react";
import { computed, signal } from "@takazudo/zfb/zudo-react";
const name = signal("Ada");
const view: ReadonlySignal<string> = name;
const writable: Signal<string> = name;
const label = computed(() => name.value.toUpperCase());
<input modelValue={name} />;
<p>{label}</p>;computed と読み取り専用の signal は、テキストなど読み取り用のバインディングに使います。子が signal を読むだけなら有効な入力です。readonly 型注釈を付けても実行時の writable ブランドは消えませんが、その TypeScript 型のままモデルに渡すことはできません。ランタイムはモデルへの書き込み前に、writable ブランドのないモデルを拒否します。
v1 は複数選択に対応しません。multiple の select は島モードで拒否され、model や defaultValue も使えません。file と contenteditable のモデル、および上の表にないすべての input type(color、number、range、date、time など)のモデルには対応しません。select、optgroup、option の構造は静的にし、選択肢のラベルやリストをリアクティブにはできません。一致する選択肢のないプログラムからのモデル値やデフォルト値は、部分的に更新せず ZR_MODEL_VALUE を報告します。
表示専用のコントロール
一方向のリアクティブな value を使えるのは、<textarea>、または type を静的に省略するか、text | search | email | url | tel | password のいずれかを静的に指定した <input> に限られます。さらに、readonly または disabled に真偽値リテラルの true を指定する必要があります。リアクティブな readonly、disabled、type では対象になりません。readonly={false}、disabled={false}、空文字列、"readonly" のような文字列も対象外です。その他の input type は引き続き非対応で、checked は静的な値に限られます。
value には ReadonlySignal<string> を渡します。computed() の結果も ReadonlySignal<string> です。このバインディングは signal の値を表示するだけで、ユーザーの編集を signal に書き戻さず、modelValue も使いません。input、change、composition のリスナーも追加しません。<input value={address} readOnly /> のような Preact の表示専用フィールドを移行する際に使えます。
import { computed, signal } from "@takazudo/zfb/zudo-react";
function AddressBar() {
const address = signal("https://example.test/search?q=ada");
const displayedAddress = computed(() => address.value);
return <input type="url" value={displayedAddress} readonly={true} />;
}セットアップ時には、サーバーが出力した input の value 属性または textarea のテキスト内容をリセット時の初期値として維持します。クライアントのみでマウントする場合は初期値を一度設定し、どちらの場合も実際の要素の value プロパティを初期化します。その後の signal 更新ではプロパティだけを設定するため、SSR 後にハイドレーションした場合もクライアントのみでマウントした場合も、form.reset() で初期値に戻ります。リアクティブな value と defaultValue または modelValue を併用しないでください。textarea の子要素もリアクティブな value と競合します。フォームコントロール以外に表示するなら、代わりに <output> または <pre> を使ってください。
数値入力の静的な値と input イベント
数値入力にはモデルアダプターがありません。初期値には静的な value を使い、その後の編集は on:input で読みます。値を範囲内に丸めるなど変換した場合は、結果を DOM に明示的に書き戻してください。signal の更新だけでは非制御の input の表示は変わりません。
import { signal } from "@takazudo/zfb/zudo-react";
function Quantity() {
const quantity = signal(2);
return (
<input
type="number"
min="1"
max="8"
value={String(quantity.value)}
on:input={(event) => {
const raw = Number(event.currentTarget.value);
const next = Math.min(8, Math.max(1, Number.isFinite(raw) ? raw : 2));
quantity.value = next;
event.currentTarget.value = String(next);
}}
/>
);
}この value 式は input の作成時に読み取られます。その後の signal への代入では DOM プロパティは更新されません。この方法にはモデルのハイドレーション時の同期動作もありません。
ハイドレーションは入力済みの値を保つ
ハイドレーション時、ランタイムはモデルに書き込む前にすべての生きたコントロールの値を読み、検証します。一つのルート全体に対する batch で現在の DOM が優先されるため、有効化前に入力された内容が保たれます。同じ signal にバインドされた複数の radio 以外のコントロールは、値が一致する必要があります。radio グループは走査順に依存せず、グループ単位で同期します。
mount は新しいコントロールを作るため、モデルかデフォルト値から初期化します。有効化後、アダプターは同じ値のプロパティ代入を省いてカーソルと選択範囲を保ちます。ネイティブのフォーム reset では、ブラウザがデフォルトの動作を適用した後に関係するモデルを更新します。reset 時のデフォルトは、後の signal 値ではなく元のデフォルトのままです。
変換中の入力と日本語 IME
テキスト用アダプターは compositionstart、compositionend、input.isComposing、blur を監視します。変換中の入力でモデルが更新されることはありますが、signal の通知は変換中のコントロールへ書き戻しません。外部からの書き込みは保留され、変換の終了または blur 時には最終的な DOM 上の編集を正とみなして読み取ります。
共通の zfb 起動処理は島のスケジュールより前に capture リスナーを登録し、アダプターがバンドルをまたいで進行中の変換を識別できるようにします。フォーカス中で変換状態が不明なコントロールへの書き込みは compositionend または blur まで延期し、フォーカスしていないコントロールは更新できます。合成 composition イベントでこの状態プロトコルを自動検証できますが、実際の日本語 IME の動作を検証したことにはなりません。実際の IME 動作は DD17 に基づく人手での検証として保留されています。