aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordan <[email protected]>2023-08-30 14:41:37 -0400
committerdan <[email protected]>2023-08-30 14:41:37 -0400
commit7fdb76bca64e2e72768f6de7bfa3fea98d2d5bbb (patch)
treead1d59388758eeecce29567342c870b6af8019ea
parent4c4f90af55fa2e560f3417efd9bd7a106f80b084 (diff)
downloaddraggable-form-demo-7fdb76bca64e2e72768f6de7bfa3fea98d2d5bbb.tar.gz
draggable-form-demo-7fdb76bca64e2e72768f6de7bfa3fea98d2d5bbb.tar.bz2
draggable-form-demo-7fdb76bca64e2e72768f6de7bfa3fea98d2d5bbb.zip
feat: add form builder component
-rw-r--r--src/components/FormBuilder/index.js315
1 files changed, 315 insertions, 0 deletions
diff --git a/src/components/FormBuilder/index.js b/src/components/FormBuilder/index.js
new file mode 100644
index 0000000..b9f0389
--- /dev/null
+++ b/src/components/FormBuilder/index.js
@@ -0,0 +1,315 @@
+import { DragIndicator, Clear } from "@mui/icons-material";
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ IconButton,
+ Stack,
+ TextField,
+} from "@mui/material";
+import React, { useRef, useState } from "react";
+export default function FormBuilder() {
+ return (
+ <Stack direction={"column"}>
+ <h3>FormBuilder</h3>
+ <Stack
+ direction={"row"}
+ justifyContent={"space-between"}
+ alignItems={"flex-start"}
+ spacing={2}
+ >
+ <Form initialForm={false} editable />
+ <WidgetsLibrary />
+ </Stack>
+ </Stack>
+ );
+}
+
+const availableWidgets = {
+ label: {
+ name: "Label",
+ element: (props) => (
+ <TextField
+ id="outlined-basic"
+ variant="standard"
+ value={props?.name}
+ onChange={(e) =>
+ props?.setFormField({ ...props, name: e.target.value })
+ }
+ fullWidth
+ />
+ ),
+ finalElement: (props) => <div>{props?.id}</div>,
+ },
+ number: {
+ name: "Number",
+ element: (props) => (
+ <TextField
+ id="outlined-basic"
+ label={props?.id ? `example field ${props.id}` : "Outlined"}
+ variant="outlined"
+ type={"number"}
+ fullWidth
+ />
+ ),
+ },
+ text: {
+ name: "Text",
+ element: (props) => (
+ <TextField
+ id="outlined-basic"
+ label={props?.id ? `example field ${props.id}` : "Outlined"}
+ variant="outlined"
+ fullWidth
+ />
+ ),
+ },
+ multiline: {
+ name: "Multiline",
+ element: (props) => (
+ <TextField
+ id="outlined-textarea"
+ label={props?.id ? `example field ${props.id}` : "Multiline"}
+ placeholder="Placeholder"
+ multiline
+ minRows={4}
+ fullWidth
+ />
+ ),
+ },
+};
+
+function WidgetsLibrary() {
+ return (
+ <Stack
+ direction={"column"}
+ spacing={0.5}
+ alignItems={"flex-start"}
+ sx={{ maxHeight: "100vh", height: "100%", overflow: "scroll" }}
+ >
+ {Object.keys(availableWidgets).map((k) => {
+ const props = availableWidgets[k];
+ return <WidgetCard key={k} type={k} {...props} />;
+ })}
+ </Stack>
+ );
+}
+
+function WidgetCard({ name, element, type, finalElement }) {
+ function onDragStart(e) {
+ e.dataTransfer.setData("application/formwidgettype", type);
+ e.dataTransfer.effectAllowed = "copy";
+ console.log(e);
+ }
+
+ const disabledFieldOverlay = {
+ pointerEvents: "none",
+ background: "#7773",
+ zIndex: 10,
+ };
+ if (!element) {
+ return;
+ } else {
+ return (
+ <Card
+ draggable
+ onDragStart={onDragStart}
+ sx={{ cursor: "grab", width: "100%" }}
+ >
+ <Stack
+ direction={"row"}
+ justifyContent={"start"}
+ alignItems={"center"}
+ alignContent={"center"}
+ >
+ <DragIndicator />
+ <div>
+ <CardHeader title={name} />
+ <CardContent>
+ <div style={disabledFieldOverlay}>
+ {finalElement ? finalElement() : element()}
+ </div>
+ </CardContent>
+ </div>
+ </Stack>
+ </Card>
+ );
+ }
+}
+
+function FormField(props) {
+ if (!props?.type) {
+ console.log("formfield render failed", props);
+ return;
+ }
+ const { element: Element } = availableWidgets[props?.type];
+ const dragHandleRef = useRef();
+ const [target, setTarget] = useState();
+ return (
+ <Stack
+ direction={"row"}
+ sx={{ width: "100%", cursor: "grab" }}
+ alignItems={"center"}
+ justifyContent={"space-between"}
+ draggable
+ onMouseDown={(e) => setTarget(e.target)}
+ onDragStart={(e) => {
+ if (dragHandleRef.current.contains(target)) {
+ e.dataTransfer.setData("application/formwidgetid", props?.id);
+ } else {
+ e.preventDefault();
+ }
+ }}
+ >
+ <span ref={dragHandleRef}>
+ <DragIndicator />
+ </span>
+ <Element {...props} />
+ <IconButton onClick={() => props?.deleteFormField()}>
+ <Clear />
+ </IconButton>
+ </Stack>
+ );
+}
+
+function capture(e) {
+ e.stopPropagation();
+ e.preventDefault();
+}
+
+function DropZone({ insertField, index, moveField }) {
+ const [dragOver, setDragOver] = useState(false);
+ return (
+ <span
+ style={{
+ marginTop: "2px",
+ marginBottom: "2px",
+ width: "100%",
+ color: "transparent",
+ background: dragOver ? "#22a9" : "#2221",
+ height: dragOver ? "2em" : "4px",
+ borderRadius: 8,
+ }}
+ onDragOver={(e) => capture(e) || setDragOver(true)}
+ onDragExit={() => setDragOver(false)}
+ onDrop={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ setDragOver(false);
+ console.log(e);
+ if ([...e.dataTransfer.types].includes("application/formwidgettype")) {
+ const type = e.dataTransfer.getData("application/formwidgettype");
+ insertField({ index, type });
+ } else if (
+ [...e.dataTransfer.types].includes("application/formwidgetid")
+ ) {
+ const oldId = e.dataTransfer.getData("application/formwidgetid");
+ moveField(oldId, index);
+ }
+ }}
+ />
+ );
+}
+
+function Form({ initialForm, editable }) {
+ const [form, setForm] = useState({ fields: [], ...initialForm });
+
+ function updateFormField(newField) {
+ setForm({
+ ...form,
+ fields: form.fields.map((field) =>
+ field?.id === newField.id ? newField : field,
+ ),
+ });
+ }
+
+ function deleteFormField(id) {
+ const newForm = {
+ ...form,
+ fields: form.fields.filter((field) => id !== field?.id),
+ };
+ console.log("deleteFormField", { id, form, newForm });
+ setForm({ ...newForm });
+ }
+ console.log("render", form.fields);
+
+ function insertField({ index, type }) {
+ const id = crypto.randomUUID();
+ const newField = {
+ id,
+ type,
+ };
+ form?.fields?.splice(index + 1, 0, newField);
+ setForm({ ...form });
+ }
+
+ function moveField(oldId, newIndex) {
+ console.log({ oldId, newIndex });
+ const oldIndex = form.fields.findIndex(({ id }) => id === oldId);
+ const [oldFormField] = form.fields.splice(oldIndex, 1);
+ if (oldIndex > newIndex) {
+ newIndex++;
+ }
+ form.fields.splice(newIndex, 0, oldFormField);
+ console.log("newform", form);
+ setForm({ ...form });
+ }
+
+ if (!editable && !initialForm) {
+ return;
+ // else if (!initialForm) {
+ // return <FormInitialPrompt />;
+ // }
+ } else {
+ return (
+ <Stack sx={{ width: "100%" }}>
+ <FormHeader form={form} />
+ {editable && (
+ <DropZone
+ moveField={moveField}
+ insertField={insertField}
+ index={-1}
+ />
+ )}
+ {form?.fields?.map((props, i) => (
+ <Grouping key={props?.id}>
+ <FormField
+ setFormField={(field) => updateFormField(field)}
+ deleteFormField={() => deleteFormField(props.id)}
+ {...props}
+ />
+ {editable && (
+ <DropZone
+ moveField={moveField}
+ insertField={insertField}
+ index={i}
+ />
+ )}
+ </Grouping>
+ ))}
+ </Stack>
+ );
+ }
+}
+
+function FormHeader() {
+ return;
+}
+
+//function FormInitialPrompt() {
+// return (
+// <div
+// style={{
+// minWidth: "50vh",
+// minHeight: "50vh",
+// background: "#33333333",
+// }}
+// >
+// Drag and drop and field here to get started
+// </div>
+// );
+//}
+//
+function Grouping({ children }) {
+ return <>{children}</>;
+}