Skip to content

Commit 6e88f58

Browse files
committed
feat: implement the multiline element
1 parent 7f51363 commit 6e88f58

File tree

6 files changed

+311
-8
lines changed

6 files changed

+311
-8
lines changed

src/components/custom/all-elements-folder/_CentralPlace.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import {
2-
ControllerFieldState,
3-
ControllerRenderProps,
4-
UseFormReturn,
5-
} from "react-hook-form";
1+
import { ControllerFieldState, ControllerRenderProps } from "react-hook-form";
62

73
import { FormBuilderElementType } from "../buttons/question-type-button/QuestionTypeButton";
4+
import MultiLine from "./multi-line/MultiLine";
85
import ParagraphField from "./paragraph/Paragraph";
96
import SeparatorField from "./separator/Separator";
107
import SpacerField from "./spacer/Spacer";
@@ -18,7 +15,8 @@ export type AllElementsType =
1815
| "SubTitle"
1916
| "Paragraph"
2017
| "Separator"
21-
| "Spacer";
18+
| "Spacer"
19+
| "Multiline";
2220

2321
export type FormElement = {
2422
type: AllElementsType;
@@ -71,4 +69,5 @@ export const FormElements: FormElementsType = {
7169
Paragraph: ParagraphField,
7270
Separator: SeparatorField,
7371
Spacer: SpacerField,
72+
Multiline: MultiLine,
7473
};
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import { zodResolver } from "@hookform/resolvers/zod";
2+
import { useEffect } from "react";
3+
import {
4+
ControllerFieldState,
5+
ControllerRenderProps,
6+
useForm,
7+
} from "react-hook-form";
8+
import { FaStarOfLife } from "react-icons/fa";
9+
10+
import {
11+
Form,
12+
FormControl,
13+
FormField,
14+
FormItem,
15+
FormLabel,
16+
FormMessage,
17+
} from "@/components/ui/form";
18+
import { Input } from "@/components/ui/input";
19+
import { Label } from "@/components/ui/label";
20+
import { Switch } from "@/components/ui/switch";
21+
import { Textarea } from "@/components/ui/textarea";
22+
import useFormContext from "@/hooks/useFormContext";
23+
import { cn } from "@/lib/utils";
24+
import {
25+
MultiLineSchema,
26+
MultiLineSchemaType,
27+
} from "@/schemas/input-properties";
28+
29+
import { FormElement } from "../_CentralPlace";
30+
import { FormElementInstance } from "../_CentralPlace";
31+
32+
const MultiLine: FormElement = {
33+
type: "Multiline",
34+
construct: (id) => {
35+
return {
36+
id: id,
37+
type: "Multiline",
38+
attributes: {
39+
label: "Label for large text area",
40+
defaultValue: "default value here",
41+
required: true,
42+
placeholder:
43+
"This gives users the ability to enter or handle text that spans more than a single line, ideal for detailed information, comments, or descriptions. It enables free-form text input that spans across lines, unlike the standard input field.",
44+
helperText: "helper text here",
45+
styles: {
46+
width: "full",
47+
alignment: "left",
48+
},
49+
},
50+
};
51+
},
52+
sidebarComponent: "Multiline",
53+
designerComponent: MultiLineDesignerComponent,
54+
propertiesComponent: MultiLinePropertiesComponent,
55+
previewComponent: MultiLinePreviewComponent,
56+
submitComponent: MultiLineSubmitComponent,
57+
};
58+
59+
export function MultiLineDesignerComponent({
60+
elementInstance,
61+
}: {
62+
elementInstance: FormElementInstance;
63+
}) {
64+
return (
65+
<div className="flex w-full flex-col items-start gap-y-1">
66+
<Label
67+
htmlFor={elementInstance.id}
68+
className="relative text-sm font-medium text-gray-800"
69+
>
70+
{elementInstance?.attributes?.label || "No label"}
71+
{elementInstance?.attributes?.required && (
72+
<FaStarOfLife className="absolute top-0 -right-2 h-[7.5px] w-[7.5px] text-red-500" />
73+
)}
74+
</Label>
75+
<Textarea
76+
rows={30}
77+
readOnly
78+
name={elementInstance.id}
79+
id={elementInstance.id}
80+
className="pointer-events-none min-h-[110px] w-full gap-x-1 rounded-none border border-gray-200 bg-white px-2 py-1.5 text-xs font-light text-gray-400"
81+
placeholder={elementInstance.attributes.placeholder || "No placeholder"}
82+
/>
83+
{elementInstance.attributes.helperText && (
84+
<p className="text-xs font-light text-gray-500">
85+
{elementInstance.attributes.helperText}
86+
</p>
87+
)}
88+
</div>
89+
);
90+
}
91+
92+
export function MultiLinePropertiesComponent({
93+
elementInstance,
94+
}: {
95+
elementInstance: FormElementInstance;
96+
}) {
97+
const { selectedElement } = useFormContext();
98+
99+
const { updateElement } = useFormContext();
100+
101+
const form = useForm<MultiLineSchemaType>({
102+
resolver: zodResolver(MultiLineSchema),
103+
defaultValues: {
104+
label: elementInstance?.attributes?.label || "No label",
105+
required: elementInstance?.attributes?.required ?? false,
106+
helperText: elementInstance?.attributes?.helperText || "helper text here",
107+
placeholder: elementInstance.attributes.placeholder,
108+
},
109+
mode: "all",
110+
});
111+
112+
function onSubmit(values: MultiLineSchemaType) {
113+
updateElement(elementInstance.id, {
114+
...elementInstance,
115+
attributes: {
116+
...values,
117+
},
118+
});
119+
}
120+
121+
useEffect(() => {
122+
form.reset(elementInstance.attributes);
123+
}, [elementInstance, selectedElement, form]);
124+
125+
return (
126+
<Form {...form}>
127+
<form
128+
onBlur={form.handleSubmit(onSubmit)}
129+
className="flex flex-col gap-y-5"
130+
>
131+
<FormField
132+
control={form.control}
133+
name="label"
134+
render={({ field }) => {
135+
return (
136+
<FormItem className="mt-5 flex flex-col gap-y-2.5">
137+
<FormLabel>Field Label</FormLabel>
138+
<FormControl>
139+
<Input {...field} />
140+
</FormControl>
141+
<FormMessage />
142+
</FormItem>
143+
);
144+
}}
145+
/>
146+
147+
<FormField
148+
control={form.control}
149+
name="placeholder"
150+
render={({ field }) => {
151+
return (
152+
<FormItem className="mt-5 flex flex-col gap-y-2.5">
153+
<FormLabel>Placeholder</FormLabel>
154+
<FormControl>
155+
<Input {...field} />
156+
</FormControl>
157+
<FormMessage />
158+
</FormItem>
159+
);
160+
}}
161+
/>
162+
163+
<FormField
164+
control={form.control}
165+
name="required"
166+
render={({ field }) => {
167+
return (
168+
<FormItem className="flex flex-col gap-y-2.5">
169+
<FormLabel>Required</FormLabel>
170+
<FormControl>
171+
<Switch
172+
checked={field.value}
173+
onCheckedChange={field.onChange}
174+
/>
175+
</FormControl>
176+
<FormMessage />
177+
</FormItem>
178+
);
179+
}}
180+
/>
181+
<FormField
182+
control={form.control}
183+
name="helperText"
184+
render={({ field }) => {
185+
return (
186+
<FormItem className="flex flex-col gap-y-2.5">
187+
<FormLabel>Helper Text</FormLabel>
188+
<FormControl>
189+
<Input {...field} />
190+
</FormControl>
191+
<FormMessage />
192+
</FormItem>
193+
);
194+
}}
195+
/>
196+
</form>
197+
</Form>
198+
);
199+
}
200+
201+
export function MultiLinePreviewComponent({
202+
elementInstance,
203+
field,
204+
fieldState,
205+
}: {
206+
elementInstance: FormElementInstance;
207+
field?: ControllerRenderProps<Record<string, string>, string>;
208+
fieldState?: ControllerFieldState;
209+
}) {
210+
return (
211+
<div className="flex w-full flex-col items-start gap-y-1">
212+
<Label
213+
htmlFor={elementInstance.id}
214+
className="relative text-sm font-medium text-gray-800"
215+
>
216+
{elementInstance?.attributes?.label || "No label"}
217+
{elementInstance?.attributes?.required && (
218+
<FaStarOfLife className="absolute top-0 -right-2 h-[7.5px] w-[7.5px] text-red-500" />
219+
)}
220+
</Label>
221+
<Textarea
222+
{...field}
223+
rows={30}
224+
name={elementInstance.id}
225+
id={elementInstance.id}
226+
className={cn(
227+
"min-h-[110px] w-full gap-x-1 rounded-none border border-gray-200 bg-white px-2 py-1.5 text-xs font-light text-gray-400",
228+
{ "border-red-500": fieldState?.error },
229+
)}
230+
placeholder={elementInstance.attributes.placeholder || "No placeholder"}
231+
/>
232+
{elementInstance.attributes.helperText && (
233+
<p className="text-xs font-light text-gray-500">
234+
{elementInstance.attributes.helperText}
235+
</p>
236+
)}
237+
{fieldState?.error?.message && (
238+
<p className="text-xs font-medium text-red-500">
239+
{fieldState.error.message}
240+
</p>
241+
)}
242+
</div>
243+
);
244+
}
245+
246+
export function MultiLineSubmitComponent({
247+
elementInstance,
248+
setFormValues,
249+
}: {
250+
elementInstance: FormElementInstance;
251+
setFormValues: React.Dispatch<
252+
React.SetStateAction<{ [key: string]: string }>
253+
>;
254+
}) {
255+
return (
256+
<div className="flex w-full flex-col items-start gap-y-1">
257+
<Label
258+
htmlFor={elementInstance.id}
259+
className="relative text-sm font-medium text-gray-800"
260+
>
261+
{elementInstance?.attributes?.label || "No label"}
262+
{elementInstance?.attributes?.required && (
263+
<FaStarOfLife className="absolute top-0 -right-2 h-[7.5px] w-[7.5px] text-red-500" />
264+
)}
265+
</Label>
266+
<Textarea
267+
rows={30}
268+
readOnly
269+
name={elementInstance.id}
270+
id={elementInstance.id}
271+
className="pointer-events-none w-full gap-x-1 rounded-none border border-gray-200 bg-white px-2 py-1.5 text-xs font-light text-gray-400"
272+
placeholder={elementInstance.attributes.placeholder || "No placeholder"}
273+
/>
274+
{elementInstance.attributes.helperText && (
275+
<p className="text-xs font-light text-gray-500">
276+
{elementInstance.attributes.helperText}
277+
</p>
278+
)}
279+
</div>
280+
);
281+
}
282+
283+
export default MultiLine;

src/components/custom/all-elements-folder/text-field/TextField.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
ControllerFieldState,
55
ControllerRenderProps,
66
useForm,
7-
UseFormReturn,
87
} from "react-hook-form";
98
import { FaStarOfLife } from "react-icons/fa";
109

src/components/custom/dialogs/preview-dialog/PreviewDialog.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ const PreviewDialog = () => {
3636
const previewPageSchema = z.object(getZodObjectFromElementsArray(elements));
3737
type PreviewPageType = z.infer<typeof previewPageSchema>;
3838

39+
// console.log("element: ", elements[0]);
40+
// console.log("schema: ", previewPageSchema);
41+
// console.log("default values: ", getDefaultValuesFromElementsArray(elements));
3942
const form = useForm<PreviewPageType>({
4043
resolver: zodResolver(previewPageSchema),
4144
defaultValues: getDefaultValuesFromElementsArray(elements),

src/lib/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ export function getDefaultValuesFromElementsArray(
5353
return Object.fromEntries(
5454
elements
5555
.filter((el) => !elementDoesntRequireValidation(el))
56-
.map((el) => [el.attributes.label, el.attributes.defaultValue || ""]),
56+
.map((el) => [
57+
el.attributes.label,
58+
el.attributes.defaultValue || el.attributes.placeholder || "",
59+
]),
5760
);
5861
}
5962

src/schemas/input-properties.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,19 @@ export const SeparatorSchema = z.object({
7373
});
7474

7575
export type SperatorSchemaType = z.infer<typeof SeparatorSchema>;
76+
77+
export const MultiLineSchema = z.object({
78+
label: z
79+
.string()
80+
.min(1, { error: "Label is required" })
81+
.min(3, { error: "Label must have at least three characters" }),
82+
required: z.boolean(),
83+
helperText: z.string(),
84+
placeholder: z.string().optional(),
85+
styles: z.object({
86+
width: z.enum(["half", "full"]),
87+
alignment: z.enum(["left", "center", "right"]),
88+
}),
89+
});
90+
91+
export type MultiLineSchemaType = z.infer<typeof MultiLineSchema>;

0 commit comments

Comments
 (0)