Skip to content

Commit 72670d2

Browse files
committed
docs: improve form docs
1 parent 5803dac commit 72670d2

4 files changed

Lines changed: 103 additions & 14 deletions

File tree

apps/docs/content/docs/ui/form.mdx

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@ function LoginForm() {
4545

4646
return (
4747
<Form.Root form={form} onSubmit={form.handleSubmit((data) => console.log(data))}>
48-
<Form.Field name="email">
48+
<Form.Field control={form.control} name="email">
4949
<Form.Label>Email</Form.Label>
5050
<Form.Input type="email" />
5151
<Form.ErrorMessage />
5252
</Form.Field>
5353

54-
<Form.Field name="password">
54+
<Form.Field control={form.control} name="password">
5555
<Form.Label>Password</Form.Label>
5656
<Form.Input type="password" />
5757
<Form.ErrorMessage />
@@ -63,16 +63,70 @@ function LoginForm() {
6363
}
6464
```
6565

66+
## Control Usage & Type Safety
67+
68+
While `Form.Root` shares the internal context required to make the `control` and field-inherited props optional, **it is highly recommended to pass them explicitly.**
69+
70+
Providing these props directly allows TypeScript to link your components to your specific form schema, enabling full type safety and robust auto-completion for every field path. Note that while sub-components can inherit the name from their parent `Form.Field`, the field itself always requires a `name` prop to function.
71+
72+
```tsx
73+
// ✅ Recommended: schema-aware name prop
74+
<Form.Field control={form.control} name="email">
75+
<Form.Label>Email</Form.Label>
76+
<Form.Input />
77+
</Form.Field>
78+
```
79+
80+
```tsx
81+
// ⚠️ Optional: "name" falls back to generic string
82+
<Form.Field name="email">
83+
<Form.Label>Email</Form.Label>
84+
<Form.Input />
85+
</Form.Field>
86+
```
87+
88+
## Controlled Components
89+
90+
To integrate third-party components that don't expose a native `ref` (such as a Combobox or Switch), use the controlled component variants. These wrap React Hook Form's `Controller` while remaining fully integrated with the form's accessibility and error handling system.
91+
92+
### Form.FieldBoundController
93+
94+
This is the recommended approach for most controlled components. It must be nested within a `Form.Field`, which allows it to automatically bind to the parent's `name` and `control`.
95+
96+
```tsx
97+
<Form.Field control={form.control} name="country">
98+
<Form.Label>Country</Form.Label>
99+
<Form.FieldBoundController
100+
render={({ field }) => (
101+
<Combobox options={countries} value={field.value} onChange={field.onChange} />
102+
)}
103+
/>
104+
<Form.ErrorMessage />
105+
</Form.Field>
106+
```
107+
108+
### Form.FieldWithController
109+
110+
Use this variant if you need a standalone controller that doesn't require a parent `Form.Field`. It provides its own field context, making it useful for specialized layouts or direct usage under `Form.Root`.
111+
112+
```tsx
113+
<Form.FieldWithController
114+
control={form.control}
115+
name="notifications"
116+
render={({ field }) => <Switch checked={field.value} onCheckedChange={field.onChange} />}
117+
/>
118+
```
119+
66120
## Reactive State
67121

68122
To avoid unnecessary full-form re-renders, use the `Form.Watch` and `Form.StateSubscribe` components. They isolate re-renders by subscribing only to the specific field values or form states you need.
69123

70124
```tsx
71125
<Form.Root form={form} onSubmit={onSubmit}>
72-
<Form.Watch name="accountType">
126+
<Form.Watch control={form.control} name="accountType">
73127
{(type) =>
74128
type === "business" && (
75-
<Form.Field name="taxId">
129+
<Form.Field control={form.control} name="taxId">
76130
<Form.Label>Tax ID</Form.Label>
77131
<Form.Input />
78132
</Form.Field>
@@ -101,6 +155,12 @@ To avoid unnecessary full-form re-renders, use the `Form.Watch` and `Form.StateS
101155
<Accordion title="Form.Field">
102156
<auto-type-table path="../../../lib/ui-options.ts" name="FormFieldProps" />
103157
</Accordion>
158+
<Accordion title="Form.FieldBoundController">
159+
<auto-type-table path="../../../lib/ui-options.ts" name="FormFieldBoundControllerProps" />
160+
</Accordion>
161+
<Accordion title="Form.FieldWithController">
162+
<auto-type-table path="../../../lib/ui-options.ts" name="FormFieldWithControllerProps" />
163+
</Accordion>
104164
<Accordion title="Form.Label">
105165
<auto-type-table path="../../../lib/ui-options.ts" name="FormLabelProps" />
106166
</Accordion>
@@ -132,9 +192,12 @@ To avoid unnecessary full-form re-renders, use the `Form.Watch` and `Form.StateS
132192

133193
## Data Attributes
134194

135-
| Component | Attribute | Description |
136-
| :------------------ | :------------- | :--------------------------------------------- |
137-
| `Form.Field` | `data-invalid` | Present when the field has a validation error. |
138-
| `Form.Label` | `data-invalid` | Matches the field error state. |
139-
| `Form.Input` | `data-invalid` | Matches the field error state for styling. |
140-
| `Form.ErrorMessage` | `data-shake` | Triggers a visual feedback animation on error. |
195+
| Component | Attribute | Description |
196+
| :------------------ | :-------------- | :--------------------------------------------- |
197+
| `Form.Field` | `data-invalid` | Present when the field has a validation error. |
198+
| | `data-disabled` | Present when the field is disabled. |
199+
| `Form.Label` | `data-invalid` | Matches the field error state. |
200+
| | `data-disabled` | Matches the field disabled state. |
201+
| `Form.Input` | `data-invalid` | Matches the field error state for styling. |
202+
| | `data-disabled` | Matches the field disabled state. |
203+
| `Form.ErrorMessage` | `data-index` | The zero-based index of the error message. |

apps/docs/lib/ui-options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export type {
3333
FormSubmitProps,
3434
FormWatchProps,
3535
FormStateSubscribeProps,
36+
FormFieldBoundControllerProps,
37+
FormFieldWithControllerProps,
3638
} from "@zayne-labs/ui-react/ui/form";
3739

3840
export type { AwaitRootProps } from "@zayne-labs/ui-react/common/await";

packages/ui-react/src/components/ui/form/form-parts.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,27 @@ export {
2020
FormTextAreaPrimitive as TextAreaPrimitive,
2121
FormWatch as Watch,
2222
} from "./form";
23+
24+
export type {
25+
FormDescriptionProps,
26+
FormErrorMessagePrimitiveProps,
27+
FormErrorMessageProps,
28+
FormFieldBoundControllerProps,
29+
FormFieldContextProps,
30+
FormFieldProps,
31+
FormFieldWithControllerProps,
32+
FormInputCombinedProps,
33+
FormInputGroupProps,
34+
FormInputPrimitiveProps,
35+
FormInputProps,
36+
FormLabelProps,
37+
FormRootProps,
38+
FormSelectPrimitiveProps,
39+
FormSelectProps,
40+
FormSideItemProps,
41+
FormStateSubscribeProps,
42+
FormSubmitProps,
43+
FormTextAreaPrimitiveProps,
44+
FormTextAreaProps,
45+
FormWatchProps,
46+
} from "./form";

packages/ui-react/src/components/ui/form/form.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export function FormField<
141141
);
142142
}
143143

144-
type FormFieldControlledFieldProps<
144+
export type FormFieldWithControllerProps<
145145
TFieldValues extends FieldValues,
146146
TName extends FieldPath<TFieldValues>,
147147
TTransformedValues = TFieldValues,
@@ -151,7 +151,7 @@ export function FormFieldWithController<
151151
TFieldValues extends FieldValues,
152152
TName extends FieldPath<TFieldValues>,
153153
TTransformedValues = TFieldValues,
154-
>(props: FormFieldControlledFieldProps<TFieldValues, TName, TTransformedValues>) {
154+
>(props: FormFieldWithControllerProps<TFieldValues, TName, TTransformedValues>) {
155155
const formMethods = useFormMethodsContext({ strict: false });
156156

157157
const { control, name, render, ...restOfProps } = props;
@@ -183,7 +183,7 @@ export function FormFieldWithController<
183183
);
184184
}
185185

186-
type FormFieldBoundControllerProps<TFieldValues extends FieldValues, TTransformedValues> = Omit<
186+
export type FormFieldBoundControllerProps<TFieldValues extends FieldValues, TTransformedValues> = Omit<
187187
ControllerProps<TFieldValues, never, TTransformedValues>,
188188
"control" | "name"
189189
>;
@@ -828,9 +828,9 @@ export function FormErrorMessage<
828828

829829
return (
830830
<FormErrorMessagePrimitive
831+
type={type as "root"}
831832
control={control}
832833
fieldName={errorField ?? (fieldContextValues?.name as NonNullable<typeof errorField>)}
833-
type={type as "root"}
834834
renderItem={({ props: renderProps, state }) => (
835835
<li
836836
key={state.errorMessage}

0 commit comments

Comments
 (0)