React.js
Pocket

ドラッグ&ドロップでリストの順番を変えるようなものを実装する時のメモ。

React DnDというのを使いました。

環境構築はCreate React App。ただ、React DnDのサンプルコードがDecorators構文を使っていて、Create React Appを使った場合エラーが出てしまいます。たぶんESなんちゃらの対応の問題。

そこで、シンプルなリストの入れ替えの書き方をちょっと変更して動くようにしました。

準備

Create React Appをした後に以下を追加。

yarn add react-dnd react-dnd-html5-backend

コード

App.js

import React, { Component } from 'react'
import './App.css'
import { DragDropContext } from 'react-dnd'
import HTML5Backend from 'react-dnd-html5-backend'
import Container from './Container'

class App extends Component {
  render () {
    return (
      <Container />
    )
  }
}

export default DragDropContext(HTML5Backend)(App)

Container.js

import React, { Component } from 'react'
import update from 'react/lib/update'
import { DropTarget } from 'react-dnd'
import Card from './Card'

const style = {
  width: 400
}

class Container extends Component {
  constructor (props) {
    super(props)
    this.state = {
      cards: [{
        id: 1,
        text: 'Write a cool JS library'
      }, {
        id: 2,
        text: 'Make it generic enough'
      }, {
        id: 3,
        text: 'Write README'
      }, {
        id: 4,
        text: 'Create some examples'
      }, {
        id: 5,
        text: 'Spam in Twitter and IRC to promote it (note that this element is taller than the others)'
      }, {
        id: 6,
        text: '???'
      }, {
        id: 7,
        text: 'PROFIT'
      }]
    }
  }

  pushCard (card) {
    this.setState(update(this.state, {
      cards: {
        $push: [ card ]
      }
    }))
  }

  removeCard (index) {
    this.setState(update(this.state, {
      cards: {
        $splice: [
          [index, 1]
        ]
      }
    }))
  }

  moveCard (dragIndex, hoverIndex) {
    const { cards } = this.state
    const dragCard = cards[dragIndex]

    this.setState(update(this.state, {
      cards: {
        $splice: [
          [dragIndex, 1],
          [hoverIndex, 0, dragCard]
        ]
      }
    }))
  }

  render () {
    const { cards } = this.state
    const { connectDropTarget } = this.props

    return connectDropTarget(
      <div style={style}>
        {cards.map((card, i) => (
          <Card
            key={card.id}
            card={card}
            index={i}
            removeCard={this.removeCard.bind(this)}
            moveCard={this.moveCard.bind(this)}
          />
        ))}
      </div>
    )
  }
}

const cardTarget = {
  drop (props, monitor, component) {
    const { id } = props
    const sourceObj = monitor.getItem()
    if (id !== sourceObj.listId) component.pushCard(sourceObj.card)
    return {
      listId: id
    }
  }
}

export default DropTarget('CARD', cardTarget, (connect, monitor) => ({
  connectDropTarget: connect.dropTarget(),
  isOver: monitor.isOver(),
  canDrop: monitor.canDrop()
}))(Container)

Card.js

import React, { Component } from 'react'
import { findDOMNode } from 'react-dom'
import { DragSource, DropTarget } from 'react-dnd'
import flow from 'lodash/flow'

const style = {
  border: '1px dashed gray',
  padding: '0.5rem 1rem',
  marginBottom: '.5rem',
  backgroundColor: 'white',
  cursor: 'move'
}

const cardSource = {
  beginDrag (props) {
    return {
      id: props.id,
      index: props.index
    }
  }
}

const cardTarget = {
  hover (props, monitor, component) {
    const dragIndex = monitor.getItem().index
    const hoverIndex = props.index

    if (dragIndex === hoverIndex) {
      return
    }

    const hoverBoundingRect = findDOMNode(component).getBoundingClientRect()

    const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2

    const clientOffset = monitor.getClientOffset()

    const hoverClientY = clientOffset.y - hoverBoundingRect.top

    if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
      return
    }

    if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
      return
    }

    props.moveCard(dragIndex, hoverIndex)

    monitor.getItem().index = hoverIndex
  }
}

class Card extends Component {
  render () {
    const { card, isDragging, connectDragSource, connectDropTarget } = this.props
    const opacity = isDragging ? 0 : 1

    return connectDragSource(connectDropTarget(
      <div style={{ ...style, opacity }}>
        {card.text}
      </div>
    ))
  }
}

export default flow(
  DropTarget('CARD', cardTarget, connect => ({
    connectDropTarget: connect.dropTarget()
  })),
  DragSource('CARD', cardSource, (connect, monitor) => ({
    connectDragSource: connect.dragSource(),
    isDragging: monitor.isDragging()
  }))
)(Card)

これで動くと思います。

とりあえず動くようになったコードを貼り付けただけなので、不要なコードがあるかもですが、もうちょっと精査して変更があれば書き直します。

Pocket

カテゴリー: タグ: