There are a lot of great drag and drop Layout tools out there, form Designer, Layui drag and drop Layout, Vue-Layout.

We recently implemented a similar feature, so without further comment, we posted the preview (I don’t know why the nuggets don’t support GIF images now, so I have to upload them to the graph bed myself).

In the process of implementing this function, we also took a little detours. In the internal 1.0 version, we used SorTableJS. Due to the chaotic code writing, the drag and drop function often got stuck. The same author as Redux, but Dnd does not quite meet our needs. The API of drag and drop is really powerful, but sorting, cross-level drag and drop and many other functions need to be implemented manually. After the implementation of cross-level drag and drop, the boss let me change to SorTableJS.

Drag tools: SorTableJS, React Dnd

Let’s talk about ideas first, and be careful about the hole we dug for ourselves in 1.0 😂.

For those of you who have had experience with tree component development, you are familiar with recursion. The structure of the page on the left is used, and the rendering on the right is used. Overall, the component tree on the left and the canvas area on the right are two recursive functions.

Pages are arrays, components are objects

We want to generate the page, certainly not just to see what the page looks like, good or not, but to save the data, to generate the format we want, the first problem is to face, what the data should look like, what are the fields, respectively what to use.

A page can have multiple components, and they have order, so it’s easier to use an array. What if there are child elements? Whether it’s vue or React, there will be a Tree component in the UI box, and the data format is children nested downward, right?

For example, iView’s Tree component document

So what do children have to do with it? If you look at the form designer and the Layui drag and drop layout, you have a container component, and what do you mean by that? You can drag and drop from that component into that component, and if the data goes down indefinitely, then you need children to help you nest it down indefinitely.

The nesting problem is solved, so how do we render the component? Let’s not worry about the drag and drop problem, but how do we render the component from a single data object? If we use Ant Design components, we need to know the name of the component, right? You gotta give me props, right? We have two fields named and attr, including the children field mentioned above. Let’s make a simple demo first, using the Ant template.

import React, { Component } from 'react';
import { Rate,Input,DatePicker } from 'antd';
const { MonthPicker, RangePicker, WeekPicker } = DatePicker;

const GlobalComponent = {
    Rate,
    Input,
    MonthPicker,
    RangePicker,
    WeekPicker,
}


class EditPage extends Component {

    render() {
        
        // Test data
        const Data = [
            {
                name: 'Input'.attr: {
                    size:'large'.value:'The first'}}, {name: 'Input'.attr: {
                    size:'default'.value:'The second'}}, {name: 'Input'.attr: {
                    size:'small'.value:'The third'}}, {name: 'Containers'.attr: {
                    style: {border:'1px solid red'}},children:[
                    {
                        name: 'Input'.attr: {
                            size:'small'.value:'Nested Input'}}, {name: 'Rate'.attr: {
                            size:'small'.value:'Nested Input'}}, {name: 'MonthPicker'.attr: {}}, {name: 'RangePicker'.attr: {}}, {name: 'WeekPicker'.attr: {}},]},];// Recursive function
        const loop = (arr) = > (
            arr.map(item= > {
                if(item.children){
                    return <div {. item.attr} >{loop(item.children)}</div>
                }
                const ComponentInfo = GlobalComponent[item.name]
                return <ComponentInfo {. item.attr} / >})); return (<>
                {loop(Data)}
            </>
        );
    }
}

export default EditPage;

Copy the code

The page has been rendered, how is it simple? Now let’s do drag and drop

Drag-and-drop implementation

We have the data format, rendering is implemented, the rest is drag and drop, we first have a look at the sorTableJS plug-in, the official react version of the component react-sorTableJS.

Install dependencies, and introduce components into the page. Without going into detail, look at the React-sorTableJS documentation.

Let’s first talk about what sorTableJs provides us.

  1. If you drag from container A to container B, two containersgroupParameters of thenameBe consistent so you can drag each other.
  2. Whether the container can be moved in and out is ingroupIn the configurationpullandputProperties.
  3. The container has two listener events, one that is moved inonAddMethods, one is neweronUpdatemethods
  4. onAddandonUpdateCan only listen to drag and drop elementsdata-idattribute

How do we leverage this functionality?

  1. The list of components, which is the list of source components on the left that we can drag and drop, cannot be moved out, anddata-idYou need the component name to tell the container on the right what component to drag into.
  2. The container on the right needs to be nested, recursively displayed, and the container, not the component, should be displayed if there are children.
  3. The container on the right should be removable and easy to drag across containers.
  4. In order to put the new component data into the corresponding position on the right side of the containerdata-idChange the path to the subscriptThe 2-3-2This form corresponds to the second child of the third child of the second element of the root array.

Ok, let’s write down the list of source components that we can drag and drop

Then modify the container on the right to show a new container if there are children, and add the listening method for add.

onAdd
data-id
To compare the added and deleted paths, operate the lower path first, and then the upper path

import React, { Component } from 'react';
import { Rate,Input,DatePicker,Tag } from 'antd';
import Sortable from 'react-sortablejs';
import uniqueId from 'lodash/uniqueId';
const { MonthPicker, RangePicker, WeekPicker } = DatePicker;
import { indexToArray, getItem, setInfo, isPath, getCloneItem, itemRemove, itemAdd } from './utils';
import find from 'find-process';
const GlobalComponent = {
    Rate,
    Input,
    MonthPicker,
    RangePicker,
    WeekPicker,
}


const soundData = [
    {
        name: 'MonthPicker'.attr: {}}, {name: 'RangePicker'.attr: {}}, {name: 'WeekPicker'.attr: {}}, {name: 'Input'.attr: {
            size:'large'.value:'The first'}}, {name: 'Containers'.attr: {
            style: {border:'1px solid red'}}}]class EditPage extends Component {

    constructor(props) {
        super(props);
        this.state = {
            Data: [{name: 'Input'.attr: {
                    size:'large'.value:'The first']}}}; }// Drag to add method
     sortableAdd = evt= > {
        // Component name or path
        const nameOrIndex = evt.clone.getAttribute('data-id');
        // Parent node path
        const parentPath = evt.path[1].getAttribute('data-id');
        // Drag the target path of the element
        const { newIndex } = evt;
        // If the new path is the root node, use index directly
        const newPath = parentPath ? `${parentPath}-${newIndex}` : newIndex;
        // Check whether the path path is moved. If the path is not moved, the path is added
        if (isPath(nameOrIndex)) {
            // The old path index
            const oldIndex = nameOrIndex;
            // Clone the element to be moved
            const dragItem = getCloneItem(oldIndex, this.state.Data)
            // Compare the upper and lower positions of the path to execute the lower data before executing the test data
            if (indexToArray(oldIndex) > indexToArray(newPath)) {
                // Delete the element to get the new data
                let newTreeData = itemRemove(oldIndex, this.state.Data);
                // Add drag element
                newTreeData = itemAdd(newPath, newTreeData, dragItem)
                // Update the view
                this.setState({Data:newTreeData})
                return
            }
            // Add drag element
            let newData = itemAdd(newPath, this.state.Data, dragItem)
            // Delete the element to get the new data
            newData = itemRemove(oldIndex, newData);

            this.setState({Data:newData})
            return
        }

        // Add process to create element => Insert element => Update view
        const id = nameOrIndex
        
        const newItem = _.cloneDeep(soundData.find(item= > (item.name === id)))
        
        // Add child elements to the container or popup
        if ( newItem.name === 'Containers') {
            const ComponentsInfo = _.cloneDeep(GlobalComponent[newItem.name])
            // Determine whether to include the default data
            newItem.children = []
        }
        
        let Data = itemAdd(newPath, this.state.Data, newItem)
        
        this.setState({Data})
    }

    render() {
        
        // Recursive function
        const loop = (arr,index) = > (
            arr.map((item,i) = > {
                const indexs = index === ' ' ? String(i) : `${index}-${i}`;
                if(item.children){
                    return <div {. item.attr} 
                        data-id={indexs}
                    >
                        <Sortable
                            key={uniqueId()}
                            style={{
                                minHeight:100.margin:10,}}ref={c= >c && (this.sortable = c.sortable)} options={{ ... sortableOption, // onUpdate: evt => (this.sortableUpdate(evt)), onAdd: evt => (this.sortableAdd(evt)), }} > {loop(item.children,indexs)}</Sortable>
                    </div>
                }
                const ComponentInfo = GlobalComponent[item.name]
                return <div data-id={indexs}><ComponentInfo {. item.attr} / ></div>})) const sortableOption = {animation: 150, fallbackOnBody: true, swapThreshold: 0.65, group: {name: 'formItem', pull: true, put: true, }, } return (<>  
                <h2>Component list</h2>
                <Sortable
                    options = {{
                            group:{
                                name: 'formItem',
                                pull: 'clone',
                                put: false,},sort: false,}} >
                    {
                        soundData.map(item => {
                            return <div data-id={item.name}><Tag>{item.name}</Tag></div>})}</Sortable>
                <h2>The container</h2>
                <Sortable
                    ref={c= >c && (this.sortable = c.sortable)} options={{ ... sortableOption, // onUpdate: evt => (this.sortableUpdate(evt)), onAdd: evt => (this.sortableAdd(evt)), }} key={uniqueId()} > {loop(this.state.Data,'')}</Sortable>
            </>
        );
    }
}

export default EditPage;
Copy the code

Now that cross-level operations and new additions have been completed, let’s add the function of swapping positions at the same level. We use immutabilty-Helper, a tool function. Please refer to the document for details, but we only use array transposing.

import update from 'immutability-helper'

// Drag sorting method
sortableUpdate = evt= > {
    // Swap arrays
    const { newIndex, oldIndex } = evt;

    // Parent node path
    const parentPath = evt.path[1].getAttribute('data-id');

    // Call data directly when the parent element root node
    let parent = parentPath ? getItem(parentPath, this.state.Data) : this.state.Data;
    // Drag the element currently
    const dragItem = parent[oldIndex];
    // The updated parent node
    parent = update(parent, {
        $splice: [[oldIndex, 1], [newIndex, 0, dragItem]],
    });

    // Call data directly for the latest data root node
    const Data = parentPath ? setInfo(parentPath, this.state.Data, parent) : parent
    // Call the parent component update method
    this.setState({Data})
}
Copy the code

Now that sorting across levels and peers is complete, let’s take a look at the preview.

In the onUpdate and onAdd functions, they encapsulate some methods according to the subscript operation array, is also in accordance with the functional way, each function returns a new result, writing is not particularly good, many sorry ha, the rest of the delete, selected, according to their needs to increase the function can be, I put the source code on Github, Need to take it, code word hand acid, eat to 😂.

Demo address

Source: https://github.com/nihaojob/DragLayout