React JS事件未触发最后渲染元素

6
我创建了一个拖放界面,最终将成为表单生成器。我已经创建了一个表单生成器的演示 http://jsfiddle.net/szASZ/1/。当你将顶部项目之一拖到其下方的灰色区域时,一切似乎都正常工作。
然而,问题出现在当你刷新页面或演示,并尝试对落区内的项目进行排序时。如果你抓住第一个落区顶部的“单选框输入”并将其移动到该落区内或外,一切都能正常工作。
现在,请尝试将最后一个项目“复选框输入”拖动到其落区内,一切也应该正常工作。最后,刷新页面/演示并将同样的最后一个“复选框输入”移动到另一个落区。占位符会显示,然后永远不会消失。我发现,在此次拖放过程中,“onDragEnd”事件从未被调用,但其他所有时候都被调用。通常,该事件被转发到表单生成器的顶层,它会将被拖动的项目设置在新的数据数组中。
更有趣的是,如果我在包含已落下项目的数组中添加一个空对象 (http://jsfiddle.net/szASZ/ - 第30行、34行),一切都按预期工作,因此似乎“落区”项目数组中的最后一个项目不能正确触发“onDragEnd”事件。
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 });
}

基本上,我遇到的主要问题是为什么除了最后一个条目以外,所有其他条目都可以被排序、删除并且事件触发正常?谢谢!

感谢您提供详细的描述和重现步骤。 - Sophie Alpert
3个回答

7

这与我几个月前提交的React bug直接相关/导致:

#1355: touchmove doesn't fire on removed element

React利用事件冒泡并在文档根绑定所有事件,在大多数情况下都能正常工作,但在这种特殊情况下则会出现问题。与鼠标事件不同,触摸和拖动事件始终发送到接收touchstartdragstart事件的元素(而不是指针当前悬停的元素)。如果接收dragstart事件的元素从DOM中删除(在此特定情况下因为它从其开始的列表中消失),那么(分离的)元素仍将接收dragend事件,但它不会冒泡到文档根,因此React永远不会看到该事件。

由于您当前错误地使用了“key”属性,所以只有在拖动最后一个元素时才会看到此问题。 当您当前使用索引作为键时,例如列表A B C D,开始拖动B(导致列表变为A C D),React会将第二个元素(先前的B)突变为文本“C”,将第三个元素(先前的C)突变为文本“D”,并删除第四个元素(先前的D)。 如果您使用每个项目的唯一ID作为键而不是索引,则React将始终删除您正在拖动的元素,这将更容易理解。 (由于错误,您随后将看到此行为始终发生而不仅仅在拖动最后一个元素时发生。)
作为解决方法,您可以在dragstart处理程序中手动绑定dragend / dragenter / dragleave处理程序,并在dragend上清除它们。 这将完全避开React的事件系统来处理这些事件。
// 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);
},

有了这样的设置,你只需要在render中指定onDragStart

(我会尝试解决这个问题 - 据我所知,除了我之外,你是第一个遇到这个问题的人,所以这对我们来说不是优先考虑的事情。)


我也遇到了这个问题。谢谢你提供的解决方案! - Quinthexadec

2

我曾经遇到一个类似的bug,即重新渲染后无法触发onDragEnd事件。

@Ben Alpert,感谢您提供的解决方法,非常好!我建议进行一点修改:

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


0

我曾经遇到过类似的问题,然后将其转换为一个功能组件。

function DragCell () {
    const [ DragTarget, SetDragTarget ] = React.useState<HTMLSpanElement>();

    // Manually bind the event, because if the component un-renders, we still want to fire the event
    React.useEffect(() => {
        if (!DragTarget) {
            return;
        }

        const onDragEnd = () => SetDragTarget(undefined);
        DragTarget.addEventListener('dragend', onDragEnd);
        return () => {
            // In the event the component is unloaded, we still want to call the clean up function
            // Put your code you want to run on dragEnd here
            DragTarget.removeEventListener('dragend', onDragEnd);
        };
    }, [DragTarget]);

    return (
        <td data-isdragging={ !!DragTarget } className='DragCell'>
            <span
                className='DragHandle'
                draggable
                onDragStart={event => {
                    event.dataTransfer.setData('dragging', '');
                    SetDragTarget(event.currentTarget);
                }}
            >Drag Handle</span>
        </td>
    );
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接