Webpack&React (八) 实现拖拽功能

来源:互联网 发布:linux 用户组权限 编辑:程序博客网 时间:2024/06/06 02:53

渣翻译, 原文链接

实现拖拽功能


我们的看板应用差不多可以使用了,并有了一些基本的功能。在本章,将会更进一步,我们将使用React DnD加入拖拽的功能。这章之后你就可以在一个栏中拖动排序节点并也可以在不同的栏中进行拖拽。

设置 React DnD


首先,我们需要在我们的工程中加入React DnD,我们将要使用HTML5拖拽基于backend(实现 DnD 的方式)。触摸设备使用特定的backend。我们需要使用DragDropContext 注解并设置一个backend:

app/components/App.jsx

...leanpub-start-insertimport {DragDropContext} from 'react-dnd';import HTML5Backend from 'react-dnd-html5-backend';leanpub-end-insertleanpub-start-insert@DragDropContext(HTML5Backend)leanpub-end-insertexport default class App extends React.Component {  ...}

看起来应用和之前并没有什么不同,但现在我们将要增加一些功能。

如果你没有使用npm-install-webpack-plugin,记得手动安装react-dnd和其相关的工具到你的工程中。npm i react-dnd react-dnd-html5-backend react-addons-update -S

排序Notes


Next, we will need to tell React DnD what can be dragged and where. Since we want to move notes, we’ll need to annotate them accordingly. In addition, we’ll need some logic to tell what happens during this process.
下面,我们需要告诉React Dnd什么可以拖动及拖动到哪里,即然我们想要移动notes,我们需要相应的注解它们。另外,我们需要写一些逻辑说明在此过程中发生了什么。

我们能使用一个小技巧来避免代码重复。我们使Note成为一个封装组件,它接受Editable 并渲染它。这将让我们在Note中保持DnD逻辑关系,从而就避免Editable中重复的逻辑。

在React中称为children,React会在{this.props.children}处渲染所有它的子组件。像向面这样设置Note.jsx

app/components/Note.jsx

import React from 'react';export default class Note extends React.Component {  render() {    return <li {...this.props}>{this.props.children}</li>;  }}

我们还需要调整Notes使用我们封装好的组件。我们将使用Note包装Editable,我们将传递note数据到这个包装并在后面处理它:

app/components/Notes.jsx

import React from 'react';import Editable from './Editable.jsx';leanpub-start-insertimport Note from './Note.jsx';leanpub-end-insertexport default ({notes, onValueClick, onEdit, onDelete}) => {  return (    <ul className="notes">{notes.map(note =>leanpub-start-delete      <li className="note" key={note.id}>        <Editable          editing={note.editing}          value={note.task}          onValueClick={onValueClick.bind(null, note.id)}          onEdit={onEdit.bind(null, note.id)}          onDelete={onDelete.bind(null, note.id)} />      </li>leanpub-end-deleteleanpub-start-insert      <Note className="note" id={note.id} key={note.id}>        <Editable          editing={note.editing}          value={note.task}          onValueClick={onValueClick.bind(null, note.id)}          onEdit={onEdit.bind(null, note.id)}          onDelete={onDelete.bind(null, note.id)} />      </Note>leanpub-end-insert    )}</ul>  );}

使Notes被拖动


React DnD uses constants to tell different draggables apart. Set up a file for tracking Note as follows:

React DnD使用常量来区分不同的拖拽目标.新建如下文件来跟踪Note:

app/constants/itemTypes.js

export default {  NOTE: 'note'};

这个定义将在后面进行扩展.会增加新的类型.
下面,我们需要告诉我们的Note是可以被拖放的.这是通过@DragSource@DropTarget注解完成的.

设置 Note @DragSource

Marking a component as a @DragSource simply means that it can be dragged. Set up the annotation like this:
在组件上使用@DragSource意味着它是可以被拖动的.设置注解如下:

app/components/Note.jsx

import React from 'react';leanpub-start-insertimport {DragSource} from 'react-dnd';import ItemTypes from '../constants/itemTypes';const noteSource = {  beginDrag(props) {    console.log('begin dragging note', props);    return {};  }};leanpub-end-insertleanpub-start-deleteexport default class Note extends React.Component {  render() {    return <li {...this.props}>{this.props.children}</li>;  }}leanpub-end-deleteleanpub-start-insert@DragSource(ItemTypes.NOTE, noteSource, (connect) => ({  connectDragSource: connect.dragSource()}))export default class Note extends React.Component {  render() {    const {connectDragSource, id, onMove, ...props} = this.props;    return connectDragSource(      <li {...props}>{props.children}</li>    );  }}leanpub-end-insert

有几个重要的变化:

  • 我们定义了一个noteSource.它有一个beginDrag方法.我们能在这设置拖动中的初始状态.现在我们在这只有一个调试日志.
  • @DragSource noteSource关联之前定义的NOTE类型.
  • idonMove是引用自this.props.我们将稍后设置回调函数.在父Note里能处理移动相关的逻辑.
  • 最后connectDragSource属性在render()中包装这个元素.它能被方便的使用在一个指定的部分.

如果现在拖动一个Note,你能在控制台中看到调试信息.

我们还需要使用@DropTarget来确保Note工作.

设置 Note @DropTarget

@DropTarget 允许一个组件接收带有@DragSource注解的组件.当触发@DropTarget我们能基于这个组件执行实际的逻辑,像下面这样做:

app/components/Note.jsx

import React from 'react';leanpub-start-deleteimport {DragSource} from 'react-dnd';leanpub-end-deleteleanpub-start-insertimport {DragSource, DropTarget} from 'react-dnd';leanpub-end-insertimport ItemTypes from '../constants/itemTypes';const noteSource = {  beginDrag(props) {    console.log('begin dragging note', props);    return {};  }};leanpub-start-insertconst noteTarget = {  hover(targetProps, monitor) {    const sourceProps = monitor.getItem();    console.log('dragging note', sourceProps, targetProps);  }};leanpub-end-insert@DragSource(ItemTypes.NOTE, noteSource, (connect) => ({  connectDragSource: connect.dragSource()}))leanpub-start-insert@DropTarget(ItemTypes.NOTE, noteTarget, (connect) => ({  connectDropTarget: connect.dropTarget()}))leanpub-end-insertexport default class Note extends React.Component {leanpub-start-delete  render() {    const {connectDragSource, id, onMove, ...props} = this.props;    return connectDragSource(      <li {...props}>{props.children}</li>    );  }leanpub-end-deleteleanpub-start-insert  render() {    const {connectDragSource, connectDropTarget,      id, onMove, ...props} = this.props;    return connectDragSource(connectDropTarget(      <li {...props}>{props.children}</li>    ));  }leanpub-end-insert}

刷新浏览器并尝试在周围拖动节点.你会看到大量的日志信息.

这两个注解使我们仿问Note属性.在这个例子中,我们在noteTarget中使用monitor.getItem()仿问它们.

onMove API 在Notes


现在,我们能在附近移动Note,我们还需要完成一些逻辑.如下:

  1. beginDrag中捕获Noteid.
  2. hover中捕获目标Noteid.
  3. hover中触发onMove回调函数,这样我们能在上级处理这个逻辑.

实现如下:

app/components/Note.jsx

...const noteSource = {leanpub-start-delete  beginDrag(props) {    console.log('begin dragging note', props);    return {};  }leanpub-end-deleteleanpub-start-insert  beginDrag(props) {    return {      id: props.id    };  }leanpub-end-insert};const noteTarget = {leanpub-start-delete  hover(targetProps, monitor) {    const sourceProps = monitor.getItem();    console.log('dragging note', sourceProps, targetProps);  }leanpub-end-deleteleanpub-start-insert  hover(targetProps, monitor) {    const targetId = targetProps.id;    const sourceProps = monitor.getItem();    const sourceId = sourceProps.id;    if(sourceId !== targetId) {      targetProps.onMove({sourceId, targetId});    }  }leanpub-end-insert};...

如果现在尝试拖动.会看到许多onMove相关错误.我们应该在Notes中定义它:

app/components/Notes.jsx

import React from 'react';import Editable from './Editable.jsx';import Note from './Note.jsx';export default ({notes, onValueClick, onEdit, onDelete}) => {  return (    <ul className="notes">{notes.map(note => {      return (leanpub-start-delete        <Note className="note" id={note.id} key={note.id}>leanpub-end-deleteleanpub-start-insert        <Note className="note" id={note.id} key={note.id}          onMove={({sourceId, targetId}) =>            console.log(`source: ${sourceId}, target: ${targetId}`)        }>leanpub-end-insert          <Editable            editing={note.editing}            value={note.task}            onValueClick={onValueClick.bind(null, note.id)}            onEdit={onEdit.bind(null, note.id)}            onDelete={onDelete.bind(null, note.id)} />        </Note>      );    })}    </ul>  );}

现在拖动Note,应该可以在控制台上看到信息如source <id> target <id>,下面会处理这些id.

增加针对移动的Action和Store方法


这个拖放逻辑如下,假如我们有一个栏中有A,B,C节点,当我们把A移动到C下面,结果将为B,C,A.假如这时还有其它栏列,如D,E,F,并且移动A到这个栏的开如处.我们应该得到B,C 和A,D,E,F.

在本例中,因为涉及到栏到栏之间的拖动会增加程序的复杂性.当我们移动一个Note,我们将知道它的源位置和目标位置.栏通过id知道哪些Notes属于它.我们将需要告诉LaneStore在指定的notes上它应该执行的逻辑.为此我们定义LaneActions.move:

app/actions/LaneActions.js

import alt from '../libs/alt';export default alt.generateActions(  'create', 'update', 'delete',  'attachToLane', 'detachFromLane',  'move');

我们在onMove中使用这个行为:

app/components/Notes.jsx

import React from 'react';import Editable from './Editable.jsx';import Note from './Note.jsx';leanpub-start-insertimport LaneActions from '../actions/LaneActions';leanpub-end-insertexport default ({notes, onValueClick, onEdit, onDelete}) => {  return (    <ul className="notes">{notes.map(note => {      return (leanpub-start-delete        <Note className="note" id={note.id} key={note.id}          onMove={({sourceId, targetId}) =>            console.log(`source: ${sourceId}, target: ${targetId}`)        }>leanpub-end-deleteleanpub-start-insert        <Note className="note" id={note.id} key={note.id}          onMove={LaneActions.move}>leanpub-end-insert          <Editable            editing={note.editing}            value={note.task}            onValueClick={onValueClick.bind(null, note.id)}            onEdit={onEdit.bind(null, note.id)}            onDelete={onDelete.bind(null, note.id)} />        </Note>      );    })}    </ul>  );}

我们还需要在LaneStore中补充相关的逻辑:

app/stores/LaneStore.js

...class LaneStore {  ...  detachFromLane({laneId, noteId}) {    ...  }leanpub-start-insert  move({sourceId, targetId}) {    console.log(`source: ${sourceId}, target: ${targetId}`);  }leanpub-end-insert}export default alt.createStore(LaneStore, 'LaneStore');

这与之前的运行效果一样.下一步.我们将要增加一些逻辑使其工作.有两个难点:在栏内部移动和在栏间移动.

实现Note的拖放逻辑


栏内部移动有点复杂.当你基于id操作并一次执行一个操作,你需要考虑索引的改变.这例子中.我使用React的immutability helperupdate解决这个问题.

使用splice可以解决栏间移动问题.首先,我们splice出源节点,然后splice它到目标栏中.虽然update也可以使其工作.但用splice更简单合理.看下面代码:

app/stores/LaneStore.js

...leanpub-start-insertimport update from 'react-addons-update';leanpub-end-insertclass LaneStore {  ...leanpub-start-delete  move({sourceId, targetId}) {    console.log(`source: ${sourceId}, target: ${targetId}`);  }leanpub-end-deleteleanpub-start-insert  move({sourceId, targetId}) {    const lanes = this.lanes;    const sourceLane = lanes.filter(lane => lane.notes.includes(sourceId))[0];    const targetLane = lanes.filter(lane => lane.notes.includes(targetId))[0];    const sourceNoteIndex = sourceLane.notes.indexOf(sourceId);    const targetNoteIndex = targetLane.notes.indexOf(targetId);    if(sourceLane === targetLane) {      // move at once to avoid complications      sourceLane.notes = update(sourceLane.notes, {        $splice: [          [sourceNoteIndex, 1],          [targetNoteIndex, 0, sourceId]        ]      });    }    else {      // get rid of the source      sourceLane.notes.splice(sourceNoteIndex, 1);      // and move it to target      targetLane.notes.splice(targetNoteIndex, 0, sourceId);    }    this.setState({lanes});  }leanpub-end-insert}export default alt.createStore(LaneStore, 'LaneStore');

现在拖放都是没有问题的.向空栏中拖动还不能工作.

如果我们指明拖动节点的位置会更清晰.为此我们可以从列表中隐藏拖动的节点.React DnD 提供给我们这个方法.

表明移动位置

React DnD 提供了一个叫做状态监听器的功能.通过它我们能使用monitor.isDragging()来确定我们当前拖动的Note.像下面这样设置.

app/components/Note.jsx

...leanpub-start-delete@DragSource(ItemTypes.NOTE, noteSource, (connect, monitor) => ({  connectDragSource: connect.dragSource()}))leanpub-end-deleteleanpub-start-insert@DragSource(ItemTypes.NOTE, noteSource, (connect, monitor) => ({  connectDragSource: connect.dragSource(),  isDragging: monitor.isDragging() // map isDragging() state to isDragging prop}))leanpub-end-insert@DropTarget(ItemTypes.NOTE, noteTarget, (connect) => ({  connectDropTarget: connect.dropTarget()}))export default class Note extends React.Component {leanpub-start-delete  render() {    const {connectDragSource, connectDropTarget,      id, onMove, ...props} = this.props;    return connectDragSource(connectDropTarget(      <li {...props}>{props.children}</li>    ));  }leanpub-end-deleteleanpub-start-insert  render() {    const {connectDragSource, connectDropTarget, isDragging,      onMove, id, ...props} = this.props;    return connectDragSource(connectDropTarget(      <li style={{        opacity: isDragging ? 0 : 1      }} {...props}>{props.children}</li>    ));  }leanpub-end-insert}

现在在栏内部移动会看到一个空白的占位块.但如果你尝试从一个栏拖动到另一个栏并在这个栏内移动时会发现没有这个空白的占位块.

这个问题是我们的节点组件在这个过程中已装被卸下了.所以它丢失了isDragging状态.但是我们能重写这个默认行为实现一个isDragging如下:

app/components/Note.jsx

...leanpub-start-deleteconst noteSource = {  beginDrag(props) {    return {      id: props.id    };  }};leanpub-end-deleteleanpub-start-insertconst noteSource = {  beginDrag(props) {    return {      id: props.id    };  },  isDragging(props, monitor) {    return props.id === monitor.getItem().id;  }};leanpub-end-insert...

拖放Notes到空栏中


拖动节点到空栏中,应该让它接收节点.首先我们需要在栏上捕获拖动:

app/components/Lane.jsx

...leanpub-start-insertimport {DropTarget} from 'react-dnd';import ItemTypes from '../constants/itemTypes';const noteTarget = {  hover(targetProps, monitor) {    const targetId = targetProps.lane.id;    const sourceProps = monitor.getItem();    const sourceId = sourceProps.id;    console.log(`source: ${sourceId}, target: ${targetId}`);  }};@DropTarget(ItemTypes.NOTE, noteTarget, (connect) => ({  connectDropTarget: connect.dropTarget()}))leanpub-end-insertexport default class Lane extends React.Component {  render() {leanpub-start-delete    const {lane, ...props} = this.props;leanpub-end-deleteleanpub-start-insert    const {connectDropTarget, lane, ...props} = this.props;leanpub-end-insertleanpub-start-delete    return (leanpub-end-deleteleanpub-start-insert    return connectDropTarget(leanpub-end-insert      ...    );  }  ...}

现在你拖动一个节点到一个栏上会看到一些信息.问题是我们如何处理这些数据?实际移动这个节点到栏上前,我们应该检查它是不是空的.如果有内容了.这个操作没有意义.

因为在我们hover时在noteTarget 中可以知道目标栏,我们能检查它的notes:

app/components/Lane.jsx

...const noteTarget = {leanpub-start-delete  hover(targetProps, monitor) {    const targetId = targetProps.lane.id;    const sourceProps = monitor.getItem();    const sourceId = sourceProps.id;    console.log(`source: ${sourceId}, target: ${targetId}`);  }leanpub-end-deleteleanpub-start-insert  hover(targetProps, monitor) {    const sourceProps = monitor.getItem();    const sourceId = sourceProps.id;    if(!targetProps.lane.notes.length) {      console.log('source', sourceId, 'target', targetProps);    }  }leanpub-end-insert};...

触发move逻辑

现在我们知道哪些Note移动到Lane. 可以使用LaneStore.attachToLane:

app/components/Lane.jsx

...const noteTarget = {leanpub-start-delete  hover(targetProps, monitor) {    const sourceProps = monitor.getItem();    const sourceId = sourceProps.id;    if(!targetProps.lane.notes.length) {      console.log('source', sourceId, 'target', targetProps);    }  }leanpub-end-deleteleanpub-start-insert  hover(targetProps, monitor) {    const sourceProps = monitor.getItem();    const sourceId = sourceProps.id;    if(!targetProps.lane.notes.length) {      LaneActions.attachToLane({        laneId: targetProps.lane.id,        noteId: sourceId      });    }  }leanpub-end-insert};...

这样做有些问题.会产生数据的重复.之前我们使用detachFromLane来解决这个问题.现在我们不能使用这个方法因为我们不知道节点属于哪一栏中.虽然我们能通过组件层传递这个数据.但这样做不是很好.

我们可以通过在attachToLane中增加时检查:

app/stores/LaneStore.js

...class LaneStore {  ...  attachToLane({laneId, noteId}) {    const lanes = this.lanes.map(lane => {leanpub-start-insert      if(lane.notes.includes(noteId)) {        lane.notes = lane.notes.filter(note => note !== noteId);      }leanpub-end-insert      if(lane.id === laneId) {        if(lane.notes.includes(noteId)) {          console.warn('Already attached note to lane', lanes);        }        else {          lane.notes.push(noteId);        }      }      return lane;    });    this.setState({lanes});  }  ...}

修复拖动中的编辑行为

当前实现有些小问题.如果你编辑一个节点.你仍能拖动它.这不符合大多数人的使用习惯.你不能例如双击输入选择所有文本.

解决很简单,对于每个Note,我们需要使用这个editing状态调整它的行为.首先我们要传递空上editing状态到Note:

app/components/Notes.jsx

...export default ({notes, onValueClick, onEdit, onDelete}) => {  return (    <ul className="notes">{notes.map(note =>leanpub-start-delete      <Note className="note" id={note.id} key={note.id}        onMove={LaneActions.move}>leanpub-end-deleteleanpub-start-insert      <Note className="note" id={note.id} key={note.id}        editing={note.editing} onMove={LaneActions.move}>leanpub-end-insert        <Editable          editing={note.editing}          value={note.task}          onValueClick={onValueClick.bind(null, note.id)}          onEdit={onEdit.bind(null, note.id)}          onDelete={onDelete.bind(null, note.id)} />      </Note>    )}</ul>  );}

app/components/Note.jsx

...@DragSource(ItemTypes.NOTE, noteSource, (connect, monitor) => ({  connectDragSource: connect.dragSource(),  isDragging: monitor.isDragging()}))@DropTarget(ItemTypes.NOTE, noteTarget, (connect) => ({  connectDropTarget: connect.dropTarget()}))export default class Note extends React.Component {  render() {leanpub-start-delete    const {connectDragSource, connectDropTarget, isDragging,      onMove, id, ...props} = this.props;leanpub-end-deleteleanpub-start-insert    const {connectDragSource, connectDropTarget, isDragging,      onMove, id, editing, ...props} = this.props;    // Pass through if we are editing    const dragSource = editing ? a => a : connectDragSource;leanpub-end-insertleanpub-start-delete    return connectDragSource(connectDropTarget(leanpub-end-deleteleanpub-start-insert    return dragSource(connectDropTarget(leanpub-end-insert      <li style={{        opacity: isDragging ? 0 : 1      }} {...props}>{props.children}</li>    ));  }}

现在我们有了一个完整的Kanban应用.我们能创建新的栏目和节点.并编辑和删除它们.此外还能移动它们.任务完成!

结论


下一章,我们将学习如何在生产环境下创建我们的应用程序.

0 1
原创粉丝点击