インテグレーション / UI ライブラリ

UI ライブラリとのインテグレーション

このガイドでは、カスタム入力コンポーネントを Conform とインテグレーションする方法を紹介します。

#イベント移譲

Conform は、ドキュメントに直接 inputfocusout イベントリスナーをアタッチすることで、すべてのネイティブ入力をサポートしています。 <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 ライブラリの例を準備するためのコントリビューターを探しています。