Prevent error thrown when removing event target

Fixes #1105.
This commit is contained in:
Ben Alpert
2014-02-17 10:33:34 -08:00
parent 9e160df868
commit 2f0bb69708
2 changed files with 55 additions and 3 deletions
+22 -3
View File
@@ -49,6 +49,9 @@ function findParent(node) {
return parent;
}
// Used to store ancestor hierarchy in top level callback
var topLevelCallbackReusableArray = [];
/**
* Top-level callback creator used to implement event handling using delegation.
* This is used via dependency injection.
@@ -89,8 +92,21 @@ var ReactEventTopLevelCallback = {
getEventTarget(nativeEvent)
) || window;
var ancestors = topLevelCallbackReusableArray;
ancestors.length = 0;
// Loop through the hierarchy, in case there's any nested components.
while (topLevelTarget) {
// It's important that we build the array of ancestors before calling any
// event handlers, because event handlers can modify the DOM, leading to
// inconsistencies with ReactMount's node cache. See #1105.
var ancestor = topLevelTarget;
while (ancestor) {
ancestors.push(ancestor);
ancestor = findParent(ancestor);
}
for (var i = 0, l = ancestors.length; i < l; i++) {
topLevelTarget = ancestors[i];
var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
ReactEventEmitter.handleTopLevel(
topLevelType,
@@ -98,9 +114,12 @@ var ReactEventTopLevelCallback = {
topLevelTargetID,
nativeEvent
);
topLevelTarget = findParent(topLevelTarget);
}
// Emptying ancestors/topLevelCallbackReusableArray is
// not necessary for correctness, but it helps the GC reclaim
// any nodes that were left at the end of the search.
ancestors.length = 0;
};
}
@@ -84,6 +84,39 @@ describe('ReactEventTopLevelCallback', function() {
expect(calls[2][EVENT_TARGET_PARAM])
.toBe(grandParentControl.getDOMNode());
});
it('should not get confused by disappearing elements', function() {
var childContainer = document.createElement('div');
var childControl = <div>Child</div>;
var parentContainer = document.createElement('div');
var parentControl = <div>Parent</div>;
ReactMount.renderComponent(childControl, childContainer);
ReactMount.renderComponent(parentControl, parentContainer);
parentControl.getDOMNode().appendChild(childContainer);
// ReactEventEmitter.handleTopLevel might remove the target from the DOM.
// Here, we have handleTopLevel remove the node when the first event
// handlers are called; we'll still expect to receive a second call for
// the parent control.
var childNode = childControl.getDOMNode();
ReactEventEmitter.handleTopLevel.mockImplementation(
function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) {
if (topLevelTarget === childNode) {
ReactMount.unmountComponentAtNode(childContainer);
}
}
);
var callback = ReactEventTopLevelCallback.createTopLevelCallback('test');
callback({
target: childNode
});
var calls = ReactEventEmitter.handleTopLevel.mock.calls;
expect(calls.length).toBe(2);
expect(calls[0][EVENT_TARGET_PARAM]).toBe(childNode);
expect(calls[1][EVENT_TARGET_PARAM]).toBe(parentControl.getDOMNode());
});
});
it('should not fire duplicate events for a React DOM tree', function() {