UI ライブラリとのインテグレーション
このガイドでは、カスタム入力コンポーネントを Conform とインテグレーションする方法を紹介します。
#イベント移譲
Conform は、ドキュメントに直接 input と focusout イベントリスナーをアタッチすることで、すべてのネイティブ入力をサポートしています。 <input />
、 <select />
、または <textarea />
要素にイベントハンドラーを設定する必要はありません。唯一の要件は、 <form />
要素にフォーム id を設定し、すべての入力に name 属性が設定されていて、 form 属性を使用するか、 <form />
要素内にネストすることでフォームに関連付けられていることです。
1function Example() {
2 const [form, fields] = useForm({
3 // オプション。指定されていない場合は Conform がランダムなIDを生成します。
4 id: 'example',
5 });
6
7 return (
8 <form id={form.id}>
9 <div>
10 <label>Title</label>
11 <input type="text" name="title" />
12 <div>{fields.title.errors}</div>
13 </div>
14 <div>
15 <label>Description</label>
16 <textarea name="description" />
17 <div>{fields.description.errors}</div>
18 </div>
19 <div>
20 <label>Color</label>
21 <select name="color">
22 <option>Red</option>
23 <option>Green</option>
24 <option>Blue</option>
25 </select>
26 <div>{fields.color.errors}</div>
27 </div>
28 <button form={form.id}>Submit</button>
29 </form>
30 );
31}
#インテグレーションが必要かどうかの判別
Conform はイベント移譲に依存してフォームをバリデートし、フォームイベントを発行する限り、どのようなカスタム入力とも動作します。これは通常、 <Input />
や <Textarea />
のように、ネイティブの入力要素をラップするだけのシンプルなコンポーネントに対して当てはまります。しかし、 <Select />
や <DatePicker />
のようなカスタム入力コンポーネントでは、ユーザーが非ネイティブのフォーム要素で操作し、隠された入力を使うため、フォームイベントが発行されない可能性が高いです。
入力がネイティブ入力かどうかを識別するために、カスタム入力を操作している間にフォームイベントが発行され、バブルアップするかどうかを確認するために、イベントリスナーを添付した div で入力をラップすることができます。また、以下にはいくつかの人気のある UI ライブラリに関する 例 も掲載されています。
1import { CustomInput } from 'your-ui-library';
2
3function Example() {
4 return (
5 <div onInput={console.log} onBlur={console.log}>
6 <CustomInput />
7 </div>
8 );
9}
#useInputControl
を使用してカスタム入力コンポーネントを強化する
この問題を解決するために、 Conform は useInputControl というフックを提供しています。これにより、必要なときにフォームイベントを発行するようにカスタム入力を強化できます。このフックは以下のプロパティを持つコントロールオブジェクトを返します:
value
: フォームのリセットおよび更新のインテントに対応した、入力の現在の値change
: 現在の値を更新し、change
およびinput
イベントの両方を発行するための関数focus
:focus
およびfocusin
イベントを発行するための関数blur
:blur
およびfocusout
イベントを発行するための関数
以下は、Radix UI のSelect コンポーネントをラップする例です:
1import {
2 type FieldMetadata,
3 useForm,
4 useInputControl,
5} from '@conform-to/react';
6import * as Select from '@radix-ui/react-select';
7import {
8 CheckIcon,
9 ChevronDownIcon,
10 ChevronUpIcon,
11} from '@radix-ui/react-icons';
12
13type SelectFieldProps = {
14 // `FieldMetadata` 型を使用して `meta` プロパティを定義し、
15 // そのジェネリクスを通じて受け入れるフィールドの型を制限することができます。
16 meta: FieldMetadata<string>;
17 options: Array<string>;
18};
19
20function SelectField({ meta, options }: SelectFieldProps) {
21 const control = useInputControl(meta);
22
23 return (
24 <Select.Root
25 name={meta.name}
26 value={control.value}
27 onValueChange={(value) => {
28 control.change(value);
29 }}
30 onOpenChange={(open) => {
31 if (!open) {
32 control.blur();
33 }
34 }}
35 >
36 <Select.Trigger>
37 <Select.Value />
38 <Select.Icon>
39 <ChevronDownIcon />
40 </Select.Icon>
41 </Select.Trigger>
42 <Select.Portal>
43 <Select.Content>
44 <Select.ScrollUpButton>
45 <ChevronUpIcon />
46 </Select.ScrollUpButton>
47 <Select.Viewport>
48 {options.map((option) => (
49 <Select.Item key={option} value={option}>
50 <Select.ItemText>{option}</Select.ItemText>
51 <Select.ItemIndicator>
52 <CheckIcon />
53 </Select.ItemIndicator>
54 </Select.Item>
55 ))}
56 </Select.Viewport>
57 <Select.ScrollDownButton>
58 <ChevronDownIcon />
59 </Select.ScrollDownButton>
60 </Select.Content>
61 </Select.Portal>
62 </Select.Root>
63 );
64}
65
66function Example() {
67 const [form, fields] = useForm();
68
69 return (
70 <form id={form.id}>
71 <div>
72 <label>Currency</label>
73 <SelectField meta={fields.color} options={['red', 'green', 'blue']} />
74 <div>{fields.color.errors}</div>
75 </div>
76 <button>Submit</button>
77 </form>
78 );
79}
#フォームコンテキストでシンプルに
useField フックを FormProvider と共に使用することで、ラッパーコンポーネントをさらにシンプルにすることもできます。
1import {
2 type FieldName,
3 FormProvider,
4 useForm,
5 useField,
6 useInputControl,
7} from '@conform-to/react';
8import * as Select from '@radix-ui/react-select';
9import {
10 CheckIcon,
11 ChevronDownIcon,
12 ChevronUpIcon,
13} from '@radix-ui/react-icons';
14
15type SelectFieldProps = {
16 // `FieldMetadata` 型の代わりに `FieldName` 型を使用します。
17 // また、そのジェネリクスを通じて受け入れるフィールドの型を制限することもできます。
18 name: FieldName<string>;
19 options: Array<string>;
20};
21
22function Select({ name, options }: SelectFieldProps) {
23 const [meta] = useField(name);
24 const control = useInputControl(meta);
25
26 return (
27 <Select.Root
28 name={meta.name}
29 value={control.value}
30 onValueChange={(value) => {
31 control.change(value);
32 }}
33 onOpenChange={(open) => {
34 if (!open) {
35 control.blur();
36 }
37 }}
38 >
39 {/* ... */}
40 </Select.Root>
41 );
42}
43
44function Example() {
45 const [form, fields] = useForm();
46
47 return (
48 <FormProvider context={form.context}>
49 <form id={form.id}>
50 <div>
51 <label>Color</label>
52 <Select name={fields.color.name} options={['red', 'green', 'blue']} />
53 <div>{fields.color.errors}</div>
54 </div>
55 <button>Submit</button>
56 </form>
57 </FormProvider>
58 );
59}
#例
こちらでは、いくつかの人気のある UI ライブラリとの統合例を見ることができます。
Radix UI や React Aria Component など、さらに多くの UI ライブラリの例を準備するためのコントリビューターを探しています。