8

我正在使用 React DnD 和 Redux(使用 Kea)来构建表单构建器。我的拖放部分工作得很好,并且我设法在元素掉落时调度了一个动作,然后我使用调度更改的状态来渲染构建器。但是,为了以正确的顺序渲染元素,我(我想我)需要保存相对于它的兄弟姐妹的放置元素位置,但我无法弄清楚任何不是绝对疯狂的东西。我已经尝试过使用 refs 并使用唯一 ID 查询 DOM(我知道我不应该这样做),但是这两种方法都感觉很糟糕,甚至都不起作用。

这是我的应用程序结构的简化表示:

@DragDropContext(HTML5Backend)
@connect({ /* redux things */ })
<Builder>
  <Workbench tree={this.props.tree} />
  <Sidebar fields={this.props.field}/>
</Builder>

工作台:

const boxTarget = {
  drop(props, monitor, component) {
    const item = monitor.getItem()
    console.log(component, item.unique, component[item.unique]); // last one is undefined
    window.component = component; // doing it manually works, so the element just isn't in the DOM yet

    return {
      key: 'workbench',
    }
  },
}

@DropTarget(ItemTypes.FIELD, boxTarget, (connect, monitor) => ({
  connectDropTarget: connect.dropTarget(),
  isOver: monitor.isOver(),
  canDrop: monitor.canDrop(),
}))
export default class Workbench extends Component {
  render() {
    const { tree } = this.props;
    const { canDrop, isOver, connectDropTarget } = this.props

    return connectDropTarget(
      <div className={this.props.className}>
        {tree.map((field, index) => {
          const { key, attributes, parent, unique } = field;
          if (parent === 'workbench') { // To render only root level nodes. I know how to render the children recursively, but to keep things simple...
            return (
              <Field
                unique={unique}
                key={key}
                _key={key}
                parent={this} // I'm passing the parent because the refs are useless in the Field instance (?) I don't know if this is a bad idea or not
              />
            );
          }

          return null;
        }).filter(Boolean)}
      </div>,
    )


    // ...

场地:

const boxSource = {
  beginDrag(props) {
    return {
      key: props._key,
      unique: props.unique || shortid.generate(),
      attributes: props.attributes,
    }
  },

  endDrag(props, monitor) {
    const item = monitor.getItem()
    const dropResult = monitor.getDropResult()

    console.log(dropResult);

    if (dropResult) {
      props.actions.onDrop({
        item,
        dropResult,
      });
    }
  },
}

@connect({ /* redux stuff */ })
@DragSource(ItemTypes.FIELD, boxSource, (connect, monitor) => ({
  connectDragSource: connect.dragSource(),
  isDragging: monitor.isDragging(),
}))
export default class Field extends Component {  
  render() {
    const { TagName, title, attributes, parent } = this.props
    const { isDragging, connectDragSource } = this.props
    const opacity = isDragging ? 0.4 : 1

    return connectDragSource(
      <div
        className={classes.frame}
        style={{opacity}}
        data-unique={this.props.unique || false}
        ref={(x) => parent[this.props.unique || this.props.key] = x} // If I save the ref to this instance, how do I access it in the drop function that works in context to boxTarget & Workbench? 
      >
        <header className={classes.header}>
          <span className={classes.headerName}>{title}</span>
        </header>
      <div className={classes.wrapper}>
        <TagName {...attributes} />
      </div>
    </div>
    )
  }
}

侧边栏不是很相关。

我的状态是一个平面数组,由可用于呈现字段的对象组成,因此我根据 DOM 中的元素位置对其进行重新排序。

[
  {
    key: 'field_type1',
    parent: 'workbench',
    children: ['DAWPNC'], // If there's more children, "mutate" this according to the DOM
    unique: 'AWJOPD',
    attributes: {},
  },
  {
    key: 'field_type2',
    parent: 'AWJOPD',
    children: false,
    unique: 'DAWPNC',
    attributes: {},
  },
]

这个问题的相关部分围绕

const boxTarget = {
  drop(props, monitor, component) {
    const item = monitor.getItem()
    console.log(component, item.unique, component[item.unique]); // last one is undefined
    window.component = component; // doing it manually works, so the element just isn't in the DOM yet

    return {
      key: 'workbench',
    }
  },
}

我想我只是以某种方式获得对元素的引用,但它似乎还不存在于 DOM 中。如果我尝试用 ReactDOM 破解,也是一样的:

 // still inside the drop function, "works" with the timeout, doesn't without, but this is a bad idea
 setTimeout(() => {
    const domNode = ReactDOM.findDOMNode(component);
    const itemEl = domNode.querySelector(`[data-unique="${item.unique}"]`);
    const parentEl = itemEl.parentNode;

    const index = Array.from(parentEl.children).findIndex(x => x.getAttribute('data-unique') === item.unique);

    console.log(domNode, itemEl, index);
  });

我如何实现我想要的?

为我对分号的不一致使用道歉,我不知道我想从他们那里得到什么。我恨他们。

4

1 回答 1

5

I think the key here is realizing that the Field component can be both a DragSource and a DropTarget. We can then define a standard set of drop types that would influence how the state is mutated.

const DropType = {
  After: 'DROP_AFTER',
  Before: 'DROP_BEFORE',
  Inside: 'DROP_INSIDE'
};

After and Before would allow re-ordering of fields, while Inside would allow nesting of fields (or dropping into the workbench).

Now, the action creator for handling any drop would be:

const drop = (source, target, dropType) => ({
  type: actions.DROP,
  source,
  target,
  dropType
});

It just takes the source and target objects, and the type of drop occurring, which will then be translated into the state mutation.

A drop type is really just a function of the target bounds, the drop position, and (optionally) the drag source, all within the context of a particular DropTarget type:

(bounds, position, source) => dropType

This function should be defined for each type of DropTarget supported. This would allow each DropTarget to support a different set of drop types. For instance, the Workbench only knows how to drop something inside of itself, not before or after, so the implementation for the workbench could look like:

(bounds, position) => DropType.Inside

For a Field, you could use the logic from the Simple Card Sort example, where the upper half of the DropTarget translates to a Before drop while the lower half translates to an After drop:

(bounds, position) => {
  const middleY = (bounds.bottom - bounds.top) / 2;
  const relativeY = position.y - bounds.top;
  return relativeY < middleY ? DropType.Before : DropType.After;
};

This approach also means that each DropTarget could handle the drop() spec method in the same manner:

  • get bounds of the drop target's DOM element
  • get the drop position
  • calculate the drop type from the bounds, position, and source
  • if any drop type occurred, handle the drop action

With React DnD, we have to be careful to appropriately handle nested drop targets since we have Fields in a Workbench:

const configureDrop = getDropType => (props, monitor, component) => {
  // a nested element handled the drop already
  if (monitor.didDrop())
    return;

  // requires that the component attach the ref to a node property
  const { node } = component;
  if (!node) return;

  const bounds = node.getBoundingClientRect();
  const position = monitor.getClientOffset();
  const source = monitor.getItem();

  const dropType = getDropType(bounds, position, source);

  if (!dropType)
    return;

  const { onDrop, ...target } = props;
  onDrop(source, target, dropType);

  // won't be used, but need to declare that the drop was handled
  return { dropped: true };
};

The Component class would end up looking something like this:

@connect(...)
@DragSource(ItemTypes.FIELD, { 
  beginDrag: ({ unique, parent, attributes }) => ({ unique, parent, attributes })
}, dragCollect)
// IMPORTANT: DropTarget has to be applied first so we aren't receiving
// the wrapped DragSource component in the drop() component argument
@DropTarget(ItemTypes.FIELD, { 
  drop: configureDrop(getFieldDropType)
  canDrop: ({ parent }) => parent // don't drop if it isn't on the Workbench
}, dropCollect)
class Field extends React.Component {
  render() { 
    return (
      // ref prop used to provide access to the underlying DOM node in drop()
      <div ref={ref => this.node = ref}>
        // field stuff
      </div>
    );
}

Couple things to note:

Be mindful of the decorator order. DropTarget should wrap the component, then DragSource should wrap the wrapped component. This way, we have access to the correct component instance inside drop().

The drop target's root node needs to be a native element node, not a custom component node.

Any component that will be decorated with the DropTarget utilizing configureDrop() will require that the component set its root node's DOM ref to a node property.

Since we are handling the drop in the DropTarget, the DragSource just needs to implement the beginDrag() method, which would just return whatever state you want mixed into your application state.

The last thing to do is handle each drop type in your reducer. Important to remember is that every time you move something around, you need to remove the source from its current parent (if applicable), then insert it into the new parent. Each action could mutate the state of up to three elements, the source's existing parent (to clean up its children), the source (to assign its parent reference), and the target's parent or the target if an Inside drop (to add to its children).

You also might want to consider making your state an object instead of an array, which might be easier to work with when implementing the reducer.

{
  AWJOPD: { ... },
  DAWPNC: { ... },
  workbench: {
    key: 'workbench',
    parent: null,
    children: [ 'DAWPNC' ]
  }
}
于 2017-12-04T17:24:22.840 回答