React не имеет порога для достижения макета перетаскивания, конструктора форм

React.js

Существует множество отличных инструментов компоновки с помощью перетаскивания.конструктор форм,макет перетаскивания Laui, Vue-Layout.

Мы тоже недавно реализовали подобную функцию, без лишних слов, давайте сначала выложим превью (я не знаю, почему наггеты больше не поддерживают gif, и им приходится самим загружать его на кровать изображений).

В процессе реализации этой функции мы тоже немного отклонились: при внутренней версии 1.0 мы использовалиsortablejs, поскольку код написан в беспорядке, функция перетаскивания часто зависает, думая, что этоsortablejs, а затем заменил его на знаменитыйReact Dnd,а такжеReduxЭто тот же автор, но Dnd не слишком подходит для наших нужд.API перетаскивания действительно очень мощный, но многие функции, такие как сортировка и перетаскивание между уровнями, должны быть реализованы вручную.После реализации перетаскивания между уровнями, босс позволяет заменить его наsortablejs.

Инструмент перетаскивания:sortablejs,React Dnd

Давайте сначала поговорим об идеях и ямах, которые мы вырыли для себя в 1.0, вы должны быть осторожны.

Если у вас есть опыт разработки компонентов дерева, вам следуетрекурсияОчень знакомо, используется структура страницы слева, а также используется рендеринг справа.В целом, дерево компонентов слева и область холста справа — это две рекурсивные функции.

Страница — это массив, а компонент — это объект

Если мы хотим сгенерировать страницу, мы должны не просто посмотреть, как выглядит страница и хорошо ли она выглядит, а сохранить данные и сгенерировать нужный нам формат. как выглядят данные?какие поля для чего используются.

Легко понять, что страница представляет собой массив.На странице может быть несколько компонентов, и они имеют порядок.Массивом пользоваться удобнее.А если есть подэлементы?Неважно vue или react, там будет в поле пользовательского интерфейса.Treeкомпоненты, форматы данных используютсяchildrenВложение групп данных вниз легко понять, верно?

Например, древовидный компонент iviewДокументация

Какое отношение к нам имеет дочерний элемент, давайте разберемсяконструктор форма такжемакет перетаскивания Lauiв, естькомпонент контейнера, что это значит, то есть можно продолжать перетаскивать компоненты под этот компонент.Если данные простираются вниз бесконечно, нужноchildrenПомогает вкладываться бесконечно вниз.

Проблема вложенности решена.Как рендерить компоненты?Давайте не будем рассматривать проблему перетаскивания,а как рендерить один объект данных в компонент? Подумайте сначала, если мы используемAnt Designкомпонента, мы должны знать имя компонента, верно? должен иметь себяpropsБар? Ориентировочно два поляnameа такжеattr, в том числе указанные вышеchildrenФилд, давайте сначала просто сделаем демонстрацию, используя шаблон ant.

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() {
        
        // 测试数据
        const Data = [
            {
                name: 'Input',
                attr: {
                    size:'large',
                    value:'第一个'
                }
            },
            {
                name: 'Input',
                attr: {
                    size:'default',
                    value:'第二个'
                }
            },
            {
                name: 'Input',
                attr: {
                    size:'small',
                    value:'第三个'
                }
            },
            {
                name: 'Containers',
                attr: {
                    style:{
                        border:'1px solid red'
                    }
                },
                children:[
                    {
                        name: 'Input',
                        attr: {
                            size:'small',
                            value:'嵌套的input'
                        }
                    },
                    {
                        name: 'Rate',
                        attr: {
                            size:'small',
                            value:'嵌套的input'
                        }
                    },
                    {
                        name: 'MonthPicker',
                        attr: {}
                    },
                    {
                        name: 'RangePicker',
                        attr: {}
                    },
                    {
                        name: 'WeekPicker',
                        attr: {}
                    },
                ]

            },
        ];
        
        // 递归函数
        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;

Страница отрендерилась, как это просто? Далее давайте реализуем перетаскивание вместе.

Перетащите реализацию

Формат данных у нас есть, рендеринг тоже реализован, остальное перетаскивание, давайте сначала разберемсяsortablejsЭтот плагин официально предоставляетсяreactверсии компонентовreact-sortablejs.

Установить зависимости, ввести компоненты на страницу, особо нечего сказать, см.документация по реакции-сортировке.

Далее поговоримsortablejsКакие функции нам предоставляются.

  1. Если вы перетащите из контейнера A в контейнер B, оба контейнераgroupпараметрическийnameЧтобы быть последовательным, можно добиться взаимного перетаскивания.
  2. Можно ли вводить и выдвигать контейнер, находится вgroupСредняя конфигурацияpullа такжеputАтрибуты.
  3. Контейнер имеет два события слушателя, одно перемещается внутрьonAddметод, один обновляетсяonUpdateметод
  4. onAddа такжеonUpdateМожно только слушать перетаскивание элементовdata-idАтрибуты

Как мы можем использовать эти функции для достижения?

  1. Список компонентов, который представляет собой список исходных компонентов слева, который мы можем перетаскивать, не может быть перемещен, иdata-idИмя компонента необходимо, чтобы сообщить контейнеру справа, в какой компонент перетаскивается.
  2. Правильный контейнер должен быть вложенным, отображаться рекурсивно, а при наличии дочерних элементов должен отображаться контейнер вместо компонента.
  3. Контейнер справа должен перемещаться внутрь и наружу, чтобы облегчить перетаскивание по контейнеру.
  4. Чтобы поместить данные вновь добавленного компонента в соответствующую позицию справа,data-idИзменено на указанный путь2-3-2Эта форма соответствует второму дочернему элементу третьего дочернего элемента второго элемента корневого массива.

Хорошо, теперь давайте напишем список исходных компонентов, чтобы мы могли его перетаскивать.

Затем преобразуйте контейнер справа.Если есть дочерние элементы, будет отображаться новый контейнер, и будет добавлен метод добавления слушателя.

Следует отметить, что он срабатывает при перетаскивании по уровням.onAdd, надо судить о входеdata-idЭто индекс или компонент, если он добавлен непосредственно для компонента, если это индекс, тоСравните вновь добавленные и удаленные пути, сначала используйте нижний путь, а затем верхний путь..

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:'第一个'
        }
    },
    {
        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:'第一个'
                } 
            }],
        };
    }

     // 拖拽的添加方法
     sortableAdd = evt => {
        // 组件名或路径
        const nameOrIndex = evt.clone.getAttribute('data-id');
        // 父节点路径
        const parentPath = evt.path[1].getAttribute('data-id');
        // 拖拽元素的目标路径
        const { newIndex } = evt;
        // 新路径 为根节点时直接使用index
        const newPath = parentPath ? `${parentPath}-${newIndex}` : newIndex;
        // 判断是否为路径 路径执行移动,非路径为新增
        if (isPath(nameOrIndex)) {
            // 旧的路径index
            const oldIndex = nameOrIndex;
            // 克隆要移动的元素
            const dragItem = getCloneItem(oldIndex, this.state.Data)
            // 比较路径的上下位置 先执行靠下的数据 再执行考上数据
            if (indexToArray(oldIndex) > indexToArray(newPath)) {
                // 删除元素 获得新数据
                let newTreeData = itemRemove(oldIndex, this.state.Data);
                // 添加拖拽元素
                newTreeData = itemAdd(newPath, newTreeData, dragItem)
                // 更新视图
                this.setState({Data:newTreeData})
                return
            }
            // 添加拖拽元素
            let newData = itemAdd(newPath, this.state.Data, dragItem)
            // 删除元素 获得新数据
            newData = itemRemove(oldIndex, newData);

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

        // 新增流程 创建元素 => 插入元素 => 更新视图
        const id = nameOrIndex
        
        const newItem = _.cloneDeep(soundData.find(item => (item.name === id)))
        
        // 为容器或者弹框时增加子元素
        if ( newItem.name === 'Containers') {
            const ComponentsInfo = _.cloneDeep(GlobalComponent[newItem.name])
            // 判断是否包含默认数据
            newItem.children = []
        }
        
        let Data = itemAdd(newPath, this.state.Data, newItem)
        
        this.setState({Data})
    }

    render() {
        
        // 递归函数
        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>组件列表</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>容器</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;

Теперь, когда кросс-уровневая операция и добавление нового завершены, добавим функцию переключения позиций на этом же уровне.immutability-helperПодробнее об этой инструментальной функции см. в документации, но она использует только транспонирование массива.

import update from 'immutability-helper'

// 拖拽的排序方法
sortableUpdate = evt => {
    // 交换数组
    const { newIndex, oldIndex } = evt;

    // 父节点路径
    const parentPath = evt.path[1].getAttribute('data-id');

    // 父元素 根节点时直接调用data
    let parent = parentPath ? getItem(parentPath, this.state.Data) : this.state.Data;
    // 当前拖拽元素
    const dragItem = parent[oldIndex];
    // 更新后的父节点
    parent = update(parent, {
        $splice: [[oldIndex, 1], [newIndex, 0, dragItem]],
    });

    // 最新的数据 根节点时直接调用data
    const Data = parentPath ? setInfo(parentPath, this.state.Data, parent) : parent
    // 调用父组件更新方法
    this.setState({Data})
}

Теперь, когда функции кросс-уровневой и одноуровневой сортировки завершены, давайте взглянем на изображение для предварительного просмотра.

существуетonUpdateа такжеonAddВ функции от , я инкапсулировал некоторые методы работы с массивами по индексам.Это тоже по функционалу.Каждая функция возвращает новый результат.Написано не очень.Прошу прощения.Остальные удаляются и выбираются. Вы можете добавлять функции под свои нужды.Исходники я выложил на гитхаб, берите если надо, код кислый, пора кушать.

демонстрационный адрес

Исходный код: https://github.com/nihaojob/DragLayout