Get element position in DOM on React DnD?

I use React DnD and Redux (using Kea) to create the shaper. I have a drag and drop part that works fine, and I managed to dispatch the action when the item crashes, and subsequently I create a builder using the state in which the dispatch was changed. However, in order to display the elements in the correct order, I (I think) must maintain the position of the discarded elements relative to my siblings, but I cannot understand anything that is not completely insane. I experimented with refs and requested a DOM with a unique identifier (I know I shouldn't), but both approaches feel pretty awful and don't even work.

Here's a simplified view of the structure of my application:

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

Workbench:

 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>, ) // ... 

Field:

 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> ) } } 

The sidebar is not very relevant.

My state is a flat array consisting of objects that I can use to render fields, so I reorder it based on the positions of the elements in the DOM.

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

The relevant part of this question revolves around

 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', } }, } 

I figured somehow I was just getting a link to an element, but it doesn't seem to exist in the DOM. This is the same if I try to hack 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); }); 

How can I achieve what I want?

Sorry for my inconsistent use of semicolons; I don't know what I want from them. I hate them.

+5
source share
1 answer

I think the key here is that the Field component can be either a DragSource or a DropTarget . Then we can define a standard set of drop types that will affect how the state is mutated.

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

After and Before will allow you to reorder fields, and Inside allows nesting of fields (or discarding in a workbench).

Now the creator of the action for processing any drop will be:

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

It simply accepts the source and target objects, as well as the type of drop, which will then be translated into a state mutation.

A drag type is simply a function of the target borders, the drag position and (optionally) the drag source, all in the context of a specific DropTarget type:

 (bounds, position, source) => dropType 

This function must be defined for each supported DropTarget type. This will allow each DropTarget support a different set of drop types. For example, Workbench knows how to drop something inside itself, and not earlier or later, so the implementation for a workbench might look like this:

 (bounds, position) => DropType.Inside 

For a Field you can use the logic from the Example of sorting simple maps , where the upper half of DropTarget translates to Before drop and the lower half to 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 every DropTarget can handle the spec drop() method in the same way:

  • get the bounds of the target element's DOM element
  • get quotation mark position
  • calculate fall type from borders, position and source
  • If any type of reset occurs, handle the frame action

With React DnD, we need to be careful to properly handle nested drop targets, since we have Field in the 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 will look 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> ); } 

A couple of notes:

Remember the order of the decorator. DropTarget should wrap the component, then DragSource should wrap the wrapped component. Thus, we have access to the correct instance of Component inside drop() .

The final root of the node abbreviation should be the native element of the node, not the user component of node.

Any component that will be embellished with DropTarget using configureDrop() will require the component to set its node DOM ref root to the node property.

Since we are handling drop in DropTarget , DragSource just needs to implement the beginDrag() method, which simply returns any state you want to mix to the state of your application.

The last thing to do is process each drop type in your reducer. It is important to remember that every time you move something, you need to remove the source from the current parent (if applicable), and then insert it into the new parent. Each action can change the state of up to three elements, the source parent (clear its children ), the source (assign it a parent link) and the target parent or target if Inside drop (add to its children ).

You may also need to make your state an object instead of an array, which might be easier to work with when implementing a gearbox.

 { AWJOPD: { ... }, DAWPNC: { ... }, workbench: { key: 'workbench', parent: null, children: [ 'DAWPNC' ] } } 
+3
source

Source: https://habr.com/ru/post/1273669/


All Articles