フォーム
@k8ordo/formのform.field(name).inputを、@k8ordo/uiの入力コンポーネントにそのまま展開できるようになります。スキーマごとにどのコンポーネントを使い、何を足して渡すかが分かります。
このページの内容
inputの展開
talk-form.tsxconst title = form.field('title');
<FormControl
errorText={title.error}
invalid={title.invalid}
label="Title"
required={title.required}
renderInput={(props) => <TextField {...props} {...title.input} />}
/>form.field(name).inputには、@k8ordo/formがスキーマから導いたtypeやrequiredなどの属性が入っています。useFormの使い方は、@k8ordo/formのはじめるを見てください。
FormControlにはlabelとerrorText、invalid、requiredだけを渡します。idとaria-*の結び付けはFormControlが作ります。inputはFormControlから受け取ったpropsのあとに展開します。取り除いておく属性はありません。TextFieldは日付や時刻のtypeでも、そのtypeの入力欄として描画します。Textareaは<textarea>に無いtypeを捨てます。
どのコンポーネントも非制御で使えます。値は入力要素に入っているので、リセットするとdefaultValueに戻ります。isDirtyもdefaultValueと比べます。増減ボタンや選択肢の操作でコンポーネントが値を変えたときも、キー入力と同じinputイベントが発火します。
スキーマ別のコンポーネント
スキーマの型ごとに、使えるコンポーネントと{...props} {...input}のほかに渡すものは次のとおりです。
z.string()とz.email()、z.url()、z.iso.*():TextFieldかTextarea。ほかに渡すものはありません。- パスワード:
PasswordInput。typeを展開しても、表示を切り替えるボタンはそのまま働きます。 z.coerce.number():NumberFieldかSlider。文字列のmin、max、step="any"もそのまま受け取ります。z.enum([…]):Select、Radio、RadioCard。optionsを渡し、ラジオボタンではFormControlにlabelAs="legend"を付けます。z.boolean()、z.literal(true):CheckboxかSwitch。labelを渡し、FormControlは使いません。z.array(z.enum([…])):CheckboxGroup.Root、CheckboxCard、Autocomplete。defaultValueを渡します。z.file():FileField.Root。FileField.TriggerとFileField.ItemListを中に置きます。
選択肢のコンポーネント
tags-field.tsxconst tags = form.field('tags');
const checked = state.values?.tags;
<FormControl
errorText={tags.error}
invalid={tags.invalid}
label="Tags"
labelAs="legend"
renderInput={(props) => (
<CheckboxCard
{...props}
{...tags.input}
defaultValue={Array.isArray(checked) ? checked : []}
options={options}
/>
)}
/>チェックボックスの集まりの値は、送信後にstate.valuesへ配列で入ります。inputにはnameしか入っていないので、この配列をdefaultValueで渡します。minCheckedはAutocompleteにも効きます。Autocompleteは、何も選んでいなくても残る隠れた<select multiple>で値を送り、エラーはこの要素に付きます。失敗したあとのフォーカスは、この要素から入力欄へ移ります。
RadioとRadioCardには、inputをそのまま展開します。defaultValueを選ばれている選択肢として読み、requiredをすべてのラジオボタンに付けます。
Selectでrequiredを効かせるには、optionsの先頭に{ value: '', label: '…' }を置きます。空の選択肢が無い<select>は、いつもどれかが選ばれています。
注意点
落とし穴
z.stringbool()のフィールドにCheckboxを使うと、展開したvalueが<input>に渡らず、ブラウザの既定のonが送られます。z.stringbool()は初期設定のままならonをtrueと読みます。truthyを変えると、送られたonはスキーマに合わない値になります。@k8ordo/stateがURLに書く値とも一致しません。このフィールドは<input {...field.input} />で直接描画してください。
NumberFieldは値の整形と増減のためにtype="text"で描画します。そのためブラウザはminとmaxを確かめません。範囲の外の値はNumberField自身がsetCustomValidityで知らせます。その文言は@k8ordo/uiの辞書のもので、zodの文言とは別です。クライアントの文言がzodとそろわないのは、ここだけです。
Textareaで描画するフィールドの正規表現はpattern属性になりますが、<textarea>はこの属性を無視します。この検証はサーバーでだけ行われます。formFieldsはどの要素で描画するかを知らないので、droppedにも載りません。
FileFieldは、FileField.ItemListに表示されているファイルを送ります。一覧から外したファイルは送信からも外れ、リセットすると一覧も空になります。
フォーム全体のエラー
talk-form.tsx{form.formError.message !== undefined && (
<Alert
{...form.formError.props}
message={form.formError.message}
tone="error"
/>
)}form.formErrorはAlertで表示できます。AlertはidとtabIndexを要素に渡すので、form.formError.propsを展開するとフォーカスを受け取れます。
Alertはrole="alert"を持つので、表示されたときとフォーカスが移ったときの2回、スクリーンリーダーに読まれることがあります。エラーの一覧にalertロールを付けた場合と同じです。