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

Propagate events based on component tree, not IDs

Ben Alpert committed Oct 17, 2015 at 18:10 UTC d11ccd6956ccdcb5801e3577d5385f7826ea085c
8 files changed +172 -141
src/renderers/dom/client/ReactDOMTreeTraversal.js
+51
@@ -70,14 +70,65 @@ function isAncestor(instA, instB) {
70 return false;
71 }
72
73 +/**
74 + * Return the parent instance of the passed-in instance.
75 + */
76 function getParentInstance(inst) {
77 invariant('_nativeNode' in inst, 'getParentInstance: Invalid argument.');
78
79 return inst._nativeParent;
80 }
81
82 +/**
83 + * Simulates the traversal of a two-phase, capture/bubble event dispatch.
84 + */
85 +function traverseTwoPhase(inst, fn, arg) {
86 + var path = [];
87 + while (inst) {
88 + path.push(inst);
89 + inst = inst._nativeParent;
90 + }
91 + var i;
92 + for (i = path.length; i-- > 0;) {
93 + fn(path[i], false, arg);
94 + }
95 + for (i = 0; i < path.length; i++) {
96 + fn(path[i], true, arg);
97 + }
98 +}
99 +
100 +/**
101 + * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
102 + * should would receive a `mouseEnter` or `mouseLeave` event.
103 + *
104 + * Does not invoke the callback on the nearest common ancestor because nothing
105 + * "entered" or "left" that element.
106 + */
107 +function traverseEnterLeave(from, to, fn, argFrom, argTo) {
108 + var common = from && to ? getLowestCommonAncestor(from, to) : null;
109 + var pathFrom = [];
110 + while (from && from !== common) {
111 + pathFrom.push(from);
112 + from = from._nativeParent;
113 + }
114 + var pathTo = [];
115 + while (to && to !== common) {
116 + pathTo.push(to);
117 + to = to._nativeParent;
118 + }
119 + var i;
120 + for (i = 0; i < pathFrom.length; i++) {
121 + fn(pathFrom[i], true, argFrom);
122 + }
123 + for (i = pathTo.length; i-- > 0;) {
124 + fn(pathTo[i], false, argTo);
125 + }
126 +}
127 +
128 module.exports = {
129 isAncestor: isAncestor,
130 getLowestCommonAncestor: getLowestCommonAncestor,
131 getParentInstance: getParentInstance,
132 + traverseTwoPhase: traverseTwoPhase,
133 + traverseEnterLeave: traverseEnterLeave,
134 };
src/renderers/dom/shared/ReactDefaultInjection.js
-4
@@ -27,8 +27,6 @@ var ReactDOMTextComponent = require('ReactDOMTextComponent');
27 var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
28 var ReactEventListener = require('ReactEventListener');
29 var ReactInjection = require('ReactInjection');
30 -var ReactInstanceHandles = require('ReactInstanceHandles');
31 -var ReactMount = require('ReactMount');
30 var ReactReconcileTransaction = require('ReactReconcileTransaction');
31 var SVGDOMPropertyConfig = require('SVGDOMPropertyConfig');
32 var SelectEventPlugin = require('SelectEventPlugin');
@@ -53,10 +51,8 @@ function inject() {
51 * Inject modules for resolving DOM hierarchy and plugin ordering.
52 */
53 ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder);
56 - ReactInjection.EventPluginHub.injectInstanceHandle(ReactInstanceHandles);
54 ReactInjection.EventPluginUtils.injectComponentTree(ReactDOMComponentTree);
55 ReactInjection.EventPluginUtils.injectTreeTraversal(ReactDOMTreeTraversal);
59 - ReactInjection.EventPluginUtils.injectMount(ReactMount);
56
57 /**
58 * Some important event plugins included by default (without having to require
src/renderers/shared/event/EventPluginHub.js
-36
@@ -18,7 +18,6 @@ var ReactErrorUtils = require('ReactErrorUtils');
18 var accumulateInto = require('accumulateInto');
19 var forEachAccumulated = require('forEachAccumulated');
20 var invariant = require('invariant');
21 -var warning = require('warning');
21
22 /**
23 * Internal store for event listeners
@@ -54,23 +53,6 @@ var executeDispatchesAndReleaseTopLevel = function(e) {
53 return executeDispatchesAndRelease(e, false);
54 };
55
57 -/**
58 - * - `InstanceHandle`: [required] Module that performs logical traversals of DOM
59 - * hierarchy given ids of the logical DOM elements involved.
60 - */
61 -var InstanceHandle = null;
62 -
63 -function validateInstanceHandle() {
64 - var valid =
65 - InstanceHandle &&
66 - InstanceHandle.traverseTwoPhase &&
67 - InstanceHandle.traverseEnterLeave;
68 - warning(
69 - valid,
70 - 'InstanceHandle not injected before use!'
71 - );
72 -}
73 -
56 /**
57 * This is a unified interface for event plugins to be installed and configured.
58 *
@@ -100,24 +82,6 @@ var EventPluginHub = {
82 */
83 injection: {
84
103 - /**
104 - * @param {object} InjectedInstanceHandle
105 - * @public
106 - */
107 - injectInstanceHandle: function(InjectedInstanceHandle) {
108 - InstanceHandle = InjectedInstanceHandle;
109 - if (__DEV__) {
110 - validateInstanceHandle();
111 - }
112 - },
113 -
114 - getInstanceHandle: function() {
115 - if (__DEV__) {
116 - validateInstanceHandle();
117 - }
118 - return InstanceHandle;
119 - },
120 -
85 /**
86 * @param {array} InjectedEventPluginOrder
87 * @public
src/renderers/shared/event/EventPluginUtils.js
+43 -43
@@ -24,11 +24,8 @@ var warning = require('warning');
24 /**
25 * - `ComponentTree`: [required] Module that can convert between React instances
26 * and actual node references.
27 - * - `Mount`: [required] Module that can convert between React dom IDs and
28 - * actual node references.
27 */
28 var ComponentTree;
31 -var Mount;
29 var TreeTraversal;
30 var injection = {
31 injectComponentTree: function(Injected) {
@@ -43,16 +40,6 @@ var injection = {
40 );
41 }
42 },
46 - injectMount: function(Injected) {
47 - Mount = Injected;
48 - if (__DEV__) {
49 - warning(
50 - Injected && Injected.getNode && Injected.getID,
51 - 'EventPluginUtils.injection.injectMount(...): Injected Mount ' +
52 - 'module is missing getNode or getID.'
53 - );
54 - }
55 - },
43 injectTreeTraversal: function(Injected) {
44 TreeTraversal = Injected;
45 if (__DEV__) {
@@ -87,17 +74,20 @@ var validateEventDispatches;
74 if (__DEV__) {
75 validateEventDispatches = function(event) {
76 var dispatchListeners = event._dispatchListeners;
90 - var dispatchIDs = event._dispatchIDs;
77 + var dispatchInstances = event._dispatchInstances;
78
79 var listenersIsArr = Array.isArray(dispatchListeners);
93 - var idsIsArr = Array.isArray(dispatchIDs);
94 - var IDsLen = idsIsArr ? dispatchIDs.length : dispatchIDs ? 1 : 0;
80 var listenersLen = listenersIsArr ?
81 dispatchListeners.length :
82 dispatchListeners ? 1 : 0;
83
84 + var instancesIsArr = Array.isArray(dispatchInstances);
85 + var instancesLen = instancesIsArr ?
86 + dispatchInstances.length :
87 + dispatchInstances ? 1 : 0;
88 +
89 warning(
100 - idsIsArr === listenersIsArr && IDsLen === listenersLen,
90 + instancesIsArr === listenersIsArr && instancesLen === listenersLen,
91 'EventPluginUtils: Invalid `event`.'
92 );
93 };
@@ -108,20 +98,19 @@ if (__DEV__) {
98 * @param {SyntheticEvent} event SyntheticEvent to handle
99 * @param {boolean} simulated If the event is simulated (changes exn behavior)
100 * @param {function} listener Application-level callback
111 - * @param {string} domID DOM id to pass to the callback.
101 + * @param {*} inst Internal component instance
102 */
113 -function executeDispatch(event, simulated, listener, domID) {
103 +function executeDispatch(event, simulated, listener, inst) {
104 var type = event.type || 'unknown-event';
115 - event.currentTarget = EventPluginUtils.getNode(domID);
105 + event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
106 if (simulated) {
107 ReactErrorUtils.invokeGuardedCallbackWithCatch(
108 type,
109 listener,
120 - event,
121 - domID
110 + event
111 );
112 } else {
124 - ReactErrorUtils.invokeGuardedCallback(type, listener, event, domID);
113 + ReactErrorUtils.invokeGuardedCallback(type, listener, event);
114 }
115 event.currentTarget = null;
116 }
@@ -131,7 +120,7 @@ function executeDispatch(event, simulated, listener, domID) {
120 */
121 function executeDispatchesInOrder(event, simulated) {
122 var dispatchListeners = event._dispatchListeners;
134 - var dispatchIDs = event._dispatchIDs;
123 + var dispatchInstances = event._dispatchInstances;
124 if (__DEV__) {
125 validateEventDispatches(event);
126 }
@@ -140,14 +129,19 @@ function executeDispatchesInOrder(event, simulated) {
129 if (event.isPropagationStopped()) {
130 break;
131 }
143 - // Listeners and IDs are two parallel arrays that are always in sync.
144 - executeDispatch(event, simulated, dispatchListeners[i], dispatchIDs[i]);
132 + // Listeners and Instances are two parallel arrays that are always in sync.
133 + executeDispatch(
134 + event,
135 + simulated,
136 + dispatchListeners[i],
137 + dispatchInstances[i]
138 + );
139 }
140 } else if (dispatchListeners) {
147 - executeDispatch(event, simulated, dispatchListeners, dispatchIDs);
141 + executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
142 }
143 event._dispatchListeners = null;
150 - event._dispatchIDs = null;
144 + event._dispatchInstances = null;
145 }
146
147 /**
@@ -159,7 +153,7 @@ function executeDispatchesInOrder(event, simulated) {
153 */
154 function executeDispatchesInOrderStopAtTrueImpl(event) {
155 var dispatchListeners = event._dispatchListeners;
162 - var dispatchIDs = event._dispatchIDs;
156 + var dispatchInstances = event._dispatchInstances;
157 if (__DEV__) {
158 validateEventDispatches(event);
159 }
@@ -168,14 +162,14 @@ function executeDispatchesInOrderStopAtTrueImpl(event) {
162 if (event.isPropagationStopped()) {
163 break;
164 }
171 - // Listeners and IDs are two parallel arrays that are always in sync.
172 - if (dispatchListeners[i](event, dispatchIDs[i])) {
173 - return dispatchIDs[i];
165 + // Listeners and Instances are two parallel arrays that are always in sync.
166 + if (dispatchListeners[i](event, dispatchInstances[i])) {
167 + return dispatchInstances[i];
168 }
169 }
170 } else if (dispatchListeners) {
177 - if (dispatchListeners(event, dispatchIDs)) {
178 - return dispatchIDs;
171 + if (dispatchListeners(event, dispatchInstances)) {
172 + return dispatchInstances;
173 }
174 }
175 return null;
@@ -186,7 +180,7 @@ function executeDispatchesInOrderStopAtTrueImpl(event) {
180 */
181 function executeDispatchesInOrderStopAtTrue(event) {
182 var ret = executeDispatchesInOrderStopAtTrueImpl(event);
189 - event._dispatchIDs = null;
183 + event._dispatchInstances = null;
184 event._dispatchListeners = null;
185 return ret;
186 }
@@ -205,16 +199,16 @@ function executeDirectDispatch(event) {
199 validateEventDispatches(event);
200 }
201 var dispatchListener = event._dispatchListeners;
208 - var dispatchID = event._dispatchIDs;
202 + var dispatchInstance = event._dispatchInstances;
203 invariant(
204 !Array.isArray(dispatchListener),
205 'executeDirectDispatch(...): Invalid `event`.'
206 );
213 - var res = dispatchListener ?
214 - dispatchListener(event, dispatchID) :
215 - null;
207 + event.currentTarget = EventPluginUtils.getNodeFromInstance(dispatchInstance);
208 + var res = dispatchListener ? dispatchListener(event) : null;
209 + event.curentTarget = null;
210 event._dispatchListeners = null;
217 - event._dispatchIDs = null;
211 + event._dispatchInstances = null;
212 return res;
213 }
214
@@ -239,12 +233,12 @@ var EventPluginUtils = {
233 executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,
234 hasDispatches: hasDispatches,
235
242 - getNode: function(id) {
243 - return Mount.getNode(id);
244 - },
236 getInstanceFromNode: function(node) {
237 return ComponentTree.getInstanceFromNode(node);
238 },
239 + getNodeFromInstance: function(node) {
240 + return ComponentTree.getNodeFromInstance(node);
241 + },
242 isAncestor: function(a, b) {
243 return TreeTraversal.isAncestor(a, b);
244 },
@@ -254,6 +248,12 @@ var EventPluginUtils = {
248 getParentInstance: function(inst) {
249 return TreeTraversal.getParentInstance(inst);
250 },
251 + traverseTwoPhase: function(target, fn, arg) {
252 + return TreeTraversal.traverseTwoPhase(target, fn, arg);
253 + },
254 + traverseEnterLeave: function(from, to, fn, argFrom, argTo) {
255 + return TreeTraversal.traverseEnterLeave(from, to, fn, argFrom, argTo);
256 + },
257
258 injection: injection,
259 };
src/renderers/shared/event/EventPropagators.js
+23 -24
@@ -13,27 +13,23 @@
13
14 var EventConstants = require('EventConstants');
15 var EventPluginHub = require('EventPluginHub');
16 -
17 -var warning = require('warning');
16 +var EventPluginUtils = require('EventPluginUtils');
17
18 var accumulateInto = require('accumulateInto');
19 var forEachAccumulated = require('forEachAccumulated');
20 +var warning = require('warning');
21
22 var PropagationPhases = EventConstants.PropagationPhases;
23 var getListener = EventPluginHub.getListener;
24
25 -function getID(inst) {
26 - return inst ? inst._rootNodeID : '';
27 -}
28 -
25 /**
26 * Some event types have a notion of different registration names for different
27 * "phases" of propagation. This finds listeners by a given phase.
28 */
33 -function listenerAtPhase(id, event, propagationPhase) {
29 +function listenerAtPhase(inst, event, propagationPhase) {
30 var registrationName =
31 event.dispatchConfig.phasedRegistrationNames[propagationPhase];
36 - return getListener(id, registrationName);
32 + return getListener(inst._rootNodeID, registrationName);
33 }
34
35 /**
@@ -42,19 +38,19 @@ function listenerAtPhase(id, event, propagationPhase) {
38 * Mutating the event's members allows us to not have to create a wrapping
39 * "dispatch" object that pairs the event with the listener.
40 */
45 -function accumulateDirectionalDispatches(domID, upwards, event) {
41 +function accumulateDirectionalDispatches(inst, upwards, event) {
42 if (__DEV__) {
43 warning(
48 - domID,
49 - 'Dispatching id must not be null'
44 + inst,
45 + 'Dispatching inst must not be null'
46 );
47 }
48 var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
53 - var listener = listenerAtPhase(domID, event, phase);
49 + var listener = listenerAtPhase(inst, event, phase);
50 if (listener) {
51 event._dispatchListeners =
52 accumulateInto(event._dispatchListeners, listener);
57 - event._dispatchIDs = accumulateInto(event._dispatchIDs, domID);
53 + event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
54 }
55 }
56
@@ -67,8 +63,8 @@ function accumulateDirectionalDispatches(domID, upwards, event) {
63 */
64 function accumulateTwoPhaseDispatchesSingle(event) {
65 if (event && event.dispatchConfig.phasedRegistrationNames) {
70 - EventPluginHub.injection.getInstanceHandle().traverseTwoPhase(
71 - getID(event._targetInst),
66 + EventPluginUtils.traverseTwoPhase(
67 + event._targetInst,
68 accumulateDirectionalDispatches,
69 event
70 );
@@ -80,8 +76,11 @@ function accumulateTwoPhaseDispatchesSingle(event) {
76 */
77 function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
78 if (event && event.dispatchConfig.phasedRegistrationNames) {
83 - EventPluginHub.injection.getInstanceHandle().traverseTwoPhaseSkipTarget(
84 - getID(event._targetInst),
79 + var targetInst = event._targetInst;
80 + var parentInst =
81 + targetInst ? EventPluginUtils.getParentInstance(targetInst) : null;
82 + EventPluginUtils.traverseTwoPhase(
83 + parentInst,
84 accumulateDirectionalDispatches,
85 event
86 );
@@ -94,14 +93,14 @@ function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
93 * registration names. Same as `accumulateDirectDispatchesSingle` but without
94 * requiring that the `dispatchMarker` be the same as the dispatched ID.
95 */
97 -function accumulateDispatches(id, ignoredDirection, event) {
96 +function accumulateDispatches(inst, ignoredDirection, event) {
97 if (event && event.dispatchConfig.registrationName) {
98 var registrationName = event.dispatchConfig.registrationName;
100 - var listener = getListener(id, registrationName);
99 + var listener = getListener(inst._rootNodeID, registrationName);
100 if (listener) {
101 event._dispatchListeners =
102 accumulateInto(event._dispatchListeners, listener);
104 - event._dispatchIDs = accumulateInto(event._dispatchIDs, id);
103 + event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
104 }
105 }
106 }
@@ -113,7 +112,7 @@ function accumulateDispatches(id, ignoredDirection, event) {
112 */
113 function accumulateDirectDispatchesSingle(event) {
114 if (event && event.dispatchConfig.registrationName) {
116 - accumulateDispatches(getID(event._targetInst), null, event);
115 + accumulateDispatches(event._targetInst, null, event);
116 }
117 }
118
@@ -126,9 +125,9 @@ function accumulateTwoPhaseDispatchesSkipTarget(events) {
125 }
126
127 function accumulateEnterLeaveDispatches(leave, enter, from, to) {
129 - EventPluginHub.injection.getInstanceHandle().traverseEnterLeave(
130 - getID(from),
131 - getID(to),
128 + EventPluginUtils.traverseEnterLeave(
129 + from,
130 + to,
131 accumulateDispatches,
132 leave,
133 enter
src/renderers/shared/event/eventPlugins/ResponderEventPlugin.js
+2 -21
@@ -352,31 +352,12 @@ function setResponderAndExtractTransfer(
352 } else {
353 EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent);
354 }
355 - var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
355 + var wantsResponderInst = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
356 if (!shouldSetEvent.isPersistent()) {
357 shouldSetEvent.constructor.release(shouldSetEvent);
358 }
359
360 - if (!wantsResponderID) {
361 - return null;
362 - }
363 -
364 - // HACK: Temporary tree-crawling until executeDispatchesInOrderStopAtTrue
365 - // returns the instance instead of the ID.
366 - var wantsResponderInst;
367 - for (
368 - var inst = bubbleShouldSetFrom;
369 - inst;
370 - inst = EventPluginUtils.getParentInstance(inst)
371 - ) {
372 - if (wantsResponderID === inst._rootNodeID) {
373 - wantsResponderInst = inst;
374 - break;
375 - }
376 - }
377 - invariant(wantsResponderInst, 'Requested responder instance not found.');
378 -
379 - if (wantsResponderInst === responderInst) {
360 + if (!wantsResponderInst || wantsResponderInst === responderInst) {
361 return null;
362 }
363 var extracted;
src/renderers/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js
+8 -13
@@ -341,19 +341,6 @@ describe('ResponderEventPlugin', function() {
341 ReactInstanceHandles = require('ReactInstanceHandles');
342 ResponderEventPlugin = require('ResponderEventPlugin');
343
344 - EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles);
345 -
346 - // Only needed because SyntheticEvent supports the `currentTarget`
347 - // property.
348 - EventPluginUtils.injection.injectMount({
349 - getNode: function(id) {
350 - return id;
351 - },
352 - getID: function(nodeHandle) {
353 - return nodeHandle;
354 - },
355 - });
356 -
344 EventPluginUtils.injection.injectComponentTree({
345 getInstanceFromNode: function(id) {
346 return idToInstance[id];
@@ -385,6 +372,14 @@ describe('ResponderEventPlugin', function() {
372 var parentID = id.substr(0, id.lastIndexOf('.'));
373 return idToInstance[parentID] || null;
374 },
375 + traverseTwoPhase: function(target, fn, arg) {
376 + ReactInstanceHandles.traverseTwoPhase(
377 + target._rootNodeID,
378 + function(id, upwards) {
379 + fn(idToInstance[id], upwards, arg);
380 + }
381 + );
382 + },
383 });
384
385 topLevelTypes = EventConstants.topLevelTypes;
src/renderers/shared/reconciler/__tests__/ReactInstanceHandles-test.js
+45
@@ -216,6 +216,51 @@ describe('ReactInstanceHandles', function() {
216 });
217 });
218
219 + describe('traverseTwoPhaseSkipTarget', function() {
220 + it('should not traverse when traversing outside DOM', function() {
221 + var targetID = '';
222 + var expectedAggregation = [];
223 + ReactInstanceHandles.traverseTwoPhaseSkipTarget(
224 + targetID,
225 + argAggregator,
226 + ARG
227 + );
228 + expect(aggregatedArgs).toEqual(expectedAggregation);
229 + });
230 +
231 + it('should traverse two phase across component boundary', function() {
232 + var parent = renderParentIntoDocument();
233 + var targetID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
234 + var expectedAggregation = [
235 + {id: getNodeID(parent.refs.P), isUp: false, arg: ARG},
236 + {id: getNodeID(parent.refs.P_P1), isUp: false, arg: ARG},
237 + {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: false, arg: ARG},
238 +
239 + {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: true, arg: ARG},
240 + {id: getNodeID(parent.refs.P_P1), isUp: true, arg: ARG},
241 + {id: getNodeID(parent.refs.P), isUp: true, arg: ARG},
242 + ];
243 + ReactInstanceHandles.traverseTwoPhaseSkipTarget(
244 + targetID,
245 + argAggregator,
246 + ARG
247 + );
248 + expect(aggregatedArgs).toEqual(expectedAggregation);
249 + });
250 +
251 + it('should traverse two phase at shallowest node', function() {
252 + var parent = renderParentIntoDocument();
253 + var targetID = getNodeID(parent.refs.P);
254 + var expectedAggregation = [];
255 + ReactInstanceHandles.traverseTwoPhaseSkipTarget(
256 + targetID,
257 + argAggregator,
258 + ARG
259 + );
260 + expect(aggregatedArgs).toEqual(expectedAggregation);
261 + });
262 + });
263 +
264 describe('traverseEnterLeave', function() {
265 it('should not traverse when enter/leaving outside DOM', function() {
266 var targetID = '';