拖拽神器React DnD你真的瞭解了嗎?

簡介

最近在研究用 React 繪製拓撲圖的時候涉及到了 HTML5 拖放 API,瞭解到了 React DnD 這個拖放神器。React DnD 幫咱們封裝了一系列的拖放 API,大大簡化了拖放 API 的使用方式,今天就結合下面這個示例給你們介紹下 React DnD 的用法。
css

重要概念

React Dnd 提供了幾個重要的 API 供咱們使用:html

  • DragSource
  • DropTarget
  • DragDropContext && DragDropContextProvider

DragSource

DragSource 是一個高階組件,使用 DragSource 高階組件包裹的組件能夠進行拖拽操做。html5

基本用法:react

import { DragSource } from 'react-dnd'

class MyComponent {
  /* ... */
}

export default DragSource(type, spec, collect)(MyComponent)

參數:git

  • type:指定拖拽元素的類型,值的類型能夠是 stringsymbol 或者 func ,只有具備相同type類型的元素才能被 drop target 所響應。
  • spec: 一個js對象,上面定義了一些方法,用來描述 drag source 如何對拖動事件進行響應。github

    • beginDrag(props, monitor, component): 必填項。當拖拽開始的時候,這個方法就會被調用,這個方法必需要返回一個js 對象來描述被拖拽的元素,好比返回一個 { id: props.id },經過monitor.getItem() 方法能夠獲取到返回結果。
    • endDrag(props, monitor, component): 非必填項。當拖拽中止的時候,這個方法會被調用,經過 monitor.didDrop() 能夠判斷 drag source 是否已經被 drop target 處理完畢。若是在 drop targetdrop 方法中返回了一個對象,在這裏能夠經過 monitor.getDropResult() 獲取到返回結果。
    • canDrag(props, monitor): 可選參數。能夠指定當前的拖拽操做是否被容許。
    • isDragging(props, monitor): 可選參數。拖拽時觸發的事件,注意,在這個方法裏面不能再調用 monitor.isDragging()
方法中的參數解釋:
- props:當前組件的 `props` 參數。
- monitor:一個 `DragSourceMonitor` 實例。經過它能夠獲取當前的拖拽信息,好比能夠獲取當前被拖拽的項目及其類型,當前和初始座標和偏移,以及它是否已被刪除。
- component:是組件的實例。使用它能夠訪問DOM元素來進行位置或大小測量,或調用組件裏面定義的方法,或者進行 `setState` 操做。有時候在 isDragging、 canDrag 方法裏可能獲取不到 `component` 這個參數,由於它們被調用時實例可能不可用。
  • collect: 必填項,把拖拽過程當中須要的信息注入組件的 props,接收兩個參數 connectmonitorapi

    • connect: DragSourceConnector 的實例,包括 dragPreview()dragSource() 兩個方法,經常使用的是 dragSource() 這個方法。bash

      • dragSource: 返回一個函數,傳遞給組件用來將 source DOMReact DnD Backend 鏈接起來。
      • dragPreview: 返回一個函數,傳遞給組件用來將拖動時預覽的 DOM 節點 和 React DnD Backend 鏈接起來。
    • monitor: DragSourceMonitor 的實例,包含的具體方法能夠參考這裏

DropTarget

DropTarget 是一個高階組件,被 DropTarget 包裹的組件可以放置拖拽組件,可以對 hover 或者 dropped 事件做出響應。app

基本用法:dom

import { DropTarget } from 'react-dnd'

class MyComponent {
  /* ... */
}

export default DropTarget(types, spec, collect)(MyComponent)

參數:

  • types: 指定拖拽元素的類型,值的類型能夠是 stringsymbol 或者 arraydrop target 只接受具備相同 type 類型的 drag source
  • spec: 一個js對象,上面定義了一些方法,描述了拖放目標對拖放事件的反應。

    • drop(props, monitor, component): 可選參數。當item被放置到目標元素上時會被調用。若是這個方法返回的是一個js對象,在 drag sourceendDrag 方法裏面,調用 monitor.getDropResult() 能夠得到返回結果。
    • hover(props, monitor, component): 可選參數。當item通過 drop target 的時候被調用。能夠經過 monitor.isOver({ shallow: true }) 方法來檢查懸停是僅發生在當前目標上仍是嵌套上。
    • canDrop(props, monitor): 可選參數。這個方法能夠用來檢測 drop target 是否接受 item。
方法中的參數解釋:
- props:當前組件的 `props` 參數。
- monitor:一個 `DropTargetMonitor` 實例。經過它能夠獲取當前的拖拽信息,好比能夠獲取當前被拖拽的項目及其類型,當前和初始座標和偏移,以及它是否已被刪除。
- component:是組件的實例。使用它能夠訪問DOM元素來進行位置或大小測量,或調用組件裏面定義的方法,或者進行 `setState` 操做。有時候在 isDragging、 canDrag 方法裏可能獲取不到 `component` 這個參數,由於它們被調用時實例可能不可用。
  • collect: 必填項,把拖拽過程當中須要的信息注入組件的 props,接收兩個參數 connectmonitor

    • connect: DropTargetConnector 的實例,包括 dropTarget 一個方法。

      • dropTarget: 返回一個函數,傳遞給組件用來將 source DOMReact DnD Backend 鏈接起來。
    • monitor: DropTargetMonitor 的實例,包含的具體方法能夠參考這裏

DragDropContext && DragDropContextProvider

使用 DragSourceDropTarget 包裹的組件必須放置在 DragDropContext 或者 DragDropContextProvider 組件內部。

基本用法:

import Backend from 'react-dnd-html5-backend'
import { DndProvider } from 'react-dnd'

export default function MyReactApp() {
  return (
    <DndProvider backend={Backend}>
      /* your drag-and-drop application */
    </DndProvider>
  )
}

參數

  • backend: 必填項。HTML5 DnD API 兼容性不怎麼樣,而且不適用於移動端,因此乾脆把 DnD 相關具體DOM事件抽離出去,單獨做爲一層,即 Backend,咱們能夠根據 React DnD提供的約定協議定義本身的 Backend。

示例

瞭解了上述 API 的基本使用,如今咱們就來實現下開頭的demo。

本示例是基於 create-react-app 開發的,經過create-react-app的CLI工具建立咱們的demo工程:

$ create-react-app react-dnd-demo

src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import Container from './Container'
import { DndProvider } from 'react-dnd'
import Backend from 'react-dnd-html5-backend'

function App() {
  return (
    <div className="App">
      <DndProvider backend={Backend}>
        <Container />
      </DndProvider>
    </div>
  )
}

const rootElement = document.getElementById('root')
ReactDOM.render(<App />, rootElement)

src/Container.js

import React from 'react';
import { DropTarget } from 'react-dnd';
import DraggableBox from './DraggableBox';
import Types from './types'

const styles = {
  width: '500px',
  height: '300px',
  position: 'relative',
  border: '1px solid black',
}

@DropTarget(
  Types.Box,
  {
    drop: (props, monitor, component) => {
      if(!component) {
        return;
      }

      const delta = monitor.getDifferenceFromInitialOffset();
      const item = monitor.getItem();
      const left = Math.round(delta.x + item.left);
      const top = Math.round(delta.y + item.top);

      component.moveBox(item.id, left, top);
    },
  },
  (connect, monitor) => ({
    connectDropTarget: connect.dropTarget(),
    isOver: monitor.isOver(),
    canDrop: monitor.canDrop(),
  })
)
class Container extends React.Component {
  state = {
    boxes: {
      a: { top: 20, left: 80, title: 'Drag me around' },
      b: { top: 180, left: 20, title: 'Drag me too' },
    },
  }

  moveBox = (id, left, top) => {
    const { boxes }  = this.state;
    this.setState({
      boxes: {
        ...boxes,
        [id]: {
          ...boxes[id],
          left,
          top
        }
      }
    })
  }

  render() {
    const { isOver, canDrop, connectDropTarget} = this.props;
    const { boxes } = this.state;
    const isActive = isOver && canDrop;

    let backgroundColor = '#ccc';
    // 拖拽組件此時正處於 drag target 區域時,當前組件背景色變爲 darkgreen
    if (isActive) {
      backgroundColor = '#453467';
    }

    console.log('qqqq', this.state.boxes)

    return connectDropTarget && connectDropTarget(
      <div style={{ ...styles, backgroundColor}}>
        {Object.keys(boxes).map(item => <DraggableBox {...boxes[item]} id={item} />)}
      </div>
    )
  }
}

export default Container;

能夠看到,在 drop 方法裏,經過 monitor.getDifferenceFromInitialOffset() 方法計算出每次 drop 的時候,當前元素與拖拽前元素位置的偏移量,monitor.getItem() 方法能夠得到當前 哪一個元素被拖拽(必需要在 drag sourcebeginDrag 方法中返回),調用 component 上的 moveBox 方法從新設置拖拽以後的最新位置,從而實現元素的移動。

collectconnect 方法中經過 monitor.isOver()monitor.canDrop() 方法將 isOvercanDrop 參數傳遞到組件的 props 中來判斷當前組件是否處於拖拽狀態中,這裏能夠用來設置拖拽時容器的背景色。

這裏有個細節須要注意的是,Container 容器的 position 屬性被設置爲了 relative,這樣被拖拽的元素就會相對於該容器進行定位。

src/DraggableBox.js

import React from 'react';
import { DragSource } from 'react-dnd';
import Box from './Box';
import Types from './types'

@DragSource(
  Types.Box,
  {
    beginDrag: (props) => {
      const { id, title, left, top } = props
      return { id, title, left, top }
    }
  },
  (connect, monitor)=> ({
    connectDragSource: connect.dragSource(),
    isDragging: monitor.isDragging(),
  })
)
class DraggableBox extends React.Component {
  getStyle = () => {
    const { left, top } = this.props;

    const transform = `translate(${left}px, ${top}px)`
    return {
      position: 'absolute',
      transform,
    }
  }

  render() {
    const { connectDragSource } = this.props;
    return connectDragSource(
      <div style={this.getStyle()}><Box {...this.props}/></div>
    )
  }
}

export default DraggableBox;

beginDrag 方法必須返回一個對象,之前在 drop 方法中獲取到當前被拖拽組件的信息。positon 屬性必須被設置爲 absolute,以方便相對容器進行定位。元素的移動是經過 csstransform 屬性進行控制的。

src/Box.js

import React from 'react';

const styles = {
  border: '1px dashed gray',
  backgroundColor: 'white',
  padding: '0.5rem 1rem',
  marginRight: '1.5rem',
  marginBottom: '1.5rem',
  cursor: 'move',
  display: 'inline-block'
}

class Box extends React.Component {
  render() {
    const { title, left, right } = this.props;
    return (
      <div style={{...styles}}>
        {title}
      </div>
    )
  }
}

export default Box;

總結

關於 React DnD 的介紹,這裏只是作了一個基本介紹,更多的示例你們能夠參考官網,本示例的代碼你們在這裏能夠找到。

相關文章
相關標籤/搜索