Skip to content

Commit 447f64c

Browse files
committed
wip: custom FieldRadioGroup radio render
1 parent 2c32596 commit 447f64c

File tree

2 files changed

+137
-40
lines changed

2 files changed

+137
-40
lines changed

app/components/form/field-radio-group/docs.stories.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { zodResolver } from '@hookform/resolvers/zod';
2+
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
23
import { useForm } from 'react-hook-form';
34
import { z } from 'zod';
45

6+
import { cn } from '@/lib/tailwind/utils';
57
import { zu } from '@/lib/zod/zod-utils';
68

79
import { FormFieldController } from '@/components/form';
@@ -172,3 +174,55 @@ export const WithDisabledOption = () => {
172174
</Form>
173175
);
174176
};
177+
178+
export const RenderOption = () => {
179+
const form = useForm(formOptions);
180+
181+
return (
182+
<Form {...form} onSubmit={onSubmit}>
183+
<div className="flex flex-col gap-4">
184+
<FormField>
185+
<FormFieldLabel>Bearstronaut</FormFieldLabel>
186+
<FormFieldHelper>Select your favorite bearstronaut</FormFieldHelper>
187+
<FormFieldController
188+
control={form.control}
189+
type="radio-group"
190+
name="bear"
191+
options={options}
192+
renderRadio={(option, { field }) => {
193+
const _radioId = `radio-card-${option.value}`;
194+
195+
return (
196+
<label
197+
htmlFor={_radioId}
198+
className={cn(
199+
'relative flex cursor-pointer items-start gap-4 rounded-lg border p-4 transition-colors',
200+
'hover:bg-muted/50',
201+
'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:outline-none',
202+
option.checked
203+
? 'border-primary bg-primary/5'
204+
: 'border-border'
205+
)}
206+
>
207+
<RadioGroupPrimitive.Item
208+
id={_radioId}
209+
className="sr-only"
210+
value={option.value}
211+
disabled={option.disabled}
212+
onBlur={field.onBlur}
213+
/>
214+
<div className="flex flex-col gap-1">
215+
<span className="font-medium">{option.label}</span>
216+
</div>
217+
</label>
218+
);
219+
}}
220+
/>
221+
</FormField>
222+
<div>
223+
<Button type="submit">Submit</Button>
224+
</div>
225+
</div>
226+
</Form>
227+
);
228+
};
Lines changed: 83 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,41 @@
1+
import { ReactNode } from '@tanstack/react-router';
12
import { ComponentProps } from 'react';
23
import {
34
Controller,
5+
ControllerProps,
46
ControllerRenderProps,
57
FieldPath,
68
FieldValues,
79
} from 'react-hook-form';
10+
import { isNonNullish } from 'remeda';
811

912
import { cn } from '@/lib/tailwind/utils';
13+
import { getUiState } from '@/lib/ui-state';
1014

1115
import { Radio, RadioGroup } from '@/components/ui/radio-group';
1216

1317
import { useFormField } from '../form-field';
14-
import { FieldCommonProps } from '../form-field-controller';
18+
import { FieldProps } from '../form-field-controller';
1519
import { FormFieldError } from '../form-field-error';
1620

21+
type RadioOptionProps = {
22+
value: string;
23+
label: string;
24+
disabled?: boolean;
25+
};
26+
1727
export type FieldRadioGroupProps<
1828
TFieldValues extends FieldValues = FieldValues,
1929
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
20-
> = FieldCommonProps<TFieldValues, TName> & {
30+
> = FieldProps<TFieldValues, TName> & {
2131
type: 'radio-group';
22-
options: Array<{
23-
value: string;
24-
label: string;
25-
disabled?: boolean;
26-
}>;
32+
options: Array<RadioOptionProps>;
33+
renderRadio?: (
34+
radioOptions: RadioOptionProps & { checked: boolean },
35+
fieldOptions: Parameters<
36+
Pick<ControllerProps<TFieldValues, TName>, 'render'>['render']
37+
>[0]
38+
) => ReactNode;
2739
containerProps?: ComponentProps<'div'>;
2840
} & RemoveFromType<
2941
Omit<
@@ -48,52 +60,83 @@ export const FieldRadioGroup = <
4860
shouldUnregister,
4961
control,
5062
containerProps,
63+
renderRadio,
5164
...rest
5265
} = props;
5366

5467
const ctx = useFormField();
5568

69+
const ui = getUiState((set) => {
70+
if (isNonNullish(renderRadio)) {
71+
return set('render-radio', { renderRadio });
72+
}
73+
74+
return set('default');
75+
});
76+
5677
return (
5778
<Controller
5879
name={name}
5980
control={control}
6081
disabled={disabled}
6182
defaultValue={defaultValue}
6283
shouldUnregister={shouldUnregister}
63-
render={({ field: { onChange, onBlur, ...field }, fieldState }) => (
64-
<div
65-
{...containerProps}
66-
className={cn(
67-
'flex flex-1 flex-col gap-1',
68-
containerProps?.className
69-
)}
70-
>
71-
<RadioGroup
72-
id={ctx.id}
73-
aria-invalid={fieldState.error ? true : undefined}
74-
aria-describedby={
75-
!fieldState.error
76-
? `${ctx.descriptionId}`
77-
: `${ctx.descriptionId} ${ctx.errorId}`
78-
}
79-
{...rest}
80-
onValueChange={onChange}
81-
{...field}
84+
render={(controllerRenderOptions) => {
85+
const {
86+
field: { onChange, onBlur, ...field },
87+
fieldState,
88+
} = controllerRenderOptions;
89+
90+
return (
91+
<div
92+
{...containerProps}
93+
className={cn(
94+
'flex flex-1 flex-col gap-1',
95+
containerProps?.className
96+
)}
8297
>
83-
{options.map((option) => (
84-
<Radio
85-
key={`${ctx.id}-${option.value}`}
86-
value={option.value}
87-
disabled={option.disabled}
88-
onBlur={onBlur}
89-
>
90-
{option.label}
91-
</Radio>
92-
))}
93-
</RadioGroup>
94-
<FormFieldError />
95-
</div>
96-
)}
98+
<RadioGroup
99+
id={ctx.id}
100+
aria-invalid={fieldState.error ? true : undefined}
101+
aria-describedby={
102+
!fieldState.error
103+
? `${ctx.descriptionId}`
104+
: `${ctx.descriptionId} ${ctx.errorId}`
105+
}
106+
{...rest}
107+
onValueChange={onChange}
108+
{...field}
109+
>
110+
{ui
111+
.match('render-radio', ({ renderRadio }) =>
112+
options.map((option) =>
113+
renderRadio(
114+
{
115+
...option,
116+
checked: option.value === field.value,
117+
},
118+
controllerRenderOptions
119+
)
120+
)
121+
)
122+
.match('default', () =>
123+
options.map((option) => (
124+
<Radio
125+
key={`${ctx.id}-${option.value}`}
126+
value={option.value}
127+
disabled={option.disabled}
128+
onBlur={onBlur}
129+
>
130+
{option.label}
131+
</Radio>
132+
))
133+
)
134+
.exhaustive()}
135+
</RadioGroup>
136+
<FormFieldError />
137+
</div>
138+
);
139+
}}
97140
/>
98141
);
99142
};

0 commit comments

Comments
 (0)