0

I am working on a form builder. The basic implementation works but every time when I move elements from the tool box to the form the current state resets to initial then add newly dropped element so previously dragged element is missed from the form. Below is the linked to the source code at sandbox.

https://codesandbox.io/s/nifty-https-qx8lko

index.js

import Example from "./Example.jsx";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";

import "./style.sass";

function App() {
  return (
    <div className="App">
      <DndProvider backend={HTML5Backend}>
        <Example />
      </DndProvider>
    </div>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);

Example.jsx

import React, { useState } from "react";
import Form from "./Form";
import Toolbar from "./Toolbar";

const Example = (props) => {
  const [Items, setItems] = useState([{ name: "Test" }]);

  const addItem = (item) => {
    setItems([...Items, item]);
  };

  return (
    <div className="card" style={{ border: "1px dashed rgb(219 163 163)" }}>
      <div className="card-header  d-flex justify-content-between">
        <h5 className="cart-title m-0">View</h5>
      </div>
      <div className="card-body">
        <div className="row">
          <div className="col-9">
            <Form items={Items} addItems={addItem} />
          </div>
          <div className="col-3">
            <Toolbar />
          </div>
        </div>
      </div>
    </div>
  );
};

export default Example;

Form.jsx

import React from "react";
import { useDrop } from "react-dnd";

import { ItemTypes } from "./ItemTypes";

function Form(props) {
  const addItem = (item) => {
    props.addItems(item);
  };

  const [{ canDrop, isOver }, drop] = useDrop(() => ({
    accept: ItemTypes.CARD,
    drop: (item, monitor) => addItem(item),
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      canDrop: monitor.canDrop()
    })
  }));

  return (
    <div className="card card-default shadow-sm mt-3">
      <div className="card-body">
        {props.items.map((item, index) => {
          return (
            <div key={index} className="form-item">
              {item.name}
            </div>
          );
        })}
        <div ref={drop}>
          {/* {canDrop ? "Release to drop" : "Drag a box here"} */}
          {isOver && canDrop && (
            <div className="form-place-holder">
              <div>Release to Drop</div>
            </div>
          )}
          {!isOver && (
            <div className="form-place-holder">
              <div>Dropzone</div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

export default Form;

Toolbar.jsx

import React from "react";

import ToolbarItem from "./ToolbarItem";

const Toolbar = () => {
  const _defaultItems = () => {
    return [
      {
        key: "Header",
        name: "Header Text",
        icon: "fas fa-heading",
        static: true,
        content: "Place holder text"
      },
      {
        key: "Label",
        name: "Label",
        static: true,
        icon: "fas fa-font",
        content: "Place holder text"
      },
      {
        key: "Paragraph",
        name: "Paragraph",
        static: true,
        icon: "fas fa-paragraph",
        content: "Place holder text"
      },
      {
        key: "LineBreak",
        name: "Line break",
        static: true,
        icon: "fas fa-arrows-alt-h"
      },
      {
        key: "Dropdown",
        canHaveAnswer: true,
        name: "Dropdown",
        icon: "far fa-caret-square-down",
        label: "Place holder label",
        field_name: "dropdown_",
        options: []
      },
      {
        key: "Tags",
        canHaveAnswer: true,
        name: "Tags",
        icon: "fas fa-tags",
        label: "Place holder label",
        field_name: "tags_",
        options: []
      },
      {
        key: "Checkboxes",
        canHaveAnswer: true,
        name: "Checkboxes",
        icon: "far fa-check-square",
        label: "Place holder label",
        field_name: "checkboxes_",
        options: []
      },
      {
        key: "RadioButtons",
        canHaveAnswer: true,
        name: "Multiple choices",
        icon: "far fa-dot-circle",
        label: "Place holder label",
        field_name: "radiobuttons_",
        options: []
      },
      {
        key: "TextInput",
        canHaveAnswer: true,
        name: "Text input",
        label: "Place holder label",
        icon: "fas fa-font",
        field_name: "text_input_"
      },
      {
        key: "NumberInput",
        canHaveAnswer: true,
        name: "Number input",
        label: "Place holder label",
        icon: "fas fa-plus",
        field_name: "number_input_"
      },
      {
        key: "TextArea",
        canHaveAnswer: true,
        name: "Multi line input",
        label: "Place holder label",
        icon: "fas fa-text-height",
        field_name: "text_area_"
      },
      {
        key: "TwoColumnRow",
        canHaveAnswer: false,
        name: "Two columns row",
        label: "",
        icon: "fas fa-columns",
        field_name: "two_col_row_"
      },
      {
        key: "ThreeColumnRow",
        canHaveAnswer: false,
        name: "Three columns row",
        label: "",
        icon: "fas fa-columns",
        field_name: "three_col_row_"
      },
      {
        key: "FourColumnRow",
        canHaveAnswer: false,
        name: "Four columns row",
        label: "",
        icon: "fas fa-columns",
        field_name: "four_col_row_"
      },
      {
        key: "Image",
        name: "Image",
        label: "",
        icon: "far fa-image",
        field_name: "image_",
        src: ""
      },
      {
        key: "Rating",
        canHaveAnswer: true,
        name: "Rating",
        label: "Place holder label",
        icon: "fas fa-star",
        field_name: "rating_"
      },
      {
        key: "DatePicker",
        canDefaultToday: true,
        canReadOnly: true,
        dateFormat: "MM/dd/yyyy",
        timeFormat: "hh:mm aa",
        showTimeSelect: false,
        showTimeSelectOnly: false,
        showTimeInput: false,
        name: "Date",
        icon: "far fa-calendar-alt",
        label: "Place holder label",
        field_name: "date_picker_"
      },
      {
        key: "Signature",
        canReadOnly: true,
        name: "Signature",
        icon: "fas fa-pen-square",
        label: "Signature",
        field_name: "signature_"
      },
      {
        key: "HyperLink",
        name: "Website",
        icon: "fas fa-link",
        static: true,
        content: "Place holder website link",
        href: "http://www.example.com"
      },
      {
        key: "Download",
        name: "File attachment",
        icon: "fas fa-file",
        static: true,
        content: "Place holder file name",
        field_name: "download_",
        file_path: "",
        _href: ""
      },
      {
        key: "Range",
        name: "Range",
        icon: "fas fa-sliders-h",
        label: "Place holder label",
        field_name: "range_",
        step: 1,
        default_value: 3,
        min_value: 1,
        max_value: 5,
        min_label: "Easy",
        max_label: "Difficult"
      },
      {
        key: "Camera",
        name: "Camera",
        icon: "fas fa-camera",
        label: "Place holder label",
        field_name: "camera_"
      },
      {
        key: "FileUpload",
        name: "File upload",
        icon: "fas fa-file",
        label: "Place holder label",
        field_name: "file_upload_"
      }
    ];
  };
  return (
    <div className="react-form-builder-toolbar">
      <h4>Toolbox</h4>
      <ul>
        {_defaultItems().map((item, index) => {
          return <ToolbarItem data={item} key={index} />;
        })}
      </ul>
    </div>
  );
};

export default Toolbar;

ToolbarItem.jsx

import React from "react";
import { useDrag } from "react-dnd";

import { ItemTypes } from "./ItemTypes";

function ToolbarItem(props) {
  const data = props.data;
  const [{ isDragging }, drag, dragPreview] = useDrag(() => ({
    type: ItemTypes.CARD,
    collect: (monitor) => ({
      isDragging: monitor.isDragging()
    }),
    item: props.data
  }));
  return (
    <li ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }}>
      <i className={data.icon}></i>
      {data.name}
    </li>
  );
}

export default ToolbarItem;

I have tried also storing the state in redux and still same behavior. For simplicity I have kept the state in parent component in Sandbox example.

Nish
  • 1,067
  • 4
  • 17
  • 37

1 Answers1

1

In Example.js file try replacing with this setItems((items)=>[...items, item]);

I think this will fix the issue. Check this here https://codesandbox.io/s/reactjs-dnd-example-forked-1lu5i9?file=/src/Form.jsx

Najmus Sakib
  • 447
  • 5
  • 14
  • Thank you, I spend more time trying to find the issue. It was not clear from the examples in the React DnD website. – Nish Jul 21 '22 at 20:52
  • Do you know to handle this with Redux. For simplicity of explaining I used useState here but in actual implementation with redux still not managed to do. props.dispatch method not able retain current state and experiencing the same problem.; – Nish Jul 22 '22 at 09:42
  • 1
    I don't know why your state resets. Somehow your component is unmounted and remounted every time, when you dropped something. But with redux this should not be happened. And more confusion about if your state is resets, why setState callback function store it? If it resets then any way this should not work. But I wonder somehow it is working with callback setState. No idea why this is happening. But please share your redux code maybe I can help with that. – Najmus Sakib Jul 22 '22 at 10:33
  • I tried to recreate in Codesandbox with redux state and but it works over there. So something in my current code base. Because it has also got many nested route setup, I couldn't replicate exact same scenario. I will further analyze to see if I can find something. – Nish Jul 22 '22 at 15:30
  • I found an answer from another SO question. Here is the reference https://stackoverflow.com/questions/70566291/react-dnd-usedrop-is-not-using-the-current-state-when-the-method-is-called It seems we need pass an additional parameter with the list of properties changed in parent when item is dropped. Otherwise the change is not dragged by DnD and lost. – Nish Jul 22 '22 at 20:41