使用 react-beautiful-dnd 快速實(shí)現(xiàn)可拖拽看板

當(dāng)我還在使用 react-dnd 設(shè)計(jì)拖拽邏輯和交互、當(dāng)我還在為計(jì)算拖拽元素和 hover 元素的位置坐標(biāo)而煩躁不已,當(dāng)我還在為即將到來(lái)的 deadline 發(fā)愁之際谐腰,我發(fā)現(xiàn)了 react-beautiful-dnd。寫(xiě)了個(gè) demo 試了下废封,真香!

先上代碼

話不多說(shuō)丧蘸,先上代碼漂洋。(我等伸手黨福音~(yú))

如果懶得看代碼,這里是 >>Github 地址<<

安裝所需庫(kù):

$ 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: '睡覺(jué)' }, { id: 3, name: '打豆豆' }],
  },
  {
    id: 200,
    name: 'doing',
    issues: [{ id: 4, name: '刪庫(kù)' }, { 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;
}

效果是這樣子的:

示例

因?yàn)槲铱戳司W(wǎng)上的拖拽 demo 都不夠精美力喷,有些代碼也比較老了刽漂,所以貼個(gè)最新的希望能對(duì)大家有所幫助吧。

簡(jiǎn)單介紹 API

詳細(xì) API 就不人肉翻譯啦~沒(méi)啥意義弟孟,看 >>這里<<就好贝咙。

PS:一開(kāi)始在首頁(yè) README 找 API 居然沒(méi)有找到,卻跑到了一個(gè)教學(xué)視頻網(wǎng)站去了拂募。把我給氣的……后來(lái)才想起來(lái)按道理會(huì)有一個(gè)叫 doc 的目錄 - -

簡(jiǎn)單說(shuō)下我用到的 API:

  • <DragDropContext /> 是為了給拖拽提供上下文的庭猩,只有在 <DragDropContext /> 中去寫(xiě)拖拽才是有效的窟她。這里注意它必須要一個(gè) onDragEnd 的 props 來(lái)操作拖拽結(jié)束事件。其他事件有如下蔼水,具體說(shuō)用看一眼就知道震糖。
 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 />,它用來(lái)定義放置拖拽元素的容器徙缴。它有一個(gè)必填屬性 droppableId试伙,另外它的 children 屬性被定義成了一個(gè)函數(shù),函數(shù)提供的 provided 用來(lái)綁定 DOM 節(jié)點(diǎn)于样,提供的 snapshot 可以讓我們獲取當(dāng)前放置容器的屬性和狀態(tài)。
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 /> 包含的東西就是那個(gè)拖拽元素啦潘靖。它必須包含有 draggableId 穿剖、indexchildren 三個(gè)屬性。它和 <Droppable /> 類似也是將 children 定義為了函數(shù)卦溢,函數(shù)提供的 provided 用來(lái)綁定 DOM 節(jié)點(diǎn)糊余,提供的 snapshot 可以讓我們獲取當(dāng)前放置容器的屬性和狀態(tài)。
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>;

注意单寂,三者是包含關(guān)系贬芥,必須逐層實(shí)現(xiàn)才能夠做到拖拽哦。所以呈現(xiàn)的 DOM 結(jié)構(gòu)樣子應(yīng)該是醬紫的:

<DragDropContext>
  <Droppable>
      <Draggable></Draggable>
      <Draggable></Draggable>
      <Draggable></Draggable>
      ......
  </Droppable>
  <Droppable>
      <Draggable></Draggable>
      <Draggable></Draggable>
      ......
  </Droppable>
  ......
</DragDropContext>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宣决,一起剝皮案震驚了整個(gè)濱河市蘸劈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌尊沸,老刑警劉巖威沫,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異洼专,居然都是意外死亡棒掠,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)屁商,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)烟很,“玉大人,你說(shuō)我怎么就攤上這事蜡镶∥砀ぃ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵帽哑,是天一觀的道長(zhǎng)谜酒。 經(jīng)常有香客問(wèn)我,道長(zhǎng)妻枕,這世上最難降的妖魔是什么僻族? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任粘驰,我火速辦了婚禮,結(jié)果婚禮上述么,老公的妹妹穿的比我還像新娘蝌数。我一直安慰自己,他們只是感情好度秘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布顶伞。 她就那樣靜靜地躺著,像睡著了一般剑梳。 火紅的嫁衣襯著肌膚如雪唆貌。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天垢乙,我揣著相機(jī)與錄音锨咙,去河邊找鬼。 笑死追逮,一個(gè)胖子當(dāng)著我的面吹牛酪刀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播钮孵,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼骂倘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了巴席?” 一聲冷哼從身側(cè)響起历涝,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎情妖,沒(méi)想到半個(gè)月后睬关,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡毡证,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年电爹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片料睛。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡丐箩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出恤煞,到底是詐尸還是另有隱情屎勘,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布居扒,位于F島的核電站概漱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏喜喂。R本人自食惡果不足惜瓤摧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一竿裂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧照弥,春花似錦腻异、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至给赞,卻和暖如春机打,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背塞俱。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工姐帚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人障涯。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像膳汪,于是被迫代替她去往敵國(guó)和親唯蝶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355