こんにちは、ミツバです。
今回はReact-DnDを利用して、ドラッグ&ドロップを用いたリストの並び替えを実現したいと思います。
ソースコードは以下に置いています。
動作イメージ
テスト環境
テストを行った際の各ライブラリのバージョンを明記しておきます。
- react: ^15.6.2
- react-dnd: ^2.6.0
- react-dnd-html5-backend: ^2.6.0
- react-dnd-touch-backend: ^0.4.0
- react-dom: ^15.6.2
- react-scripts: 2.1.5
setup
create-react-appを用いてサクッと作成します。
npx create-react-app react-dnd-example
cd react-dnd-example
# install
npm install --save react-dnd@2.6.0
npm install --save react-dnd-html5-backend@2.6.0
npm install --save react-dnd-touch-backend@0.4.0
package.jsonの各バージョンは適宜変更してください。
App.jsを変更する
import React, { Component } from 'react';
import SortableList from './SortableList.js';
class App extends Component {
render() {
return (
<div className="App">
<SortableList />
</div>
);
}
}
export default App;
SortableListを作成する
import React, { Component } from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import TouchBackend from 'react-dnd-touch-backend';
import DragItem from './DragItem';
import CustomDragLayer from './CustomDragLayer';
class SortableList extends Component {
constructor(props) {
super(props);
this.state = {
items: [
{id: 1, name: "aaa"},
{id: 2, name: "bbb"},
{id: 3, name: "ccc"}
],
}
}
onDrop = (toId, fromId) => {
const items = this.state.items.slice();
const toIndex = items.findIndex(i => i.id === toId);
const fromIndex = items.findIndex(i => i.id === fromId);
const toItem = items[toIndex];
const fromItem = items[fromIndex];
items[toIndex] = fromItem;
items[fromIndex] = toItem;
this.setState({items})
}
render () {
return (
<div>
{(isAndroid() || isIOS()) && <CustomDragLayer/>}
{
this.state.items.map(item => {
return (
<DragItem
key={item.id}
id={item.id}
onDrop={this.onDrop.bind(this)}
name={item.name}
/>
)
})
}
</div>
)
}
}
function isAndroid() {
return !!window.navigator.userAgent.match(/Android/);
}
function isIOS() {
return !!window.navigator.userAgent.match(/iPhone|iPad|iPod/);
}
export default DragDropContext((isAndroid() || isIOS()) ? TouchBackend : HTML5Backend)(SortableList);
いろいろ概念が登場していますが、CustomDragLayer
とDragItem
をrenderしています。
そして最後にDragDropContext
に分岐を書いてモバイルの場合とそうでない場合のBackend
を切り替えています。
このBackend
は、今回HTML5とTouchの2種類ありデバイスによって、Backend
を変更することでそのデバイスに対応します。
DragItem, CustomDragLayerを作成する
DragItemは以下のような感じです。
import React, { Component } from 'react';
import { DragSource, DropTarget } from 'react-dnd'
// ドラッグされるSourceの動作を定義する
const dragSource = DragSource("item", {
beginDrag(props) {
return props;
}
}, (connect, monitor) => {
return {
connectDragSource: connect.dragSource(),
connectDragPreview: connect.dragPreview(),
isDragging: monitor.isDragging()
};
})
// ドロップされるTargetの動作を定義する
const dropTarget = DropTarget("item", {
drop(dropProps, monitor) {
const dragProps = monitor.getItem();
if (dropProps.id !== dragProps.id) {
dragProps.onDrop(dragProps.id, dropProps.id);
}
}
}, connect => {
return {
connectDropTarget: connect.dropTarget()
};
})
class DragItem extends Component {
constructor(props) {
super(props)
}
getItemStyles() {
const { isDragging } = this.props
return {
opacity: isDragging ? 0.4 : 1,
}
}
render() {
return this.props.connectDragSource(
this.props.connectDropTarget(
<div style={this.getItemStyles()}>
{this.props.name}
</div>
)
)
}
}
export default dragSource(dropTarget(DragItem));
ドラッグされるSourceとドロップされるTargetの動作を定義し、最後にComponentを包んでやる流れです。
今回はドラッグされるものもドロップされるものも同じなので、同じComponent(DragItem)を包んでいます。
これでだいたいオッケーなのですが、モバイルだとドラッグした時の軌跡が表示されません。
そのためにDragLayerを導入し、ドラッグの軌跡をそいつに描画してもらいます。
動作イメージ
DragLayerを利用して、包んだComponentであるCustomDragLayerは以下のような感じ。
import * as React from 'react'
import { DragLayer } from 'react-dnd'
const layerStyles = {
position: 'fixed',
pointerEvents: 'none',
top: 0,
left: 0,
width: '100%',
height: '100%',
}
function collect(monitor) {
return {
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging()
}
}
class CustomDragLayer extends React.Component {
getItemStyles(currentOffset) {
if (!currentOffset) {
return {
display: 'none'
}
}
// move position
const x = currentOffset.x
const y = currentOffset.y
const transform = `translate(${x}px, ${y}px) scale(1.05)`
return {
WebkitTransform: transform,
transform: transform,
}
}
render() {
const { item, itemType, isDragging, currentOffset } = this.props
if (!isDragging) {
return null
}
// render
if (itemType === 'item') {
return (
<div style={layerStyles}>
<div style={this.getItemStyles(currentOffset)}>
{item.name}
</div>
</div>
)
}
return null;
}
}
export default DragLayer(collect)(CustomDragLayer)
これでモバイルでもドラッグの軌跡が表示されたと思います。
細かい対応
DragLayerをwebの方にも適応する
今回、DragLayerをモバイルのドラッグ軌跡の描画に用いましたが、ドラッグしたときのアニメーションを個別で決めたいときにも利用できます。
しかし、デフォルトだとwebのドラッグの軌跡は表示されているので、軌跡が二重に表示されます。
なので、それを防ぐために以下のissueのようにドラッグされているときは、軌跡を描画しないようにします。
connectDragPreview() component is also displayed as regular component
componentDidMount() {
// Preview表示する時のみ、元のComponentの描画を消す
if (this.props.connectDragPreview) {
this.props.connectDragPreview(getEmptyImage(), {
captureDraggingState: true,
});
}
}
hoverを利用して入れ替え処理を行う
DropでItemを変えるよりもより直感的に入れ替える方法として、hoverというものがあります。
それを紹介します。
DragItem
を以下のように変更します。
今回はdropをhoverに変更し、そこに処理を書いていきました。
他にもいろいろAPIが提供されているので調べてみてください。
import React, { Component } from 'react';
import {
DragSource,
DropTarget,
} from 'react-dnd'
const dragSource = DragSource("item", {
// 変更部分
beginDrag(props) {
return {
id: props.id,
name: props.name,
originalIndex: props.findItem(props.id)
};
},
endDrag(props: CardProps, monitor: DragSourceMonitor) {
const { id, originalIndex } = monitor.getItem()
const didDrop = monitor.didDrop()
if (!didDrop) {
props.onDrop(id, originalIndex)
}
},
}, (connect, monitor) => {
return {
connectDragSource: connect.dragSource(),
connectDragPreview: connect.dragPreview(),
isDragging: monitor.isDragging()
};
})
const dropTarget = DropTarget("item", {
// 変更部分
canDrop() {
return false
},
hover(props: CardProps, monitor: DropTargetMonitor) {
const draggedId = monitor.getItem().id
const overId = props.id
if (draggedId !== overId) {
const overIndex = props.findItem(overId)
props.onDrop(draggedId, overIndex)
}
},
}, connect => {
return {
connectDropTarget: connect.dropTarget()
};
})
class DragItem extends Component {
constructor(props) {
super(props)
}
getItemStyles() {
const { isDragging } = this.props
return {
opacity: isDragging ? 0 : 1,
}
}
render() {
return this.props.connectDragSource(
this.props.connectDropTarget(
<div style={this.getItemStyles()}>
{this.props.name}
</div>
)
)
}
}
export default dragSource(dropTarget(DragItem));
そしてfindItemは以下のような定義です。
これをDragItemのpropsとして渡します。
findItem = (index) => {
return this.state.items.filter(c => c.id === index)[0].id
}
動作イメージ
まとめ
react-dndについて調べることがあったので、少し記事にしてみました。
コードペタペタ貼っただけなので、各自雰囲気で理解してください。
ドラッグ・アンド・ドロップ周りでこれがいいよとか他にあったら教えてください。