@samitouri / QOS-React-2 / commits / 2f0bb69708

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() {