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.
- If you drag from container A to container B, two containers
group
Parameters of thename
Be consistent so you can drag each other. - Whether the container can be moved in and out is in
group
In the configurationpull
andput
Properties. - The container has two listener events, one that is moved in
onAdd
Methods, one is neweronUpdate
methods onAdd
andonUpdate
Can only listen to drag and drop elementsdata-id
attribute
How do we leverage this functionality?
- The list of components, which is the list of source components on the left that we can drag and drop, cannot be moved out, and
data-id
You need the component name to tell the container on the right what component to drag into. - The container on the right needs to be nested, recursively displayed, and the container, not the component, should be displayed if there are children.
- The container on the right should be removable and easy to drag across containers.
- In order to put the new component data into the corresponding position on the right side of the container
data-id
Change the path to the subscriptThe 2-3-2
This 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