@k8ordo/uiと組み合わせる
@k8ordo/formは@k8ordo/uiに依存しません。その代わり、@k8ordo/uiの入力部品のほうが、formFieldsが作ったinputをそのまま受け取れるように作られています。このページでは、部品ごとの組み合わせ方と、いくつかの例外を紹介します。
このページの内容
FormControlと組み合わせる
FormControlには、ラベルとエラー、invalid、requiredだけを渡します。idやaria-*による結び付けはFormControlが作るので、自分で書く必要はありません。
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} />}
/>inputは、FormControlから受け取ったpropsのあとに展開します。inputから取り除いておく属性はありません。TextFieldは日付や時刻のtypeも描き、Textareaは<textarea>に無いtypeを捨てます。
どの部品も値をDOMに持つので、リセットすると送信した値に戻り、isDirtyもその値を読みます。増減ボタンや選択肢の選択のように部品がコードで値を変えたときも、キー入力と同じ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。state.valuesからdefaultValueを渡します。z.file():FileField.Root。FileField.TriggerとFileField.ItemListを中に置きます。
選択肢の部品
RadioとRadioCardには、inputをそのまま展開します。自分で書くラジオボタンとは違い、defaultValueを選ばれている選択肢として読み、requiredをすべてのラジオボタンに付けます。
Selectでrequiredを意味のあるものにするには、optionsの先頭に{ value: '', label: '…' }の選択肢を置きます。空の選択肢が無い<select>は、いつもどれかが選ばれているからです。
チェックボックスの集まりは、送信した値が配列で戻ります。inputにはnameしか入っていないので、チェックされていた値をdefaultValueで渡します。
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}
/>
)}
/>minCheckedはAutocompleteにも効きます。Autocompleteは何も選んでいなくても残る隠れた<select multiple>で値を送るので、ルールがエラーを付ける要素があり、失敗したあとのフォーカスは入力欄へ移ります。
気をつける部品
落とし穴
z.stringbool()の欄にCheckboxを使うと、展開したvalueが届かず、ブラウザの既定のonが送られます。既定のz.stringbool()はonをtrueと読みますが、truthyを変えた場合やURLの状態と合わせる場合は食い違います。この欄は素の<input {...field.input} />で描いてください。
NumberFieldは値を整形して増減させるためにtype="text"で描くので、ブラウザはminとmaxを確かめません。範囲の外の値はNumberField自身がsetCustomValidityで知らせますが、その文言はzodではなく@k8ordo/uiの辞書のものです。クライアントの文言がzodとそろわないのは、ここだけです。
Textareaで描く欄の正規表現はpattern属性になりますが、<textarea>はこの属性を無視します。この検証はサーバーでだけ行われ、どの部品で描くかをformFieldsは知らないので、droppedにも載りません。
FileFieldは、FileField.ItemListに表示されているファイルを送ります。一覧から外したファイルは送信からも外れ、リセットすると一覧も空になります。
フォーム全体のエラーをAlertで出す
form.formErrorはAlertで表示できます。AlertはidとtabIndexを要素まで渡すので、form.formError.propsを展開すればフォーカスを受け取れます。
talk-form.tsx{form.formError.message !== undefined && (
<Alert
{...form.formError.props}
message={form.formError.message}
tone="error"
/>
)}Alertはrole="alert"を持つので、表示されたときとフォーカスが移ったときの2回、スクリーンリーダーに読まれることがあります。エラーの一覧にalertロールを付けた場合と同じ割り切りです。