本文是向大家介绍react中拖拽组件的使用,它能够简洁的实现页面元素的拖拽排序和拖拽复制等功能,能够带来更好的用户体验。
原生js中,我们可以通过onDrag和onDrop事件来实现拖拽效果。而在react中,有一个强大的库,react-dnd,对拖拽相关能力进行了封装。react-dnd强大的好处是高度自由性,但是各种代码需要去手动实现。
我们项目中的装修页面需要实现一个拖拽(排序和复制),这里我们选择一个基于react-dnd二次封装的库,react-smooth-dnd。
文档
安装
npm i react-smooth-dnd
示例
import React, { Component } from 'react'; import { Container, Draggable } from 'react-smooth-dnd'; class SimpleSortableList extends Component { render() { return ( <div> <Container onDrop={this.props.onDrop}> {this.props.items.map(item => { return ( <Draggable key={item.id}> {this.props.renderItem(item)} </Draggable> ); })} </Container> </div> ); } }
API
组件包括Container和Draggable两个。其中Draggable是被拖拽的元素,Container是这些元素的父容器。试验了一下,Draggable必须是Container的子元素。Draggable没有什么属性,相关的属性和方法都在container上设置。
常用的有这些:
- behaviour,设置这个容器是接收draggable的move,还是接收其他容器draggable的copy行为。默认move。
- orientation,决定内部draggable的排布方向,是水平还是垂直,这个比较死板,只有这两种排列方式。
- groupName,这个属性很重要,只有相同groupName之间才可以互相拖拽。
- dropPlaceholder,设置放置时占位元素的样式
- dragBeginDelay,拖拽生效延时,以避免点击事件触发拖拽
- getChildPayload,被拖拽元素要传递的payload数据。
- onDrop,放置函数,接收一个事件,里面包含addedIndex,removedIndex,payload。这样我们就可以根据这些数据去修改列表的值,实现排序或插入。
- shouldAcceptDrop, 可以过滤一些不可放置的元素
- getGhostParent,这个也很重要,不同container之间可能所处的层级不同,通过这个都挂到body上,可以防止拖拽效果被遮挡。
官方demo
拖拽排序
很显然,拖拽排序是默认设置,一个container自身的draggable拖拽即可。无需设置groupName,只需设置onDrop即可,通过addedIndex和removedIndex去修改列表。
拖拽移动
两个或多个container,设置相同的groupName,这样除了自身的拖拽排序,还可以拖拽到另一个container实现跨container的移动。通过onDrop设置移动后的行为,因为groupName相同,我们除了addedIndex和removedIndex,还要知道元素是从哪个container来的,可以在getChildPayload设置。
拖拽复制
同上面拖拽复制,但是其中一个container的behaviour要设置为copy。这样这个container自身的draggable就只能拖到另一个container去实现复制插入,而它本身拖拽无效。
项目中的拖拽实现
可视化组件装修页面需求,我们有几个拖拽区域:页面列表自身的拖拽排序;组件列表自身的拖拽排序;从组件库拖拽组件到组件列表进行复制插入。
其中页面列表是独立的,按照前面拖拽排序的实现即可。
<div className="page-list"> <Container onDrop={onDropPage} dropPlaceholder={{ className: 'page-item placeholder' }} dragBeginDelay={100}> {props.pages && props.pages.map((item,index) => ( <Draggable key={item.path}> <div className="page-item"> ... </div> </Draggable> ))} </Container> </div>
const onDropPage = (e) => { const {addedIndex, removedIndex} = e props.dispatch({ type: 'appDecorate/MOVE_PAGE', payload: { fromIndex: removedIndex, toIndex: addedIndex } }) }
MOVE_PAGE(state, {payload}) { const {fromIndex, toIndex} = payload const pages = JSON.parse(JSON.stringify(state.config.pages)) const page = pages[fromIndex] // 交换 if (fromIndex > toIndex) { pages.splice(fromIndex, 1) pages.splice(toIndex, 0, page) } else if (fromIndex < toIndex) { pages.splice(fromIndex, 1) pages.splice(toIndex, 0, page) } state.config.pages = pages }
组件列表和组件库要设置相同的groupName,组件库behoviour设置为copy。并且组件列表container的onDrop事件,在payload要区分Draggable对象来源,做不同的处理。
<Container dragBeginDelay={100} groupName="modules" getChildPayload={i => ({ source: 'selected-module-list', })} dropPlaceholder={{ className: 'module-item placeholder' }} getGhostParent={() => document.body } onDrop={onDropModule}> {props.curPageModules && props.curPageModules.map((item,index) => ( <Draggable key={index + '-' + item.value}> <div className="module-item"> ... </div> </Draggable> ))} </Container>
<Drawer className="module-drawer" title="选择组件" width="840px" placement={props.placement || 'right'} closable={false} onClose={props.onClose} visible={props.visible} destroyOnClose closable mask={false} > <p className="tip">拖拽到页面组件区域添加</p> <Tabs tabPosition="left"> { props.moduleList && props.moduleList.map((group, groupIndex) => ( <Tabs.TabPane tab={group.name} key={groupIndex}> <div className="module-list"> <Container behaviour="copy" groupName="modules" getChildPayload={i => ({ source: 'module-to-select', data: group.children[i] })} getGhostParent={() => document.body }> { group.children && group.children.map((module, moduleIndex) => ( <Draggable key={groupIndex + '-' + moduleIndex}> <div className="module-item"> ... </div> </Draggable> )) } </Container> </div> </Tabs.TabPane> )) } </Tabs> </Drawer>
const onDropModule = (e) => { const {addedIndex, removedIndex, payload} = e const {source, data} = payload if (source === 'module-to-select') { props.dispatch({ type: 'appDecorate/ADD_MODULE', payload: { moduleIndex: addedIndex, module: data } }) } else if (source === 'selected-module-list') { props.dispatch({ type: 'appDecorate/MOVE_MODULE', payload: { fromModuleIndex: removedIndex, toModuleIndex: addedIndex, } }) } }
最终实现效果如下:
其中,组件库只能水平或垂直,这里直接通过class名设置了换行效果。另外,copy的时候,组件列表的container高度初始很小,不好拖放,也是通过class名设置了container高度100%。