Integrating with UI libraries
In this guide, we will show you how to integrate custom inputs with Conform.
#Event delegation
Conform supports all native inputs out of the box by attaching an input and focusout event listener on the document directly. There is no need to setup any event handlers on the <input />
, <select />
or <textarea />
elements. The only requirement is to set a form id on the <form />
element and make sure all the inputs have a name attribute set and are associated with the form either by using the form attribute or by nesting them inside the <form />
element.
1function Example() {
2 const [form, fields] = useForm({
3 // Optional, Conform will generate a random id if not provided
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}
#Identifying if integration is needed
Conform relies on event delegation to validate the form and will work with any custom input as long as it dispatches the form events. This is usually true for simple components that are just a wrapper around the native input element like <Input />
or <Textarea />
. However, custom inputs such as <Select />
or <DatePicker />
will likely require users to interact with non native form element with a hidden input and so no form event would be dispatched.
To identify if the input is a native input, you can wrap it in a div with event listeners attached to see if the form events are dispatched and bubble up while you are interacting with the custom input. You will also find examples below for some of the popular UI libraries.
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}
#Enhancing custom inputs with useInputControl
To fix this issue, Conform provides a hook called useInputControl that let you enhance a custom input so that it dispatch the form events when needed. The hook returns a control object with the following properties:
value
: The current value of the input with respect to form reset and update intentschange
: A function to update the current value and dispatch bothchange
andinput
eventsfocus
: A function to dispatchfocus
andfocusin
eventsblur
: A function to dispatchblur
andfocusout
events
Here is an example wrapping the Select component from Radix UI:
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 // You can use the `FieldMetadata` type to define the `meta` prop
15 // And restrict the type of the field it accepts through its generics
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}
#Simplify it with Form Context
You can also simplify the wrapper component by using the useField hook with a 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 // Instead of using the `FieldMetadata` type, we will use the `FieldName` type
17 // We can also restrict the type of the field it accepts through its generics
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}
#Examples
Here you can find examples integrating with some of the popular UI libraries.
We are looking for contributors to help preparing examples for more UI libraries, like Radix UI and React Aria Component.