I have tried adding a custom Radio Button Field but I'm always getting an error from the shadcn Radio Group Component:
"use client";
import {
ElementsType,
FormElement,
FormElementInstance,
SubmitFunction,
} from "../FormElements";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useDesigner } from "~/hooks";
import {
Label,
Checkbox,
Input,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Switch,
RadioGroupItem,
Button,
} from "~/components";
import { RadioGroup } from "@radix-ui/react-dropdown-menu";
import {
AiOutlineClose,
AiOutlinePlus,
AiOutlineRadarChart,
} from "react-icons/ai";
const type: ElementsType = "RadioGroupField";
const extraAttributes = {
label: "Radio Group field",
required: false,
options: [],
};
const propertiesSchema = z.object({
label: z.string().min(2).max(50),
required: z.boolean().default(false),
options: z.array(z.string()).default([]),
});
export const RadioGroupFieldFormElement: FormElement = {
type,
construct: (id: string) => ({
id,
type,
extraAttributes,
}),
designerBtnElement: {
icon: AiOutlineRadarChart,
label: "Radiogroup Field",
},
designerComponent: DesignerComponent,
formComponent: FormComponent,
propertiesComponent: PropertiesComponent,
validate: (
formElement: FormElementInstance,
currentValue: string,
): boolean => {
const element = formElement as CustomInstance;
if (element.extraAttributes.required) {
return currentValue === "true";
}
return true;
},
};
type CustomInstance = FormElementInstance & {
extraAttributes: typeof extraAttributes;
};
function DesignerComponent({
elementInstance,
}: {
elementInstance: FormElementInstance;
}) {
const element = elementInstance as CustomInstance;
const { label, required, options } = element.extraAttributes;
const id = `radio-${element.id}`;
return (
<div className="items-top flex space-x-2">
<RadioGroup value={options[0]}>
<Label>
{label}
{required && "*"}
</Label>
{options.map((option) => (
<RadioGroupItem key={option} value={option}>
{option}
</RadioGroupItem>
))}
</RadioGroup>
</div>
);
}
function FormComponent({
elementInstance,
submitValue,
isInvalid,
defaultValue,
}: {
elementInstance: FormElementInstance;
submitValue?: SubmitFunction;
isInvalid?: boolean;
defaultValue?: string;
}) {
const element = elementInstance as CustomInstance;
const { label, required, options } = element.extraAttributes;
const id = `radio-${element.id}`;
const [selectedOption, setSelectedOption] = useState(
options.length > 0 ? options[0] : "",
);
return (
<div className="items-top flex space-x-2">
<RadioGroup
value={selectedOption}
id={id}
onChange={(event) =>
setSelectedOption((event.target as HTMLInputElement).value)
}
>
<Label>
{label}
{required && "*"}
</Label>
{options.map((option, index) => (
<RadioGroupItem key={index} value={option}>
{option}
</RadioGroupItem>
))}
</RadioGroup>
</div>
);
}
type propertiesFormSchemaType = z.infer<typeof propertiesSchema>;
function PropertiesComponent({
elementInstance,
}: {
elementInstance: FormElementInstance;
}) {
const element = elementInstance as CustomInstance;
const { updateElement } = useDesigner();
const form = useForm<propertiesFormSchemaType>({
resolver: zodResolver(propertiesSchema),
mode: "onBlur",
defaultValues: {
label: element.extraAttributes.label,
required: element.extraAttributes.required,
options: element.extraAttributes.options,
},
});
useEffect(() => {
form.reset(element.extraAttributes);
}, [element, form]);
function applyChanges(values: propertiesFormSchemaType) {
const { label, required, options } = values;
updateElement(element.id, {
...element,
extraAttributes: {
label,
required,
options,
},
});
}
return (
<Form {...form}>
<form
onBlur={form.handleSubmit(applyChanges)}
onSubmit={(e) => {
e.preventDefault();
}}
className="space-y-3"
>
<FormField
control={form.control}
name="label"
render={({ field }) => (
<FormItem>
<FormLabel>Label</FormLabel>
<FormControl>
<Input
{...field}
onKeyDown={(e) => {
if (e.key === "Enter") e.currentTarget.blur();
}}
/>
</FormControl>
<FormDescription>
The label of the field. <br /> It will be displayed above the
field
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="options"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Options</FormLabel>
<Button
variant={"outline"}
className="gap-2"
onClick={(e) => {
e.preventDefault(); // avoid submit
form.setValue("options", field.value.concat("New option"));
}}
>
<AiOutlinePlus />
Add
</Button>
</div>
<div className="flex flex-col gap-2">
{form.watch("options").map((option, index) => (
<div
key={index}
className="flex items-center justify-between gap-1"
>
<Input
placeholder=""
value={option}
onChange={(e) => {
field.value[index] = e.target.value;
field.onChange(field.value);
}}
/>
<Button
variant={"ghost"}
size={"icon"}
onClick={(e) => {
e.preventDefault();
const newOptions = [...field.value];
newOptions.splice(index, 1);
field.onChange(newOptions);
}}
>
<AiOutlineClose />
</Button>
</div>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="required"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Required</FormLabel>
<FormDescription>
The helper text of the field. <br />
It will be displayed below the field.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
}