Prevent error thrown when removing event target
Fixes #1105.
Ben Alpert committed
Feb 17, 2014 at 01:24 UTC
2f0bb69708e224fb7b6eb90766578841112a22d2
2 files changed
+55
-3
src/browser/ReactEventTopLevelCallback.js
+22
-3
@@ -49,6 +49,9 @@ function findParent(node) {
49
return parent;
50
}
51
52
+// Used to store ancestor hierarchy in top level callback
53
+var topLevelCallbackReusableArray = [];
54
+
55
/**
56
* Top-level callback creator used to implement event handling using delegation.
57
* This is used via dependency injection.
@@ -89,8 +92,21 @@ var ReactEventTopLevelCallback = {
92
getEventTarget(nativeEvent)
93
) || window;
94
95
+ var ancestors = topLevelCallbackReusableArray;
96
+ ancestors.length = 0;
97
+
98
// Loop through the hierarchy, in case there's any nested components.
93
- while (topLevelTarget) {
99
+ // It's important that we build the array of ancestors before calling any
100
+ // event handlers, because event handlers can modify the DOM, leading to
101
+ // inconsistencies with ReactMount's node cache. See #1105.
102
+ var ancestor = topLevelTarget;
103
+ while (ancestor) {
104
+ ancestors.push(ancestor);
105
+ ancestor = findParent(ancestor);
106
+ }
107
+
108
+ for (var i = 0, l = ancestors.length; i < l; i++) {
109
+ topLevelTarget = ancestors[i];
110
var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
111
ReactEventEmitter.handleTopLevel(
112
topLevelType,
@@ -98,9 +114,12 @@ var ReactEventTopLevelCallback = {
114
topLevelTargetID,
115
nativeEvent
116
);
101
-
102
- topLevelTarget = findParent(topLevelTarget);
117
}
118
+
119
+ // Emptying ancestors/topLevelCallbackReusableArray is
120
+ // not necessary for correctness, but it helps the GC reclaim
121
+ // any nodes that were left at the end of the search.
122
+ ancestors.length = 0;
123
};
124
}
125
src/browser/__tests__/ReactEventTopLevelCallback-test.js
+33
@@ -84,6 +84,39 @@ describe('ReactEventTopLevelCallback', function() {
84
expect(calls[2][EVENT_TARGET_PARAM])
85
.toBe(grandParentControl.getDOMNode());
86
});
87
+
88
+ it('should not get confused by disappearing elements', function() {
89
+ var childContainer = document.createElement('div');
90
+ var childControl = <div>Child</div>;
91
+ var parentContainer = document.createElement('div');
92
+ var parentControl = <div>Parent</div>;
93
+ ReactMount.renderComponent(childControl, childContainer);
94
+ ReactMount.renderComponent(parentControl, parentContainer);
95
+ parentControl.getDOMNode().appendChild(childContainer);
96
+
97
+ // ReactEventEmitter.handleTopLevel might remove the target from the DOM.
98
+ // Here, we have handleTopLevel remove the node when the first event
99
+ // handlers are called; we'll still expect to receive a second call for
100
+ // the parent control.
101
+ var childNode = childControl.getDOMNode();
102
+ ReactEventEmitter.handleTopLevel.mockImplementation(
103
+ function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) {
104
+ if (topLevelTarget === childNode) {
105
+ ReactMount.unmountComponentAtNode(childContainer);
106
+ }
107
+ }
108
+ );
109
+
110
+ var callback = ReactEventTopLevelCallback.createTopLevelCallback('test');
111
+ callback({
112
+ target: childNode
113
+ });
114
+
115
+ var calls = ReactEventEmitter.handleTopLevel.mock.calls;
116
+ expect(calls.length).toBe(2);
117
+ expect(calls[0][EVENT_TARGET_PARAM]).toBe(childNode);
118
+ expect(calls[1][EVENT_TARGET_PARAM]).toBe(parentControl.getDOMNode());
119
+ });
120
});
121
122
it('should not fire duplicate events for a React DOM tree', function() {