React-dnd Intro

React-dnd 简析

对于开源插件或者代码来说,我更喜欢推荐其 github 地址,而不是官网地址:

https://github.com/gaearon/react-dnd

安装

1
2
npm install react-dnd -D
npm install react-dnd-html5-backend -D

react-dnd-html5-backend 是一个必要的可选组件,因为 html5 的拖拽API 支持拖动的阴影,总的来说,这插件是必要的。

详细的各名称就不在此说明了,直接介绍关键点吧。

基础

拖拽功能,首先需要知道两个基本的部分:拖起、放下。

拖起,就是鼠标放下并移动的过程;放下,就是鼠标放下拖动元素。 在这一整个过程中,需要声明哪个元素是可以拖动的,哪个元素是可以接收拖动元素的。

react-dnd 中,使用 DragSource 来声明拖动的元素,用DropTarget来声明接收拖动元素的容器。

下面将会通过 看板 的常用功能进行相关的介绍。

看板 基本会涉及到 卡片的上下拖动 和 卡片在不同区间的拖动。

实体 Card, CardBox, Kanban

为了实现基本的功能,需要拖动的实体 Card 有两个基本属性:

1
2
3
id
text // 用于显示卡片
boxId // 用于表示容器间的拖动

Card 实体的简单显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Card = React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { children, id, text } = this.props;
    return (
      <div  className={styles.card} >
        {text}
      </div>
    );
  }
}

CardBox 容器用来接收 Card, 其属性为

1
boxId

显示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CardBox extends React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { children } = this.props;
    return (
      <div className={styles.cardBox}>
        { children }
      </div>
    );
  }
}

children 表示显示的容器中的多个卡片。

最后,看板的代码就是循环了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Kanban extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      cards: [
        {id: 1, text: 'card 1', boxId: 1 },
        {id: 2, text: 'card 2', boxId: 1 },
        {id: 3, text: 'card 3', boxId: 2 },
        {id: 4, text: 'card 4', boxId: 2 },
      ],
      boxes: [
        {id: 1, boxId: 1},
        {id: 2, boxId: 2}
      ]
    }
  }

  render() {
    return (
      <div >
        { this.state.boxes.map( box => {
          return (
            <CardBox
              key={'cb-'+box.boxId}
              boxid={box.boxId}>
              { this.state.cards.map( (card, index) => {
                if (box.boxId==card.boxId)
                  return (
                    <Card
                      key={'c-'+card.id}
                      id={card.id}
                      index={index}
                      boxid={card.boxId}
                      text={card.text}
                      />
                  )
                else return '';
               })}
            </CardBox>
          )
        })}
      </div>
    );
  }
}

先用 boxes 进行列的划分,再通过其的boxId的判断,来划分 cards对应于哪个列中。

为什么这样子循环来显示卡片,而不是先进行分组,再对组进行渲染。 此处主要考虑到,只想用一个内存来存储所有的卡片。如果分组,即相当于将所有 的卡片分开为多个内存存储,本文不想让其间卡片的内存跳转频繁。

但这实现方法有个缺陷,页面渲染会有空元素,else return '' 会造成一个空的span DOM元素。

所以,注意,此处是用到了卡片的index的。

这是基本实体,它们包含关系为 Kanban > CardBox > Card

DragSource

api

DragSource(type, spec, collect)(MyComponent)

DragSource, 顾名思义,就是可拖动的元素。在本文场景中,拖动元素就是Card了。

API 中,有必要的三个参数,分别是type, speccollect

type

type 就是可拖动元素的名称,这会与 DropTargettype 有关联。实际中,名称就是一个字符串, 本文场景中,可以有:

1
2
3
const ItemTypes = {
  CARD: 'Card'
}
spec

spec 拖动元素的对象,注意,它必须是对象。它定义有四个方法,分别是beginDrag(props, monitor, component)endDrag(props, monitor, component)canDrag(props, monitor)isDragging(props, monitor),详细说明可链接到 Drag Source Specification

现在,主要对 beginDrag(props, monitor, component)isDragging(props, monitor) 进行说明。

beginDrag(props, monitor, component) 是必须声明的,它要求返回一个普通的对象,例如{ id: props.id }。 其实,它就是将你用的实体,选择拖动使用的属性进行返回。

isDragging(props, monitor) 是可选的,是用来判断一个元素是否是在拖动的过滤中。默认下,一个对象是否是拖动中, dnd 只会在拖动元素的开始拖动时设置其值。为什么要在这里突出说明一下,因为在操作场景中,初始化的设置在拖动的过程中, 很可能会不正确的,所以需要重新定义其方法实现。

在本文场景中,可以

1
2
3
4
5
6
7
8
9
10
11
12
13
const cardSource = {
  beginDrag(props) {
    return {
      id: props.id,
      index: props.index,
      text: props.text,
      boxId: props.boxid
    };
  },
  isDragging(props, monitor) {
    return props.id === monitor.getItem().id;
  }
};

monitorcomponent 还是需要大家了解一下的。 monitor 就是整合拖动实体和拖动状态的新对象,component粗略可以理解为 DOM 元素。 详情看 Drag Source Specification

collect

collect 为一个函数,而函数返回的是一个对象。本质起到桥接作用,将拖动元素的属性和拖动状态整合。

本文场景中,collect 可以为,

1
2
3
4
5
6
function dragCollect(connect, monitor) {
  return {
    connectDragSource: connect.dragSource(),
    isDragging: monitor.isDragging()
  };
}

综合以上,需要将 Card 声明为可拖动的元素,就是

1
let DragCard = DragSource( ItemTypes.CARD, cardSource, dragCollect)( Card);

同时,需要调整Card类的代码,将绑定引入的属性声明关联到元素中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Card = React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { children, id, text, connectDragSource } = this.props;
    return connectDragSource(
      <div  className={styles.card} >
        {text}
      </div>
    );
  }
}

DropTarget

api

DropTarget(types, spec, collect)(MyComponent)

就是接收拖动元素的容器元素。

types

注意,第一个参数是 types,也就是说,它可以接收多种类型,但也可以是字符串。本文场景中,就是ItemTypes.CARD

spec

同样是普通对象,但不同于拖动元素,容器元素中的说明参数中的方法有drop(props, monitor, component)hover(props, monitor, component)canDrop(props, monitor)

hover(props, monitor, component) 这里重点说一下此方法,因为这个比较常用,当然drop也很常用,但主要与后台交互时才会用到。比如,不同容器中的拖动,需要将放下时的boxId保存到后台,而hover是实时变化的,不适合与后台交互的action进行数据操作。

在拖动元素从一个容器元素,到另一容器元素时,其的boxid会发生变化,这才合理。

所以,需要在 CardBox 的中定义 changeCard(index, newBoxId) 的方法。 其中, index是所有cards列表中的index

本文场景中,

1
2
3
4
5
6
7
8
const cardBoxTarget = {
  hover(props, monitor, component) {
    const hoverCard = monitor.getItem();
    if (hoverCard.id && props.boxid != card.boxId){
      props.changeCard(hoverCard.index, props.boxid);
    }
  }
};
collect

类似于DropSource中的collect, 由于是容器,则没有拖动的属性了。

本文场景中,

1
2
3
4
5
function dropCollect(connect, monitor) {
  return {
    connectDropTarget: connect.dropTarget()
  };
}

综合上面,将BoxCard声明为拖动容器,就是

1
let DropCardBox = DropTarget(ItemTypes.CARD, cardBoxTarget, dropCollect)(CardBox);

同时,绑定后,需要在模型中关联渲染的元素,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CardBox extends React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { children, connectDropTarget } = this.props;
    return connectDropTarget(
      <div className={styles.cardBox}>
        { children }
      </div>
    );
  }
}

另外

基本上,上面大致介绍了一个卡片在不同的容器间相互拖动的故事。

另外,需要指出的是,react-dnd 所有的元素需要使用 DragDropContext 来包裹起来,

1
DragDropContext(HTML5Backend)(Kanban)

上面其中有提到,卡片中容器间相互拖动,需要改变其 boxId 的值, 那么,需要在CardBox中传入changeCard(index, newBoxId) 方法属性。 所以,Kanban需要调整为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Kanban extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      cards: [
        {id: 1, text: 'card 1', boxId: 1 },
        {id: 2, text: 'card 2', boxId: 1 },
        {id: 3, text: 'card 3', boxId: 2 },
        {id: 4, text: 'card 4', boxId: 2 },
      ],
      boxes: [
        {id: 1, boxId: 1},
        {id: 2, boxId: 2}
      ]
    }
  }

  changeCard(index, newBoxId) {
    const dragCard = this.state.cards[index]
    if (!dragCard){ return }

    dragCard.boxId = newBoxId

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

  render() {
    return (
      <div >
        { this.state.boxes.map( box => {
          return (
            <CardBox
              key={'cb-'+box.boxId}
              changeCard={this.changeCard}
              boxid={box.boxId}>
              { this.state.cards.map( (card, index) => {
                if (box.boxId==card.boxId)
                  return (
                    <Card
                      key={'c-'+card.id}
                      id={card.id}
                      index={index}
                      boxid={card.boxId}
                      text={card.text}
                      />
                  )
                else return '';
               })}
            </CardBox>
          )
        })}
      </div>
    );
  }
}

Comments