Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React JS Events Not Firing for the Last rendered element

I have created a drag and drop interface that will eventually be turned into a form builder. I've created a fiddle of the form builder http://jsfiddle.net/szASZ/1/. Everything seems to be working properly when you drag one of the top items into the gray drop zones beneath it.

However, where I am getting the issue is when you refresh the page/fiddle and try to sort the items within the drop zone. If you grab the "Radio Input" at the top of the first drop zone and move it within or outside of that drop zone, everything works properly.

Now, try dragging the last item "Checkbox Input" within its drop zone and everything should work correctly as well. Finally, refresh the page/fiddle and move that same, last "Checkbox Input" to another drop zone. The placeholder will show and then never go away. What I am finding is that "onDragEnd" event is never called during this instance of the drag and drop cycle, but it is called every other time. This event is normally relayed to the top level of the form builder where it takes the item that is being dragged and sets it in the new array of data.

To make things all the more fun, if I add an empty object onto the arrays that contain the dropped items (http://jsfiddle.net/szASZ/ - Line 30, 34), everything works as expected so it would seem that the last item in the array of "drop zone" items does not properly fire the "onDragEnd" event.

componentWillMount: function () {
    var newInput = [
        { "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Text Input" },
        { "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Checkbox Input" },
        { "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Radio Input" }
    ];

    var newZones = [
        [
            { "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Radio Input" },
            { "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Text Input" },
            { "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Checkbox Input" }
            ,{}
        ],
        [
            { "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Text Input" },
            { "classes": "soft-half push-half--bottom brand-bg--gray-dark brand-color--white", "title": "Checkbox Input" }
            ,{}
        ]
    ];

    this.setState({ inputs: newInput });
    this.setState({ zones: newZones });
}

Basically, this is the main problem that I am having and I have no idea why every other item gets sorted, dropped and the events trigger properly but the last one? Thanks!

like image 300
jheigs Avatar asked Jul 02 '14 17:07

jheigs


2 Answers

This is directly related to/caused by a React bug I filed a couple months ago:

#1355: touchmove doesn't fire on removed element

React takes advantage of event bubbling and binds all events at the document root, which works in most situations but falls down in this particular case. Unlike mouse events, touch and drag events are always sent to the event which received the touchstart or dragstart event (rather than the element that the pointer is currently over). If the element that received the dragstart event is removed from the DOM (as it is in this particular case since it disappears from the list it started in) then that (detached) element will still receive the dragend event but it won't bubble up to the document root, so React never sees the event.

You only see this when dragging the last element because you're using the key prop improperly. Because you're using the index as the key currently, when you have a list A B C D and start dragging B (causing the list to become A C D), React mutates the second element (previously B) to have text "C", mutates the third element (previously C) to have text "D", and deletes the fourth element (previously D). If instead of the index you use a unique per-item ID as the key, React will always remove the element that you're dragging, which will be easier to reason about. (Because of the bug, you'd then see that this behavior happens always instead of only when dragging the last element.)

As a workaround, you can bind the dragend/dragenter/dragleave handlers manually in the dragstart handler and clean them up on dragend. This will sidestep React's event system completely for those events:

// DRAGGING
dragStart: function (e) {
    this.getDOMNode().addEventListener('dragend', this.dragEnd, false);
    this.getDOMNode().addEventListener('dragenter', this.dragEnter, false);
    this.getDOMNode().addEventListener('dragleave', this.dragLeave, false);

    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/html', e.target.innerHTML);

    this.props.dragStart(this.props.item, { sorting: true });
},
dragEnd: function (e) {
    this.getDOMNode().removeEventListener('dragend', this.dragEnd, false);
    this.getDOMNode().removeEventListener('dragenter', this.dragEnter, false);
    this.getDOMNode().removeEventListener('dragleave', this.dragLeave, false);

    var opts = e.dataTransfer.dropEffect === 'none' ? { success: false } : { success: true };

    this.props.dragEnd(opts);
},

With that setup, you'd only specify onDragStart in render.

(I'll see if I can get this issue fixed – as far as I know, you're the first person to hit it besides me so it hasn't been a priority for us.)

like image 146
Sophie Alpert Avatar answered Oct 16 '22 03:10

Sophie Alpert


I had a similar bug with onDragEnd not firing for a node which was re-rendered.

@Ben Alpert, thanks for your workaround, it's great! I would suggest one little modification:

dragEnd: function(e) {

  // component can be umounted and getDOMNode will throw in that case
  if (this.isMounted()) {
    this.getDOMNode().removeEventListener('dragend', this.dragEnd, false);
    // ...
  }
  // ... invoke the callback
},
like image 2
vpavkin Avatar answered Oct 16 '22 04:10

vpavkin