diff --git a/src/renderers/dom/client/ReactDOMComponentTree.js b/src/renderers/dom/client/ReactDOMComponentTree.js new file mode 100644 index 0000000000000..99dcb2654ae78 --- /dev/null +++ b/src/renderers/dom/client/ReactDOMComponentTree.js @@ -0,0 +1,188 @@ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDOMComponentTree + */ + +'use strict'; + +var DOMProperty = require('DOMProperty'); +var ReactDOMComponentFlags = require('ReactDOMComponentFlags'); + +var invariant = require('invariant'); + +var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME; +var Flags = ReactDOMComponentFlags; + +var internalInstanceKey = + '__reactInternalInstance$' + Math.random().toString(36).slice(2); + +/** + * Drill down (through composites and empty components) until we get a native or + * native text component. + * + * This is pretty polymorphic but unavoidable with the current structure we have + * for `_renderedChildren`. + */ +function getRenderedNativeOrTextFromComponent(component) { + var rendered; + while ((rendered = component._renderedComponent)) { + component = rendered; + } + return component; +} + +/** + * Populate `_nativeNode` on the rendered native/text component with the given + * DOM node. The passed `inst` can be a composite. + */ +function precacheNode(inst, node) { + var nativeInst = getRenderedNativeOrTextFromComponent(inst); + nativeInst._nativeNode = node; + node[internalInstanceKey] = nativeInst; +} + +function uncacheNode(inst) { + var node = inst._nativeNode; + if (node) { + delete node[internalInstanceKey]; + inst._nativeNode = null; + } +} + +/** + * Populate `_nativeNode` on each child of `inst`, assuming that the children + * match up with the DOM (element) children of `node`. + * + * We cache entire levels at once to avoid an n^2 problem where we access the + * children of a node sequentially and have to walk from the start to our target + * node every time. + * + * Since we update `_renderedChildren` and the actual DOM at (slightly) + * different times, we could race here and not get the + */ +function precacheChildNodes(inst, node) { + if (inst._flags & Flags.hasCachedChildNodes) { + return; + } + var children = inst._renderedChildren; + var childNode = node.firstChild; + outer: for (var name in children) { + if (!children.hasOwnProperty(name)) { + continue; + } + var childInst = children[name]; + var childID = childInst._rootNodeID; + if (childID == null) { + // We're currently unmounting this child in ReactMultiChild; skip it. + continue; + } + // We assume the child nodes are in the same order as the child instances. + for (; childNode !== null; childNode = childNode.nextSibling) { + if (childNode.nodeType === 1 && + childNode.getAttribute(ATTR_NAME) === childID) { + precacheNode(childInst, childNode); + continue outer; + } + } + // We reached the end of the DOM children without finding an ID match. + invariant(false, 'Unable to find element with ID %s.', childID); + } + inst._flags |= Flags.hasCachedChildNodes; +} + +/** + * Given a DOM node, return the closest ReactDOMComponent or + * ReactDOMTextComponent instance ancestor. + */ +function getClosestInstanceFromNode(node) { + if (node[internalInstanceKey]) { + return node[internalInstanceKey]; + } + + // Walk up the tree until we find an ancestor whose instance we have cached. + var parents = []; + while (!node[internalInstanceKey]) { + parents.push(node); + if (node.parentNode) { + node = node.parentNode; + } else { + // Top of the tree. This node must not be part of a React tree (or is + // unmounted, potentially). + return null; + } + } + + var closest; + var inst; + for (; node && (inst = node[internalInstanceKey]); node = parents.pop()) { + closest = inst; + if (parents.length) { + precacheChildNodes(inst, node); + } + } + + return closest; +} + +/** + * Given a DOM node, return the ReactDOMComponent or ReactDOMTextComponent + * instance, or null if the node was not rendered by this React. + */ +function getInstanceFromNode(node) { + var inst = getClosestInstanceFromNode(node); + if (inst != null && inst._nativeNode === node) { + return inst; + } else { + return null; + } +} + +/** + * Given a ReactDOMComponent or ReactDOMTextComponent, return the corresponding + * DOM node. + */ +function getNodeFromInstance(inst) { + // Without this first invariant, passing a non-DOM-component causes the next + // invariant for a missing parent, which is super confusing. + invariant('_nativeNode' in inst, 'getNodeFromInstance: Invalid argument.'); + + if (inst._nativeNode) { + return inst._nativeNode; + } + + // Walk up the tree until we find an ancestor whose DOM node we have cached. + var parents = []; + while (!inst._nativeNode) { + parents.push(inst); + invariant( + inst._nativeParent, + 'React DOM tree root should always have a node reference.' + ); + inst = inst._nativeParent; + } + + // Now parents contains each ancestor that does *not* have a cached native + // node, and `inst` is the deepest ancestor that does. + for (; parents.length; inst = parents.pop()) { + precacheChildNodes(inst, inst._nativeNode); + } + + return inst._nativeNode; +} + +var ReactDOMComponentTree = { + getClosestInstanceFromNode: getClosestInstanceFromNode, + getInstanceFromNode: getInstanceFromNode, + getNodeFromInstance: getNodeFromInstance, + precacheChildNodes: precacheChildNodes, + precacheNode: precacheNode, + uncacheNode: uncacheNode, +}; + +module.exports = ReactDOMComponentTree; diff --git a/src/renderers/dom/client/ReactDOMIDOperations.js b/src/renderers/dom/client/ReactDOMIDOperations.js index 7b419e2bf9663..98bd1df70f1ff 100644 --- a/src/renderers/dom/client/ReactDOMIDOperations.js +++ b/src/renderers/dom/client/ReactDOMIDOperations.js @@ -13,56 +13,14 @@ 'use strict'; var DOMChildrenOperations = require('DOMChildrenOperations'); -var DOMPropertyOperations = require('DOMPropertyOperations'); -var ReactMount = require('ReactMount'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactPerf = require('ReactPerf'); -var invariant = require('invariant'); - -/** - * Errors for properties that should not be updated with `updatePropertyByID()`. - * - * @type {object} - * @private - */ -var INVALID_PROPERTY_ERRORS = { - dangerouslySetInnerHTML: - '`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.', - style: '`style` must be set using `updateStylesByID()`.', -}; - /** * Operations used to process updates to DOM nodes. */ var ReactDOMIDOperations = { - /** - * Updates a DOM node with new property values. This should only be used to - * update DOM properties in `DOMProperty`. - * - * @param {string} id ID of the node to update. - * @param {string} name A valid property name, see `DOMProperty`. - * @param {*} value New value of the property. - * @internal - */ - updatePropertyByID: function(id, name, value) { - var node = ReactMount.getNode(id); - invariant( - !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), - 'updatePropertyByID(...): %s', - INVALID_PROPERTY_ERRORS[name] - ); - - // If we're updating to null or undefined, we should remove the property - // from the DOM node instead of inadvertantly setting to a string. This - // brings us in line with the same behavior we have on initial render. - if (value != null) { - DOMPropertyOperations.setValueForProperty(node, name, value); - } else { - DOMPropertyOperations.deleteValueForProperty(node, name); - } - }, - /** * Updates a component's children by processing a series of updates. * @@ -72,7 +30,9 @@ var ReactDOMIDOperations = { */ dangerouslyProcessChildrenUpdates: function(updates, markup) { for (var i = 0; i < updates.length; i++) { - updates[i].parentNode = ReactMount.getNode(updates[i].parentID); + var update = updates[i]; + var node = ReactDOMComponentTree.getNodeFromInstance(update.parentInst); + update.parentNode = node; } DOMChildrenOperations.processUpdates(updates, markup); }, diff --git a/src/renderers/dom/client/ReactDOMTreeTraversal.js b/src/renderers/dom/client/ReactDOMTreeTraversal.js new file mode 100644 index 0000000000000..726a051bebf18 --- /dev/null +++ b/src/renderers/dom/client/ReactDOMTreeTraversal.js @@ -0,0 +1,134 @@ +/** + * Copyright 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDOMTreeTraversal + */ + +'use strict'; + +var invariant = require('invariant'); + +/** + * Return the lowest common ancestor of A and B, or null if they are in + * different trees. + */ +function getLowestCommonAncestor(instA, instB) { + invariant('_nativeNode' in instA, 'getNodeFromInstance: Invalid argument.'); + invariant('_nativeNode' in instB, 'getNodeFromInstance: Invalid argument.'); + + var depthA = 0; + for (var tempA = instA; tempA; tempA = tempA._nativeParent) { + depthA++; + } + var depthB = 0; + for (var tempB = instB; tempB; tempB = tempB._nativeParent) { + depthB++; + } + + // If A is deeper, crawl up. + while (depthA - depthB > 0) { + instA = instA._nativeParent; + depthA--; + } + + // If B is deeper, crawl up. + while (depthB - depthA > 0) { + instB = instB._nativeParent; + depthB--; + } + + // Walk in lockstep until we find a match. + var depth = depthA; + while (depth--) { + if (instA === instB) { + return instA; + } + instA = instA._nativeParent; + instB = instB._nativeParent; + } + return null; +} + +/** + * Return if A is an ancestor of B. + */ +function isAncestor(instA, instB) { + invariant('_nativeNode' in instA, 'isAncestor: Invalid argument.'); + invariant('_nativeNode' in instB, 'isAncestor: Invalid argument.'); + + while (instB) { + if (instB === instA) { + return true; + } + instB = instB._nativeParent; + } + return false; +} + +/** + * Return the parent instance of the passed-in instance. + */ +function getParentInstance(inst) { + invariant('_nativeNode' in inst, 'getParentInstance: Invalid argument.'); + + return inst._nativeParent; +} + +/** + * Simulates the traversal of a two-phase, capture/bubble event dispatch. + */ +function traverseTwoPhase(inst, fn, arg) { + var path = []; + while (inst) { + path.push(inst); + inst = inst._nativeParent; + } + var i; + for (i = path.length; i-- > 0;) { + fn(path[i], false, arg); + } + for (i = 0; i < path.length; i++) { + fn(path[i], true, arg); + } +} + +/** + * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that + * should would receive a `mouseEnter` or `mouseLeave` event. + * + * Does not invoke the callback on the nearest common ancestor because nothing + * "entered" or "left" that element. + */ +function traverseEnterLeave(from, to, fn, argFrom, argTo) { + var common = from && to ? getLowestCommonAncestor(from, to) : null; + var pathFrom = []; + while (from && from !== common) { + pathFrom.push(from); + from = from._nativeParent; + } + var pathTo = []; + while (to && to !== common) { + pathTo.push(to); + to = to._nativeParent; + } + var i; + for (i = 0; i < pathFrom.length; i++) { + fn(pathFrom[i], true, argFrom); + } + for (i = pathTo.length; i-- > 0;) { + fn(pathTo[i], false, argTo); + } +} + +module.exports = { + isAncestor: isAncestor, + getLowestCommonAncestor: getLowestCommonAncestor, + getParentInstance: getParentInstance, + traverseTwoPhase: traverseTwoPhase, + traverseEnterLeave: traverseEnterLeave, +}; diff --git a/src/renderers/dom/client/ReactEventListener.js b/src/renderers/dom/client/ReactEventListener.js index 422b0ee650ee2..307e5debad145 100644 --- a/src/renderers/dom/client/ReactEventListener.js +++ b/src/renderers/dom/client/ReactEventListener.js @@ -15,32 +15,28 @@ var EventListener = require('EventListener'); var ExecutionEnvironment = require('ExecutionEnvironment'); var PooledClass = require('PooledClass'); -var ReactInstanceHandles = require('ReactInstanceHandles'); -var ReactMount = require('ReactMount'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactUpdates = require('ReactUpdates'); var assign = require('Object.assign'); var getEventTarget = require('getEventTarget'); var getUnboundedScrollPosition = require('getUnboundedScrollPosition'); -var DOCUMENT_FRAGMENT_NODE_TYPE = 11; - /** - * Finds the parent React component of `node`. - * - * @param {*} node - * @return {?DOMEventTarget} Parent container, or `null` if the specified node - * is not nested. + * Find the deepest React component completely containing the root of the + * passed-in instance (for use when entire React trees are nested within each + * other). If React trees are not nested, returns null. */ -function findParent(node) { +function findParent(inst) { // TODO: It may be a good idea to cache this to prevent unnecessary DOM // traversal, but caching is difficult to do correctly without using a // mutation observer to listen for all DOM changes. - var nodeID = ReactMount.getID(node); - var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID); - var container = ReactMount.findReactContainerForID(rootID); - var parent = ReactMount.getFirstReactDOM(container); - return parent; + while (inst._nativeParent) { + inst = inst._nativeParent; + } + var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst); + var container = rootNode.parentNode; + return ReactDOMComponentTree.getClosestInstanceFromNode(container); } // Used to store ancestor hierarchy in top level callback @@ -62,91 +58,26 @@ PooledClass.addPoolingTo( ); function handleTopLevelImpl(bookKeeping) { - // TODO: Re-enable event.path handling - // - // if (bookKeeping.nativeEvent.path && bookKeeping.nativeEvent.path.length > 1) { - // // New browsers have a path attribute on native events - // handleTopLevelWithPath(bookKeeping); - // } else { - // // Legacy browsers don't have a path attribute on native events - // handleTopLevelWithoutPath(bookKeeping); - // } - - void handleTopLevelWithPath; // temporarily unused - handleTopLevelWithoutPath(bookKeeping); -} - -// Legacy browsers don't have a path attribute on native events -function handleTopLevelWithoutPath(bookKeeping) { - var topLevelTarget = ReactMount.getFirstReactDOM( - getEventTarget(bookKeeping.nativeEvent) - ) || window; + var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent); + var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode( + nativeEventTarget + ); // Loop through the hierarchy, in case there's any nested components. // 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) { + var ancestor = targetInst; + do { bookKeeping.ancestors.push(ancestor); - ancestor = findParent(ancestor); - } + ancestor = ancestor && findParent(ancestor); + } while (ancestor); for (var i = 0; i < bookKeeping.ancestors.length; i++) { - topLevelTarget = bookKeeping.ancestors[i]; - var topLevelTargetID = ReactMount.getID(topLevelTarget) || ''; - ReactEventListener._handleTopLevel( - bookKeeping.topLevelType, - topLevelTarget, - topLevelTargetID, - bookKeeping.nativeEvent, - getEventTarget(bookKeeping.nativeEvent) - ); - } -} - -// New browsers have a path attribute on native events -function handleTopLevelWithPath(bookKeeping) { - var path = bookKeeping.nativeEvent.path; - var currentNativeTarget = path[0]; - var eventsFired = 0; - for (var i = 0; i < path.length; i++) { - var currentPathElement = path[i]; - if (currentPathElement.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE) { - currentNativeTarget = path[i + 1]; - } - // TODO: slow - var reactParent = ReactMount.getFirstReactDOM(currentPathElement); - if (reactParent === currentPathElement) { - var currentPathElementID = ReactMount.getID(currentPathElement); - var newRootID = ReactInstanceHandles.getReactRootIDFromNodeID( - currentPathElementID - ); - bookKeeping.ancestors.push(currentPathElement); - - var topLevelTargetID = ReactMount.getID(currentPathElement) || ''; - eventsFired++; - ReactEventListener._handleTopLevel( - bookKeeping.topLevelType, - currentPathElement, - topLevelTargetID, - bookKeeping.nativeEvent, - currentNativeTarget - ); - - // Jump to the root of this React render tree - while (currentPathElementID !== newRootID) { - i++; - currentPathElement = path[i]; - currentPathElementID = ReactMount.getID(currentPathElement); - } - } - } - if (eventsFired === 0) { + targetInst = bookKeeping.ancestors[i]; ReactEventListener._handleTopLevel( bookKeeping.topLevelType, - window, - '', + targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent) ); diff --git a/src/renderers/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js index a1704f47fce92..88ed3a01a854c 100644 --- a/src/renderers/dom/client/ReactMount.js +++ b/src/renderers/dom/client/ReactMount.js @@ -16,7 +16,9 @@ var DOMLazyTree = require('DOMLazyTree'); var DOMProperty = require('DOMProperty'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); var ReactCurrentOwner = require('ReactCurrentOwner'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactDOMContainerInfo = require('ReactDOMContainerInfo'); +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); var ReactElement = require('ReactElement'); var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactMarkupChecksum = require('ReactMarkupChecksum'); @@ -26,7 +28,6 @@ var ReactUpdateQueue = require('ReactUpdateQueue'); var ReactUpdates = require('ReactUpdates'); var emptyObject = require('emptyObject'); -var containsNode = require('containsNode'); var instantiateReactComponent = require('instantiateReactComponent'); var invariant = require('invariant'); var setInnerHTML = require('setInnerHTML'); @@ -34,7 +35,6 @@ var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); var warning = require('warning'); var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME; -var nodeCache = {}; var ELEMENT_NODE_TYPE = 1; var DOC_NODE_TYPE = 9; @@ -52,9 +52,6 @@ if (__DEV__) { var rootElementsByReactRootID = {}; } -// Used to store breadth-first search state in findComponentRoot. -var findComponentRootReusableArray = []; - /** * Finds the index of the first character * that's not common between the two given strings. @@ -97,33 +94,6 @@ function getReactRootID(container) { return rootElement && internalGetID(rootElement); } -/** - * Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form - * element can return its control whose name or ID equals ATTR_NAME. All - * DOM nodes support `getAttributeNode` but this can also get called on - * other objects so just return '' if we're given something other than a - * DOM node (such as window). - * - * @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node. - * @return {string} ID of the supplied `domNode`. - */ -function getID(node) { - var id = internalGetID(node); - if (id) { - var cached = nodeCache[id]; - // TODO: Fix this whole concept of "validity" -- the cache just shouldn't - // have nodes that have been unmounted. - invariant( - !cached || cached === node || !isValid(cached, id), - 'ReactMount: Two valid but unequal nodes with the same `%s`: %s', - ATTR_NAME, id - ); - nodeCache[id] = node; - } - - return id; -} - function internalGetID(node) { // If node is something like a window, document, or text node, none of // which support attributes or a .getAttribute method, gracefully return @@ -131,112 +101,6 @@ function internalGetID(node) { return node && node.getAttribute && node.getAttribute(ATTR_NAME) || ''; } -/** - * Sets the React-specific ID of the given node. - * - * @param {DOMElement} node The DOM node whose ID will be set. - * @param {string} id The value of the ID attribute. - */ -function setID(node, id) { - var oldID = internalGetID(node); - if (oldID !== id) { - delete nodeCache[oldID]; - } - node.setAttribute(ATTR_NAME, id); - nodeCache[id] = node; -} - -/** - * Finds the node with the supplied ID if present in the cache. - */ -function getNodeIfCached(id) { - var node = nodeCache[id]; - // TODO: Fix this whole concept of "validity" -- the cache just shouldn't have - // nodes that have been unmounted. - if (node && isValid(node, id)) { - return node; - } -} - -/** - * Finds the node with the supplied React-generated DOM ID. - * - * @param {string} id A React-generated DOM ID. - * @return {DOMElement} DOM node with the suppled `id`. - * @internal - */ -function getNode(id) { - var node = getNodeIfCached(id); - if (node) { - return node; - } else { - return nodeCache[id] = ReactMount.findReactNodeByID(id); - } -} - -/** - * A node is "valid" if it is contained by a currently mounted container. - * - * This means that the node does not have to be contained by a document in - * order to be considered valid. - * - * @param {?DOMElement} node The candidate DOM node. - * @param {string} id The expected ID of the node. - * @return {boolean} Whether the node is contained by a mounted container. - */ -function isValid(node, id) { - if (node) { - invariant( - internalGetID(node) === id, - 'ReactMount: Unexpected modification of `%s`', - ATTR_NAME - ); - - var container = ReactMount.findReactContainerForID(id); - if (container && containsNode(container, node)) { - return true; - } - } - - return false; -} - -/** - * Causes the cache to forget about one React-specific ID. - * - * @param {string} id The ID to forget. - */ -function purgeID(id) { - delete nodeCache[id]; -} - -var deepestNodeSoFar = null; -function findDeepestCachedAncestorImpl(ancestorID) { - var ancestor = getNodeIfCached(ancestorID); - if (ancestor) { - deepestNodeSoFar = ancestor; - } else { - // This node isn't populated in the cache, so presumably none of its - // descendants are. Break out of the loop. - return false; - } -} - -/** - * Return the deepest cached node whose ID is a prefix of `targetID`. - */ -function findDeepestCachedAncestor(targetID) { - deepestNodeSoFar = null; - ReactInstanceHandles.traverseAncestors( - targetID, - findDeepestCachedAncestorImpl - ); - - var foundNode = deepestNodeSoFar; - deepestNodeSoFar = null; - return foundNode; -} - /** * Mounts this component and inserts it into the DOM. * @@ -266,6 +130,7 @@ function mountComponentIntoNode( ReactMount._mountImageIntoNode( markup, container, + componentInstance, shouldReuseMarkup, transaction ); @@ -287,7 +152,8 @@ function batchedMountComponentIntoNode( context ) { var transaction = ReactUpdates.ReactReconcileTransaction.getPooled( - /* forceHTML */ shouldReuseMarkup + /* useCreateElement */ + !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement ); transaction.perform( mountComponentIntoNode, @@ -340,47 +206,6 @@ function hasNonRootReactChild(node) { ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID) : false; } -/** - * Returns the first (deepest) ancestor of a node which is rendered by this copy - * of React. - */ -function findFirstReactDOMImpl(node) { - // This node might be from another React instance, so we make sure not to - // examine the node cache here - for (; node && node.parentNode !== node; node = node.parentNode) { - if (node.nodeType !== 1) { - // Not a DOMElement, therefore not a React component - continue; - } - var nodeID = internalGetID(node); - if (!nodeID) { - continue; - } - var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID); - - // If containersByReactRootID contains the container we find by crawling up - // the tree, we know that this instance of React rendered the node. - // nb. isValid's strategy (with containsNode) does not work because render - // trees may be nested and we don't want a false positive in that case. - var current = node; - var lastID; - do { - lastID = internalGetID(current); - current = current.parentNode; - if (current == null) { - // The passed-in node has been detached from the container it was - // originally rendered into. - return null; - } - } while (lastID !== reactRootID); - - if (current === containersByReactRootID[reactRootID]) { - return node; - } - } - return null; -} - /** * Temporary (?) hack so that we can store all top-level pending updates on * composites instead of having to worry about different types of components @@ -790,165 +615,10 @@ var ReactMount = { return true; }, - /** - * Finds the container DOM element that contains React component to which the - * supplied DOM `id` belongs. - * - * @param {string} id The ID of an element rendered by a React component. - * @return {?DOMElement} DOM element that contains the `id`. - */ - findReactContainerForID: function(id) { - var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id); - var container = containersByReactRootID[reactRootID]; - - if (__DEV__) { - var rootElement = rootElementsByReactRootID[reactRootID]; - if (rootElement && rootElement.parentNode !== container) { - warning( - // Call internalGetID here because getID calls isValid which calls - // findReactContainerForID (this function). - internalGetID(rootElement) === reactRootID, - 'ReactMount: Root element ID differed from reactRootID.' - ); - var containerChild = container.firstChild; - if (containerChild && - reactRootID === internalGetID(containerChild)) { - // If the container has a new child with the same ID as the old - // root element, then rootElementsByReactRootID[reactRootID] is - // just stale and needs to be updated. The case that deserves a - // warning is when the container is empty. - rootElementsByReactRootID[reactRootID] = containerChild; - } else { - warning( - false, - 'ReactMount: Root element has been removed from its original ' + - 'container. New container: %s', - rootElement.parentNode - ); - } - } - } - - return container; - }, - - /** - * Finds an element rendered by React with the supplied ID. - * - * @param {string} id ID of a DOM node in the React component. - * @return {DOMElement} Root DOM node of the React component. - */ - findReactNodeByID: function(id) { - var reactRoot = ReactMount.findReactContainerForID(id); - return ReactMount.findComponentRoot(reactRoot, id); - }, - - /** - * Traverses up the ancestors of the supplied node to find a node that is a - * DOM representation of a React component rendered by this copy of React. - * - * @param {*} node - * @return {?DOMEventTarget} - * @internal - */ - getFirstReactDOM: function(node) { - return findFirstReactDOMImpl(node); - }, - - /** - * Finds a node with the supplied `targetID` inside of the supplied - * `ancestorNode`. Exploits the ID naming scheme to perform the search - * quickly. - * - * @param {DOMEventTarget} ancestorNode Search from this root. - * @pararm {string} targetID ID of the DOM representation of the component. - * @return {DOMEventTarget} DOM node with the supplied `targetID`. - * @internal - */ - findComponentRoot: function(ancestorNode, targetID) { - var firstChildren = findComponentRootReusableArray; - var childIndex = 0; - - var deepestAncestor = findDeepestCachedAncestor(targetID) || ancestorNode; - - if (__DEV__) { - // This will throw on the next line; give an early warning - warning( - deepestAncestor != null, - 'React can\'t find the root component node for data-reactid value ' + - '`%s`. If you\'re seeing this message, it probably means that ' + - 'you\'ve loaded two copies of React on the page. At this time, only ' + - 'a single copy of React can be loaded at a time.', - targetID - ); - } - - firstChildren[0] = deepestAncestor.firstChild; - firstChildren.length = 1; - - while (childIndex < firstChildren.length) { - var child = firstChildren[childIndex++]; - var targetChild; - - while (child) { - var childID = ReactMount.getID(child); - if (childID) { - // Even if we find the node we're looking for, we finish looping - // through its siblings to ensure they're cached so that we don't have - // to revisit this node again. Otherwise, we make n^2 calls to getID - // when visiting the many children of a single node in order. - - if (targetID === childID) { - targetChild = child; - } else if (ReactInstanceHandles.isAncestorIDOf(childID, targetID)) { - // If we find a child whose ID is an ancestor of the given ID, - // then we can be sure that we only want to search the subtree - // rooted at this child, so we can throw out the rest of the - // search state. - firstChildren.length = childIndex = 0; - firstChildren.push(child.firstChild); - } - - } else { - // If this child had no ID, then there's a chance that it was - // injected automatically by the browser, as when a `` - // element sprouts an extra `` child as a side effect of - // `.innerHTML` parsing. Optimistically continue down this - // branch, but not before examining the other siblings. - firstChildren.push(child.firstChild); - } - - child = child.nextSibling; - } - - if (targetChild) { - // Emptying firstChildren/findComponentRootReusableArray is - // not necessary for correctness, but it helps the GC reclaim - // any nodes that were left at the end of the search. - firstChildren.length = 0; - - return targetChild; - } - } - - firstChildren.length = 0; - - invariant( - false, - 'findComponentRoot(..., %s): Unable to find element. This probably ' + - 'means the DOM was unexpectedly mutated (e.g., by the browser), ' + - 'usually due to forgetting a when using tables, nesting tags ' + - 'like ,

, or , or using non-SVG elements in an ' + - 'parent. ' + - 'Try inspecting the child nodes of the element with React ID `%s`.', - targetID, - ReactMount.getID(ancestorNode) - ); - }, - _mountImageIntoNode: function( markup, container, + instance, shouldReuseMarkup, transaction ) { @@ -964,6 +634,7 @@ var ReactMount = { if (shouldReuseMarkup) { var rootElement = getReactRootElementInContainer(container); if (ReactMarkupChecksum.canReuseMarkup(markup, rootElement)) { + ReactDOMComponentTree.precacheNode(instance, rootElement); return; } else { var checksum = rootElement.getAttribute( @@ -1047,24 +718,9 @@ var ReactMount = { DOMLazyTree.insertTreeBefore(container, markup, null); } else { setInnerHTML(container, markup); + ReactDOMComponentTree.precacheNode(instance, container.firstChild); } }, - - /** - * React ID utilities. - */ - - getReactRootID: getReactRootID, - - getID: getID, - - setID: setID, - - getNode: getNode, - - isValid: isValid, - - purgeID: purgeID, }; ReactPerf.measureMethods(ReactMount, 'ReactMount', { diff --git a/src/renderers/dom/client/ReactReconcileTransaction.js b/src/renderers/dom/client/ReactReconcileTransaction.js index a460629e31d55..fdd43ac781bfc 100644 --- a/src/renderers/dom/client/ReactReconcileTransaction.js +++ b/src/renderers/dom/client/ReactReconcileTransaction.js @@ -15,7 +15,6 @@ var CallbackQueue = require('CallbackQueue'); var PooledClass = require('PooledClass'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); -var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); var ReactInputSelection = require('ReactInputSelection'); var Transaction = require('Transaction'); @@ -107,7 +106,7 @@ var TRANSACTION_WRAPPERS = [ * * @class ReactReconcileTransaction */ -function ReactReconcileTransaction(forceHTML) { +function ReactReconcileTransaction(useCreateElement) { this.reinitializeTransaction(); // Only server-side rendering really needs this option (see // `ReactServerRendering`), but server-side uses @@ -116,8 +115,7 @@ function ReactReconcileTransaction(forceHTML) { // `ReactTextComponent` checks it in `mountComponent`.` this.renderToStaticMarkup = false; this.reactMountReady = CallbackQueue.getPooled(null); - this.useCreateElement = - !forceHTML && ReactDOMFeatureFlags.useCreateElement; + this.useCreateElement = useCreateElement; } var Mixin = { diff --git a/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js b/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js index 7d84831a688ba..2c6599fb71d70 100644 --- a/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js +++ b/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js @@ -13,25 +13,17 @@ var keyOf = require('keyOf'); -var ReactMount = require('ReactMount'); -var idToNode = {}; -var getID = ReactMount.getID; -var setID = function(el, id) { - ReactMount.setID(el, id); - idToNode[id] = el; -}; -var oldGetNode; -var oldGetFirstReactDOM; - +var EventListener; var EventPluginHub; var EventPluginRegistry; +var React; var ReactBrowserEventEmitter; +var ReactDOMComponentTree; var ReactTestUtils; var TapEventPlugin; -var EventListener; var tapMoveThreshold; -var idCallOrder = []; +var idCallOrder; var recordID = function(id) { idCallOrder.push(id); }; @@ -49,27 +41,19 @@ var ON_TOUCH_TAP_KEY = keyOf({onTouchTap: null}); var ON_CHANGE_KEY = keyOf({onChange: null}); var ON_MOUSE_ENTER_KEY = keyOf({onMouseEnter: null}); - -/** - * Since `ReactBrowserEventEmitter` is fairly well separated from the DOM, we - * can test almost all of `ReactBrowserEventEmitter` without ever rendering - * anything in the DOM. As long as we provide IDs that follow `React's` - * conventional id namespace hierarchy. The only reason why we create these DOM - * nodes is so that when we feed them into `ReactBrowserEventEmitter` (through - * `ReactTestUtils`), the event handlers may receive a DOM node to inspect. - */ -var CHILD = document.createElement('div'); -var PARENT = document.createElement('div'); -var GRANDPARENT = document.createElement('div'); -setID(CHILD, '.0.0.0.0'); -setID(PARENT, '.0.0.0'); -setID(GRANDPARENT, '.0.0'); +var GRANDPARENT; +var PARENT; +var CHILD; function registerSimpleTestHandler() { - EventPluginHub.putListener(getID(CHILD), ON_CLICK_KEY, LISTENER); - var listener = EventPluginHub.getListener(getID(CHILD), ON_CLICK_KEY); + EventPluginHub.putListener(getInternal(CHILD), ON_CLICK_KEY, LISTENER); + var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY); expect(listener).toEqual(LISTENER); - return EventPluginHub.getListener(getID(CHILD), ON_CLICK_KEY); + return EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY); +} + +function getInternal(node) { + return ReactDOMComponentTree.getInstanceFromNode(node); } @@ -77,22 +61,22 @@ describe('ReactBrowserEventEmitter', function() { beforeEach(function() { jest.resetModuleRegistry(); LISTENER.mockClear(); + EventListener = require('EventListener'); EventPluginHub = require('EventPluginHub'); EventPluginRegistry = require('EventPluginRegistry'); - TapEventPlugin = require('TapEventPlugin'); - ReactMount = require('ReactMount'); - EventListener = require('EventListener'); + React = require('React'); ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); + ReactDOMComponentTree = require('ReactDOMComponentTree'); ReactTestUtils = require('ReactTestUtils'); + TapEventPlugin = require('TapEventPlugin'); - oldGetNode = ReactMount.getNode; - oldGetFirstReactDOM = ReactMount.oldGetFirstReactDOM; - ReactMount.getNode = function(id) { - return idToNode[id]; - }; - ReactMount.getFirstReactDOM = function(node) { - return node; - }; + ReactTestUtils.renderIntoDocument( +

GRANDPARENT = c}> +
PARENT = c}> +
CHILD = c} /> +
+
+ ); idCallOrder = []; tapMoveThreshold = TapEventPlugin.tapMoveThreshold; @@ -101,27 +85,22 @@ describe('ReactBrowserEventEmitter', function() { }); }); - afterEach(function() { - ReactMount.getNode = oldGetNode; - ReactMount.getFirstReactDOM = oldGetFirstReactDOM; - }); - it('should store a listener correctly', function() { registerSimpleTestHandler(); - var listener = EventPluginHub.getListener(getID(CHILD), ON_CLICK_KEY); + var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY); expect(listener).toBe(LISTENER); }); it('should retrieve a listener correctly', function() { registerSimpleTestHandler(); - var listener = EventPluginHub.getListener(getID(CHILD), ON_CLICK_KEY); + var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY); expect(listener).toEqual(LISTENER); }); it('should clear all handlers when asked to', function() { registerSimpleTestHandler(); - EventPluginHub.deleteAllListeners(getID(CHILD)); - var listener = EventPluginHub.getListener(getID(CHILD), ON_CLICK_KEY); + EventPluginHub.deleteAllListeners(getInternal(CHILD)); + var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY); expect(listener).toBe(undefined); }); @@ -146,152 +125,152 @@ describe('ReactBrowserEventEmitter', function() { it('should bubble simply', function() { EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_CLICK_KEY, - recordID.bind(null, getID(CHILD)) + recordID.bind(null, getInternal(CHILD)) ); EventPluginHub.putListener( - getID(PARENT), + getInternal(PARENT), ON_CLICK_KEY, - recordID.bind(null, getID(PARENT)) + recordID.bind(null, getInternal(PARENT)) ); EventPluginHub.putListener( - getID(GRANDPARENT), + getInternal(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, getID(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)) ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(getID(CHILD)); - expect(idCallOrder[1]).toBe(getID(PARENT)); - expect(idCallOrder[2]).toBe(getID(GRANDPARENT)); + expect(idCallOrder[0]).toBe(getInternal(CHILD)); + expect(idCallOrder[1]).toBe(getInternal(PARENT)); + expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT)); }); it('should continue bubbling if an error is thrown', function() { EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_CLICK_KEY, - recordID.bind(null, getID(CHILD)) + recordID.bind(null, getInternal(CHILD)) ); EventPluginHub.putListener( - getID(PARENT), + getInternal(PARENT), ON_CLICK_KEY, function() { - recordID(getID(PARENT)); + recordID(getInternal(PARENT)); throw new Error('Handler interrupted'); } ); EventPluginHub.putListener( - getID(GRANDPARENT), + getInternal(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, getID(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)) ); expect(function() { ReactTestUtils.Simulate.click(CHILD); }).toThrow(); expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(getID(CHILD)); - expect(idCallOrder[1]).toBe(getID(PARENT)); - expect(idCallOrder[2]).toBe(getID(GRANDPARENT)); + expect(idCallOrder[0]).toBe(getInternal(CHILD)); + expect(idCallOrder[1]).toBe(getInternal(PARENT)); + expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT)); }); it('should set currentTarget', function() { EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_CLICK_KEY, function(event) { - recordID(getID(CHILD)); + recordID(getInternal(CHILD)); expect(event.currentTarget).toBe(CHILD); } ); EventPluginHub.putListener( - getID(PARENT), + getInternal(PARENT), ON_CLICK_KEY, function(event) { - recordID(getID(PARENT)); + recordID(getInternal(PARENT)); expect(event.currentTarget).toBe(PARENT); } ); EventPluginHub.putListener( - getID(GRANDPARENT), + getInternal(GRANDPARENT), ON_CLICK_KEY, function(event) { - recordID(getID(GRANDPARENT)); + recordID(getInternal(GRANDPARENT)); expect(event.currentTarget).toBe(GRANDPARENT); } ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(getID(CHILD)); - expect(idCallOrder[1]).toBe(getID(PARENT)); - expect(idCallOrder[2]).toBe(getID(GRANDPARENT)); + expect(idCallOrder[0]).toBe(getInternal(CHILD)); + expect(idCallOrder[1]).toBe(getInternal(PARENT)); + expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT)); }); it('should support stopPropagation()', function() { EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_CLICK_KEY, - recordID.bind(null, getID(CHILD)) + recordID.bind(null, getInternal(CHILD)) ); EventPluginHub.putListener( - getID(PARENT), + getInternal(PARENT), ON_CLICK_KEY, - recordIDAndStopPropagation.bind(null, getID(PARENT)) + recordIDAndStopPropagation.bind(null, getInternal(PARENT)) ); EventPluginHub.putListener( - getID(GRANDPARENT), + getInternal(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, getID(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)) ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(2); - expect(idCallOrder[0]).toBe(getID(CHILD)); - expect(idCallOrder[1]).toBe(getID(PARENT)); + expect(idCallOrder[0]).toBe(getInternal(CHILD)); + expect(idCallOrder[1]).toBe(getInternal(PARENT)); }); it('should stop after first dispatch if stopPropagation', function() { EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_CLICK_KEY, - recordIDAndStopPropagation.bind(null, getID(CHILD)) + recordIDAndStopPropagation.bind(null, getInternal(CHILD)) ); EventPluginHub.putListener( - getID(PARENT), + getInternal(PARENT), ON_CLICK_KEY, - recordID.bind(null, getID(PARENT)) + recordID.bind(null, getInternal(PARENT)) ); EventPluginHub.putListener( - getID(GRANDPARENT), + getInternal(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, getID(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)) ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(getID(CHILD)); + expect(idCallOrder[0]).toBe(getInternal(CHILD)); }); it('should not stopPropagation if false is returned', function() { EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_CLICK_KEY, - recordIDAndReturnFalse.bind(null, getID(CHILD)) + recordIDAndReturnFalse.bind(null, getInternal(CHILD)) ); EventPluginHub.putListener( - getID(PARENT), + getInternal(PARENT), ON_CLICK_KEY, - recordID.bind(null, getID(PARENT)) + recordID.bind(null, getInternal(PARENT)) ); EventPluginHub.putListener( - getID(GRANDPARENT), + getInternal(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, getID(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)) ); spyOn(console, 'error'); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(getID(CHILD)); - expect(idCallOrder[1]).toBe(getID(PARENT)); - expect(idCallOrder[2]).toBe(getID(GRANDPARENT)); + expect(idCallOrder[0]).toBe(getInternal(CHILD)); + expect(idCallOrder[1]).toBe(getInternal(PARENT)); + expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT)); expect(console.error.calls.length).toEqual(0); }); @@ -307,15 +286,15 @@ describe('ReactBrowserEventEmitter', function() { it('should invoke handlers that were removed while bubbling', function() { var handleParentClick = jest.genMockFn(); var handleChildClick = function(event) { - EventPluginHub.deleteAllListeners(getID(PARENT)); + EventPluginHub.deleteAllListeners(getInternal(PARENT)); }; EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_CLICK_KEY, handleChildClick ); EventPluginHub.putListener( - getID(PARENT), + getInternal(PARENT), ON_CLICK_KEY, handleParentClick ); @@ -327,13 +306,13 @@ describe('ReactBrowserEventEmitter', function() { var handleParentClick = jest.genMockFn(); var handleChildClick = function(event) { EventPluginHub.putListener( - getID(PARENT), + getInternal(PARENT), ON_CLICK_KEY, handleParentClick ); }; EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_CLICK_KEY, handleChildClick ); @@ -343,20 +322,20 @@ describe('ReactBrowserEventEmitter', function() { it('should have mouse enter simulated by test utils', function() { EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_MOUSE_ENTER_KEY, - recordID.bind(null, getID(CHILD)) + recordID.bind(null, getInternal(CHILD)) ); ReactTestUtils.Simulate.mouseEnter(CHILD); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(getID(CHILD)); + expect(idCallOrder[0]).toBe(getInternal(CHILD)); }); it('should infer onTouchTap from a touchStart/End', function() { EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, getID(CHILD)) + recordID.bind(null, getInternal(CHILD)) ); ReactTestUtils.SimulateNative.touchStart( CHILD, @@ -367,14 +346,14 @@ describe('ReactBrowserEventEmitter', function() { ReactTestUtils.nativeTouchData(0, 0) ); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(getID(CHILD)); + expect(idCallOrder[0]).toBe(getInternal(CHILD)); }); it('should infer onTouchTap from when dragging below threshold', function() { EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, getID(CHILD)) + recordID.bind(null, getInternal(CHILD)) ); ReactTestUtils.SimulateNative.touchStart( CHILD, @@ -385,14 +364,14 @@ describe('ReactBrowserEventEmitter', function() { ReactTestUtils.nativeTouchData(0, tapMoveThreshold - 1) ); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(getID(CHILD)); + expect(idCallOrder[0]).toBe(getInternal(CHILD)); }); it('should not onTouchTap from when dragging beyond threshold', function() { EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, getID(CHILD)) + recordID.bind(null, getInternal(CHILD)) ); ReactTestUtils.SimulateNative.touchStart( CHILD, @@ -447,19 +426,19 @@ describe('ReactBrowserEventEmitter', function() { it('should bubble onTouchTap', function() { EventPluginHub.putListener( - getID(CHILD), + getInternal(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, getID(CHILD)) + recordID.bind(null, getInternal(CHILD)) ); EventPluginHub.putListener( - getID(PARENT), + getInternal(PARENT), ON_TOUCH_TAP_KEY, - recordID.bind(null, getID(PARENT)) + recordID.bind(null, getInternal(PARENT)) ); EventPluginHub.putListener( - getID(GRANDPARENT), + getInternal(GRANDPARENT), ON_TOUCH_TAP_KEY, - recordID.bind(null, getID(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)) ); ReactTestUtils.SimulateNative.touchStart( CHILD, @@ -470,9 +449,9 @@ describe('ReactBrowserEventEmitter', function() { ReactTestUtils.nativeTouchData(0, 0) ); expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(getID(CHILD)); - expect(idCallOrder[1]).toBe(getID(PARENT)); - expect(idCallOrder[2]).toBe(getID(GRANDPARENT)); + expect(idCallOrder[0]).toBe(getInternal(CHILD)); + expect(idCallOrder[1]).toBe(getInternal(PARENT)); + expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT)); }); }); diff --git a/src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js b/src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js new file mode 100644 index 0000000000000..3488447b9cdd2 --- /dev/null +++ b/src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js @@ -0,0 +1,109 @@ +/** + * Copyright 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ + +'use strict'; + +describe('ReactDOMComponentTree', function() { + var React; + var ReactDOM; + var ReactDOMComponentTree; + var ReactDOMServer; + + function renderMarkupIntoDocument(elt) { + var container = document.createElement('div'); + // Force server-rendering path: + container.innerHTML = ReactDOMServer.renderToString(elt); + return ReactDOM.render(elt, container); + } + + beforeEach(function() { + React = require('React'); + ReactDOM = require('ReactDOM'); + ReactDOMComponentTree = require('ReactDOMComponentTree'); + ReactDOMServer = require('ReactDOMServer'); + }); + + it('finds nodes for instances', function() { + // This is a little hard to test directly. But refs rely on it -- so we + // check that we can find a ref at arbitrary points in the tree, even if + // other nodes don't have a ref. + var Component = React.createClass({ + render: function() { + var toRef = this.props.toRef; + return ( +
+

hello

+

+ +

+ goodbye. +
+ ); + }, + }); + + function renderAndGetRef(toRef) { + var inst = renderMarkupIntoDocument(); + return inst.refs.target.nodeName; + } + + expect(renderAndGetRef('div')).toBe('DIV'); + expect(renderAndGetRef('h1')).toBe('H1'); + expect(renderAndGetRef('p')).toBe('P'); + expect(renderAndGetRef('input')).toBe('INPUT'); + }); + + it('finds instances for nodes', function() { + var Component = React.createClass({ + render: function() { + return ( +
+

hello

+

+ +

+ goodbye. +
'}} /> +
+ ); + }, + }); + + function renderAndQuery(sel) { + var root = renderMarkupIntoDocument(
); + return sel ? root.querySelector(sel) : root; + } + + function renderAndGetInstance(sel) { + return ReactDOMComponentTree.getInstanceFromNode(renderAndQuery(sel)); + } + + function renderAndGetClosest(sel) { + return ReactDOMComponentTree.getClosestInstanceFromNode( + renderAndQuery(sel) + ); + } + + expect(renderAndGetInstance(null)._currentElement.type).toBe('section'); + expect(renderAndGetInstance('div')._currentElement.type).toBe('div'); + expect(renderAndGetInstance('h1')._currentElement.type).toBe('h1'); + expect(renderAndGetInstance('p')._currentElement.type).toBe('p'); + expect(renderAndGetInstance('input')._currentElement.type).toBe('input'); + expect(renderAndGetInstance('main')._currentElement.type).toBe('main'); + + // This one's a text component! + expect(renderAndGetInstance('span')._stringText).toBe('goodbye.'); + + expect(renderAndGetClosest('b')._currentElement.type).toBe('main'); + expect(renderAndGetClosest('img')._currentElement.type).toBe('main'); + }); + +}); diff --git a/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js b/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js index 18eb7378a2ee5..2def47ff204fe 100644 --- a/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js +++ b/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js @@ -12,42 +12,16 @@ 'use strict'; describe('ReactDOMIDOperations', function() { - var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); - var ReactMount = require('ReactMount'); var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); - var keyOf = require('keyOf'); - - it('should disallow updating special properties', function() { - spyOn(ReactMount, 'getNode'); - spyOn(DOMPropertyOperations, 'setValueForProperty'); - - expect(function() { - ReactDOMIDOperations.updatePropertyByID( - 'testID', - keyOf({dangerouslySetInnerHTML: null}), - {__html: 'testContent'} - ); - }).toThrow(); - - expect( - ReactMount.getNode.argsForCall[0][0] - ).toBe('testID'); - - expect( - DOMPropertyOperations.setValueForProperty.calls.length - ).toBe(0); - }); it('should update innerHTML and preserve whitespace', function() { var stubNode = document.createElement('div'); - spyOn(ReactMount, 'getNode').andReturn(stubNode); - var html = '\n \t \n testContent \t \n \t'; ReactDOMIDOperations.dangerouslyProcessChildrenUpdates( [{ - parentID: 'testID', + parentInst: {_nativeNode: stubNode}, parentNode: null, type: ReactMultiChildUpdateTypes.SET_MARKUP, markupIndex: null, @@ -58,10 +32,6 @@ describe('ReactDOMIDOperations', function() { [] ); - expect( - ReactMount.getNode.argsForCall[0][0] - ).toBe('testID'); - expect(stubNode.innerHTML).toBe(html); }); }); diff --git a/src/renderers/dom/client/__tests__/ReactEventListener-test.js b/src/renderers/dom/client/__tests__/ReactEventListener-test.js index 42e5dbc55211e..6e53f1a12cd43 100644 --- a/src/renderers/dom/client/__tests__/ReactEventListener-test.js +++ b/src/renderers/dom/client/__tests__/ReactEventListener-test.js @@ -16,10 +16,8 @@ var EVENT_TARGET_PARAM = 1; describe('ReactEventListener', function() { var React; - var ReactDOM; - - var ReactMount; + var ReactDOMComponentTree; var ReactEventListener; var ReactTestUtils; var handleTopLevel; @@ -27,10 +25,8 @@ describe('ReactEventListener', function() { beforeEach(function() { jest.resetModuleRegistry(); React = require('React'); - ReactDOM = require('ReactDOM'); - - ReactMount = require('ReactMount'); + ReactDOMComponentTree = require('ReactDOMComponentTree'); ReactEventListener = require('ReactEventListener'); ReactTestUtils = require('ReactTestUtils'); @@ -40,7 +36,7 @@ describe('ReactEventListener', function() { it('should dispatch events from outside React tree', function() { var otherNode = document.createElement('h1'); - var component = ReactMount.render(
, document.createElement('div')); + var component = ReactDOM.render(
, document.createElement('div')); expect(handleTopLevel.mock.calls.length).toBe(0); ReactEventListener.dispatchEvent( 'topMouseOut', @@ -64,9 +60,9 @@ describe('ReactEventListener', function() { var childControl =
Child
; var parentContainer = document.createElement('div'); var parentControl =
Parent
; - childControl = ReactMount.render(childControl, childContainer); + childControl = ReactDOM.render(childControl, childContainer); parentControl = - ReactMount.render(parentControl, parentContainer); + ReactDOM.render(parentControl, parentContainer); ReactDOM.findDOMNode(parentControl).appendChild(childContainer); var callback = ReactEventListener.dispatchEvent.bind(null, 'test'); @@ -77,9 +73,9 @@ describe('ReactEventListener', function() { var calls = handleTopLevel.mock.calls; expect(calls.length).toBe(2); expect(calls[0][EVENT_TARGET_PARAM]) - .toBe(ReactDOM.findDOMNode(childControl)); + .toBe(ReactDOMComponentTree.getInstanceFromNode(childControl)); expect(calls[1][EVENT_TARGET_PARAM]) - .toBe(ReactDOM.findDOMNode(parentControl)); + .toBe(ReactDOMComponentTree.getInstanceFromNode(parentControl)); }); it('should propagate events two levels down', function() { @@ -89,11 +85,11 @@ describe('ReactEventListener', function() { var parentControl =
Parent
; var grandParentContainer = document.createElement('div'); var grandParentControl =
Parent
; - childControl = ReactMount.render(childControl, childContainer); + childControl = ReactDOM.render(childControl, childContainer); parentControl = - ReactMount.render(parentControl, parentContainer); + ReactDOM.render(parentControl, parentContainer); grandParentControl = - ReactMount.render(grandParentControl, grandParentContainer); + ReactDOM.render(grandParentControl, grandParentContainer); ReactDOM.findDOMNode(parentControl).appendChild(childContainer); ReactDOM.findDOMNode(grandParentControl).appendChild(parentContainer); @@ -105,11 +101,11 @@ describe('ReactEventListener', function() { var calls = handleTopLevel.mock.calls; expect(calls.length).toBe(3); expect(calls[0][EVENT_TARGET_PARAM]) - .toBe(ReactDOM.findDOMNode(childControl)); + .toBe(ReactDOMComponentTree.getInstanceFromNode(childControl)); expect(calls[1][EVENT_TARGET_PARAM]) - .toBe(ReactDOM.findDOMNode(parentControl)); + .toBe(ReactDOMComponentTree.getInstanceFromNode(parentControl)); expect(calls[2][EVENT_TARGET_PARAM]) - .toBe(ReactDOM.findDOMNode(grandParentControl)); + .toBe(ReactDOMComponentTree.getInstanceFromNode(grandParentControl)); }); it('should not get confused by disappearing elements', function() { @@ -117,9 +113,9 @@ describe('ReactEventListener', function() { var childControl =
Child
; var parentContainer = document.createElement('div'); var parentControl =
Parent
; - childControl = ReactMount.render(childControl, childContainer); + childControl = ReactDOM.render(childControl, childContainer); parentControl = - ReactMount.render(parentControl, parentContainer); + ReactDOM.render(parentControl, parentContainer); ReactDOM.findDOMNode(parentControl).appendChild(childContainer); // ReactBrowserEventEmitter.handleTopLevel might remove the @@ -130,7 +126,7 @@ describe('ReactEventListener', function() { handleTopLevel.mockImplementation( function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { if (topLevelTarget === childNode) { - ReactMount.unmountComponentAtNode(childContainer); + ReactDOM.unmountComponentAtNode(childContainer); } } ); @@ -142,19 +138,20 @@ describe('ReactEventListener', function() { var calls = handleTopLevel.mock.calls; expect(calls.length).toBe(2); - expect(calls[0][EVENT_TARGET_PARAM]).toBe(childNode); + expect(calls[0][EVENT_TARGET_PARAM]) + .toBe(ReactDOMComponentTree.getInstanceFromNode(childNode)); expect(calls[1][EVENT_TARGET_PARAM]) - .toBe(ReactDOM.findDOMNode(parentControl)); + .toBe(ReactDOMComponentTree.getInstanceFromNode(parentControl)); }); it('should batch between handlers from different roots', function() { var childContainer = document.createElement('div'); var parentContainer = document.createElement('div'); - var childControl = ReactMount.render( + var childControl = ReactDOM.render(
Child
, childContainer ); - var parentControl = ReactMount.render( + var parentControl = ReactDOM.render(
Parent
, parentContainer ); @@ -165,7 +162,7 @@ describe('ReactEventListener', function() { var childNode = ReactDOM.findDOMNode(childControl); handleTopLevel.mockImplementation( function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { - ReactMount.render( + ReactDOM.render(
{topLevelTarget === childNode ? '1' : '2'}
, childContainer ); @@ -210,6 +207,6 @@ describe('ReactEventListener', function() { var calls = handleTopLevel.mock.calls; expect(calls.length).toBe(1); expect(calls[0][EVENT_TARGET_PARAM]) - .toBe(ReactDOM.findDOMNode(instance.getInner())); + .toBe(ReactDOMComponentTree.getInstanceFromNode(instance.getInner())); }); }); diff --git a/src/renderers/dom/client/__tests__/ReactMount-test.js b/src/renderers/dom/client/__tests__/ReactMount-test.js index 6328a06092102..bbeb67620b57c 100644 --- a/src/renderers/dom/client/__tests__/ReactMount-test.js +++ b/src/renderers/dom/client/__tests__/ReactMount-test.js @@ -196,31 +196,6 @@ describe('ReactMount', function() { }); } - it('warns when using two copies of React before throwing', function() { - jest.resetModuleRegistry(); - var RD1 = require('ReactDOM'); - jest.resetModuleRegistry(); - var RD2 = require('ReactDOM'); - - var X = React.createClass({ - render: function() { - return
; - }, - }); - - var container = document.createElement('div'); - spyOn(console, 'error'); - var component = RD1.render(, container); - expect(console.error.argsForCall.length).toBe(0); - - // This fails but logs a warning first - expect(function() { - RD2.findDOMNode(component); - }).toThrow(); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain('two copies of React'); - }); - it('should warn if render removes React-rendered children', function() { var container = document.createElement('container'); var Component = React.createClass({ @@ -243,61 +218,6 @@ describe('ReactMount', function() { ); }); - it('should not crash in node cache when unmounting', function() { - var Component = React.createClass({ - render: function() { - // Add refs to some nodes so that they get traversed and cached - return ( -
-
b
- {this.props.showC &&
c
} -
- ); - }, - }); - - var container = document.createElement('container'); - - ReactDOM.render(
, container); - - // Right now, A and B are in the cache. When we add C, it won't get added to - // the cache (assuming markup-string mode). - ReactDOM.render(
, container); - - // Remove A, B, and C. Unmounting C shouldn't cause B to get recached. - ReactDOM.render(
, container); - - // Add them back -- this shouldn't cause a cached node collision. - ReactDOM.render(
, container); - - ReactDOM.unmountComponentAtNode(container); - }); - - it('should not crash in node cache when unmounting, case 2', function() { - var A = React.createClass({ - render: function() { - return
{this.props.innerKey}; - }, - }); - var Component = React.createClass({ - render: function() { - return ( - - {this.props.step === 1 && } - {this.props.step === 1 && } - - ); - }, - }); - - var container = document.createElement('container'); - - ReactDOM.render(, container); - ReactDOM.render(, container); - ReactDOM.render(, container); - ReactMount.getID(container.querySelector('a')); - }); - it('passes the correct callback context', function() { var container = document.createElement('div'); var calls = 0; diff --git a/src/renderers/dom/client/__tests__/ReactRenderDocument-test.js b/src/renderers/dom/client/__tests__/ReactRenderDocument-test.js index 2c9d6427244b3..02230bf46b097 100644 --- a/src/renderers/dom/client/__tests__/ReactRenderDocument-test.js +++ b/src/renderers/dom/client/__tests__/ReactRenderDocument-test.js @@ -17,8 +17,6 @@ jest var React; var ReactDOM; var ReactDOMServer; -var ReactInstanceMap; -var ReactMount; var getTestDocument; @@ -46,14 +44,12 @@ describe('rendering React components at document', function() { React = require('React'); ReactDOM = require('ReactDOM'); ReactDOMServer = require('ReactDOMServer'); - ReactInstanceMap = require('ReactInstanceMap'); - ReactMount = require('ReactMount'); getTestDocument = require('getTestDocument'); testDocument = getTestDocument(); }); - it('should be able to get root component id for document node', function() { + it('should be able to adopt server markup', function() { expect(testDocument).not.toBeUndefined(); var Root = React.createClass({ @@ -64,22 +60,24 @@ describe('rendering React components at document', function() { Hello World - Hello world + {'Hello ' + this.props.hello} ); }, }); - var markup = ReactDOMServer.renderToString(); + var markup = ReactDOMServer.renderToString(); testDocument = getTestDocument(markup); - var component = ReactDOM.render(, testDocument); + var body = testDocument.body; + + ReactDOM.render(, testDocument); expect(testDocument.body.innerHTML).toBe('Hello world'); - // TODO: This is a bad test. I have no idea what this is testing. - // Node IDs is an implementation detail and not part of the public API. - var componentID = ReactMount.getReactRootID(testDocument); - expect(componentID).toBe(ReactInstanceMap.get(component)._rootNodeID); + ReactDOM.render(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello moon'); + + expect(body).toBe(testDocument.body); }); it('should not be able to unmount component from document node', function() { diff --git a/src/renderers/dom/client/eventPlugins/BeforeInputEventPlugin.js b/src/renderers/dom/client/eventPlugins/BeforeInputEventPlugin.js index fdcf0bf8deb1a..278104917f59c 100644 --- a/src/renderers/dom/client/eventPlugins/BeforeInputEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/BeforeInputEventPlugin.js @@ -227,16 +227,11 @@ function getDataFromCustomEvent(nativeEvent) { var currentComposition = null; /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. * @return {?object} A SyntheticCompositionEvent. */ function extractCompositionEvent( topLevelType, - topLevelTarget, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ) { @@ -261,7 +256,8 @@ function extractCompositionEvent( // The current composition is stored statically and must not be // overwritten while composition continues. if (!currentComposition && eventType === eventTypes.compositionStart) { - currentComposition = FallbackCompositionState.getPooled(topLevelTarget); + currentComposition = + FallbackCompositionState.getPooled(nativeEventTarget); } else if (eventType === eventTypes.compositionEnd) { if (currentComposition) { fallbackData = currentComposition.getData(); @@ -271,7 +267,7 @@ function extractCompositionEvent( var event = SyntheticCompositionEvent.getPooled( eventType, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ); @@ -403,16 +399,11 @@ function getFallbackBeforeInputChars(topLevelType, nativeEvent) { * Extract a SyntheticInputEvent for `beforeInput`, based on either native * `textInput` or fallback behavior. * - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. * @return {?object} A SyntheticInputEvent. */ function extractBeforeInputEvent( topLevelType, - topLevelTarget, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ) { @@ -432,7 +423,7 @@ function extractBeforeInputEvent( var event = SyntheticInputEvent.getPooled( eventTypes.beforeInput, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ); @@ -464,33 +455,22 @@ var BeforeInputEventPlugin = { eventTypes: eventTypes, - /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} - */ extractEvents: function( topLevelType, - topLevelTarget, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ) { return [ extractCompositionEvent( topLevelType, - topLevelTarget, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ), extractBeforeInputEvent( topLevelType, - topLevelTarget, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ), diff --git a/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js b/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js index f990004abdbbc..c78827f70e93d 100644 --- a/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js @@ -15,6 +15,7 @@ var EventConstants = require('EventConstants'); var EventPluginHub = require('EventPluginHub'); var EventPropagators = require('EventPropagators'); var ExecutionEnvironment = require('ExecutionEnvironment'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactUpdates = require('ReactUpdates'); var SyntheticEvent = require('SyntheticEvent'); @@ -48,7 +49,7 @@ var eventTypes = { * For IE shims */ var activeElement = null; -var activeElementID = null; +var activeElementInst = null; var activeElementValue = null; var activeElementValueProp = null; @@ -74,7 +75,7 @@ if (ExecutionEnvironment.canUseDOM) { function manualDispatchChangeEvent(nativeEvent) { var event = SyntheticEvent.getPooled( eventTypes.change, - activeElementID, + activeElementInst, nativeEvent, getEventTarget(nativeEvent) ); @@ -99,9 +100,9 @@ function runEventInBatch(event) { EventPluginHub.processEventQueue(false); } -function startWatchingForChangeEventIE8(target, targetID) { +function startWatchingForChangeEventIE8(target, targetInst) { activeElement = target; - activeElementID = targetID; + activeElementInst = targetInst; activeElement.attachEvent('onchange', manualDispatchChangeEvent); } @@ -111,28 +112,27 @@ function stopWatchingForChangeEventIE8() { } activeElement.detachEvent('onchange', manualDispatchChangeEvent); activeElement = null; - activeElementID = null; + activeElementInst = null; } -function getTargetIDForChangeEvent( +function getTargetInstForChangeEvent( topLevelType, - topLevelTarget, - topLevelTargetID + targetInst ) { if (topLevelType === topLevelTypes.topChange) { - return topLevelTargetID; + return targetInst; } } function handleEventsForChangeEventIE8( topLevelType, - topLevelTarget, - topLevelTargetID + target, + targetInst ) { if (topLevelType === topLevelTypes.topFocus) { // stopWatching() should be a noop here but we call it just in case we // missed a blur event somehow. stopWatchingForChangeEventIE8(); - startWatchingForChangeEventIE8(topLevelTarget, topLevelTargetID); + startWatchingForChangeEventIE8(target, targetInst); } else if (topLevelType === topLevelTypes.topBlur) { stopWatchingForChangeEventIE8(); } @@ -173,9 +173,9 @@ var newValueProp = { * and override the value property so that we can distinguish user events from * value changes in JS. */ -function startWatchingForValueChange(target, targetID) { +function startWatchingForValueChange(target, targetInst) { activeElement = target; - activeElementID = targetID; + activeElementInst = targetInst; activeElementValue = target.value; activeElementValueProp = Object.getOwnPropertyDescriptor( target.constructor.prototype, @@ -211,7 +211,7 @@ function stopWatchingForValueChange() { } activeElement = null; - activeElementID = null; + activeElementInst = null; activeElementValue = null; activeElementValueProp = null; } @@ -236,22 +236,21 @@ function handlePropertyChange(nativeEvent) { /** * If a `change` event should be fired, returns the target's ID. */ -function getTargetIDForInputEvent( +function getTargetInstForInputEvent( topLevelType, - topLevelTarget, - topLevelTargetID + targetInst ) { if (topLevelType === topLevelTypes.topInput) { // In modern browsers (i.e., not IE8 or IE9), the input event is exactly // what we want so fall through here and trigger an abstract event - return topLevelTargetID; + return targetInst; } } function handleEventsForInputEventIE( topLevelType, - topLevelTarget, - topLevelTargetID + target, + targetInst ) { if (topLevelType === topLevelTypes.topFocus) { // In IE8, we can capture almost all .value changes by adding a @@ -268,17 +267,16 @@ function handleEventsForInputEventIE( // stopWatching() should be a noop here but we call it just in case we // missed a blur event somehow. stopWatchingForValueChange(); - startWatchingForValueChange(topLevelTarget, topLevelTargetID); + startWatchingForValueChange(target, targetInst); } else if (topLevelType === topLevelTypes.topBlur) { stopWatchingForValueChange(); } } // For IE8 and IE9. -function getTargetIDForInputEventIE( +function getTargetInstForInputEventIE( topLevelType, - topLevelTarget, - topLevelTargetID + targetInst ) { if (topLevelType === topLevelTypes.topSelectionChange || topLevelType === topLevelTypes.topKeyUp || @@ -295,7 +293,7 @@ function getTargetIDForInputEventIE( // fire selectionchange normally. if (activeElement && activeElement.value !== activeElementValue) { activeElementValue = activeElement.value; - return activeElementID; + return activeElementInst; } } } @@ -314,13 +312,12 @@ function shouldUseClickEvent(elem) { ); } -function getTargetIDForClickEvent( +function getTargetInstForClickEvent( topLevelType, - topLevelTarget, - topLevelTargetID + targetInst ) { if (topLevelType === topLevelTypes.topClick) { - return topLevelTargetID; + return targetInst; } } @@ -338,49 +335,39 @@ var ChangeEventPlugin = { eventTypes: eventTypes, - /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} - */ extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent, - nativeEventTarget) { - - var getTargetIDFunc, handleEventFunc; - if (shouldUseChangeEvent(topLevelTarget)) { + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget + ) { + var targetNode = targetInst ? + ReactDOMComponentTree.getNodeFromInstance(targetInst) : window; + + var getTargetInstFunc, handleEventFunc; + if (shouldUseChangeEvent(targetNode)) { if (doesChangeEventBubble) { - getTargetIDFunc = getTargetIDForChangeEvent; + getTargetInstFunc = getTargetInstForChangeEvent; } else { handleEventFunc = handleEventsForChangeEventIE8; } - } else if (isTextInputElement(topLevelTarget)) { + } else if (isTextInputElement(targetNode)) { if (isInputEventSupported) { - getTargetIDFunc = getTargetIDForInputEvent; + getTargetInstFunc = getTargetInstForInputEvent; } else { - getTargetIDFunc = getTargetIDForInputEventIE; + getTargetInstFunc = getTargetInstForInputEventIE; handleEventFunc = handleEventsForInputEventIE; } - } else if (shouldUseClickEvent(topLevelTarget)) { - getTargetIDFunc = getTargetIDForClickEvent; + } else if (shouldUseClickEvent(targetNode)) { + getTargetInstFunc = getTargetInstForClickEvent; } - if (getTargetIDFunc) { - var targetID = getTargetIDFunc( - topLevelType, - topLevelTarget, - topLevelTargetID - ); - if (targetID) { + if (getTargetInstFunc) { + var inst = getTargetInstFunc(topLevelType, targetInst); + if (inst) { var event = SyntheticEvent.getPooled( eventTypes.change, - targetID, + inst, nativeEvent, nativeEventTarget ); @@ -393,8 +380,8 @@ var ChangeEventPlugin = { if (handleEventFunc) { handleEventFunc( topLevelType, - topLevelTarget, - topLevelTargetID + targetNode, + targetInst ); } }, diff --git a/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js b/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js index 6eccf1779e0f9..27a9afce51e2c 100644 --- a/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js @@ -14,13 +14,12 @@ var EventConstants = require('EventConstants'); var EventPropagators = require('EventPropagators'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var SyntheticMouseEvent = require('SyntheticMouseEvent'); -var ReactMount = require('ReactMount'); var keyOf = require('keyOf'); var topLevelTypes = EventConstants.topLevelTypes; -var getFirstReactDOM = ReactMount.getFirstReactDOM; var eventTypes = { mouseEnter: { @@ -39,8 +38,6 @@ var eventTypes = { }, }; -var extractedEvents = [null, null]; - var EnterLeaveEventPlugin = { eventTypes: eventTypes, @@ -51,20 +48,13 @@ var EnterLeaveEventPlugin = { * we do not extract duplicate events. However, moving the mouse into the * browser from outside will not fire a `mouseout` event. In this case, we use * the `mouseover` top-level event. - * - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} */ extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent, - nativeEventTarget) { + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget + ) { if (topLevelType === topLevelTypes.topMouseOver && (nativeEvent.relatedTarget || nativeEvent.fromElement)) { return null; @@ -76,12 +66,12 @@ var EnterLeaveEventPlugin = { } var win; - if (topLevelTarget.window === topLevelTarget) { - // `topLevelTarget` is probably a window object. - win = topLevelTarget; + if (nativeEventTarget.window === nativeEventTarget) { + // `nativeEventTarget` is probably a window object. + win = nativeEventTarget; } else { // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8. - var doc = topLevelTarget.ownerDocument; + var doc = nativeEventTarget.ownerDocument; if (doc) { win = doc.defaultView || doc.parentWindow; } else { @@ -91,22 +81,15 @@ var EnterLeaveEventPlugin = { var from; var to; - var fromID = ''; - var toID = ''; if (topLevelType === topLevelTypes.topMouseOut) { - from = topLevelTarget; - fromID = topLevelTargetID; - to = getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement); - if (to) { - toID = ReactMount.getID(to); - } else { - to = win; - } - to = to || win; + from = targetInst; + var related = nativeEvent.relatedTarget || nativeEvent.toElement; + to = related ? + ReactDOMComponentTree.getClosestInstanceFromNode(related) : null; } else { - from = win; - to = topLevelTarget; - toID = topLevelTargetID; + // Moving to a node from outside the window. + from = null; + to = targetInst; } if (from === to) { @@ -114,32 +97,34 @@ var EnterLeaveEventPlugin = { return null; } + var fromNode = + from == null ? win : ReactDOMComponentTree.getNodeFromInstance(from); + var toNode = + to == null ? win : ReactDOMComponentTree.getNodeFromInstance(to); + var leave = SyntheticMouseEvent.getPooled( eventTypes.mouseLeave, - fromID, + from, nativeEvent, nativeEventTarget ); leave.type = 'mouseleave'; - leave.target = from; - leave.relatedTarget = to; + leave.target = fromNode; + leave.relatedTarget = toNode; var enter = SyntheticMouseEvent.getPooled( eventTypes.mouseEnter, - toID, + to, nativeEvent, nativeEventTarget ); enter.type = 'mouseenter'; - enter.target = to; - enter.relatedTarget = from; - - EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID); + enter.target = toNode; + enter.relatedTarget = fromNode; - extractedEvents[0] = leave; - extractedEvents[1] = enter; + EventPropagators.accumulateEnterLeaveDispatches(leave, enter, from, to); - return extractedEvents; + return [leave, enter]; }, }; diff --git a/src/renderers/dom/client/eventPlugins/SelectEventPlugin.js b/src/renderers/dom/client/eventPlugins/SelectEventPlugin.js index bda881154a5a3..2acf12357075a 100644 --- a/src/renderers/dom/client/eventPlugins/SelectEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/SelectEventPlugin.js @@ -14,6 +14,7 @@ var EventConstants = require('EventConstants'); var EventPropagators = require('EventPropagators'); var ExecutionEnvironment = require('ExecutionEnvironment'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactInputSelection = require('ReactInputSelection'); var SyntheticEvent = require('SyntheticEvent'); @@ -49,12 +50,12 @@ var eventTypes = { }; var activeElement = null; -var activeElementID = null; +var activeElementInst = null; var lastSelection = null; var mouseDown = false; // Track whether a listener exists for this plugin. If none exist, we do -// not extract events. +// not extract events. See #3639. var hasListener = false; var ON_SELECT_KEY = keyOf({onSelect: null}); @@ -117,7 +118,7 @@ function constructSelectEvent(nativeEvent, nativeEventTarget) { var syntheticEvent = SyntheticEvent.getPooled( eventTypes.select, - activeElementID, + activeElementInst, nativeEvent, nativeEventTarget ); @@ -151,37 +152,32 @@ var SelectEventPlugin = { eventTypes: eventTypes, - /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} - */ extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent, - nativeEventTarget) { + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget + ) { if (!hasListener) { return null; } + var targetNode = targetInst ? + ReactDOMComponentTree.getNodeFromInstance(targetInst) : window; + switch (topLevelType) { // Track the input node that has focus. case topLevelTypes.topFocus: - if (isTextInputElement(topLevelTarget) || - topLevelTarget.contentEditable === 'true') { - activeElement = topLevelTarget; - activeElementID = topLevelTargetID; + if (isTextInputElement(targetNode) || + targetNode.contentEditable === 'true') { + activeElement = targetNode; + activeElementInst = targetInst; lastSelection = null; } break; case topLevelTypes.topBlur: activeElement = null; - activeElementID = null; + activeElementInst = null; lastSelection = null; break; @@ -217,7 +213,7 @@ var SelectEventPlugin = { return null; }, - didPutListener: function(id, registrationName, listener) { + didPutListener: function(inst, registrationName, listener) { if (registrationName === ON_SELECT_KEY) { hasListener = true; } diff --git a/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js b/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js index 1009a12fa2809..2ac7117b0941f 100644 --- a/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js @@ -14,7 +14,7 @@ var EventConstants = require('EventConstants'); var EventListener = require('EventListener'); var EventPropagators = require('EventPropagators'); -var ReactMount = require('ReactMount'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var SyntheticClipboardEvent = require('SyntheticClipboardEvent'); var SyntheticEvent = require('SyntheticEvent'); var SyntheticFocusEvent = require('SyntheticFocusEvent'); @@ -457,20 +457,11 @@ var SimpleEventPlugin = { eventTypes: eventTypes, - /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} - */ extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent, - nativeEventTarget + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget ) { var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]; if (!dispatchConfig) { @@ -511,7 +502,7 @@ var SimpleEventPlugin = { EventConstructor = SyntheticEvent; break; case topLevelTypes.topKeyPress: - // FireFox creates a keypress event for function keys too. This removes + // Firefox creates a keypress event for function keys too. This removes // the unwanted keypress events. Enter is however both printable and // non-printable. One would expect Tab to be as well (but it isn't). if (getEventCharCode(nativeEvent) === 0) { @@ -577,7 +568,7 @@ var SimpleEventPlugin = { ); var event = EventConstructor.getPooled( dispatchConfig, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ); @@ -585,13 +576,14 @@ var SimpleEventPlugin = { return event; }, - didPutListener: function(id, registrationName, listener) { + didPutListener: function(inst, registrationName, listener) { // Mobile Safari does not fire properly bubble click events on // non-interactive elements, which means delegated click listeners do not // fire. The workaround for this bug involves attaching an empty click // listener on the target node. if (registrationName === ON_CLICK_KEY) { - var node = ReactMount.getNode(id); + var id = inst._rootNodeID; + var node = ReactDOMComponentTree.getNodeFromInstance(inst); if (!onClickListeners[id]) { onClickListeners[id] = EventListener.listen( node, @@ -602,8 +594,9 @@ var SimpleEventPlugin = { } }, - willDeleteListener: function(id, registrationName) { + willDeleteListener: function(inst, registrationName) { if (registrationName === ON_CLICK_KEY) { + var id = inst._rootNodeID; onClickListeners[id].remove(); delete onClickListeners[id]; } diff --git a/src/renderers/dom/client/eventPlugins/TapEventPlugin.js b/src/renderers/dom/client/eventPlugins/TapEventPlugin.js index 794d5ee8ba7c6..18140b202538b 100644 --- a/src/renderers/dom/client/eventPlugins/TapEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/TapEventPlugin.js @@ -89,20 +89,12 @@ var TapEventPlugin = { eventTypes: eventTypes, - /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} - */ extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent, - nativeEventTarget) { + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget + ) { if (!isStartish(topLevelType) && !isEndish(topLevelType)) { return null; } @@ -122,7 +114,7 @@ var TapEventPlugin = { if (isEndish(topLevelType) && distance < tapMoveThreshold) { event = SyntheticUIEvent.getPooled( eventTypes.touchTap, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ); diff --git a/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js b/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js index 5d3afd3ee803b..21d588f7f4bb9 100644 --- a/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js +++ b/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js @@ -15,7 +15,7 @@ var EnterLeaveEventPlugin; var EventConstants; var React; var ReactDOM; -var ReactMount; +var ReactDOMComponentTree; var topLevelTypes; @@ -27,7 +27,7 @@ describe('EnterLeaveEventPlugin', function() { EventConstants = require('EventConstants'); React = require('React'); ReactDOM = require('ReactDOM'); - ReactMount = require('ReactMount'); + ReactDOMComponentTree = require('ReactDOMComponentTree'); topLevelTypes = EventConstants.topLevelTypes; }); @@ -48,8 +48,7 @@ describe('EnterLeaveEventPlugin', function() { var extracted = EnterLeaveEventPlugin.extractEvents( topLevelTypes.topMouseOver, - div, - ReactMount.getID(div), + ReactDOMComponentTree.getInstanceFromNode(div), {target: div}, div ); diff --git a/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js b/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js index e9b8be7347760..ef845a0670478 100644 --- a/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js +++ b/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js @@ -14,7 +14,7 @@ var EventConstants; var React; var ReactDOM; -var ReactMount; +var ReactDOMComponentTree; var ReactTestUtils; var SelectEventPlugin; @@ -24,8 +24,7 @@ describe('SelectEventPlugin', function() { function extract(node, topLevelEvent) { return SelectEventPlugin.extractEvents( topLevelEvent, - node, - ReactMount.getID(node), + ReactDOMComponentTree.getInstanceFromNode(node), {target: node}, node ); @@ -35,7 +34,7 @@ describe('SelectEventPlugin', function() { EventConstants = require('EventConstants'); React = require('React'); ReactDOM = require('ReactDOM'); - ReactMount = require('ReactMount'); + ReactDOMComponentTree = require('ReactDOMComponentTree'); ReactTestUtils = require('ReactTestUtils'); SelectEventPlugin = require('SelectEventPlugin'); diff --git a/src/renderers/dom/client/findDOMNode.js b/src/renderers/dom/client/findDOMNode.js index 27e8c40ca82c5..f91ae99fb002f 100644 --- a/src/renderers/dom/client/findDOMNode.js +++ b/src/renderers/dom/client/findDOMNode.js @@ -13,7 +13,7 @@ 'use strict'; var ReactCurrentOwner = require('ReactCurrentOwner'); -var ReactDOMComponent = require('ReactDOMComponent'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactInstanceMap = require('ReactInstanceMap'); var ReactNodeTypes = require('ReactNodeTypes'); @@ -66,7 +66,7 @@ function findDOMNode(componentOrElement) { var inst = ReactInstanceMap.get(componentOrElement); if (inst) { inst = getNativeComponentFromComposite(inst); - return inst ? ReactDOMComponent.getNodeFromInstance(inst) : null; + return inst ? ReactDOMComponentTree.getNodeFromInstance(inst) : null; } if (typeof componentOrElement.render === 'function') { diff --git a/src/renderers/dom/client/syntheticEvents/SyntheticEvent.js b/src/renderers/dom/client/syntheticEvents/SyntheticEvent.js index fe76ae7c03cce..eff39b69f8307 100644 --- a/src/renderers/dom/client/syntheticEvents/SyntheticEvent.js +++ b/src/renderers/dom/client/syntheticEvents/SyntheticEvent.js @@ -50,12 +50,13 @@ var EventInterface = { * DOM interface; custom application-specific events can also subclass this. * * @param {object} dispatchConfig Configuration used to dispatch this event. - * @param {string} dispatchMarker Marker identifying the event target. + * @param {*} targetInst Marker identifying the event target. * @param {object} nativeEvent Native browser event. */ -function SyntheticEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) { +function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) { this.dispatchConfig = dispatchConfig; - this.dispatchMarker = dispatchMarker; + this._targetInst = targetInst; + this.nativeEvent = nativeEvent; this.target = nativeEventTarget; this.currentTarget = nativeEventTarget; @@ -158,7 +159,7 @@ assign(SyntheticEvent.prototype, { this[propName] = null; } this.dispatchConfig = null; - this.dispatchMarker = null; + this._targetInst = null; this.nativeEvent = null; }, diff --git a/src/renderers/dom/client/wrappers/AutoFocusUtils.js b/src/renderers/dom/client/wrappers/AutoFocusUtils.js index 0cc675d3e4d9b..be865e4a82b75 100644 --- a/src/renderers/dom/client/wrappers/AutoFocusUtils.js +++ b/src/renderers/dom/client/wrappers/AutoFocusUtils.js @@ -12,13 +12,13 @@ 'use strict'; -var ReactMount = require('ReactMount'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var focusNode = require('focusNode'); var AutoFocusUtils = { focusDOMComponent: function() { - focusNode(ReactMount.getNode(this._rootNodeID)); + focusNode(ReactDOMComponentTree.getNodeFromInstance(this)); }, }; diff --git a/src/renderers/dom/client/wrappers/ReactDOMInput.js b/src/renderers/dom/client/wrappers/ReactDOMInput.js index 0633add1fab3a..682a34961a63c 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMInput.js +++ b/src/renderers/dom/client/wrappers/ReactDOMInput.js @@ -11,9 +11,9 @@ 'use strict'; -var ReactDOMIDOperations = require('ReactDOMIDOperations'); +var DOMPropertyOperations = require('DOMPropertyOperations'); var LinkedValueUtils = require('LinkedValueUtils'); -var ReactMount = require('ReactMount'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactUpdates = require('ReactUpdates'); var assign = require('Object.assign'); @@ -131,8 +131,8 @@ var ReactDOMInput = { // TODO: Shouldn't this be getChecked(props)? var checked = props.checked; if (checked != null) { - ReactDOMIDOperations.updatePropertyByID( - inst._rootNodeID, + DOMPropertyOperations.setValueForProperty( + ReactDOMComponentTree.getNodeFromInstance(inst), 'checked', checked || false ); @@ -142,8 +142,8 @@ var ReactDOMInput = { if (value != null) { // Cast `value` to a string to ensure the value is set correctly. While // browsers typically do this as necessary, jsdom doesn't. - ReactDOMIDOperations.updatePropertyByID( - inst._rootNodeID, + DOMPropertyOperations.setValueForProperty( + ReactDOMComponentTree.getNodeFromInstance(inst), 'value', '' + value ); @@ -163,7 +163,7 @@ function _handleChange(event) { var name = props.name; if (props.type === 'radio' && name != null) { - var rootNode = ReactMount.getNode(this._rootNodeID); + var rootNode = ReactDOMComponentTree.getNodeFromInstance(this); var queryRoot = rootNode; while (queryRoot.parentNode) { @@ -188,19 +188,13 @@ function _handleChange(event) { // This will throw if radio buttons rendered by different copies of React // and the same name are rendered into the same form (same as #1939). // That's probably okay; we don't support it just as we don't support - // mixing React with non-React. - var otherID = ReactMount.getID(otherNode); + // mixing React radio buttons with non-React ones. + var otherInstance = ReactDOMComponentTree.getInstanceFromNode(otherNode); invariant( - otherID, + otherInstance, 'ReactDOMInput: Mixing React and non-React radio inputs with the ' + 'same `name` is not supported.' ); - var otherInstance = instancesByReactID[otherID]; - invariant( - otherInstance, - 'ReactDOMInput: Unknown radio button ID %s.', - otherID - ); // If this is a controlled radio button group, forcing the input that // was previously checked to update will cause it to be come re-checked // as appropriate. diff --git a/src/renderers/dom/client/wrappers/ReactDOMSelect.js b/src/renderers/dom/client/wrappers/ReactDOMSelect.js index c8d12eb969383..bf36fc19c245c 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMSelect.js +++ b/src/renderers/dom/client/wrappers/ReactDOMSelect.js @@ -12,7 +12,7 @@ 'use strict'; var LinkedValueUtils = require('LinkedValueUtils'); -var ReactMount = require('ReactMount'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactUpdates = require('ReactUpdates'); var assign = require('Object.assign'); @@ -112,7 +112,7 @@ function checkSelectPropTypes(inst, props) { */ function updateOptions(inst, multiple, propValue) { var selectedValue, i; - var options = ReactMount.getNode(inst._rootNodeID).options; + var options = ReactDOMComponentTree.getNodeFromInstance(inst).options; if (multiple) { selectedValue = {}; diff --git a/src/renderers/dom/client/wrappers/ReactDOMTextarea.js b/src/renderers/dom/client/wrappers/ReactDOMTextarea.js index 5ed8dbae3281f..b206c57d6c1d0 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMTextarea.js +++ b/src/renderers/dom/client/wrappers/ReactDOMTextarea.js @@ -11,8 +11,9 @@ 'use strict'; +var DOMPropertyOperations = require('DOMPropertyOperations'); var LinkedValueUtils = require('LinkedValueUtils'); -var ReactDOMIDOperations = require('ReactDOMIDOperations'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactUpdates = require('ReactUpdates'); var assign = require('Object.assign'); @@ -144,8 +145,8 @@ var ReactDOMTextarea = { if (value != null) { // Cast `value` to a string to ensure the value is set correctly. While // browsers typically do this as necessary, jsdom doesn't. - ReactDOMIDOperations.updatePropertyByID( - inst._rootNodeID, + DOMPropertyOperations.setValueForProperty( + ReactDOMComponentTree.getNodeFromInstance(inst), 'value', '' + value ); diff --git a/src/renderers/dom/shared/CSSPropertyOperations.js b/src/renderers/dom/shared/CSSPropertyOperations.js index af79e74f8ca37..178de629d8f1f 100644 --- a/src/renderers/dom/shared/CSSPropertyOperations.js +++ b/src/renderers/dom/shared/CSSPropertyOperations.js @@ -140,7 +140,8 @@ var CSSPropertyOperations = { } if (styleValue != null) { serialized += processStyleName(styleName) + ':'; - serialized += dangerousStyleValue(styleName, styleValue, component) + ';'; + serialized += + dangerousStyleValue(styleName, styleValue, component) + ';'; } } return serialized || null; @@ -153,7 +154,7 @@ var CSSPropertyOperations = { * @param {DOMElement} node * @param {object} styles */ - setValueForStyles: function(node, styles) { + setValueForStyles: function(node, styles, component) { var style = node.style; for (var styleName in styles) { if (!styles.hasOwnProperty(styleName)) { @@ -162,7 +163,11 @@ var CSSPropertyOperations = { if (__DEV__) { warnValidStyle(styleName, styles[styleName]); } - var styleValue = dangerousStyleValue(styleName, styles[styleName]); + var styleValue = dangerousStyleValue( + styleName, + styles[styleName], + component + ); if (styleName === 'float') { styleName = styleFloatAccessor; } diff --git a/src/renderers/dom/shared/ReactComponentBrowserEnvironment.js b/src/renderers/dom/shared/ReactComponentBrowserEnvironment.js index 3f31a52a2174c..d10cca3054a8c 100644 --- a/src/renderers/dom/shared/ReactComponentBrowserEnvironment.js +++ b/src/renderers/dom/shared/ReactComponentBrowserEnvironment.js @@ -13,7 +13,6 @@ var DOMChildrenOperations = require('DOMChildrenOperations'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); -var ReactMount = require('ReactMount'); var ReactPerf = require('ReactPerf'); /** @@ -37,7 +36,6 @@ var ReactComponentBrowserEnvironment = { * @private */ unmountIDFromEnvironment: function(rootNodeID) { - ReactMount.purgeID(rootNodeID); }, }; diff --git a/src/renderers/dom/shared/ReactDOMComponent.js b/src/renderers/dom/shared/ReactDOMComponent.js index 75f404a1bd9dc..d363c242de984 100644 --- a/src/renderers/dom/shared/ReactDOMComponent.js +++ b/src/renderers/dom/shared/ReactDOMComponent.js @@ -27,11 +27,12 @@ var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); var ReactComponentBrowserEnvironment = require('ReactComponentBrowserEnvironment'); var ReactDOMButton = require('ReactDOMButton'); +var ReactDOMComponentFlags = require('ReactDOMComponentFlags'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactDOMInput = require('ReactDOMInput'); var ReactDOMOption = require('ReactDOMOption'); var ReactDOMSelect = require('ReactDOMSelect'); var ReactDOMTextarea = require('ReactDOMTextarea'); -var ReactMount = require('ReactMount'); var ReactMultiChild = require('ReactMultiChild'); var ReactPerf = require('ReactPerf'); var ReactUpdateQueue = require('ReactUpdateQueue'); @@ -46,7 +47,9 @@ var shallowEqual = require('shallowEqual'); var validateDOMNesting = require('validateDOMNesting'); var warning = require('warning'); +var Flags = ReactDOMComponentFlags; var deleteListener = EventPluginHub.deleteListener; +var getNode = ReactDOMComponentTree.getNodeFromInstance; var listenTo = ReactBrowserEventEmitter.listenTo; var registrationNameModules = EventPluginRegistry.registrationNameModules; @@ -57,8 +60,6 @@ var CHILDREN = keyOf({children: null}); var STYLE = keyOf({style: null}); var HTML = keyOf({__html: null}); -var ELEMENT_NODE_TYPE = 1; - function getDeclarationErrorAddendum(internalInstance) { if (internalInstance) { var owner = internalInstance._currentElement._owner || null; @@ -78,7 +79,7 @@ if (__DEV__) { props: { enumerable: false, get: function() { - var component = this._reactInternalComponent; + var component = ReactDOMComponentTree.getInstanceFromNode(this); warning( false, 'ReactDOMComponent: Do not access .props of a DOM node; instead, ' + @@ -95,7 +96,7 @@ if (__DEV__) { function legacyGetDOMNode() { if (__DEV__) { - var component = this._reactInternalComponent; + var component = ReactDOMComponentTree.getInstanceFromNode(this); warning( false, 'ReactDOMComponent: Do not access .getDOMNode() of a DOM node; ' + @@ -107,7 +108,7 @@ function legacyGetDOMNode() { } function legacyIsMounted() { - var component = this._reactInternalComponent; + var component = ReactDOMComponentTree.getInstanceFromNode(this); if (__DEV__) { warning( false, @@ -120,7 +121,7 @@ function legacyIsMounted() { function legacySetStateEtc() { if (__DEV__) { - var component = this._reactInternalComponent; + var component = ReactDOMComponentTree.getInstanceFromNode(this); warning( false, 'ReactDOMComponent: Do not access .setState(), .replaceState(), or ' + @@ -131,7 +132,7 @@ function legacySetStateEtc() { } function legacySetProps(partialProps, callback) { - var component = this._reactInternalComponent; + var component = ReactDOMComponentTree.getInstanceFromNode(this); if (__DEV__) { warning( false, @@ -150,7 +151,7 @@ function legacySetProps(partialProps, callback) { } function legacyReplaceProps(partialProps, callback) { - var component = this._reactInternalComponent; + var component = ReactDOMComponentTree.getInstanceFromNode(this); if (__DEV__) { warning( false, @@ -290,7 +291,7 @@ function assertValidProps(component, props) { ); } -function enqueuePutListener(id, registrationName, listener, transaction) { +function enqueuePutListener(inst, registrationName, listener, transaction) { if (__DEV__) { // IE8 has no API for event capturing and the `onScroll` event doesn't // bubble. @@ -299,15 +300,15 @@ function enqueuePutListener(id, registrationName, listener, transaction) { 'This browser doesn\'t support the `onScroll` event' ); } - var container = ReactMount.findReactContainerForID(id); - if (container) { - var doc = container.nodeType === ELEMENT_NODE_TYPE ? - container.ownerDocument : - container; - listenTo(registrationName, doc); + var containerInfo = inst._nativeContainerInfo; + if (!containerInfo) { + // Server rendering. + return; } + var doc = containerInfo._ownerDocument; + listenTo(registrationName, doc); transaction.getReactMountReady().enqueue(putListener, { - id: id, + inst: inst, registrationName: registrationName, listener: listener, }); @@ -316,7 +317,7 @@ function enqueuePutListener(id, registrationName, listener, transaction) { function putListener() { var listenerToPut = this; EventPluginHub.putListener( - listenerToPut.id, + listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener ); @@ -493,14 +494,6 @@ function isCustomComponent(tagName, props) { return tagName.indexOf('-') >= 0 || props.is != null; } -function getNode(inst) { - if (inst._nativeNode) { - return inst._nativeNode; - } else { - return inst._nativeNode = ReactMount.getNode(inst._rootNodeID); - } -} - /** * Creates a new React class that is idempotent and capable of containing other * React components. It accepts event listeners and DOM properties that are @@ -523,11 +516,12 @@ function ReactDOMComponent(tag) { this._previousStyle = null; this._previousStyleCopy = null; this._nativeNode = null; + this._nativeParent = null; this._rootNodeID = null; this._nativeContainerInfo = null; this._wrapperState = null; this._topLevelWrapper = null; - this._nodeHasLegacyProperties = false; + this._flags = 0; if (__DEV__) { this._ancestorInfo = null; } @@ -561,6 +555,7 @@ ReactDOMComponent.Mixin = { context ) { this._rootNodeID = rootID; + this._nativeParent = nativeParent; this._nativeContainerInfo = nativeContainerInfo; var props = this._currentElement.props; @@ -663,10 +658,9 @@ ReactDOMComponent.Mixin = { this._currentElement.type ); } - this._nativeNode = el; + ReactDOMComponentTree.precacheNode(this, el); + this._flags |= Flags.hasCachedChildNodes; DOMPropertyOperations.setAttributeForID(el, this._rootNodeID); - // Populate node cache - ReactMount.getID(el); this._updateDOMProperties(null, props, transaction); var lazyTree = DOMLazyTree(el); this._createInitialChildren(transaction, props, context, lazyTree); @@ -730,7 +724,7 @@ ReactDOMComponent.Mixin = { } if (registrationNameModules.hasOwnProperty(propKey)) { if (propValue) { - enqueuePutListener(this._rootNodeID, propKey, propValue, transaction); + enqueuePutListener(this, propKey, propValue, transaction); } } else { if (propKey === STYLE) { @@ -907,7 +901,7 @@ ReactDOMComponent.Mixin = { context ); - if (!canDefineProperty && this._nodeHasLegacyProperties) { + if (!canDefineProperty && this._flags & Flags.nodeHasLegacyProperties) { this._nativeNode.props = nextProps; } @@ -958,7 +952,7 @@ ReactDOMComponent.Mixin = { // Only call deleteListener if there was a listener previously or // else willDeleteListener gets called when there wasn't actually a // listener (e.g., onClick={null}) - deleteListener(this._rootNodeID, propKey); + deleteListener(this, propKey); } } else if ( DOMProperty.properties[propKey] || @@ -1013,9 +1007,9 @@ ReactDOMComponent.Mixin = { } } else if (registrationNameModules.hasOwnProperty(propKey)) { if (nextProp) { - enqueuePutListener(this._rootNodeID, propKey, nextProp, transaction); + enqueuePutListener(this, propKey, nextProp, transaction); } else if (lastProp) { - deleteListener(this._rootNodeID, propKey); + deleteListener(this, propKey); } } else if (isCustomComponent(this._tag, nextProps)) { if (propKey === CHILDREN) { @@ -1041,7 +1035,11 @@ ReactDOMComponent.Mixin = { } } if (styleUpdates) { - CSSPropertyOperations.setValueForStyles(getNode(this), styleUpdates); + CSSPropertyOperations.setValueForStyles( + getNode(this), + styleUpdates, + this + ); } }, @@ -1142,25 +1140,19 @@ ReactDOMComponent.Mixin = { break; } - if (this._nodeHasLegacyProperties) { - this._nativeNode._reactInternalComponent = null; - } - this._nativeNode = null; - + ReactDOMComponentTree.uncacheNode(this); this.unmountChildren(); - EventPluginHub.deleteAllListeners(this._rootNodeID); + EventPluginHub.deleteAllListeners(this); ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID); this._rootNodeID = null; this._wrapperState = null; }, getPublicInstance: function() { - if (this._nodeHasLegacyProperties) { - return this._nativeNode; + var node = getNode(this); + if (this._flags & Flags.nodeHasLegacyProperties) { + return node; } else { - var node = getNode(this); - - node._reactInternalComponent = this; node.getDOMNode = legacyGetDOMNode; node.isMounted = legacyIsMounted; node.setState = legacySetStateEtc; @@ -1181,7 +1173,7 @@ ReactDOMComponent.Mixin = { node.props = this._currentElement.props; } - this._nodeHasLegacyProperties = true; + this._flags |= Flags.nodeHasLegacyProperties; return node; } }, @@ -1196,11 +1188,15 @@ ReactPerf.measureMethods(ReactDOMComponent, 'ReactDOMComponent', { assign( ReactDOMComponent.prototype, ReactDOMComponent.Mixin, - ReactMultiChild.Mixin + ReactMultiChild.Mixin, + { + prepareToManageChildren: function() { + // Before we add, remove, or reorder the children of a node, make sure + // we have references to all of its children so we don't lose them, even + // if nefarious browser plugins add extra nodes to our tree. + ReactDOMComponentTree.precacheChildNodes(this, getNode(this)); + }, + } ); -assign(ReactDOMComponent, { - getNodeFromInstance: getNode, -}); - module.exports = ReactDOMComponent; diff --git a/src/renderers/dom/shared/ReactDOMComponentFlags.js b/src/renderers/dom/shared/ReactDOMComponentFlags.js new file mode 100644 index 0000000000000..2578f90f75902 --- /dev/null +++ b/src/renderers/dom/shared/ReactDOMComponentFlags.js @@ -0,0 +1,19 @@ +/** + * Copyright 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDOMComponentFlags + */ + +'use strict'; + +var ReactDOMComponentFlags = { + nodeHasLegacyProperties: 1 << 0, + hasCachedChildNodes: 1 << 1, +}; + +module.exports = ReactDOMComponentFlags; diff --git a/src/renderers/dom/shared/ReactDOMTextComponent.js b/src/renderers/dom/shared/ReactDOMTextComponent.js index 9858379d04ae7..ff94cbd8e8506 100644 --- a/src/renderers/dom/shared/ReactDOMTextComponent.js +++ b/src/renderers/dom/shared/ReactDOMTextComponent.js @@ -17,19 +17,13 @@ var DOMLazyTree = require('DOMLazyTree'); var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactComponentBrowserEnvironment = require('ReactComponentBrowserEnvironment'); -var ReactMount = require('ReactMount'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var assign = require('Object.assign'); var escapeTextContentForBrowser = require('escapeTextContentForBrowser'); var validateDOMNesting = require('validateDOMNesting'); -function getNode(inst) { - if (inst._nativeNode) { - return inst._nativeNode; - } else { - return inst._nativeNode = ReactMount.getNode(inst._rootNodeID); - } -} +var getNode = ReactDOMComponentTree.getNodeFromInstance; /** * Text nodes violate a couple assumptions that React makes about components: @@ -61,6 +55,8 @@ assign(ReactDOMTextComponent.prototype, { this._currentElement = text; this._stringText = '' + text; this._nativeNode = null; + // ReactDOMComponentTree uses this: + this._nativeParent = null; // Properties this._rootNodeID = null; @@ -98,13 +94,12 @@ assign(ReactDOMTextComponent.prototype, { } this._rootNodeID = rootID; + this._nativeParent = nativeParent; if (transaction.useCreateElement) { var ownerDocument = nativeContainerInfo._ownerDocument; var el = ownerDocument.createElement('span'); - this._nativeNode = el; + ReactDOMComponentTree.precacheNode(this, el); DOMPropertyOperations.setAttributeForID(el, rootID); - // Populate node cache - ReactMount.getID(el); var lazyTree = DOMLazyTree(el); DOMLazyTree.queueText(lazyTree, this._stringText); return lazyTree; diff --git a/src/renderers/dom/shared/ReactDefaultInjection.js b/src/renderers/dom/shared/ReactDefaultInjection.js index 5c6dbb979a0f6..b6f8edd745fe7 100644 --- a/src/renderers/dom/shared/ReactDefaultInjection.js +++ b/src/renderers/dom/shared/ReactDefaultInjection.js @@ -20,17 +20,17 @@ var HTMLDOMPropertyConfig = require('HTMLDOMPropertyConfig'); var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); var ReactComponentBrowserEnvironment = require('ReactComponentBrowserEnvironment'); -var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); var ReactDOMComponent = require('ReactDOMComponent'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); +var ReactDOMTreeTraversal = require('ReactDOMTreeTraversal'); var ReactDOMTextComponent = require('ReactDOMTextComponent'); +var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); var ReactEventListener = require('ReactEventListener'); var ReactInjection = require('ReactInjection'); -var ReactInstanceHandles = require('ReactInstanceHandles'); -var ReactMount = require('ReactMount'); var ReactReconcileTransaction = require('ReactReconcileTransaction'); +var SVGDOMPropertyConfig = require('SVGDOMPropertyConfig'); var SelectEventPlugin = require('SelectEventPlugin'); var SimpleEventPlugin = require('SimpleEventPlugin'); -var SVGDOMPropertyConfig = require('SVGDOMPropertyConfig'); var alreadyInjected = false; @@ -51,8 +51,8 @@ function inject() { * Inject modules for resolving DOM hierarchy and plugin ordering. */ ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder); - ReactInjection.EventPluginHub.injectInstanceHandle(ReactInstanceHandles); - ReactInjection.EventPluginHub.injectMount(ReactMount); + ReactInjection.EventPluginUtils.injectComponentTree(ReactDOMComponentTree); + ReactInjection.EventPluginUtils.injectTreeTraversal(ReactDOMTreeTraversal); /** * Some important event plugins included by default (without having to require diff --git a/src/renderers/dom/shared/ReactInjection.js b/src/renderers/dom/shared/ReactInjection.js index dc114c4661d6d..0e8c0dc90ecdf 100644 --- a/src/renderers/dom/shared/ReactInjection.js +++ b/src/renderers/dom/shared/ReactInjection.js @@ -13,6 +13,7 @@ var DOMProperty = require('DOMProperty'); var EventPluginHub = require('EventPluginHub'); +var EventPluginUtils = require('EventPluginUtils'); var ReactComponentEnvironment = require('ReactComponentEnvironment'); var ReactClass = require('ReactClass'); var ReactEmptyComponent = require('ReactEmptyComponent'); @@ -27,6 +28,7 @@ var ReactInjection = { DOMProperty: DOMProperty.injection, EmptyComponent: ReactEmptyComponent.injection, EventPluginHub: EventPluginHub.injection, + EventPluginUtils: EventPluginUtils.injection, EventEmitter: ReactBrowserEventEmitter.injection, NativeComponent: ReactNativeComponent.injection, Perf: ReactPerf.injection, diff --git a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js index 5d2999f7deefe..9f6528c35b40b 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js @@ -51,7 +51,7 @@ describe('ReactDOMComponent', function() { var stubStyle = container.firstChild.style; // set initial style - var setup = {display: 'block', left: '1', top: 2, fontFamily: 'Arial'}; + var setup = {display: 'block', left: '1px', top: 2, fontFamily: 'Arial'}; ReactDOM.render(
, container); expect(stubStyle.display).toEqual('block'); expect(stubStyle.left).toEqual('1px'); @@ -152,7 +152,9 @@ describe('ReactDOMComponent', function() { var div = document.createElement('div'); var One = React.createClass({ render: function() { - return this.props.inline ? :
; + return this.props.inline ? + : +
; }, }); var Two = React.createClass({ @@ -841,7 +843,7 @@ describe('ReactDOMComponent', function() { describe('unmountComponent', function() { it('should clean up listeners', function() { var EventPluginHub = require('EventPluginHub'); - var ReactMount = require('ReactMount'); + var ReactDOMComponentTree = require('ReactDOMComponentTree'); var container = document.createElement('div'); document.body.appendChild(container); @@ -851,16 +853,16 @@ describe('ReactDOMComponent', function() { instance = ReactDOM.render(instance, container); var rootNode = ReactDOM.findDOMNode(instance); - var rootNodeID = ReactMount.getID(rootNode); + var inst = ReactDOMComponentTree.getInstanceFromNode(rootNode); expect( - EventPluginHub.getListener(rootNodeID, 'onClick') + EventPluginHub.getListener(inst, 'onClick') ).toBe(callback); expect(rootNode).toBe(ReactDOM.findDOMNode(instance)); ReactDOM.unmountComponentAtNode(container); expect( - EventPluginHub.getListener(rootNodeID, 'onClick') + EventPluginHub.getListener(inst, 'onClick') ).toBe(undefined); }); }); diff --git a/src/renderers/shared/event/EventPluginHub.js b/src/renderers/shared/event/EventPluginHub.js index d5078f5e00bbd..d8b5dc1b51088 100644 --- a/src/renderers/shared/event/EventPluginHub.js +++ b/src/renderers/shared/event/EventPluginHub.js @@ -18,7 +18,6 @@ var ReactErrorUtils = require('ReactErrorUtils'); var accumulateInto = require('accumulateInto'); var forEachAccumulated = require('forEachAccumulated'); var invariant = require('invariant'); -var warning = require('warning'); /** * Internal store for event listeners @@ -54,23 +53,6 @@ var executeDispatchesAndReleaseTopLevel = function(e) { return executeDispatchesAndRelease(e, false); }; -/** - * - `InstanceHandle`: [required] Module that performs logical traversals of DOM - * hierarchy given ids of the logical DOM elements involved. - */ -var InstanceHandle = null; - -function validateInstanceHandle() { - var valid = - InstanceHandle && - InstanceHandle.traverseTwoPhase && - InstanceHandle.traverseEnterLeave; - warning( - valid, - 'InstanceHandle not injected before use!' - ); -} - /** * This is a unified interface for event plugins to be installed and configured. * @@ -100,30 +82,6 @@ var EventPluginHub = { */ injection: { - /** - * @param {object} InjectedMount - * @public - */ - injectMount: EventPluginUtils.injection.injectMount, - - /** - * @param {object} InjectedInstanceHandle - * @public - */ - injectInstanceHandle: function(InjectedInstanceHandle) { - InstanceHandle = InjectedInstanceHandle; - if (__DEV__) { - validateInstanceHandle(); - } - }, - - getInstanceHandle: function() { - if (__DEV__) { - validateInstanceHandle(); - } - return InstanceHandle; - }, - /** * @param {array} InjectedEventPluginOrder * @public @@ -144,7 +102,7 @@ var EventPluginHub = { * @param {string} registrationName Name of listener (e.g. `onClick`). * @param {?function} listener The callback to store. */ - putListener: function(id, registrationName, listener) { + putListener: function(inst, registrationName, listener) { invariant( typeof listener === 'function', 'Expected %s listener to be a function, instead got type %s', @@ -153,12 +111,12 @@ var EventPluginHub = { var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); - bankForRegistrationName[id] = listener; + bankForRegistrationName[inst._rootNodeID] = listener; var PluginModule = EventPluginRegistry.registrationNameModules[registrationName]; if (PluginModule && PluginModule.didPutListener) { - PluginModule.didPutListener(id, registrationName, listener); + PluginModule.didPutListener(inst, registrationName, listener); } }, @@ -167,9 +125,9 @@ var EventPluginHub = { * @param {string} registrationName Name of listener (e.g. `onClick`). * @return {?function} The stored callback. */ - getListener: function(id, registrationName) { + getListener: function(inst, registrationName) { var bankForRegistrationName = listenerBank[registrationName]; - return bankForRegistrationName && bankForRegistrationName[id]; + return bankForRegistrationName && bankForRegistrationName[inst._rootNodeID]; }, /** @@ -178,17 +136,17 @@ var EventPluginHub = { * @param {string} id ID of the DOM element. * @param {string} registrationName Name of listener (e.g. `onClick`). */ - deleteListener: function(id, registrationName) { + deleteListener: function(inst, registrationName) { var PluginModule = EventPluginRegistry.registrationNameModules[registrationName]; if (PluginModule && PluginModule.willDeleteListener) { - PluginModule.willDeleteListener(id, registrationName); + PluginModule.willDeleteListener(inst, registrationName); } var bankForRegistrationName = listenerBank[registrationName]; // TODO: This should never be null -- when is it? if (bankForRegistrationName) { - delete bankForRegistrationName[id]; + delete bankForRegistrationName[inst._rootNodeID]; } }, @@ -197,19 +155,19 @@ var EventPluginHub = { * * @param {string} id ID of the DOM element. */ - deleteAllListeners: function(id) { + deleteAllListeners: function(inst) { for (var registrationName in listenerBank) { - if (!listenerBank[registrationName][id]) { + if (!listenerBank[registrationName][inst._rootNodeID]) { continue; } var PluginModule = EventPluginRegistry.registrationNameModules[registrationName]; if (PluginModule && PluginModule.willDeleteListener) { - PluginModule.willDeleteListener(id, registrationName); + PluginModule.willDeleteListener(inst, registrationName); } - delete listenerBank[registrationName][id]; + delete listenerBank[registrationName][inst._rootNodeID]; } }, @@ -217,17 +175,12 @@ var EventPluginHub = { * Allows registered plugins an opportunity to extract events from top-level * native browser events. * - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. * @return {*} An accumulation of synthetic events. * @internal */ extractEvents: function( topLevelType, - topLevelTarget, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget) { var events; @@ -238,8 +191,7 @@ var EventPluginHub = { if (possiblePlugin) { var extractedEvents = possiblePlugin.extractEvents( topLevelType, - topLevelTarget, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ); diff --git a/src/renderers/shared/event/EventPluginUtils.js b/src/renderers/shared/event/EventPluginUtils.js index bff374799e434..9154882242bbd 100644 --- a/src/renderers/shared/event/EventPluginUtils.js +++ b/src/renderers/shared/event/EventPluginUtils.js @@ -22,18 +22,31 @@ var warning = require('warning'); */ /** - * - `Mount`: [required] Module that can convert between React dom IDs and - * actual node references. + * - `ComponentTree`: [required] Module that can convert between React instances + * and actual node references. */ +var ComponentTree; +var TreeTraversal; var injection = { - Mount: null, - injectMount: function(InjectedMount) { - injection.Mount = InjectedMount; + injectComponentTree: function(Injected) { + ComponentTree = Injected; if (__DEV__) { warning( - InjectedMount && InjectedMount.getNode && InjectedMount.getID, - 'EventPluginUtils.injection.injectMount(...): Injected Mount ' + - 'module is missing getNode or getID.' + Injected && + Injected.getNodeFromInstance && + Injected.getInstanceFromNode, + 'EventPluginUtils.injection.injectComponentTree(...): Injected ' + + 'module is missing getNodeFromInstance or getInstanceFromNode.' + ); + } + }, + injectTreeTraversal: function(Injected) { + TreeTraversal = Injected; + if (__DEV__) { + warning( + Injected && Injected.isAncestor && Injected.getLowestCommonAncestor, + 'EventPluginUtils.injection.injectTreeTraversal(...): Injected ' + + 'module is missing isAncestor or getLowestCommonAncestor.' ); } }, @@ -61,17 +74,20 @@ var validateEventDispatches; if (__DEV__) { validateEventDispatches = function(event) { var dispatchListeners = event._dispatchListeners; - var dispatchIDs = event._dispatchIDs; + var dispatchInstances = event._dispatchInstances; var listenersIsArr = Array.isArray(dispatchListeners); - var idsIsArr = Array.isArray(dispatchIDs); - var IDsLen = idsIsArr ? dispatchIDs.length : dispatchIDs ? 1 : 0; var listenersLen = listenersIsArr ? dispatchListeners.length : dispatchListeners ? 1 : 0; + var instancesIsArr = Array.isArray(dispatchInstances); + var instancesLen = instancesIsArr ? + dispatchInstances.length : + dispatchInstances ? 1 : 0; + warning( - idsIsArr === listenersIsArr && IDsLen === listenersLen, + instancesIsArr === listenersIsArr && instancesLen === listenersLen, 'EventPluginUtils: Invalid `event`.' ); }; @@ -82,20 +98,19 @@ if (__DEV__) { * @param {SyntheticEvent} event SyntheticEvent to handle * @param {boolean} simulated If the event is simulated (changes exn behavior) * @param {function} listener Application-level callback - * @param {string} domID DOM id to pass to the callback. + * @param {*} inst Internal component instance */ -function executeDispatch(event, simulated, listener, domID) { +function executeDispatch(event, simulated, listener, inst) { var type = event.type || 'unknown-event'; - event.currentTarget = injection.Mount.getNode(domID); + event.currentTarget = EventPluginUtils.getNodeFromInstance(inst); if (simulated) { ReactErrorUtils.invokeGuardedCallbackWithCatch( type, listener, - event, - domID + event ); } else { - ReactErrorUtils.invokeGuardedCallback(type, listener, event, domID); + ReactErrorUtils.invokeGuardedCallback(type, listener, event); } event.currentTarget = null; } @@ -105,7 +120,7 @@ function executeDispatch(event, simulated, listener, domID) { */ function executeDispatchesInOrder(event, simulated) { var dispatchListeners = event._dispatchListeners; - var dispatchIDs = event._dispatchIDs; + var dispatchInstances = event._dispatchInstances; if (__DEV__) { validateEventDispatches(event); } @@ -114,14 +129,19 @@ function executeDispatchesInOrder(event, simulated) { if (event.isPropagationStopped()) { break; } - // Listeners and IDs are two parallel arrays that are always in sync. - executeDispatch(event, simulated, dispatchListeners[i], dispatchIDs[i]); + // Listeners and Instances are two parallel arrays that are always in sync. + executeDispatch( + event, + simulated, + dispatchListeners[i], + dispatchInstances[i] + ); } } else if (dispatchListeners) { - executeDispatch(event, simulated, dispatchListeners, dispatchIDs); + executeDispatch(event, simulated, dispatchListeners, dispatchInstances); } event._dispatchListeners = null; - event._dispatchIDs = null; + event._dispatchInstances = null; } /** @@ -133,7 +153,7 @@ function executeDispatchesInOrder(event, simulated) { */ function executeDispatchesInOrderStopAtTrueImpl(event) { var dispatchListeners = event._dispatchListeners; - var dispatchIDs = event._dispatchIDs; + var dispatchInstances = event._dispatchInstances; if (__DEV__) { validateEventDispatches(event); } @@ -142,14 +162,14 @@ function executeDispatchesInOrderStopAtTrueImpl(event) { if (event.isPropagationStopped()) { break; } - // Listeners and IDs are two parallel arrays that are always in sync. - if (dispatchListeners[i](event, dispatchIDs[i])) { - return dispatchIDs[i]; + // Listeners and Instances are two parallel arrays that are always in sync. + if (dispatchListeners[i](event, dispatchInstances[i])) { + return dispatchInstances[i]; } } } else if (dispatchListeners) { - if (dispatchListeners(event, dispatchIDs)) { - return dispatchIDs; + if (dispatchListeners(event, dispatchInstances)) { + return dispatchInstances; } } return null; @@ -160,7 +180,7 @@ function executeDispatchesInOrderStopAtTrueImpl(event) { */ function executeDispatchesInOrderStopAtTrue(event) { var ret = executeDispatchesInOrderStopAtTrueImpl(event); - event._dispatchIDs = null; + event._dispatchInstances = null; event._dispatchListeners = null; return ret; } @@ -179,16 +199,16 @@ function executeDirectDispatch(event) { validateEventDispatches(event); } var dispatchListener = event._dispatchListeners; - var dispatchID = event._dispatchIDs; + var dispatchInstance = event._dispatchInstances; invariant( !Array.isArray(dispatchListener), 'executeDirectDispatch(...): Invalid `event`.' ); - var res = dispatchListener ? - dispatchListener(event, dispatchID) : - null; + event.currentTarget = EventPluginUtils.getNodeFromInstance(dispatchInstance); + var res = dispatchListener ? dispatchListener(event) : null; + event.curentTarget = null; event._dispatchListeners = null; - event._dispatchIDs = null; + event._dispatchInstances = null; return res; } @@ -213,11 +233,26 @@ var EventPluginUtils = { executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue, hasDispatches: hasDispatches, - getNode: function(id) { - return injection.Mount.getNode(id); + getInstanceFromNode: function(node) { + return ComponentTree.getInstanceFromNode(node); + }, + getNodeFromInstance: function(node) { + return ComponentTree.getNodeFromInstance(node); + }, + isAncestor: function(a, b) { + return TreeTraversal.isAncestor(a, b); + }, + getLowestCommonAncestor: function(a, b) { + return TreeTraversal.getLowestCommonAncestor(a, b); + }, + getParentInstance: function(inst) { + return TreeTraversal.getParentInstance(inst); + }, + traverseTwoPhase: function(target, fn, arg) { + return TreeTraversal.traverseTwoPhase(target, fn, arg); }, - getID: function(node) { - return injection.Mount.getID(node); + traverseEnterLeave: function(from, to, fn, argFrom, argTo) { + return TreeTraversal.traverseEnterLeave(from, to, fn, argFrom, argTo); }, injection: injection, diff --git a/src/renderers/shared/event/EventPropagators.js b/src/renderers/shared/event/EventPropagators.js index 896382aba0a19..27b363a0d458c 100644 --- a/src/renderers/shared/event/EventPropagators.js +++ b/src/renderers/shared/event/EventPropagators.js @@ -13,11 +13,11 @@ var EventConstants = require('EventConstants'); var EventPluginHub = require('EventPluginHub'); - -var warning = require('warning'); +var EventPluginUtils = require('EventPluginUtils'); var accumulateInto = require('accumulateInto'); var forEachAccumulated = require('forEachAccumulated'); +var warning = require('warning'); var PropagationPhases = EventConstants.PropagationPhases; var getListener = EventPluginHub.getListener; @@ -26,10 +26,10 @@ var getListener = EventPluginHub.getListener; * Some event types have a notion of different registration names for different * "phases" of propagation. This finds listeners by a given phase. */ -function listenerAtPhase(id, event, propagationPhase) { +function listenerAtPhase(inst, event, propagationPhase) { var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; - return getListener(id, registrationName); + return getListener(inst, registrationName); } /** @@ -38,19 +38,19 @@ function listenerAtPhase(id, event, propagationPhase) { * Mutating the event's members allows us to not have to create a wrapping * "dispatch" object that pairs the event with the listener. */ -function accumulateDirectionalDispatches(domID, upwards, event) { +function accumulateDirectionalDispatches(inst, upwards, event) { if (__DEV__) { warning( - domID, - 'Dispatching id must not be null' + inst, + 'Dispatching inst must not be null' ); } var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured; - var listener = listenerAtPhase(domID, event, phase); + var listener = listenerAtPhase(inst, event, phase); if (listener) { event._dispatchListeners = accumulateInto(event._dispatchListeners, listener); - event._dispatchIDs = accumulateInto(event._dispatchIDs, domID); + event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } } @@ -63,8 +63,8 @@ function accumulateDirectionalDispatches(domID, upwards, event) { */ function accumulateTwoPhaseDispatchesSingle(event) { if (event && event.dispatchConfig.phasedRegistrationNames) { - EventPluginHub.injection.getInstanceHandle().traverseTwoPhase( - event.dispatchMarker, + EventPluginUtils.traverseTwoPhase( + event._targetInst, accumulateDirectionalDispatches, event ); @@ -76,8 +76,11 @@ function accumulateTwoPhaseDispatchesSingle(event) { */ function accumulateTwoPhaseDispatchesSingleSkipTarget(event) { if (event && event.dispatchConfig.phasedRegistrationNames) { - EventPluginHub.injection.getInstanceHandle().traverseTwoPhaseSkipTarget( - event.dispatchMarker, + var targetInst = event._targetInst; + var parentInst = + targetInst ? EventPluginUtils.getParentInstance(targetInst) : null; + EventPluginUtils.traverseTwoPhase( + parentInst, accumulateDirectionalDispatches, event ); @@ -90,14 +93,14 @@ function accumulateTwoPhaseDispatchesSingleSkipTarget(event) { * registration names. Same as `accumulateDirectDispatchesSingle` but without * requiring that the `dispatchMarker` be the same as the dispatched ID. */ -function accumulateDispatches(id, ignoredDirection, event) { +function accumulateDispatches(inst, ignoredDirection, event) { if (event && event.dispatchConfig.registrationName) { var registrationName = event.dispatchConfig.registrationName; - var listener = getListener(id, registrationName); + var listener = getListener(inst, registrationName); if (listener) { event._dispatchListeners = accumulateInto(event._dispatchListeners, listener); - event._dispatchIDs = accumulateInto(event._dispatchIDs, id); + event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } } } @@ -109,7 +112,7 @@ function accumulateDispatches(id, ignoredDirection, event) { */ function accumulateDirectDispatchesSingle(event) { if (event && event.dispatchConfig.registrationName) { - accumulateDispatches(event.dispatchMarker, null, event); + accumulateDispatches(event._targetInst, null, event); } } @@ -121,10 +124,10 @@ function accumulateTwoPhaseDispatchesSkipTarget(events) { forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget); } -function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) { - EventPluginHub.injection.getInstanceHandle().traverseEnterLeave( - fromID, - toID, +function accumulateEnterLeaveDispatches(leave, enter, from, to) { + EventPluginUtils.traverseEnterLeave( + from, + to, accumulateDispatches, leave, enter diff --git a/src/renderers/shared/event/eventPlugins/ResponderEventPlugin.js b/src/renderers/shared/event/eventPlugins/ResponderEventPlugin.js index 065cee28ab0f8..32f81e044ce73 100644 --- a/src/renderers/shared/event/eventPlugins/ResponderEventPlugin.js +++ b/src/renderers/shared/event/eventPlugins/ResponderEventPlugin.js @@ -14,7 +14,6 @@ var EventConstants = require('EventConstants'); var EventPluginUtils = require('EventPluginUtils'); var EventPropagators = require('EventPropagators'); -var ReactInstanceHandles = require('ReactInstanceHandles'); var ResponderSyntheticEvent = require('ResponderSyntheticEvent'); var ResponderTouchHistoryStore = require('ResponderTouchHistoryStore'); @@ -31,10 +30,10 @@ var executeDispatchesInOrderStopAtTrue = EventPluginUtils.executeDispatchesInOrderStopAtTrue; /** - * ID of element that should respond to touch/move types of interactions, as + * Instance of element that should respond to touch/move types of interactions, as * indicated explicitly by relevant callbacks. */ -var responderID = null; +var responderInst = null; /** * Count of current touches. A textInput should become responder iff the @@ -47,13 +46,13 @@ var trackedTouchCount = 0; */ var previousActiveTouches = 0; -var changeResponder = function(nextResponderID) { - var oldResponderID = responderID; - responderID = nextResponderID; +var changeResponder = function(nextResponderInst) { + var oldResponderInst = responderInst; + responderInst = nextResponderInst; if (ResponderEventPlugin.GlobalResponderHandler !== null) { ResponderEventPlugin.GlobalResponderHandler.onChange( - oldResponderID, - nextResponderID + oldResponderInst, + nextResponderInst ); } }; @@ -149,7 +148,7 @@ var eventTypes = { * - If nothing is currently the responder, the "appropriate place" is the * initiating event's `targetID`. * - If something *is* already the responder, the "appropriate place" is the - * first common ancestor of the event target and the current `responderID`. + * first common ancestor of the event target and the current `responderInst`. * - Some negotiation happens: See the timing diagram below. * - Scrolled views automatically become responder. The reasoning is that a * platform scroll view that isn't built on top of the responder system has @@ -158,7 +157,7 @@ var eventTypes = { * * - Responder being released: * As soon as no more touches that *started* inside of descendents of the - * *current* responderID, an `onResponderRelease` event is dispatched to the + * *current* responderInst, an `onResponderRelease` event is dispatched to the * current responder, and the responder lock is released. * * TODO: @@ -316,15 +315,11 @@ to return true:wantsResponderID| | * - `touchStartCapture` (`EventPluginHub` dispatches as usual) * - `touchStart` (`EventPluginHub` dispatches as usual) * - `responderGrant/Reject` (`EventPluginHub` dispatches as usual) - * - * @param {string} topLevelType Record from `EventConstants`. - * @param {string} topLevelTargetID ID of deepest React rendered element. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. */ + function setResponderAndExtractTransfer( topLevelType, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ) { @@ -336,15 +331,15 @@ function setResponderAndExtractTransfer( eventTypes.scrollShouldSetResponder; // TODO: stop one short of the the current responder. - var bubbleShouldSetFrom = !responderID ? - topLevelTargetID : - ReactInstanceHandles.getFirstCommonAncestorID(responderID, topLevelTargetID); + var bubbleShouldSetFrom = !responderInst ? + targetInst : + EventPluginUtils.getLowestCommonAncestor(responderInst, targetInst); // When capturing/bubbling the "shouldSet" event, we want to skip the target // (deepest ID) if it happens to be the current responder. The reasoning: // It's strange to get an `onMoveShouldSetResponder` when you're *already* // the responder. - var skipOverBubbleShouldSetFrom = bubbleShouldSetFrom === responderID; + var skipOverBubbleShouldSetFrom = bubbleShouldSetFrom === responderInst; var shouldSetEvent = ResponderSyntheticEvent.getPooled( shouldSetEventType, bubbleShouldSetFrom, @@ -357,29 +352,29 @@ function setResponderAndExtractTransfer( } else { EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent); } - var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent); + var wantsResponderInst = executeDispatchesInOrderStopAtTrue(shouldSetEvent); if (!shouldSetEvent.isPersistent()) { shouldSetEvent.constructor.release(shouldSetEvent); } - if (!wantsResponderID || wantsResponderID === responderID) { + if (!wantsResponderInst || wantsResponderInst === responderInst) { return null; } var extracted; var grantEvent = ResponderSyntheticEvent.getPooled( eventTypes.responderGrant, - wantsResponderID, + wantsResponderInst, nativeEvent, nativeEventTarget ); grantEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; EventPropagators.accumulateDirectDispatches(grantEvent); - if (responderID) { + if (responderInst) { var terminationRequestEvent = ResponderSyntheticEvent.getPooled( eventTypes.responderTerminationRequest, - responderID, + responderInst, nativeEvent, nativeEventTarget ); @@ -392,21 +387,20 @@ function setResponderAndExtractTransfer( } if (shouldSwitch) { - var terminateType = eventTypes.responderTerminate; var terminateEvent = ResponderSyntheticEvent.getPooled( - terminateType, - responderID, + eventTypes.responderTerminate, + responderInst, nativeEvent, nativeEventTarget ); terminateEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; EventPropagators.accumulateDirectDispatches(terminateEvent); extracted = accumulate(extracted, [grantEvent, terminateEvent]); - changeResponder(wantsResponderID); + changeResponder(wantsResponderInst); } else { var rejectEvent = ResponderSyntheticEvent.getPooled( eventTypes.responderReject, - wantsResponderID, + wantsResponderInst, nativeEvent, nativeEventTarget ); @@ -416,7 +410,7 @@ function setResponderAndExtractTransfer( } } else { extracted = accumulate(extracted, grantEvent); - changeResponder(wantsResponderID); + changeResponder(wantsResponderInst); } return extracted; } @@ -424,13 +418,13 @@ function setResponderAndExtractTransfer( /** * A transfer is a negotiation between a currently set responder and the next * element to claim responder status. Any start event could trigger a transfer - * of responderID. Any move event could trigger a transfer. + * of responderInst. Any move event could trigger a transfer. * * @param {string} topLevelType Record from `EventConstants`. * @return {boolean} True if a transfer of responder could possibly occur. */ -function canTriggerTransfer(topLevelType, topLevelTargetID) { - return topLevelTargetID && ( +function canTriggerTransfer(topLevelType, targetInst) { + return !!targetInst && ( topLevelType === EventConstants.topLevelTypes.topScroll || (trackedTouchCount > 0 && topLevelType === EventConstants.topLevelTypes.topSelectionChange) || @@ -441,7 +435,7 @@ function canTriggerTransfer(topLevelType, topLevelTargetID) { /** * Returns whether or not this touch end event makes it such that there are no - * longer any touches that started inside of the current `responderID`. + * longer any touches that started inside of the current `responderInst`. * * @param {NativeEvent} nativeEvent Native touch end event. * @return {boolean} Whether or not this touch end event ends the responder. @@ -456,12 +450,8 @@ function noResponderTouches(nativeEvent) { var target = activeTouch.target; if (target !== null && target !== undefined && target !== 0) { // Is the original touch location inside of the current responder? - var isAncestor = - ReactInstanceHandles.isAncestorIDOf( - responderID, - EventPluginUtils.getID(target) - ); - if (isAncestor) { + var targetInst = EventPluginUtils.getInstanceFromNode(target); + if (EventPluginUtils.isAncestor(responderInst, targetInst)) { return false; } } @@ -472,30 +462,24 @@ function noResponderTouches(nativeEvent) { var ResponderEventPlugin = { - getResponderID: function() { - return responderID; + /* For unit testing only */ + _getResponderID: function() { + return responderInst ? responderInst._rootNodeID : null; }, eventTypes: eventTypes, /** - * We must be resilient to `topLevelTargetID` being `undefined` on - * `touchMove`, or `touchEnd`. On certain platforms, this means that a native - * scroll has assumed control and the original touch targets are destroyed. - * - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} + * We must be resilient to `targetInst` being `null` on `touchMove` or + * `touchEnd`. On certain platforms, this means that a native scroll has + * assumed control and the original touch targets are destroyed. */ extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent, - nativeEventTarget) { + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget + ) { if (isStartish(topLevelType)) { trackedTouchCount += 1; } else if (isEndish(topLevelType)) { @@ -508,10 +492,10 @@ var ResponderEventPlugin = { ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent, nativeEventTarget); - var extracted = canTriggerTransfer(topLevelType, topLevelTargetID) ? + var extracted = canTriggerTransfer(topLevelType, targetInst) ? setResponderAndExtractTransfer( topLevelType, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget) : null; @@ -525,9 +509,9 @@ var ResponderEventPlugin = { // These multiple individual change touch events are are always bookended // by `onResponderGrant`, and one of // (`onResponderRelease/onResponderTerminate`). - var isResponderTouchStart = responderID && isStartish(topLevelType); - var isResponderTouchMove = responderID && isMoveish(topLevelType); - var isResponderTouchEnd = responderID && isEndish(topLevelType); + var isResponderTouchStart = responderInst && isStartish(topLevelType); + var isResponderTouchMove = responderInst && isMoveish(topLevelType); + var isResponderTouchEnd = responderInst && isEndish(topLevelType); var incrementalTouch = isResponderTouchStart ? eventTypes.responderStart : isResponderTouchMove ? eventTypes.responderMove : @@ -538,7 +522,7 @@ var ResponderEventPlugin = { var gesture = ResponderSyntheticEvent.getPooled( incrementalTouch, - responderID, + responderInst, nativeEvent, nativeEventTarget ); @@ -548,10 +532,10 @@ var ResponderEventPlugin = { } var isResponderTerminate = - responderID && + responderInst && topLevelType === EventConstants.topLevelTypes.topTouchCancel; var isResponderRelease = - responderID && + responderInst && !isResponderTerminate && isEndish(topLevelType) && noResponderTouches(nativeEvent); @@ -560,8 +544,9 @@ var ResponderEventPlugin = { isResponderRelease ? eventTypes.responderRelease : null; if (finalTouch) { - var finalEvent = - ResponderSyntheticEvent.getPooled(finalTouch, responderID, nativeEvent, nativeEventTarget); + var finalEvent = ResponderSyntheticEvent.getPooled( + finalTouch, responderInst, nativeEvent, nativeEventTarget + ); finalEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; EventPropagators.accumulateDirectDispatches(finalEvent); extracted = accumulate(extracted, finalEvent); diff --git a/src/renderers/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js b/src/renderers/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js index 21c719aabe29e..e9a640e6d3014 100644 --- a/src/renderers/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js +++ b/src/renderers/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js @@ -17,11 +17,6 @@ var ReactInstanceHandles; var ResponderEventPlugin; var EventPluginUtils; -var GRANDPARENT_ID = '.0'; -var PARENT_ID = '.0.0'; -var CHILD_ID = '.0.0.0'; -var CHILD_ID2 = '.0.0.1'; - var topLevelTypes; var touch = function(nodeHandle, i) { @@ -89,8 +84,7 @@ var _touchConfig = function( changedTouchObjects ), topLevelType: topType, - target: targetNodeHandle, - targetID: targetNodeHandle, + targetInst: idToInstance[targetNodeHandle], }; }; @@ -243,7 +237,7 @@ var registerTestHandlers = function(eventTestConfig, readableIDToID) { } return nodeConfig.returnVal; }.bind(null, readableID, nodeConfig); - EventPluginHub.putListener(id, registrationName, handler); + EventPluginHub.putListener(idToInstance[id], registrationName, handler); } }; /*eslint-enable no-loop-func, no-shadow */ @@ -296,8 +290,7 @@ var run = function(config, hierarchyConfig, nativeEventConfig) { // Trigger the event var extractedEvents = ResponderEventPlugin.extractEvents( nativeEventConfig.topLevelType, - nativeEventConfig.target, - nativeEventConfig.targetID, + nativeEventConfig.targetInst, nativeEventConfig.nativeEvent, nativeEventConfig.target ); @@ -316,6 +309,16 @@ var run = function(config, hierarchyConfig, nativeEventConfig) { ); // +1 for extra ++ }; +var GRANDPARENT_ID = '.0'; +var PARENT_ID = '.0.0'; +var CHILD_ID = '.0.0.0'; +var CHILD_ID2 = '.0.0.1'; + +var idToInstance = {}; +[GRANDPARENT_ID, PARENT_ID, CHILD_ID, CHILD_ID2].forEach(function(id) { + idToInstance[id] = {_rootNodeID: id}; +}); + var three = { grandParent: GRANDPARENT_ID, parent: PARENT_ID, @@ -338,16 +341,44 @@ describe('ResponderEventPlugin', function() { ReactInstanceHandles = require('ReactInstanceHandles'); ResponderEventPlugin = require('ResponderEventPlugin'); - EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles); + EventPluginUtils.injection.injectComponentTree({ + getInstanceFromNode: function(id) { + return idToInstance[id]; + }, + getNodeFromInstance: function(inst) { + return inst._rootNodeID; + }, + }); - // Only needed because SyntheticEvent supports the `currentTarget` - // property. - EventPluginUtils.injection.injectMount({ - getNode: function(id) { - return id; + EventPluginUtils.injection.injectTreeTraversal({ + isAncestor: function(a, b) { + return ReactInstanceHandles.isAncestorIDOf( + a._rootNodeID, + b._rootNodeID + ); + }, + getLowestCommonAncestor: function(a, b) { + if (!a || !b) { + return null; + } + var commonID = ReactInstanceHandles.getFirstCommonAncestorID( + a._rootNodeID, + b._rootNodeID + ); + return idToInstance[commonID] || null; }, - getID: function(nodeHandle) { - return nodeHandle; + getParentInstance: function(inst) { + var id = inst._rootNodeID; + var parentID = id.substr(0, id.lastIndexOf('.')); + return idToInstance[parentID] || null; + }, + traverseTwoPhase: function(target, fn, arg) { + ReactInstanceHandles.traverseTwoPhase( + target._rootNodeID, + function(id, upwards) { + fn(idToInstance[id], upwards, arg); + } + ); }, }); @@ -363,12 +394,12 @@ describe('ResponderEventPlugin', function() { config.startShouldSetResponder.bubbled.parent = {order: 4, returnVal: false}; config.startShouldSetResponder.bubbled.grandParent = {order: 5, returnVal: false}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); // Now no handlers should be called on `touchEnd`. config = oneEventLoopTestConfig(three); run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); @@ -384,13 +415,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.grandParent = {order: 1}; config.responderStart.grandParent = {order: 2}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; config.responderRelease.grandParent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); it('should grant responder parent while capturing', () => { @@ -400,13 +431,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.parent = {order: 2}; config.responderStart.parent = {order: 3}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); config = oneEventLoopTestConfig(three); config.responderEnd.parent = {order: 0}; config.responderRelease.parent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); it('should grant responder child while capturing', () => { @@ -417,13 +448,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.child = {order: 3}; config.responderStart.child = {order: 4}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); config = oneEventLoopTestConfig(three); config.responderEnd.child = {order: 0}; config.responderRelease.child = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); it('should grant responder child while bubbling', () => { @@ -435,13 +466,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.child = {order: 4}; config.responderStart.child = {order: 5}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); config = oneEventLoopTestConfig(three); config.responderEnd.child = {order: 0}; config.responderRelease.child = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); it('should grant responder parent while bubbling', () => { @@ -454,13 +485,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.parent = {order: 5}; config.responderStart.parent = {order: 6}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); config = oneEventLoopTestConfig(three); config.responderEnd.parent = {order: 0}; config.responderRelease.parent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); it('should grant responder grandParent while bubbling', () => { @@ -474,13 +505,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.grandParent = {order: 6}; config.responderStart.grandParent = {order: 7}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; config.responderRelease.grandParent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); @@ -506,13 +537,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.grandParent = {order: 1}; config.responderMove.grandParent = {order: 2}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; config.responderRelease.grandParent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); it('should grant responder parent while capturing move', () => { @@ -532,13 +563,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.parent = {order: 2}; config.responderMove.parent = {order: 3}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); config = oneEventLoopTestConfig(three); config.responderEnd.parent = {order: 0}; config.responderRelease.parent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); it('should grant responder child while capturing move', () => { @@ -559,13 +590,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.child = {order: 3}; config.responderMove.child = {order: 4}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); config = oneEventLoopTestConfig(three); config.responderEnd.child = {order: 0}; config.responderRelease.child = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); it('should grant responder child while bubbling move', () => { @@ -587,13 +618,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.child = {order: 4}; config.responderMove.child = {order: 5}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); config = oneEventLoopTestConfig(three); config.responderEnd.child = {order: 0}; config.responderRelease.child = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); it('should grant responder parent while bubbling move', () => { @@ -616,13 +647,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.parent = {order: 5}; config.responderMove.parent = {order: 6}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); config = oneEventLoopTestConfig(three); config.responderEnd.parent = {order: 0}; config.responderRelease.parent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); it('should grant responder grandParent while bubbling move', () => { @@ -646,13 +677,13 @@ describe('ResponderEventPlugin', function() { config.responderGrant.grandParent = {order: 6}; config.responderMove.grandParent = {order: 7}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; config.responderRelease.grandParent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); @@ -668,9 +699,9 @@ describe('ResponderEventPlugin', function() { config.responderGrant.parent = {order: 2}; config.responderStart.parent = {order: 3}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); - // While `PARENT_ID` is still responder, we create new handlers that verify + // While `parent` is still responder, we create new handlers that verify // the ordering of propagation, restarting the count at `0`. config = oneEventLoopTestConfig(three); config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false}; @@ -678,13 +709,13 @@ describe('ResponderEventPlugin', function() { config.startShouldSetResponder.bubbled.grandParent = {order: 1, returnVal: false}; config.responderStart.parent = {order: 2}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); config = oneEventLoopTestConfig(three); config.responderEnd.parent = {order: 0}; config.responderRelease.parent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); it('should bubble negotiation to first common ancestor of responder then transfer', () => { @@ -694,7 +725,7 @@ describe('ResponderEventPlugin', function() { config.responderGrant.parent = {order: 2}; config.responderStart.parent = {order: 3}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); config = oneEventLoopTestConfig(three); @@ -705,19 +736,19 @@ describe('ResponderEventPlugin', function() { config.responderTerminate.parent = {order: 3}; config.responderStart.grandParent = {order: 4}; run(config, three, startConfig(three.child, [three.child, three.child], [1])); - expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; // one remains\ /one ended \ run(config, three, endConfig(three.child, [three.child, three.child], [1])); - expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID); + expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; config.responderRelease.grandParent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); /** @@ -733,7 +764,7 @@ describe('ResponderEventPlugin', function() { config.startShouldSetResponder.bubbled.grandParent = {order: 3, returnVal: false}; run(config, three, startConfig(three.parent, [three.parent], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); config = oneEventLoopTestConfig(three); @@ -749,7 +780,7 @@ describe('ResponderEventPlugin', function() { config.responderStart.child = {order: 5}; // / Two active touches \ /one of them new\ run(config, three, startConfig(three.child, [three.parent, three.child], [1])); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); // Now we remove the original first touch, keeping the second touch that @@ -759,7 +790,7 @@ describe('ResponderEventPlugin', function() { config.responderEnd.child = {order: 0}; // / one ended\ /one remains \ run(config, three, endConfig(three.child, [three.parent, three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); // Okay, now let's add back that first touch (nothing should change) and // then we'll try peeling back the touches in the opposite order to make @@ -775,7 +806,7 @@ describe('ResponderEventPlugin', function() { config.responderStart.child = {order: 4}; // / Two active touches \ /one of them new\ run(config, three, startConfig(three.parent, [three.child, three.parent], [1])); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); // Now, move that new touch that had no effect, and did not start within @@ -790,7 +821,7 @@ describe('ResponderEventPlugin', function() { config.responderMove.child = {order: 4}; // / Two active touches \ /one of them moved\ run(config, three, moveConfig(three.parent, [three.child, three.parent], [1])); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); config = oneEventLoopTestConfig(three); @@ -798,7 +829,7 @@ describe('ResponderEventPlugin', function() { config.responderRelease.child = {order: 1}; // /child end \ /parent remain\ run(config, three, endConfig(three.child, [three.child, three.parent], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); @@ -816,7 +847,7 @@ describe('ResponderEventPlugin', function() { config.responderStart.childOne = {order: 4}; run(config, siblings, startConfig(siblings.childOne, [siblings.childOne], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne); + expect(ResponderEventPlugin._getResponderID()).toBe(siblings.childOne); // If the touch target is the sibling item, the negotiation should only // propagate to first common ancestor of current responder and sibling (so @@ -829,7 +860,7 @@ describe('ResponderEventPlugin', function() { var touchConfig = startConfig(siblings.childTwo, [siblings.childOne, siblings.childTwo], [1]); run(config, siblings, touchConfig); - expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne); + expect(ResponderEventPlugin._getResponderID()).toBe(siblings.childOne); // move childOne @@ -838,7 +869,7 @@ describe('ResponderEventPlugin', function() { config.moveShouldSetResponder.bubbled.parent = {order: 1, returnVal: false}; config.responderMove.childOne = {order: 2}; run(config, siblings, moveConfig(siblings.childOne, [siblings.childOne, siblings.childTwo], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne); + expect(ResponderEventPlugin._getResponderID()).toBe(siblings.childOne); // move childTwo: Only negotiates to `parent`. config = oneEventLoopTestConfig(siblings); @@ -846,7 +877,7 @@ describe('ResponderEventPlugin', function() { config.moveShouldSetResponder.bubbled.parent = {order: 1, returnVal: false}; config.responderMove.childOne = {order: 2}; run(config, siblings, moveConfig(siblings.childTwo, [siblings.childOne, siblings.childTwo], [1])); - expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne); + expect(ResponderEventPlugin._getResponderID()).toBe(siblings.childOne); }); @@ -862,7 +893,7 @@ describe('ResponderEventPlugin', function() { config.responderStart.child = {order: 5}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); // Suppose parent wants to become responder on move, and is rejected config = oneEventLoopTestConfig(three); @@ -877,7 +908,7 @@ describe('ResponderEventPlugin', function() { var touchConfig = moveConfig(three.child, [three.child], [0]); run(config, three, touchConfig); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); config = oneEventLoopTestConfig(three); config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false}; @@ -891,7 +922,7 @@ describe('ResponderEventPlugin', function() { touchConfig = startConfig(three.child, [three.child, three.child], [1]); run(config, three, touchConfig); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); }); @@ -907,7 +938,7 @@ describe('ResponderEventPlugin', function() { config.responderStart.child = {order: 5}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); // If the touch target is the sibling item, the negotiation should only // propagate to first common ancestor of current responder and sibling (so @@ -921,11 +952,10 @@ describe('ResponderEventPlugin', function() { run(config, three, { topLevelType: topLevelTypes.topScroll, - target: three.parent, - targetID: three.parent, + targetInst: idToInstance[three.parent], nativeEvent: {}, }); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); // Now lets let the scroll take control this time. @@ -939,11 +969,10 @@ describe('ResponderEventPlugin', function() { run(config, three, { topLevelType: topLevelTypes.topScroll, - target: three.parent, - targetID: three.parent, + targetInst: idToInstance[three.parent], nativeEvent: {}, }); - expect(ResponderEventPlugin.getResponderID()).toBe(three.parent); + expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); }); @@ -959,7 +988,7 @@ describe('ResponderEventPlugin', function() { config.responderStart.child = {order: 5}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin.getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponderID()).toBe(three.child); config = oneEventLoopTestConfig(three); config.responderEnd.child = {order: 0}; @@ -972,6 +1001,6 @@ describe('ResponderEventPlugin', function() { [0] ); run(config, three, nativeEvent); - expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponderID()).toBe(null); }); }); diff --git a/src/renderers/shared/reconciler/ReactEventEmitterMixin.js b/src/renderers/shared/reconciler/ReactEventEmitterMixin.js index 6ab34f00500a8..9295e2675e8b8 100644 --- a/src/renderers/shared/reconciler/ReactEventEmitterMixin.js +++ b/src/renderers/shared/reconciler/ReactEventEmitterMixin.js @@ -23,22 +23,15 @@ var ReactEventEmitterMixin = { /** * Streams a fired top-level event to `EventPluginHub` where plugins have the * opportunity to create `ReactEvent`s to be dispatched. - * - * @param {string} topLevelType Record from `EventConstants`. - * @param {object} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native environment event. */ handleTopLevel: function( topLevelType, - topLevelTarget, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget) { var events = EventPluginHub.extractEvents( topLevelType, - topLevelTarget, - topLevelTargetID, + targetInst, nativeEvent, nativeEventTarget ); diff --git a/src/renderers/shared/reconciler/ReactMultiChild.js b/src/renderers/shared/reconciler/ReactMultiChild.js index 31c073168c7b7..bfb9bda1f45b0 100644 --- a/src/renderers/shared/reconciler/ReactMultiChild.js +++ b/src/renderers/shared/reconciler/ReactMultiChild.js @@ -51,15 +51,15 @@ var markupQueue = []; /** * Enqueues markup to be rendered and inserted at a supplied index. * - * @param {string} parentID ID of the parent component. + * @param {object} parentInst parent component. * @param {string} markup Markup that renders into an element. * @param {number} toIndex Destination index. * @private */ -function enqueueInsertMarkup(parentID, markup, toIndex) { +function enqueueInsertMarkup(parentInst, markup, toIndex) { // NOTE: Null values reduce hidden classes. updateQueue.push({ - parentID: parentID, + parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.INSERT_MARKUP, markupIndex: markupQueue.push(markup) - 1, @@ -72,15 +72,15 @@ function enqueueInsertMarkup(parentID, markup, toIndex) { /** * Enqueues moving an existing element to another index. * - * @param {string} parentID ID of the parent component. + * @param {object} parentInst parent component. * @param {number} fromIndex Source index of the existing element. * @param {number} toIndex Destination index of the element. * @private */ -function enqueueMove(parentID, fromIndex, toIndex) { +function enqueueMove(parentInst, fromIndex, toIndex) { // NOTE: Null values reduce hidden classes. updateQueue.push({ - parentID: parentID, + parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.MOVE_EXISTING, markupIndex: null, @@ -93,14 +93,14 @@ function enqueueMove(parentID, fromIndex, toIndex) { /** * Enqueues removing an element at an index. * - * @param {string} parentID ID of the parent component. + * @param {object} parentInst parent component. * @param {number} fromIndex Index of the element to remove. * @private */ -function enqueueRemove(parentID, fromIndex) { +function enqueueRemove(parentInst, fromIndex) { // NOTE: Null values reduce hidden classes. updateQueue.push({ - parentID: parentID, + parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.REMOVE_NODE, markupIndex: null, @@ -113,14 +113,14 @@ function enqueueRemove(parentID, fromIndex) { /** * Enqueues setting the markup of a node. * - * @param {string} parentID ID of the parent component. + * @param {object} parentInst parent component. * @param {string} markup Markup that renders into an element. * @private */ -function enqueueSetMarkup(parentID, markup) { +function enqueueSetMarkup(parentInst, markup) { // NOTE: Null values reduce hidden classes. updateQueue.push({ - parentID: parentID, + parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.SET_MARKUP, markupIndex: null, @@ -133,14 +133,14 @@ function enqueueSetMarkup(parentID, markup) { /** * Enqueues setting the text content. * - * @param {string} parentID ID of the parent component. + * @param {object} parentInst parent component. * @param {string} textContent Text content to set. * @private */ -function enqueueTextContent(parentID, textContent) { +function enqueueTextContent(parentInst, textContent) { // NOTE: Null values reduce hidden classes. updateQueue.push({ - parentID: parentID, + parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.TEXT_CONTENT, markupIndex: null, @@ -371,7 +371,6 @@ var ReactMultiChild = { var nextChildren = this._reconcilerUpdateChildren( prevChildren, nextNestedChildrenElements, transaction, context ); - this._renderedChildren = nextChildren; if (!nextChildren && !prevChildren) { return; } @@ -410,6 +409,7 @@ var ReactMultiChild = { this._unmountChild(prevChildren[name]); } } + this._renderedChildren = nextChildren; }, /** @@ -424,6 +424,14 @@ var ReactMultiChild = { this._renderedChildren = null; }, + /** + * Hook used by the DOM implementation to precache the nodes before we apply + * any reorders here. + */ + prepareToManageChildren: function() { + // TODO: This sucks. Figure out a better design here. + }, + /** * Moves a child component to the supplied index. * @@ -437,7 +445,8 @@ var ReactMultiChild = { // be moved. Otherwise, we do not need to move it because a child will be // inserted or moved before `child`. if (child._mountIndex < lastIndex) { - enqueueMove(this._rootNodeID, child._mountIndex, toIndex); + this.prepareToManageChildren(); + enqueueMove(this, child._mountIndex, toIndex); } }, @@ -449,7 +458,8 @@ var ReactMultiChild = { * @protected */ createChild: function(child, mountImage) { - enqueueInsertMarkup(this._rootNodeID, mountImage, child._mountIndex); + this.prepareToManageChildren(); + enqueueInsertMarkup(this, mountImage, child._mountIndex); }, /** @@ -459,7 +469,8 @@ var ReactMultiChild = { * @protected */ removeChild: function(child) { - enqueueRemove(this._rootNodeID, child._mountIndex); + this.prepareToManageChildren(); + enqueueRemove(this, child._mountIndex); }, /** @@ -469,7 +480,7 @@ var ReactMultiChild = { * @protected */ setTextContent: function(textContent) { - enqueueTextContent(this._rootNodeID, textContent); + enqueueTextContent(this, textContent); }, /** @@ -479,7 +490,7 @@ var ReactMultiChild = { * @protected */ setMarkup: function(markup) { - enqueueSetMarkup(this._rootNodeID, markup); + enqueueSetMarkup(this, markup); }, /** diff --git a/src/renderers/shared/reconciler/ReactUpdates.js b/src/renderers/shared/reconciler/ReactUpdates.js index 479488b8edcf9..841213f7c1816 100644 --- a/src/renderers/shared/reconciler/ReactUpdates.js +++ b/src/renderers/shared/reconciler/ReactUpdates.js @@ -68,8 +68,9 @@ function ReactUpdatesFlushTransaction() { this.reinitializeTransaction(); this.dirtyComponentsLength = null; this.callbackQueue = CallbackQueue.getPooled(); - this.reconcileTransaction = - ReactUpdates.ReactReconcileTransaction.getPooled(/* forceHTML */ false); + this.reconcileTransaction = ReactUpdates.ReactReconcileTransaction.getPooled( + /* useCreateElement */ true + ); } assign( diff --git a/src/renderers/shared/reconciler/__tests__/ReactCompositeComponent-test.js b/src/renderers/shared/reconciler/__tests__/ReactCompositeComponent-test.js index 298c5a1be2277..c5ff51e1dfe2e 100644 --- a/src/renderers/shared/reconciler/__tests__/ReactCompositeComponent-test.js +++ b/src/renderers/shared/reconciler/__tests__/ReactCompositeComponent-test.js @@ -16,7 +16,6 @@ var MorphingComponent; var React; var ReactDOM; var ReactCurrentOwner; -var ReactMount; var ReactPropTypes; var ReactServerRendering; var ReactTestUtils; @@ -34,7 +33,6 @@ describe('ReactCompositeComponent', function() { ReactCurrentOwner = require('ReactCurrentOwner'); ReactPropTypes = require('ReactPropTypes'); ReactTestUtils = require('ReactTestUtils'); - ReactMount = require('ReactMount'); ReactServerRendering = require('ReactServerRendering'); ReactUpdates = require('ReactUpdates'); @@ -487,8 +485,6 @@ describe('ReactCompositeComponent', function() { var container = document.createElement('div'); var innerUnmounted = false; - spyOn(ReactMount, 'purgeID').andCallThrough(); - var Component = React.createClass({ render: function() { return ( @@ -501,11 +497,6 @@ describe('ReactCompositeComponent', function() { }); var Inner = React.createClass({ componentWillUnmount: function() { - // It's important that ReactMount.purgeID is called after any component - // lifecycle methods, because a componentWillUnmount implementation is - // likely to call ReactDOM.findDOMNode(this), which will repopulate the - // node cache after it's been cleared, causing a memory leak. - expect(ReactMount.purgeID.calls.length).toBe(0); innerUnmounted = true; }, render: function() { @@ -516,12 +507,6 @@ describe('ReactCompositeComponent', function() { ReactDOM.render(, container); ReactDOM.unmountComponentAtNode(container); expect(innerUnmounted).toBe(true); - - // The text and both
elements and their wrappers each call - // unmountIDFromEnvironment which calls purgeID, for a total of 3. - // TODO: Test the effect of this. E.g. does the node cache get repopulated - // after a getDOMNode call? - expect(ReactMount.purgeID.calls.length).toBe(3); }); it('should warn when shouldComponentUpdate() returns undefined', function() { diff --git a/src/renderers/shared/reconciler/__tests__/ReactEmptyComponent-test.js b/src/renderers/shared/reconciler/__tests__/ReactEmptyComponent-test.js index c19863f65ce67..cf88eaeba3ba1 100644 --- a/src/renderers/shared/reconciler/__tests__/ReactEmptyComponent-test.js +++ b/src/renderers/shared/reconciler/__tests__/ReactEmptyComponent-test.js @@ -18,6 +18,8 @@ var TogglingComponent; var reactComponentExpect; +var log; + describe('ReactEmptyComponent', function() { beforeEach(function() { jest.resetModuleRegistry(); @@ -28,16 +30,18 @@ describe('ReactEmptyComponent', function() { reactComponentExpect = require('reactComponentExpect'); + log = jasmine.createSpy(); + TogglingComponent = React.createClass({ getInitialState: function() { return {component: this.props.firstComponent}; }, componentDidMount: function() { - console.log(ReactDOM.findDOMNode(this)); + log(ReactDOM.findDOMNode(this)); this.setState({component: this.props.secondComponent}); }, componentDidUpdate: function() { - console.log(ReactDOM.findDOMNode(this)); + log(ReactDOM.findDOMNode(this)); }, render: function() { var Component = this.state.component; @@ -81,8 +85,6 @@ describe('ReactEmptyComponent', function() { }); it('should be able to switch between rendering null and a normal tag', () => { - spyOn(console, 'log'); - var instance1 = { - spyOn(console, 'log'); - var instance1 = { - spyOn(console, 'log'); - var GrandChild = React.createClass({ render: function() { return null; @@ -169,11 +167,11 @@ describe('ReactEmptyComponent', function() { ReactTestUtils.renderIntoDocument(instance2); }).not.toThrow(); - expect(console.log.argsForCall.length).toBe(4); - expect(console.log.argsForCall[0][0].tagName).toBe('DIV'); - expect(console.log.argsForCall[1][0]).toBe(null); - expect(console.log.argsForCall[2][0]).toBe(null); - expect(console.log.argsForCall[3][0].tagName).toBe('DIV'); + expect(log.argsForCall.length).toBe(4); + expect(log.argsForCall[0][0].tagName).toBe('DIV'); + expect(log.argsForCall[1][0]).toBe(null); + expect(log.argsForCall[2][0]).toBe(null); + expect(log.argsForCall[3][0].tagName).toBe('DIV'); } ); diff --git a/src/renderers/shared/reconciler/__tests__/ReactIdentity-test.js b/src/renderers/shared/reconciler/__tests__/ReactIdentity-test.js index f0b6b5bd9ff8e..2639ffb2d3acb 100644 --- a/src/renderers/shared/reconciler/__tests__/ReactIdentity-test.js +++ b/src/renderers/shared/reconciler/__tests__/ReactIdentity-test.js @@ -13,9 +13,9 @@ var React; var ReactDOM; +var ReactDOMComponentTree; var ReactFragment; var ReactTestUtils; -var ReactMount; describe('ReactIdentity', function() { @@ -23,14 +23,16 @@ describe('ReactIdentity', function() { jest.resetModuleRegistry(); React = require('React'); ReactDOM = require('ReactDOM'); + ReactDOMComponentTree = require('ReactDOMComponentTree'); ReactFragment = require('ReactFragment'); ReactTestUtils = require('ReactTestUtils'); - ReactMount = require('ReactMount'); }); var idExp = /^\.[^.]+(.*)$/; function checkID(child, expectedID) { - var actual = idExp.exec(ReactMount.getID(child)); + // TODO: Node IDs are not public API; rewrite these tests. + var rootID = ReactDOMComponentTree.getInstanceFromNode(child)._rootNodeID; + var actual = idExp.exec(rootID); var expected = idExp.exec(expectedID); expect(actual).toBeTruthy(); expect(expected).toBeTruthy(); @@ -297,14 +299,16 @@ describe('ReactIdentity', function() { var wrapped = ; wrapped = ReactDOM.render(wrapped, document.createElement('div')); + var div = ReactDOM.findDOMNode(wrapped); - var beforeID = ReactMount.getID(ReactDOM.findDOMNode(wrapped).firstChild); - + var beforeA = div.childNodes[0]; + var beforeB = div.childNodes[1]; wrapped.swap(); + var afterA = div.childNodes[1]; + var afterB = div.childNodes[0]; - var afterID = ReactMount.getID(ReactDOM.findDOMNode(wrapped).firstChild); - - expect(beforeID).not.toEqual(afterID); + expect(beforeA).toBe(afterA); + expect(beforeB).toBe(afterB); }); diff --git a/src/renderers/shared/reconciler/__tests__/ReactInstanceHandles-test.js b/src/renderers/shared/reconciler/__tests__/ReactInstanceHandles-test.js index c31af66422351..17bb0b8f157e3 100644 --- a/src/renderers/shared/reconciler/__tests__/ReactInstanceHandles-test.js +++ b/src/renderers/shared/reconciler/__tests__/ReactInstanceHandles-test.js @@ -13,7 +13,6 @@ var React = require('React'); var ReactTestUtils = require('ReactTestUtils'); -var ReactMount = require('ReactMount'); /** * Ensure that all callbacks are invoked, passing this unique argument. @@ -74,89 +73,6 @@ describe('ReactInstanceHandles', function() { aggregatedArgs = []; }); - describe('findComponentRoot', function() { - it('should find the correct node with prefix sibling IDs', function() { - var parentNode = ReactTestUtils.renderIntoDocument( -
-
- {[
]} -
- ); - var childNodeB = parentNode.childNodes[1]; - - expect( - ReactMount.getNode( - getNodeID(childNodeB) - ) - ).toBe(childNodeB); - }); - - it('should work around unidentified nodes', function() { - var parentNode = ReactTestUtils.renderIntoDocument( -
- {[
]} -
- ); - var childNodeB = parentNode.childNodes[0]; - - // No ID on `childNodeA`. - var childNodeA = document.createElement('div'); - parentNode.insertBefore(childNodeA, childNodeB); - - expect( - ReactMount.getNode( - getNodeID(childNodeB) - ) - ).toBe(childNodeB); - }); - - it('should throw if a rendered element cannot be found', function() { - spyOn(console, 'error'); - var parentNode = ReactTestUtils.renderIntoDocument( -
- -
- ); - var childNodeA = parentNode.childNodes[0]; - var childNodeB; - if (childNodeA.tagName === 'TR') { - childNodeB = childNodeA; - // No ID on `childNodeA`, it was "rendered by the browser". - childNodeA = document.createElement('tbody'); - childNodeA.appendChild(childNodeB); - parentNode.appendChild(childNodeA); - } else { - childNodeB = childNodeA.childNodes[0]; - } - expect(childNodeA.tagName).toBe('TBODY'); - - expect( - ReactMount.getNode( - getNodeID(childNodeB) - ) - ).toBe(childNodeB); - - var junkID = getNodeID(childNodeB) + ':junk'; - expect(function() { - ReactMount.getNode( - junkID - ); - }).toThrow( - 'findComponentRoot(..., ' + junkID + '): Unable to find element. ' + - 'This probably means the DOM was unexpectedly mutated (e.g., by the ' + - 'browser), usually due to forgetting a when using tables, ' + - 'nesting tags like ,

, or , or using non-SVG elements in ' + - 'an parent. Try inspecting the child nodes of the element with ' + - 'React ID ``.' - ); - - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - '
cannot appear as a child of
' - ); - }); - }); - describe('getReactRootIDFromNodeID', function() { it('should support strings', function() { var test = '.s_0_1.0..1'; @@ -216,6 +132,51 @@ describe('ReactInstanceHandles', function() { }); }); + describe('traverseTwoPhaseSkipTarget', function() { + it('should not traverse when traversing outside DOM', function() { + var targetID = ''; + var expectedAggregation = []; + ReactInstanceHandles.traverseTwoPhaseSkipTarget( + targetID, + argAggregator, + ARG + ); + expect(aggregatedArgs).toEqual(expectedAggregation); + }); + + it('should traverse two phase across component boundary', function() { + var parent = renderParentIntoDocument(); + var targetID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1); + var expectedAggregation = [ + {id: getNodeID(parent.refs.P), isUp: false, arg: ARG}, + {id: getNodeID(parent.refs.P_P1), isUp: false, arg: ARG}, + {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: false, arg: ARG}, + + {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: true, arg: ARG}, + {id: getNodeID(parent.refs.P_P1), isUp: true, arg: ARG}, + {id: getNodeID(parent.refs.P), isUp: true, arg: ARG}, + ]; + ReactInstanceHandles.traverseTwoPhaseSkipTarget( + targetID, + argAggregator, + ARG + ); + expect(aggregatedArgs).toEqual(expectedAggregation); + }); + + it('should traverse two phase at shallowest node', function() { + var parent = renderParentIntoDocument(); + var targetID = getNodeID(parent.refs.P); + var expectedAggregation = []; + ReactInstanceHandles.traverseTwoPhaseSkipTarget( + targetID, + argAggregator, + ARG + ); + expect(aggregatedArgs).toEqual(expectedAggregation); + }); + }); + describe('traverseEnterLeave', function() { it('should not traverse when enter/leaving outside DOM', function() { var targetID = ''; diff --git a/src/renderers/shared/reconciler/__tests__/ReactMultiChild-test.js b/src/renderers/shared/reconciler/__tests__/ReactMultiChild-test.js index fbe46a69f2748..f06e242f2e3ff 100644 --- a/src/renderers/shared/reconciler/__tests__/ReactMultiChild-test.js +++ b/src/renderers/shared/reconciler/__tests__/ReactMultiChild-test.js @@ -149,62 +149,4 @@ describe('ReactMultiChild', function() { expect(mockUnmount.mock.calls.length).toBe(1); }); }); - - describe('innerHTML', function() { - var setInnerHTML; - - // Only run this suite if `Element.prototype.innerHTML` can be spied on. - var innerHTMLDescriptor = Object.getOwnPropertyDescriptor( - Element.prototype, - 'innerHTML' - ); - if (!innerHTMLDescriptor) { - return; - } - - beforeEach(function() { - var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); - ReactDOMFeatureFlags.useCreateElement = false; - - Object.defineProperty(Element.prototype, 'innerHTML', { - set: setInnerHTML = jasmine.createSpy().andCallFake( - innerHTMLDescriptor.set - ), - }); - }); - - it('should only set `innerHTML` once on update', function() { - var container = document.createElement('div'); - - ReactDOM.render( -
-

-

-

-
, - container - ); - // Warm the cache used by `getMarkupWrap`. - ReactDOM.render( -
-

-

-

-
, - container - ); - expect(setInnerHTML).toHaveBeenCalled(); - var callCountOnMount = setInnerHTML.calls.length; - - ReactDOM.render( -
-

-

-

-
, - container - ); - expect(setInnerHTML.calls.length).toBe(callCountOnMount + 1); - }); - }); }); diff --git a/src/renderers/shared/reconciler/__tests__/ReactMultiChildReconcile-test.js b/src/renderers/shared/reconciler/__tests__/ReactMultiChildReconcile-test.js index 778aeda9aa20e..5ca3e4afb17c8 100644 --- a/src/renderers/shared/reconciler/__tests__/ReactMultiChildReconcile-test.js +++ b/src/renderers/shared/reconciler/__tests__/ReactMultiChildReconcile-test.js @@ -13,8 +13,8 @@ var React = require('React'); var ReactDOM = require('ReactDOM'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactInstanceMap = require('ReactInstanceMap'); -var ReactMount = require('ReactMount'); var mapObject = require('mapObject'); @@ -190,7 +190,8 @@ function verifyDomOrderingAccurate(parentInstance, statusDisplays) { var i; var orderedDomIDs = []; for (i = 0; i < statusDisplayNodes.length; i++) { - orderedDomIDs.push(ReactMount.getID(statusDisplayNodes[i])); + var inst = ReactDOMComponentTree.getInstanceFromNode(statusDisplayNodes[i]); + orderedDomIDs.push(inst._rootNodeID); } var orderedLogicalIDs = []; diff --git a/src/test/ReactDefaultPerf.js b/src/test/ReactDefaultPerf.js index 49041637b2efd..7fdc6e518e773 100644 --- a/src/test/ReactDefaultPerf.js +++ b/src/test/ReactDefaultPerf.js @@ -13,6 +13,7 @@ 'use strict'; var DOMProperty = require('DOMProperty'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactDefaultPerfAnalysis = require('ReactDefaultPerfAnalysis'); var ReactMount = require('ReactMount'); var ReactPerf = require('ReactPerf'); @@ -175,8 +176,7 @@ var ReactDefaultPerf = { totalTime = performanceNow() - start; if (fnName === '_mountImageIntoNode') { - var mountID = ReactMount.getID(args[1]); - ReactDefaultPerf._recordWrite(mountID, fnName, totalTime, args[0]); + ReactDefaultPerf._recordWrite('', fnName, totalTime, args[0]); } else if (fnName === 'dangerouslyProcessChildrenUpdates') { // special format args[0].forEach(function(update) { @@ -194,7 +194,7 @@ var ReactDefaultPerf = { writeArgs.markup = args[1][update.markupIndex]; } ReactDefaultPerf._recordWrite( - update.parentID, + update.parentInst._rootNodeID, update.type, totalTime, writeArgs @@ -203,8 +203,10 @@ var ReactDefaultPerf = { } else { // basic format var id = args[0]; - if (typeof id === 'object') { - id = ReactMount.getID(args[0]); + if (moduleName === 'EventPluginHub') { + id = id._rootNodeID; + } else if (typeof id === 'object') { + id = ReactDOMComponentTree.getInstanceFromNode(args[0])._rootNodeID; } ReactDefaultPerf._recordWrite( id, diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index 7efb77da1043d..464502638a962 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -18,12 +18,12 @@ var EventPluginRegistry = require('EventPluginRegistry'); var EventPropagators = require('EventPropagators'); var React = require('React'); var ReactDOM = require('ReactDOM'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactElement = require('ReactElement'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); var ReactCompositeComponent = require('ReactCompositeComponent'); var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactInstanceMap = require('ReactInstanceMap'); -var ReactMount = require('ReactMount'); var ReactUpdates = require('ReactUpdates'); var SyntheticEvent = require('SyntheticEvent'); @@ -432,7 +432,7 @@ ReactShallowRenderer.prototype.render = function(element, context) { if (!context) { context = emptyObject; } - var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(false); + var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(true); transaction.perform(this._render, this, element, transaction, context); ReactUpdates.ReactReconcileTransaction.release(transaction); }; @@ -490,7 +490,7 @@ function makeSimulator(eventType) { // properly destroying any properties assigned from `eventData` upon release var event = new SyntheticEvent( dispatchConfig, - ReactMount.getID(node), + ReactDOMComponentTree.getInstanceFromNode(node), fakeNativeEvent, node );