如今有不少優秀的拖拽佈局工具,表單設計器,layui拖拽佈局, Vue-Layout。vue
咱們最近也實現了相似的功能,廢話很少說,先把預覽貼出來(不知道爲何掘金如今圖片不支持gif了,還要本身上傳到圖牀)。react
在實現這個的功能的過程當中,也走了一點彎路,咱們內部1.0版本的時候,使用的是sortablejs
,因爲代碼寫的比較混亂,拖拽功能常常出現卡死的現象,覺得是sortablejs
的問題,而後又換成大名鼎鼎的React Dnd
,和Redux
是同一個做者,可是Dnd並非太符合咱們的需求,拖拽的API確實很強大,可是排序、跨級拖拽等好多功能都要本身手動實現,在實現完跨級拖拽之後,老大讓我換成了sortablejs
。git
拖拽工具:sortablejs ,React Dndgithub
咱們仍是先說下思路,還有咱們在1.0裏給本身挖的坑,大家也要當心哈😂。數組
若是有過樹組件開發經驗的小夥伴,應該對遞歸很熟悉了,左側的頁面結構要用到,右側的渲染也要用到,總體來講,左側的組件樹和右側的畫布區,就是兩個遞歸函數。antd
咱們要生成頁面,確定不是隻看看頁面長什麼樣子,好很差看,而是要把數據保存起來,生成咱們想要的格式,首先就要面臨的問題是,這個數據應該長什麼樣子,都有哪些字段,分別幹什麼用。iview
頁面即數組很好理解,一個頁面上能夠有多個組件,並且有順序,用數組就比較方便了,若是有子元素怎麼辦,無論vue仍是react,UI框裏都會有Tree
組件,數據格式都是使用children
向下嵌套數據組,這點好理解吧?函數
舉個栗子,iview的tree組件文檔 工具
子元素和咱們有什麼關係呢,咱們看表單設計器和layui拖拽佈局裏,都有容器組件,什麼意思呢,就是能夠在這個組件下繼續拖拽放入組件,若是數據無限的向下延伸,那就須要children
幫忙向下無限嵌套。oop
嵌套的問題解決了,組件怎麼渲染呢,咱們先不考慮拖拽的問題,單單一個數據對象渲染成組件,怎麼實現呢? 先大體想一下,咱們若是使用Ant Design
的組件,咱們得知道組件的名稱吧?得有本身props
吧? 暫定兩個字段name
和attr
,包括上面提到的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
提供給咱們什麼功能。
group
參數的name
保持一致才能實現相互拖拽。group
中配置pull
和put
屬性。onAdd
方法,一個是更新的onUpdate
方法onAdd
和onUpdate
只能監聽到拖拽元素的data-id
屬性咱們怎麼藉助這些功能實現?
data-id
須要爲組件名稱,才能告知右側的容器拖拽進入的是什麼組件。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})
}
複製代碼
如今跨級和同級的排序的功能都已經完成了,咱們看看預覽圖吧。
在onUpdate
和onAdd
的函數中,本身封裝了一些根據下標操做數組的方法,也是按照函數式的方式,每一個函數返回新的結果,寫的不是特別好,多多見諒哈,剩下的刪除、選中啦,根據本身的需求增長功能就能夠了,我把源碼放在了github上,有須要的拿去吧,碼字碼到手痠,吃飯去了😂。