React無門檻實現拖拽佈局、表單設計器

如今有不少優秀的拖拽佈局工具,表單設計器layui拖拽佈局, Vue-Layoutvue

咱們最近也實現了相似的功能,廢話很少說,先把預覽貼出來(不知道爲何掘金如今圖片不支持gif了,還要本身上傳到圖牀)。react

在實現這個的功能的過程當中,也走了一點彎路,咱們內部1.0版本的時候,使用的是sortablejs,因爲代碼寫的比較混亂,拖拽功能常常出現卡死的現象,覺得是sortablejs的問題,而後又換成大名鼎鼎的React Dnd,和Redux是同一個做者,可是Dnd並非太符合咱們的需求,拖拽的API確實很強大,可是排序、跨級拖拽等好多功能都要本身手動實現,在實現完跨級拖拽之後,老大讓我換成了sortablejsgit

拖拽工具:sortablejsReact Dndgithub

咱們仍是先說下思路,還有咱們在1.0裏給本身挖的坑,大家也要當心哈😂。數組

若是有過樹組件開發經驗的小夥伴,應該對遞歸很熟悉了,左側的頁面結構要用到,右側的渲染也要用到,總體來講,左側的組件樹和右側的畫布區,就是兩個遞歸函數。antd

頁面即數組,組件即對象

咱們要生成頁面,確定不是隻看看頁面長什麼樣子,好很差看,而是要把數據保存起來,生成咱們想要的格式,首先就要面臨的問題是,這個數據應該長什麼樣子,都有哪些字段,分別幹什麼用。iview

頁面即數組很好理解,一個頁面上能夠有多個組件,並且有順序,用數組就比較方便了,若是有子元素怎麼辦,無論vue仍是react,UI框裏都會有Tree組件,數據格式都是使用children向下嵌套數據組,這點好理解吧?函數

舉個栗子,iview的tree組件文檔 工具

子元素和咱們有什麼關係呢,咱們看表單設計器layui拖拽佈局裏,都有容器組件,什麼意思呢,就是能夠在這個組件下繼續拖拽放入組件,若是數據無限的向下延伸,那就須要children幫忙向下無限嵌套。oop

嵌套的問題解決了,組件怎麼渲染呢,咱們先不考慮拖拽的問題,單單一個數據對象渲染成組件,怎麼實現呢? 先大體想一下,咱們若是使用Ant Design的組件,咱們得知道組件的名稱吧?得有本身props吧? 暫定兩個字段nameattr,包括上面提到的children字段,咱們先簡單的作個demo,用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

安裝依賴,在頁面中引入組件,很少說,看react-sortablejs文檔

接下來先說一下sortablejs提供給咱們什麼功能

  1. 若是從容器A拖拽到容器B,兩個容器group參數的name保持一致才能實現相互拖拽。
  2. 容器是否可移入和移出,是在group中配置pullput屬性。
  3. 容器有兩個監聽事件,一個是移入的onAdd方法,一個是更新的onUpdate方法
  4. onAddonUpdate只能監聽到拖拽元素的data-id屬性

咱們怎麼藉助這些功能實現?

  1. 組件列表,就是左側供咱們拖拽的源組件列表,不能移出,而且data-id須要爲組件名稱,才能告知右側的容器拖拽進入的是什麼組件。
  2. 右側容器須要嵌套,遞歸展現,有子元素時要展現容器而不是組件。
  3. 右側的容器要可移入移出,方便跨容器拖拽。
  4. 爲了把新增的組件數據放進右側對應的位置,右側容器的data-id修改成下標的路徑2-3-2這樣的形式,對應根數組的第2個元素的第3個子元素的第2個子元素。

好了,如今先把供咱們拖拽的源組件列表寫出來

而後把右側的容器改造一下,若是有子元素則展現一個新的容器,而且加上add的監聽方法。

須要注意的是,跨級拖拽的時候觸發 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})
}
複製代碼

如今跨級和同級的排序的功能都已經完成了,咱們看看預覽圖吧。

onUpdateonAdd的函數中,本身封裝了一些根據下標操做數組的方法,也是按照函數式的方式,每一個函數返回新的結果,寫的不是特別好,多多見諒哈,剩下的刪除、選中啦,根據本身的需求增長功能就能夠了,我把源碼放在了github上,有須要的拿去吧,碼字碼到手痠,吃飯去了😂。

演示地址

源碼:https://github.com/nihaojob/DragLayout

相關文章
相關標籤/搜索