Skip to content

Commit 7a4ae68

Browse files
authored
feat(conform-react): metadata customization (#1047)
1 parent cc4446a commit 7a4ae68

File tree

33 files changed

+1752
-463
lines changed

33 files changed

+1752
-463
lines changed

.changeset/rare-olives-kneel.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
'@conform-to/react': minor
3+
---
4+
5+
Add metadata customization support to future `useForm` hook
6+
7+
This update introduces a `<FormOptionsProvider />` component that allows users to define global form options, including custom metadata properties that match your form component types when integrating with UI libraries or any custom components:
8+
9+
```tsx
10+
import {
11+
FormOptionsProvider,
12+
type BaseMetadata,
13+
} from '@conform-to/react/future';
14+
import { TextField } from './components/TextField';
15+
16+
// Define custom metadata properties that matches the type of our custom form components
17+
function defineCustomMetadata<FieldShape, ErrorShape>(
18+
metadata: BaseMetadata<FieldShape, ErrorShape>,
19+
) {
20+
return {
21+
get textFieldProps() {
22+
return {
23+
name: metadata.name,
24+
defaultValue: metadata.defaultValue,
25+
isInvalid: !metadata.valid,
26+
} satisfies Partial<React.ComponentProps<typeof TextField>>;
27+
},
28+
};
29+
}
30+
31+
// Extend the CustomMetadata interface with our implementation
32+
// This makes the custom metadata types available on all field metadata objects
33+
declare module '@conform-to/react/future' {
34+
interface CustomMetadata<FieldShape, ErrorShape>
35+
extends ReturnType<typeof defineCustomMetadata<FieldShape, ErrorShape>> {}
36+
}
37+
38+
// Wrap your app with FormOptionsProvider
39+
<FormOptionsProvider
40+
shouldValidate="onBlur"
41+
defineCustomMetadata={defineCustomMetadata}
42+
>
43+
<App />
44+
</FormOptionsProvider>;
45+
46+
// Use custom metadata properties in your components
47+
function Example() {
48+
const { form, fields } = useForm({
49+
// shouldValidate now defaults to "onBlur"
50+
});
51+
52+
return (
53+
<form {...form.props}>
54+
<TextField {...fields.email.textFieldProps} />
55+
</form>
56+
);
57+
}
58+
```
59+
60+
Additionally, you can now customize the base error shape globally using the `CustomTypes` interface:
61+
62+
```tsx
63+
declare module '@conform-to/react/future' {
64+
interface CustomTypes {
65+
errorShape: { message: string; code: string };
66+
}
67+
}
68+
```
69+
70+
This restricts the error shape expected from forms and improves type inference when using `useField` and `useFormMetadata`.
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# FormOptionsProvider
2+
3+
> The `FormOptionsProvider` component is part of Conform's future export. These APIs are experimental and may change in minor versions. [Learn more](https://github.com/edmundhung/conform/discussions/954)
4+
5+
A React component that provides global form options to all forms in your application.
6+
7+
```tsx
8+
import { FormOptionsProvider } from '@conform-to/react/future';
9+
10+
export default function App() {
11+
return (
12+
<FormOptionsProvider shouldValidate="onBlur" shouldRevalidate="onInput">
13+
{/* Your app components */}
14+
</FormOptionsProvider>
15+
);
16+
}
17+
```
18+
19+
## Props
20+
21+
All props are optional. When a prop is not provided, it inherits the default value or from a parent `FormOptionsProvider` if nested.
22+
23+
### `shouldValidate?: 'onSubmit' | 'onBlur' | 'onInput'`
24+
25+
Determines when validation should run for the first time on a field. Default is `onSubmit`.
26+
27+
This option sets the default validation timing for all forms in your application. Individual forms can override this by passing their own `shouldValidate` option to [useForm](./useForm.md).
28+
29+
```tsx
30+
<FormOptionsProvider shouldValidate="onBlur">
31+
{/* All forms will validate on blur by default */}
32+
</FormOptionsProvider>
33+
```
34+
35+
### `shouldRevalidate?: 'onSubmit' | 'onBlur' | 'onInput'`
36+
37+
Determines when validation should run again after the field has been validated once. Default is the same as `shouldValidate`.
38+
39+
This is useful when you want an immediate update after the user has interacted with a field. For example, validate on blur initially, but revalidate on every input after the first validation.
40+
41+
```tsx
42+
<FormOptionsProvider shouldValidate="onBlur" shouldRevalidate="onInput">
43+
{/* Validate on blur, but show live feedback after first validation */}
44+
</FormOptionsProvider>
45+
```
46+
47+
### `defineCustomMetadata?: <FieldShape, ErrorShape>(metadata: BaseMetadata<FieldShape, ErrorShape>) => CustomMetadata`
48+
49+
A function that defines custom metadata properties for your form fields. This is particularly useful when integrating with UI libraries or custom form components.
50+
51+
```tsx
52+
import {
53+
FormOptionsProvider,
54+
type BaseMetadata,
55+
} from '@conform-to/react/future';
56+
import type { TextField } from './components/TextField';
57+
58+
// Define custom metadata properties that matches the type of our custom form components
59+
function defineCustomMetadata<FieldShape, ErrorShape>(
60+
metadata: BaseMetadata<FieldShape, ErrorShape>,
61+
) {
62+
return {
63+
get textFieldProps() {
64+
return {
65+
name: metadata.name,
66+
defaultValue: metadata.defaultValue,
67+
isInvalid: !metadata.valid,
68+
errors: metadata.errors,
69+
} satisfies Partial<React.ComponentProps<typeof TextField>>;
70+
},
71+
};
72+
}
73+
74+
// Extend the CustomMetadata interface with our implementation
75+
// This makes the custom metadata types available on all field metadata objects
76+
declare module '@conform-to/react/future' {
77+
interface CustomMetadata<FieldShape, ErrorShape>
78+
extends ReturnType<typeof defineCustomMetadata<FieldShape, ErrorShape>> {}
79+
}
80+
81+
// Wrap your app with FormOptionsProvider
82+
<FormOptionsProvider defineCustomMetadata={defineCustomMetadata}>
83+
<App />
84+
</FormOptionsProvider>;
85+
```
86+
87+
Once defined, custom metadata properties are available on all field metadata objects:
88+
89+
```tsx
90+
function LoginForm() {
91+
const { form, fields } = useForm({
92+
// form options
93+
});
94+
95+
return (
96+
<form {...form.props}>
97+
{/* TypeScript knows about textFieldProps! */}
98+
<TextField {...fields.email.textFieldProps} />
99+
<TextField {...fields.password.textFieldProps} />
100+
<button>Login</button>
101+
</form>
102+
);
103+
}
104+
```
105+
106+
### `intentName?: string`
107+
108+
The name of the submit button field that indicates the submission intent. Default is `'__intent__'`.
109+
110+
This is an advanced option. You typically don't need to change this unless you have conflicts with existing field names.
111+
112+
### `serialize(value: unknown) => string | string[] | File | File[] | null | undefined`
113+
114+
A custom serialization function for converting form data.
115+
116+
This is an advanced option. You typically don't need to change this unless you have special serialization requirements.
117+
118+
## Tips
119+
120+
### Conditional metadata based on field shape
121+
122+
You can use TypeScript's conditional types to restrict custom metadata based on the field shape:
123+
124+
```tsx
125+
function defineCustomMetadata<FieldShape, ErrorShape>(
126+
metadata: BaseMetadata<FieldShape, ErrorShape>,
127+
) {
128+
return {
129+
get dateRangePickerProps() {
130+
// Only available for field with start and end properties
131+
const rangeFields = metadata.getFieldset<{
132+
start: string;
133+
end: string;
134+
}>();
135+
136+
return {
137+
startName: rangeFields.start.name,
138+
endName: rangeFields.end.name,
139+
defaultValue: {
140+
start: rangeFields.start.defaultValue,
141+
end: rangeFields.end.defaultValue,
142+
},
143+
isInvalid: !metadata.valid,
144+
errors: metadata.errors?.map((error) => `${error}`),
145+
} satisfies Partial<React.ComponentProps<typeof DateRangePicker>>;
146+
},
147+
};
148+
}
149+
150+
declare module '@conform-to/react/future' {
151+
interface CustomMetadata<FieldShape, ErrorShape> {
152+
// Make dateRangePickerProps only available if the field shape has start and end properties
153+
dateRangePickerProps: FieldShape extends { start: string; end: string }
154+
? ReturnType<
155+
typeof defineCustomMetadata<FieldShape, ErrorShape>
156+
>['dateRangePickerProps']
157+
: unknown;
158+
}
159+
}
160+
```

docs/api/react/future/useField.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ Array of validation error messages for this field.
7878

7979
Object containing errors for all touched subfields.
8080

81+
### `ariaInvalid: boolean | undefined`
82+
83+
Boolean value for the `aria-invalid` attribute. Indicates whether the field has validation errors for screen readers. This is `true` when the field has errors, `undefined` otherwise.
84+
85+
### `ariaDescribedBy: string | undefined`
86+
87+
String value for the `aria-describedby` attribute. Contains the `errorId` when the field is invalid, `undefined` otherwise. If you need to reference both help text and errors, merge with `descriptionId` manually (e.g., `${field.descriptionId} ${field.ariaDescribedBy}`).
88+
8189
### Validation Attributes
8290

8391
HTML validation attributes automatically derived from schema constraints:
@@ -134,7 +142,8 @@ function FormField({ name, label, type = 'text' }: FieldProps) {
134142
min={field.min}
135143
max={field.max}
136144
step={field.step}
137-
aria-describedby={field.errors ? field.errorId : undefined}
145+
aria-invalid={field.ariaInvalid}
146+
aria-describedby={field.ariaDescribedBy}
138147
/>
139148

140149
{field.errors && (

examples/chakra-ui/README.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,45 @@
11
# Chakra UI Example
22

3-
This example shows you how to integrate [chakra-ui](https://chakra-ui.com/docs/components) forms components with Conform.
3+
[Chakra UI](https://chakra-ui.com/) is a simple, modular and accessible component library that gives you the building blocks you need to build your React applications.
4+
5+
This example demonstrates how to integrate Conform with Chakra UI using custom metadata.
6+
7+
## Understanding the Integration
8+
9+
The main application ([`App.tsx`](./src/App.tsx)) uses explicit prop assignment for educational purposes, making it easy to see how field metadata maps to Chakra UI component props:
10+
11+
```tsx
12+
<Input
13+
name={fields.text.name}
14+
defaultValue={fields.text.defaultValue}
15+
isInvalid={!fields.text.valid}
16+
/>
17+
```
18+
19+
While this is clear and straightforward for learning, it becomes repetitive in production applications.
20+
21+
## Custom Metadata
22+
23+
The example also showcases metadata customization for a more DRY approach. Custom metadata properties are defined once in [`main.tsx`](./src/main.tsx) and provide type-safe props that match Chakra UI component types:
24+
25+
```tsx
26+
// Define custom metadata once
27+
function defineCustomMetadata(metadata) {
28+
return {
29+
get inputProps() {
30+
return {
31+
name: metadata.name,
32+
defaultValue: metadata.defaultValue,
33+
isInvalid: !metadata.valid,
34+
} satisfies Partial<React.ComponentProps<typeof Input>>;
35+
},
36+
// ... other component props
37+
};
38+
}
39+
40+
// Use with full type safety
41+
<Input {...fields.text.inputProps} />
42+
```
443

544
## Compatibility
645

@@ -22,6 +61,8 @@ This example shows you how to integrate [chakra-ui](https://chakra-ui.com/docs/c
2261
- PinInput
2362
- Slider
2463

64+
## Demo
65+
2566
<!-- sandbox src="/examples/chakra-ui" -->
2667

2768
Try it out on [Stackblitz](https://stackblitz.com/github/edmundhung/conform/tree/main/examples/chakra-ui).

0 commit comments

Comments
 (0)