使用 react-beautiful-dnd 快速实现可拖拽看板

当我还在使用 react-dnd 设计拖拽逻辑和交互、当我还在为计算拖拽元素和 hover 元素的位置坐标而烦躁不已,当我还在为即将到来的 deadline 发愁之际,我发现了 react-beautiful-dnd。写了个 demo 试了下,真香!

先上代码

话不多说,先上代码。(我等伸手党福音~)

如果懒得看代码,这里是 >>Github 地址<<

安装所需库:

$ yarn add react-beautiful-dnd
$ yarn add @types/react-beautiful-dnd

下面是代码:

import React, { useState } from 'react'
import { DragDropContext, Droppable, Draggable, DropResult, DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd'
import update from 'immutability-helper'
import styles from './index.less'

interface initialDataInferface {
  id: number;
  name: string;
  issues: {
    id: number;
    name: string
  }[]
}

interface ColumnProps {
  columnIndex: number
  column: initialDataInferface
}

interface IssueProps {
  id: number
  issueIndex: number,
  name: string
}

const InitialData: initialDataInferface[] = [
  {
    id: 100,
    name: 'todo',
    issues: [{ id: 1, name: '吃饭' }, { id: 2, name: '睡觉' }, { id: 3, name: '打豆豆' }],
  },
  {
    id: 200,
    name: 'doing',
    issues: [{ id: 4, name: '删库' }, { id: 5, name: '跑路' }]
  },
  {
    id: 300,
    name: 'done',
    issues: []
  }
]

const Issue = (props: IssueProps) => {
  const { id, issueIndex, name } = props

  return (
    <Draggable draggableId={`${id}`} index={issueIndex}>
      {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
        <div
          ref={provided.innerRef}
          className={snapshot.isDragging ? styles.issueDragging : styles.issue}
          {...provided.draggableProps}
          {...provided.dragHandleProps}
        >
          {name}
        </div>
      )}
    </Draggable>
  )
}

const Column = (props: ColumnProps) => {
  const { columnIndex, column } = props
  const { issues } = column
  return (
    <div className={styles.column}>
      <div className={styles.columnTitle}>
        {column.name}({column.issues.length})
      </div>
      <Droppable droppableId={`${columnIndex}`}>
        {(provided, snapshot) => (
          <div
            ref={provided.innerRef}
            className={snapshot.isDraggingOver ? styles.columnContentActive : styles.columnContent}
            {...provided.droppableProps}
          >
            {issues.map((issue, index) => (
              <Issue key={issue.id} issueIndex={index} id={issue.id} name={issue.name} />
            ))}
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </div>
  )
}

export default () => {
  const [data, setData] = useState(InitialData)

  const onDragEnd = (result: DropResult) => {
    const { destination, source } = result
    if (!destination) {
      return
    }

    const fromColumnIndex = Number(source.droppableId)
    const fromIssueIndex = source.index
    const toColumnIndex = Number(destination.droppableId)
    const toIssueIndex = destination.index

    const TempIssue = data[fromColumnIndex].issues[fromIssueIndex]

    let TempData = update(data, {
      [fromColumnIndex]: {
        issues: issues =>
          update(issues, {
            $splice: [[fromIssueIndex, 1]]
          })
      }
    })

    TempData = update(TempData, {
      [toColumnIndex]: {
        issues: issues =>
          update(issues, {
            $splice: [[toIssueIndex, 0, TempIssue]]
          })
      }
    })

    setData(TempData)
  }

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <div className={styles.container}>
        {data.map((column, index) => {
          return <Column columnIndex={index} key={column.id} column={column} />
        })}
      </div>
    </DragDropContext>
  )
}

为了好看加了些样式:

.container {
  display: flex;
  height: 640px;
  background: #f7f7f7;
}

.column {
  display: inline-block;
  width: 292px;
  height: 640px;
}

.columnTitle {
  color: #383838;
  font-size: 14px;
  font-weight: 600;
  line-height: 40px;
  margin: 0 10px;
}

.columnContent {
  height: 600px;
  overflow: auto;
}

.columnContentActive {
  overflow: auto;
  height: 600px;
  background: #ccecff;
  border: 2px solid #1b9aee;
}

.issue {
  position: relative;
  min-height: 20px;
  padding: 14px 44px;
  background: #ffffff;
  margin: 8px 10px;
}

.issueDragging {
  position: relative;
  min-height: 20px;
  padding: 14px 44px;
  background: #ffffff;
  opacity: 0.9;
  margin: 8px 10px;
}

效果是这样子的:

示例

因为我看了网上的拖拽 demo 都不够精美,有些代码也比较老了,所以贴个最新的希望能对大家有所帮助吧。

简单介绍 API

详细 API 就不人肉翻译啦~没啥意义,看 >>这里<<就好。

PS:一开始在首页 README 找 API 居然没有找到,却跑到了一个教学视频网站去了。把我给气的……后来才想起来按道理会有一个叫 doc 的目录 - -

简单说下我用到的 API:

  • <DragDropContext /> 是为了给拖拽提供上下文的,只有在 <DragDropContext /> 中去写拖拽才是有效的。这里注意它必须要一个 onDragEnd 的 props 来操作拖拽结束事件。其他事件有如下,具体说用看一眼就知道。
 onBeforeCapture = () => {
    /*...*/
  };

  onBeforeDragStart = () => {
    /*...*/
  };

  onDragStart = () => {
    /*...*/
  };
  onDragUpdate = () => {
    /*...*/
  };
  onDragEnd = () => {
    // the only one that is required
  };

  render() {
    return (
      <DragDropContext
        onBeforeCapture={this.onBeforeCapture}
        onBeforeDragStart={this.onBeforeDragStart}
        onDragStart={this.onDragStart}
        onDragUpdate={this.onDragUpdate}
        onDragEnd={this.onDragEnd}
      >
        <div>Hello world</div>
      </DragDropContext>
    );
  }
  • 然后是 <Droppable />,它用来定义放置拖拽元素的容器。它有一个必填属性 droppableId,另外它的 children 属性被定义成了一个函数,函数提供的 provided 用来绑定 DOM 节点,提供的 snapshot 可以让我们获取当前放置容器的属性和状态。
import { Droppable } from 'react-beautiful-dnd';

<Droppable droppableId="droppable-1" type="PERSON">
  {(provided, snapshot) => (
    <div
      ref={provided.innerRef}
      style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }}
      {...provided.droppableProps}
    >
      <h2>I am a droppable!</h2>
      {provided.placeholder}
    </div>
  )}
</Droppable>;
  • <Draggable /> 包含的东西就是那个拖拽元素啦。它必须包含有 draggableIdindexchildren 三个属性。它和 <Droppable /> 类似也是将 children 定义为了函数,函数提供的 provided 用来绑定 DOM 节点,提供的 snapshot 可以让我们获取当前放置容器的属性和状态。
import { Draggable } from 'react-beautiful-dnd';

<Draggable draggableId="draggable-1" index={0}>
  {(provided, snapshot) => (
    <div
      ref={provided.innerRef}
      {...provided.draggableProps}
      {...provided.dragHandleProps}
    >
      <h4>My draggable</h4>
    </div>
  )}
</Draggable>;

注意,三者是包含关系,必须逐层实现才能够做到拖拽哦。所以呈现的 DOM 结构样子应该是酱紫的:

<DragDropContext>
  <Droppable>
      <Draggable></Draggable>
      <Draggable></Draggable>
      <Draggable></Draggable>
      ......
  </Droppable>
  <Droppable>
      <Draggable></Draggable>
      <Draggable></Draggable>
      ......
  </Droppable>
  ......
</DragDropContext>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354