From 2ee66262db9fb91b0d0e9d64cefeb223074f8e05 Mon Sep 17 00:00:00 2001 From: Jordan Walke Date: Wed, 24 Jul 2013 17:19:36 -0700 Subject: [PATCH] Remove circular dependencies in React Core. There is a circular dependency between `ReactID`, `ReactMount` and `ReactInstanceHandles`. Ben and I talked about this today. It seems like the simplest solution is to consolidate a lot of the code that Ben recently wrote into `ReactMount`. We can later find ways to trim code out of this module without causing circular deps. --- src/core/ReactComponent.js | 6 +- src/core/ReactDOMIDOperations.js | 16 +- src/core/ReactDefaultInjection.js | 3 + src/core/ReactEventEmitter.js | 9 +- src/core/ReactEventTopLevelCallback.js | 9 +- src/core/ReactID.js | 163 ----------- src/core/ReactInstanceHandles.js | 105 +------ src/core/ReactMount.js | 277 ++++++++++++++++-- src/core/ReactNativeComponent.js | 4 +- src/core/ReactTextComponent.js | 4 +- .../__tests__/ReactCompositeComponent-test.js | 6 +- src/core/__tests__/ReactDOM-test.js | 8 +- .../__tests__/ReactDOMIDOperations-test.js | 10 +- src/core/__tests__/ReactEventEmitter-test.js | 14 +- src/core/__tests__/ReactIdentity-test.js | 10 +- .../__tests__/ReactInstanceHandles-test.js | 34 +-- .../ReactMultiChildReconcile-test.js | 4 +- .../__tests__/ReactNativeComponent-test.js | 4 +- .../__tests__/ReactServerRendering-test.js | 22 +- src/eventPlugins/EnterLeaveEventPlugin.js | 9 +- src/test/ReactTestUtils.js | 6 +- 21 files changed, 356 insertions(+), 367 deletions(-) delete mode 100644 src/core/ReactID.js diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index 871abf625f..b08e4ff1b5 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -23,8 +23,8 @@ var getReactRootElementInContainer = require('getReactRootElementInContainer'); var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); -var ReactID = require('ReactID'); var ReactMarkupChecksum = require('ReactMarkupChecksum'); +var ReactMount = require('ReactMount'); var ReactOwner = require('ReactOwner'); var ReactReconcileTransaction = require('ReactReconcileTransaction'); var ReactUpdates = require('ReactUpdates'); @@ -256,7 +256,7 @@ var ReactComponent = { this.isMounted(), 'getDOMNode(): A component must be mounted to have a DOM node.' ); - return ReactID.getNode(this._rootNodeID); + return ReactMount.getNode(this._rootNodeID); }, /** @@ -382,7 +382,7 @@ var ReactComponent = { if (props.ref != null) { ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]); } - ReactID.purgeID(this._rootNodeID); + ReactMount.purgeID(this._rootNodeID); this._rootNodeID = null; this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; }, diff --git a/src/core/ReactDOMIDOperations.js b/src/core/ReactDOMIDOperations.js index c98edcbe8f..7f54ac9ee2 100644 --- a/src/core/ReactDOMIDOperations.js +++ b/src/core/ReactDOMIDOperations.js @@ -24,7 +24,7 @@ var CSSPropertyOperations = require('CSSPropertyOperations'); var DOMChildrenOperations = require('DOMChildrenOperations'); var DOMPropertyOperations = require('DOMPropertyOperations'); -var ReactID = require('ReactID'); +var ReactMount = require('ReactMount'); var getTextContentAccessor = require('getTextContentAccessor'); var invariant = require('invariant'); @@ -65,7 +65,7 @@ var ReactDOMIDOperations = { * @internal */ updatePropertyByID: function(id, name, value) { - var node = ReactID.getNode(id); + var node = ReactMount.getNode(id); invariant( !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), 'updatePropertyByID(...): %s', @@ -91,7 +91,7 @@ var ReactDOMIDOperations = { * @internal */ deletePropertyByID: function(id, name, value) { - var node = ReactID.getNode(id); + var node = ReactMount.getNode(id); invariant( !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), 'updatePropertyByID(...): %s', @@ -128,7 +128,7 @@ var ReactDOMIDOperations = { * @internal */ updateStylesByID: function(id, styles) { - var node = ReactID.getNode(id); + var node = ReactMount.getNode(id); CSSPropertyOperations.setValueForStyles(node, styles); }, @@ -140,7 +140,7 @@ var ReactDOMIDOperations = { * @internal */ updateInnerHTMLByID: function(id, html) { - var node = ReactID.getNode(id); + var node = ReactMount.getNode(id); // HACK: IE8- normalize whitespace in innerHTML, removing leading spaces. // @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html node.innerHTML = (html && html.__html || '').replace(/^ /g, ' '); @@ -154,7 +154,7 @@ var ReactDOMIDOperations = { * @internal */ updateTextContentByID: function(id, content) { - var node = ReactID.getNode(id); + var node = ReactMount.getNode(id); node[textContentAccessor] = content; }, @@ -167,7 +167,7 @@ var ReactDOMIDOperations = { * @see {Danger.dangerouslyReplaceNodeWithMarkup} */ dangerouslyReplaceNodeWithMarkupByID: function(id, markup) { - var node = ReactID.getNode(id); + var node = ReactMount.getNode(id); DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup); }, @@ -176,7 +176,7 @@ var ReactDOMIDOperations = { * Detect if any elements were removed instead of blindly purging. */ manageChildrenByParentID: function(parentID, domOperations) { - var parent = ReactID.getNode(parentID); + var parent = ReactMount.getNode(parentID); DOMChildrenOperations.manageChildren(parent, domOperations); } diff --git a/src/core/ReactDefaultInjection.js b/src/core/ReactDefaultInjection.js index d642a4daf8..7d858bb7c2 100644 --- a/src/core/ReactDefaultInjection.js +++ b/src/core/ReactDefaultInjection.js @@ -24,6 +24,8 @@ var ReactDOMInput = require('ReactDOMInput'); var ReactDOMOption = require('ReactDOMOption'); var ReactDOMSelect = require('ReactDOMSelect'); var ReactDOMTextarea = require('ReactDOMTextarea'); +var ReactEventEmitter = require('ReactEventEmitter'); +var ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); var DefaultDOMPropertyConfig = require('DefaultDOMPropertyConfig'); var DOMProperty = require('DOMProperty'); @@ -37,6 +39,7 @@ var SimpleEventPlugin = require('SimpleEventPlugin'); var MobileSafariClickEventPlugin = require('MobileSafariClickEventPlugin'); function inject() { + ReactEventEmitter.TopLevelCallbackCreator = ReactEventTopLevelCallback; /** * Inject module for resolving DOM hierarchy and plugin ordering. */ diff --git a/src/core/ReactEventEmitter.js b/src/core/ReactEventEmitter.js index 4f52e383d6..a5971ff789 100644 --- a/src/core/ReactEventEmitter.js +++ b/src/core/ReactEventEmitter.js @@ -257,17 +257,20 @@ var ReactEventEmitter = { * reason, and only in some cases). * * @param {boolean} touchNotMouse Listen to touch events instead of mouse. - * @param {object} TopLevelCallbackCreator */ - ensureListening: function(touchNotMouse, TopLevelCallbackCreator) { + ensureListening: function(touchNotMouse) { invariant( ExecutionEnvironment.canUseDOM, 'ensureListening(...): Cannot toggle event listening in a Worker ' + 'thread. This is likely a bug in the framework. Please report ' + 'immediately.' ); + invariant( + ReactEventEmitter.TopLevelCallbackCreator, + 'ensureListening(...): Cannot be called without a top level callback ' + + 'creator being injected.' + ); if (!_isListening) { - ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator; listenAtTopLevel(touchNotMouse); _isListening = true; } diff --git a/src/core/ReactEventTopLevelCallback.js b/src/core/ReactEventTopLevelCallback.js index 215d23da23..b3e93a8c62 100644 --- a/src/core/ReactEventTopLevelCallback.js +++ b/src/core/ReactEventTopLevelCallback.js @@ -21,8 +21,7 @@ var ExecutionEnvironment = require('ExecutionEnvironment'); var ReactEventEmitter = require('ReactEventEmitter'); -var ReactID = require('ReactID'); -var ReactInstanceHandles = require('ReactInstanceHandles'); +var ReactMount = require('ReactMount'); var getEventTarget = require('getEventTarget'); @@ -34,7 +33,7 @@ var _topLevelListenersEnabled = true; /** * Top-level callback creator used to implement event handling using delegation. - * This is used via dependency injection in `ReactEventEmitter.ensureListening`. + * This is used via dependency injection. */ var ReactEventTopLevelCallback = { @@ -73,10 +72,10 @@ var ReactEventTopLevelCallback = { nativeEvent.srcElement !== nativeEvent.target) { nativeEvent.target = nativeEvent.srcElement; } - var topLevelTarget = ReactInstanceHandles.getFirstReactDOM( + var topLevelTarget = ReactMount.getFirstReactDOM( getEventTarget(nativeEvent) ) || ExecutionEnvironment.global; - var topLevelTargetID = ReactID.getID(topLevelTarget) || ''; + var topLevelTargetID = ReactMount.getID(topLevelTarget) || ''; ReactEventEmitter.handleTopLevel( topLevelType, topLevelTarget, diff --git a/src/core/ReactID.js b/src/core/ReactID.js deleted file mode 100644 index 55bb526570..0000000000 --- a/src/core/ReactID.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule ReactID - * @typechecks static-only - */ - -"use strict"; - -var invariant = require('invariant'); -var ReactMount = require('ReactMount'); -var ATTR_NAME = 'data-reactid'; -var nodeCache = {}; - -/** - * 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) { - if (nodeCache.hasOwnProperty(id)) { - var cached = nodeCache[id]; - if (cached !== node) { - invariant( - !isValid(cached, id), - 'ReactID: Two valid but unequal nodes with the same `%s`: %s', - ATTR_NAME, id - ); - - nodeCache[id] = node; - } - } else { - 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 - // the empty string, as if the attribute were missing. - 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 React-generated DOM ID. - * - * @param {string} id A React-generated DOM ID. - * @return {DOMElement} DOM node with the suppled `id`. - * @internal - */ -function getNode(id) { - if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) { - nodeCache[id] = ReactMount.findReactNodeByID(id); - } - return nodeCache[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, - 'ReactID: Unexpected modification of `%s`', - ATTR_NAME - ); - - var container = ReactMount.findReactContainerForID(id); - if (container && contains(container, node)) { - return true; - } - } - - return false; -} - -function contains(ancestor, descendant) { - if (ancestor.contains) { - // Supported natively in virtually all browsers, but not in jsdom. - return ancestor.contains(descendant); - } - - if (descendant === ancestor) { - return true; - } - - if (descendant.nodeType === 3) { - // If descendant is a text node, start from descendant.parentNode - // instead, so that we can assume all ancestors worth considering are - // element nodes with nodeType === 1. - descendant = descendant.parentNode; - } - - while (descendant && descendant.nodeType === 1) { - if (descendant === ancestor) { - return true; - } - descendant = descendant.parentNode; - } - - 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]; -} - -exports.ATTR_NAME = ATTR_NAME; -exports.getID = getID; -exports.rawGetID = internalGetID; -exports.setID = setID; -exports.getNode = getNode; -exports.purgeID = purgeID; diff --git a/src/core/ReactInstanceHandles.js b/src/core/ReactInstanceHandles.js index e7b91401e2..15bfe216e2 100644 --- a/src/core/ReactInstanceHandles.js +++ b/src/core/ReactInstanceHandles.js @@ -19,8 +19,6 @@ "use strict"; -var ReactID = require('ReactID'); - var invariant = require('invariant'); var SEPARATOR = '.'; @@ -239,103 +237,6 @@ var ReactInstanceHandles = { ); }, - /** - * True if the supplied `node` is rendered by React. - * - * @param {*} node DOM Element to check. - * @return {boolean} True if the DOM Element appears to be rendered by React. - * @internal - */ - isRenderedByReact: function(node) { - if (node.nodeType !== 1) { - // Not a DOMElement, therefore not a React component - return false; - } - var id = ReactID.getID(node); - return id ? id.charAt(0) === SEPARATOR : false; - }, - - /** - * Traverses up the ancestors of the supplied node to find a node that is a - * DOM representation of a React component. - * - * @param {*} node - * @return {?DOMEventTarget} - * @internal - */ - getFirstReactDOM: function(node) { - var current = node; - while (current && current.parentNode !== current) { - if (ReactInstanceHandles.isRenderedByReact(current)) { - return current; - } - current = current.parentNode; - } - return null; - }, - - /** - * Finds a node with the supplied `id` inside of the supplied `ancestorNode`. - * Exploits the ID naming scheme to perform the search quickly. - * - * @param {DOMEventTarget} ancestorNode Search from this root. - * @pararm {string} id ID of the DOM representation of the component. - * @return {DOMEventTarget} DOM node with the supplied `id`. - * @internal - */ - findComponentRoot: function(ancestorNode, id) { - var firstChildren = [ancestorNode.firstChild]; - var childIndex = 0; - - while (childIndex < firstChildren.length) { - var child = firstChildren[childIndex++]; - while (child) { - var childID = ReactID.getID(child); - if (childID) { - if (id === childID) { - return child; - } else if (isAncestorIDOf(childID, id)) { - // 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); - break; - } else { - // TODO This should not be necessary if the ID hierarchy is - // correct, but is occasionally necessary if the DOM has been - // modified in unexpected ways. - 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 (__DEV__) { - console.error( - 'Error while invoking `findComponentRoot` with the following ' + - 'ancestor node:', - ancestorNode - ); - } - invariant( - false, - 'findComponentRoot(..., %s): Unable to find element. This probably ' + - 'means the DOM was unexpectedly mutated (e.g. by the browser).', - id, - ReactID.getID(ancestorNode) - ); - }, - /** * Gets the DOM ID of the React component that is the root of the tree that * contains the React component with the supplied DOM ID. @@ -400,7 +301,11 @@ var ReactInstanceHandles = { * Exposed for unit testing. * @private */ - _getNextDescendantID: getNextDescendantID + _getNextDescendantID: getNextDescendantID, + + isAncestorIDOf: isAncestorIDOf, + + SEPARATOR: SEPARATOR }; diff --git a/src/core/ReactMount.js b/src/core/ReactMount.js index 8713f1bee4..ed1def0d4c 100644 --- a/src/core/ReactMount.js +++ b/src/core/ReactMount.js @@ -22,8 +22,11 @@ var invariant = require('invariant'); var getReactRootElementInContainer = require('getReactRootElementInContainer'); var ReactEventEmitter = require('ReactEventEmitter'); var ReactInstanceHandles = require('ReactInstanceHandles'); -var ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); -var ReactID = require('ReactID'); + +var SEPARATOR = ReactInstanceHandles.SEPARATOR; + +var ATTR_NAME = 'data-reactid'; +var nodeCache = {}; var $ = require('$'); @@ -44,7 +47,138 @@ if (__DEV__) { */ function getReactRootID(container) { var rootElement = getReactRootElementInContainer(container); - return rootElement && ReactID.getID(rootElement); + return rootElement && ReactMount.getID(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) { + if (nodeCache.hasOwnProperty(id)) { + var cached = nodeCache[id]; + if (cached !== node) { + invariant( + !isValid(cached, id), + 'ReactMount: Two valid but unequal nodes with the same `%s`: %s', + ATTR_NAME, id + ); + + nodeCache[id] = node; + } + } else { + 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 + // the empty string, as if the attribute were missing. + 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 React-generated DOM ID. + * + * @param {string} id A React-generated DOM ID. + * @return {DOMElement} DOM node with the suppled `id`. + * @internal + */ +function getNode(id) { + if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) { + nodeCache[id] = ReactMount.findReactNodeByID(id); + } + return nodeCache[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 && contains(container, node)) { + return true; + } + } + + return false; +} + +function contains(ancestor, descendant) { + if (ancestor.contains) { + // Supported natively in virtually all browsers, but not in jsdom. + return ancestor.contains(descendant); + } + + if (descendant === ancestor) { + return true; + } + + if (descendant.nodeType === 3) { + // If descendant is a text node, start from descendant.parentNode + // instead, so that we can assume all ancestors worth considering are + // element nodes with nodeType === 1. + descendant = descendant.parentNode; + } + + while (descendant && descendant.nodeType === 1) { + if (descendant === ancestor) { + return true; + } + descendant = descendant.parentNode; + } + + 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]; } /** @@ -89,14 +223,10 @@ var ReactMount = { * Ensures that the top-level event delegation listener is set up. This will * be invoked some time before the first time any React component is rendered. * - * @param {object} TopLevelCallbackCreator * @private */ - prepareTopLevelEvents: function(TopLevelCallbackCreator) { - ReactEventEmitter.ensureListening( - ReactMount.useTouchEvents, - TopLevelCallbackCreator - ); + prepareTopLevelEvents: function() { + ReactEventEmitter.ensureListening(ReactMount.useTouchEvents); }, /** @@ -132,7 +262,7 @@ var ReactMount = { * @return {string} reactRoot ID prefix */ _registerComponent: function(nextComponent, container) { - ReactMount.prepareTopLevelEvents(ReactEventTopLevelCallback); + ReactMount.prepareTopLevelEvents(); var reactRootID = ReactMount.registerContainer(container); instanceByReactRootID[reactRootID] = nextComponent; @@ -196,8 +326,7 @@ var ReactMount = { var reactRootElement = getReactRootElementInContainer(container); var containerHasReactMarkup = - reactRootElement && - ReactInstanceHandles.isRenderedByReact(reactRootElement); + reactRootElement && ReactMount.isRenderedByReact(reactRootElement); var shouldReuseMarkup = containerHasReactMarkup && !registeredComponent; @@ -295,15 +424,15 @@ var ReactMount = { var rootElement = rootElementsByReactRootID[reactRootID]; if (rootElement && rootElement.parentNode !== container) { invariant( - // Call rawGetID here because getID calls isValid which calls + // Call internalGetID here because getID calls isValid which calls // findReactContainerForID (this function). - ReactID.rawGetID(rootElement) === reactRootID, + internalGetID(rootElement) === reactRootID, 'ReactMount: Root element ID differed from reactRootID.' ); var containerChild = container.firstChild; if (containerChild && - reactRootID === ReactID.rawGetID(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 @@ -329,8 +458,122 @@ var ReactMount = { */ findReactNodeByID: function(id) { var reactRoot = ReactMount.findReactContainerForID(id); - return ReactInstanceHandles.findComponentRoot(reactRoot, id); - } + return ReactMount.findComponentRoot(reactRoot, id); + }, + + /** + * True if the supplied `node` is rendered by React. + * + * @param {*} node DOM Element to check. + * @return {boolean} True if the DOM Element appears to be rendered by React. + * @internal + */ + isRenderedByReact: function(node) { + if (node.nodeType !== 1) { + // Not a DOMElement, therefore not a React component + return false; + } + var id = ReactMount.getID(node); + return id ? id.charAt(0) === SEPARATOR : false; + }, + + /** + * Traverses up the ancestors of the supplied node to find a node that is a + * DOM representation of a React component. + * + * @param {*} node + * @return {?DOMEventTarget} + * @internal + */ + getFirstReactDOM: function(node) { + var current = node; + while (current && current.parentNode !== current) { + if (ReactMount.isRenderedByReact(current)) { + return current; + } + current = current.parentNode; + } + return null; + }, + + /** + * Finds a node with the supplied `id` inside of the supplied `ancestorNode`. + * Exploits the ID naming scheme to perform the search quickly. + * + * @param {DOMEventTarget} ancestorNode Search from this root. + * @pararm {string} id ID of the DOM representation of the component. + * @return {DOMEventTarget} DOM node with the supplied `id`. + * @internal + */ + findComponentRoot: function(ancestorNode, id) { + var firstChildren = [ancestorNode.firstChild]; + var childIndex = 0; + + while (childIndex < firstChildren.length) { + var child = firstChildren[childIndex++]; + while (child) { + var childID = ReactMount.getID(child); + if (childID) { + if (id === childID) { + return child; + } else if (ReactInstanceHandles.isAncestorIDOf(childID, id)) { + // 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); + break; + } else { + // TODO This should not be necessary if the ID hierarchy is + // correct, but is occasionally necessary if the DOM has been + // modified in unexpected ways. + 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 (__DEV__) { + console.error( + 'Error while invoking `findComponentRoot` with the following ' + + 'ancestor node:', + ancestorNode + ); + } + invariant( + false, + 'findComponentRoot(..., %s): Unable to find element. This probably ' + + 'means the DOM was unexpectedly mutated (e.g. by the browser).', + id, + ReactMount.getID(ancestorNode) + ); + }, + + + /** + * React ID utilities. + */ + + ATTR_NAME: ATTR_NAME, + + getID: getID, + + setID: setID, + + getNode: getNode, + + purgeID: purgeID, + + injection: {} }; module.exports = ReactMount; diff --git a/src/core/ReactNativeComponent.js b/src/core/ReactNativeComponent.js index c5c7aa55fe..11c1c69fad 100644 --- a/src/core/ReactNativeComponent.js +++ b/src/core/ReactNativeComponent.js @@ -25,7 +25,7 @@ var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactComponent = require('ReactComponent'); var ReactEventEmitter = require('ReactEventEmitter'); var ReactMultiChild = require('ReactMultiChild'); -var ReactID = require('ReactID'); +var ReactMount = require('ReactMount'); var escapeTextForBrowser = require('escapeTextForBrowser'); var flattenChildren = require('flattenChildren'); @@ -136,7 +136,7 @@ ReactNativeComponent.Mixin = { } var escapedID = escapeTextForBrowser(this._rootNodeID); - return ret + ' ' + ReactID.ATTR_NAME + '="' + escapedID + '">'; + return ret + ' ' + ReactMount.ATTR_NAME + '="' + escapedID + '">'; }, /** diff --git a/src/core/ReactTextComponent.js b/src/core/ReactTextComponent.js index 1d0af59858..24c07bd704 100644 --- a/src/core/ReactTextComponent.js +++ b/src/core/ReactTextComponent.js @@ -20,7 +20,7 @@ "use strict"; var ReactComponent = require('ReactComponent'); -var ReactID = require('ReactID'); +var ReactMount = require('ReactMount'); var escapeTextForBrowser = require('escapeTextForBrowser'); var mixInto = require('mixInto'); @@ -58,7 +58,7 @@ mixInto(ReactTextComponent, { mountComponent: function(rootID) { ReactComponent.Mixin.mountComponent.call(this, rootID); return ( - '' + + '' + escapeTextForBrowser(this.props.text) + '' ); diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index b79b06c524..0ee5268bb5 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -25,7 +25,7 @@ var React; var ReactCurrentOwner; var ReactPropTypes; var ReactTestUtils; -var ReactID; +var ReactMount; var ReactDoNotBindDeprecated; var cx; @@ -41,7 +41,7 @@ describe('ReactCompositeComponent', function() { ReactDoNotBindDeprecated = require('ReactDoNotBindDeprecated'); ReactPropTypes = require('ReactPropTypes'); ReactTestUtils = require('ReactTestUtils'); - ReactID = require('ReactID'); + ReactMount = require('ReactMount'); MorphingComponent = React.createClass({ getInitialState: function() { @@ -129,7 +129,7 @@ describe('ReactCompositeComponent', function() { // rerender instance.setProps({renderAnchor: true, anchorClassOn: false}); var anchorID = instance.getAnchorID(); - var actualDOMAnchorNode = ReactID.getNode(anchorID); + var actualDOMAnchorNode = ReactMount.getNode(anchorID); expect(actualDOMAnchorNode.className).toBe(''); }); diff --git a/src/core/__tests__/ReactDOM-test.js b/src/core/__tests__/ReactDOM-test.js index 6068a81327..fdd88c6757 100644 --- a/src/core/__tests__/ReactDOM-test.js +++ b/src/core/__tests__/ReactDOM-test.js @@ -25,7 +25,7 @@ var React = require('React'); var ReactDOM = require('ReactDOM'); var ReactTestUtils = require('ReactTestUtils'); var React = require('React'); -var ReactID = require('ReactID'); +var ReactMount = require('ReactMount'); describe('ref swapping', function() { // TODO: uncomment this test once we can run in phantom, which @@ -61,7 +61,7 @@ describe('ref swapping', function() { var argDiv = ReactTestUtils.renderIntoDocument( ReactDOM.div(null, 'child') ); - var argNode = ReactID.getNode(argDiv._rootNodeID); + var argNode = ReactMount.getNode(argDiv._rootNodeID); expect(argNode.innerHTML).toBe('child'); }); @@ -69,7 +69,7 @@ describe('ref swapping', function() { var conflictDiv = ReactTestUtils.renderIntoDocument( ReactDOM.div({children: 'fakechild'}, 'child') ); - var conflictNode = ReactID.getNode(conflictDiv._rootNodeID); + var conflictNode = ReactMount.getNode(conflictDiv._rootNodeID); expect(conflictNode.innerHTML).toBe('child'); }); @@ -111,7 +111,7 @@ describe('ref swapping', function() { theBird:
} }); - var root = ReactID.getNode(myDiv._rootNodeID); + var root = ReactMount.getNode(myDiv._rootNodeID); var dog = root.childNodes[0]; expect(dog.className).toBe('bigdog'); }); diff --git a/src/core/__tests__/ReactDOMIDOperations-test.js b/src/core/__tests__/ReactDOMIDOperations-test.js index 77a64475c6..0dfd2bfd47 100644 --- a/src/core/__tests__/ReactDOMIDOperations-test.js +++ b/src/core/__tests__/ReactDOMIDOperations-test.js @@ -23,11 +23,11 @@ describe('ReactDOMIDOperations', function() { var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); - var ReactID = require('ReactID'); + var ReactMount = require('ReactMount'); var keyOf = require('keyOf'); it('should disallow updating special properties', function() { - spyOn(ReactID, "getNode"); + spyOn(ReactMount, "getNode"); spyOn(DOMPropertyOperations, "setValueForProperty"); expect(function() { @@ -39,7 +39,7 @@ describe('ReactDOMIDOperations', function() { }).toThrow(); expect( - ReactID.getNode.argsForCall[0][0] + ReactMount.getNode.argsForCall[0][0] ).toBe('testID'); expect( @@ -49,7 +49,7 @@ describe('ReactDOMIDOperations', function() { it('should update innerHTML and special-case whitespace', function() { var stubNode = document.createElement('div'); - spyOn(ReactID, "getNode").andReturn(stubNode); + spyOn(ReactMount, "getNode").andReturn(stubNode); ReactDOMIDOperations.updateInnerHTMLByID( 'testID', @@ -57,7 +57,7 @@ describe('ReactDOMIDOperations', function() { ); expect( - ReactID.getNode.argsForCall[0][0] + ReactMount.getNode.argsForCall[0][0] ).toBe('testID'); expect(stubNode.innerHTML).toBe(' testContent'); diff --git a/src/core/__tests__/ReactEventEmitter-test.js b/src/core/__tests__/ReactEventEmitter-test.js index 79bf8f2390..5b5e64ccd7 100644 --- a/src/core/__tests__/ReactEventEmitter-test.js +++ b/src/core/__tests__/ReactEventEmitter-test.js @@ -22,7 +22,7 @@ require('mock-modules') .dontMock('BrowserScroll') .dontMock('CallbackRegistry') .dontMock('EventPluginHub') - .dontMock('ReactID') + .dontMock('ReactMount') .dontMock('ReactEventEmitter') .dontMock('ReactInstanceHandles') .dontMock('EventPluginHub') @@ -35,9 +35,9 @@ var keyOf = require('keyOf'); var mocks = require('mocks'); var EventPluginHub; -var ReactID = require('ReactID'); -var getID = ReactID.getID; -var setID = ReactID.setID; +var ReactMount = require('ReactMount'); +var getID = ReactMount.getID; +var setID = ReactMount.setID; var ReactEventEmitter; var ReactEventTopLevelCallback; var ReactTestUtils; @@ -89,9 +89,9 @@ describe('ReactEventEmitter', function() { require('mock-modules').dumpCache(); EventPluginHub = require('EventPluginHub'); TapEventPlugin = require('TapEventPlugin'); - ReactID = require('ReactID'); - getID = ReactID.getID; - setID = ReactID.setID; + ReactMount = require('ReactMount'); + getID = ReactMount.getID; + setID = ReactMount.setID; ReactEventEmitter = require('ReactEventEmitter'); ReactTestUtils = require('ReactTestUtils'); ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js index fac5cde23d..bd883436e0 100644 --- a/src/core/__tests__/ReactIdentity-test.js +++ b/src/core/__tests__/ReactIdentity-test.js @@ -22,7 +22,7 @@ var React; var ReactTestUtils; var reactComponentExpect; -var ReactID; +var ReactMount; describe('ReactIdentity', function() { @@ -31,12 +31,12 @@ describe('ReactIdentity', function() { React = require('React'); ReactTestUtils = require('ReactTestUtils'); reactComponentExpect = require('reactComponentExpect'); - ReactID = require('ReactID'); + ReactMount = require('ReactMount'); }); var idExp = /^\.r\[.+?\](.*)$/; function checkId(child, expectedId) { - var actual = idExp.exec(ReactID.getID(child)); + var actual = idExp.exec(ReactMount.getID(child)); var expected = idExp.exec(expectedId); expect(actual).toBeTruthy(); expect(expected).toBeTruthy(); @@ -281,11 +281,11 @@ describe('ReactIdentity', function() { React.renderComponent(wrapped, document.createElement('div')); - var beforeID = ReactID.getID(wrapped.getDOMNode().firstChild); + var beforeID = ReactMount.getID(wrapped.getDOMNode().firstChild); wrapped.swap(); - var afterID = ReactID.getID(wrapped.getDOMNode().firstChild); + var afterID = ReactMount.getID(wrapped.getDOMNode().firstChild); expect(beforeID).not.toEqual(afterID); diff --git a/src/core/__tests__/ReactInstanceHandles-test.js b/src/core/__tests__/ReactInstanceHandles-test.js index 093ae845a5..6d665a2b9b 100644 --- a/src/core/__tests__/ReactInstanceHandles-test.js +++ b/src/core/__tests__/ReactInstanceHandles-test.js @@ -21,7 +21,7 @@ var React = require('React'); var ReactTestUtils = require('ReactTestUtils'); -var ReactID = require('ReactID'); +var ReactMount = require('ReactMount'); /** * Ensure that all callbacks are invoked, passing this unique argument. @@ -78,7 +78,7 @@ describe('ReactInstanceHandles', function() { describe('isRenderedByReact', function() { it('should not crash on text nodes', function() { expect(function() { - ReactInstanceHandles.isRenderedByReact(document.createTextNode('yolo')); + ReactMount.isRenderedByReact(document.createTextNode('yolo')); }).not.toThrow(); }); }); @@ -91,14 +91,14 @@ describe('ReactInstanceHandles', function() { parentNode.appendChild(childNodeA); parentNode.appendChild(childNodeB); - ReactID.setID(parentNode, '.react[0]'); - ReactID.setID(childNodeA, '.react[0].0'); - ReactID.setID(childNodeB, '.react[0].0:1'); + ReactMount.setID(parentNode, '.react[0]'); + ReactMount.setID(childNodeA, '.react[0].0'); + ReactMount.setID(childNodeB, '.react[0].0:1'); expect( - ReactInstanceHandles.findComponentRoot( + ReactMount.findComponentRoot( parentNode, - ReactID.getID(childNodeB) + ReactMount.getID(childNodeB) ) ).toBe(childNodeB); }); @@ -110,14 +110,14 @@ describe('ReactInstanceHandles', function() { parentNode.appendChild(childNodeA); parentNode.appendChild(childNodeB); - ReactID.setID(parentNode, '.react[0]'); + ReactMount.setID(parentNode, '.react[0]'); // No ID on `childNodeA`. - ReactID.setID(childNodeB, '.react[0].0:1'); + ReactMount.setID(childNodeB, '.react[0].0:1'); expect( - ReactInstanceHandles.findComponentRoot( + ReactMount.findComponentRoot( parentNode, - ReactID.getID(childNodeB) + ReactMount.getID(childNodeB) ) ).toBe(childNodeB); }); @@ -129,22 +129,22 @@ describe('ReactInstanceHandles', function() { parentNode.appendChild(childNodeA); childNodeA.appendChild(childNodeB); - ReactID.setID(parentNode, '.react[0]'); + ReactMount.setID(parentNode, '.react[0]'); // No ID on `childNodeA`, it was "rendered by the browser". - ReactID.setID(childNodeB, '.react[0].1:0'); + ReactMount.setID(childNodeB, '.react[0].1:0'); - expect(ReactInstanceHandles.findComponentRoot( + expect(ReactMount.findComponentRoot( parentNode, - ReactID.getID(childNodeB) + ReactMount.getID(childNodeB) )).toBe(childNodeB); spyOn(console, 'error'); expect(console.error.argsForCall.length).toBe(0); expect(function() { - ReactInstanceHandles.findComponentRoot( + ReactMount.findComponentRoot( parentNode, - ReactID.getID(childNodeB) + ":junk" + ReactMount.getID(childNodeB) + ":junk" ); }).toThrow( 'Invariant Violation: findComponentRoot(..., .react[0].1:0:junk): ' + diff --git a/src/core/__tests__/ReactMultiChildReconcile-test.js b/src/core/__tests__/ReactMultiChildReconcile-test.js index 0ff3cf2eab..4d47e89207 100644 --- a/src/core/__tests__/ReactMultiChildReconcile-test.js +++ b/src/core/__tests__/ReactMultiChildReconcile-test.js @@ -23,7 +23,7 @@ require('mock-modules'); var React = require('React'); var ReactTestUtils = require('ReactTestUtils'); -var ReactID = require('ReactID'); +var ReactMount = require('ReactMount'); var objMapKeyVal = require('objMapKeyVal'); @@ -191,7 +191,7 @@ function verifyDomOrderingAccurate(parentInstance, statusDisplays) { var i; var orderedDomIds = []; for (i=0; i < statusDisplayNodes.length; i++) { - orderedDomIds.push(ReactID.getID(statusDisplayNodes[i])); + orderedDomIds.push(ReactMount.getID(statusDisplayNodes[i])); } var orderedLogicalIds = []; diff --git a/src/core/__tests__/ReactNativeComponent-test.js b/src/core/__tests__/ReactNativeComponent-test.js index b7462325e9..8e0481ecea 100644 --- a/src/core/__tests__/ReactNativeComponent-test.js +++ b/src/core/__tests__/ReactNativeComponent-test.js @@ -317,7 +317,7 @@ describe('ReactNativeComponent', function() { it("should clean up listeners", function() { var React = require('React'); var ReactEventEmitter = require('ReactEventEmitter'); - var ReactID = require('ReactID'); + var ReactMount = require('ReactMount'); var container = document.createElement('div'); document.documentElement.appendChild(container); @@ -327,7 +327,7 @@ describe('ReactNativeComponent', function() { React.renderComponent(instance, container); var rootNode = instance.getDOMNode(); - var rootNodeID = ReactID.getID(rootNode); + var rootNodeID = ReactMount.getID(rootNode); expect( ReactEventEmitter.getListener(rootNodeID, 'onClick') ).toBe(callback); diff --git a/src/environment/__tests__/ReactServerRendering-test.js b/src/environment/__tests__/ReactServerRendering-test.js index 192b54cc3b..7fd3f3138f 100644 --- a/src/environment/__tests__/ReactServerRendering-test.js +++ b/src/environment/__tests__/ReactServerRendering-test.js @@ -24,7 +24,7 @@ require('mock-modules') .dontMock('ExecutionEnvironment') .dontMock('React') - .dontMock('ReactID') + .dontMock('ReactMount') .dontMock('ReactServerRendering') .dontMock('ReactTestUtils') .dontMock('ReactMarkupChecksum'); @@ -32,7 +32,7 @@ require('mock-modules') var mocks = require('mocks'); var React; -var ReactID; +var ReactMount; var ReactTestUtils; var ReactServerRendering; var ReactMarkupChecksum; @@ -42,7 +42,7 @@ describe('ReactServerRendering', function() { beforeEach(function() { require('mock-modules').dumpCache(); React = require('React'); - ReactID = require('ReactID'); + ReactMount = require('ReactMount'); ReactTestUtils = require('ReactTestUtils'); ExecutionEnvironment = require('ExecutionEnvironment'); ExecutionEnvironment.canUseDOM = false; @@ -59,7 +59,7 @@ describe('ReactServerRendering', function() { } ); expect(response).toMatch( - 'hello world' ); }); @@ -83,11 +83,11 @@ describe('ReactServerRendering', function() { } ); expect(response).toMatch( - '
' + - '' + - 'My name is ' + - 'child' + + '' + + 'My name is ' + + 'child' + '' + '
' ); @@ -137,10 +137,10 @@ describe('ReactServerRendering', function() { ); expect(response).toMatch( - '' + - 'Component name: ' + - 'TestComponent' + + 'Component name: ' + + 'TestComponent' + '' ); expect(lifecycle).toEqual( diff --git a/src/eventPlugins/EnterLeaveEventPlugin.js b/src/eventPlugins/EnterLeaveEventPlugin.js index f7a385ba18..3a3af3fd51 100644 --- a/src/eventPlugins/EnterLeaveEventPlugin.js +++ b/src/eventPlugins/EnterLeaveEventPlugin.js @@ -22,14 +22,13 @@ var EventConstants = require('EventConstants'); var EventPropagators = require('EventPropagators'); var ExecutionEnvironment = require('ExecutionEnvironment'); -var ReactInstanceHandles = require('ReactInstanceHandles'); var SyntheticMouseEvent = require('SyntheticMouseEvent'); -var ReactID = require('ReactID'); +var ReactMount = require('ReactMount'); var keyOf = require('keyOf'); var topLevelTypes = EventConstants.topLevelTypes; -var getFirstReactDOM = ReactInstanceHandles.getFirstReactDOM; +var getFirstReactDOM = ReactMount.getFirstReactDOM; var eventTypes = { mouseEnter: {registrationName: keyOf({onMouseEnter: null})}, @@ -85,8 +84,8 @@ var EnterLeaveEventPlugin = { return null; } - var fromID = from ? ReactID.getID(from) : ''; - var toID = to ? ReactID.getID(to) : ''; + var fromID = from ? ReactMount.getID(from) : ''; + var toID = to ? ReactMount.getID(to) : ''; var leave = SyntheticMouseEvent.getPooled( eventTypes.mouseLeave, diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index b8784183d6..9eb71e5983 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -21,7 +21,7 @@ var React = require('React'); var ReactComponent = require('ReactComponent'); var ReactEventEmitter = require('ReactEventEmitter'); var ReactTextComponent = require('ReactTextComponent'); -var ReactID = require('ReactID'); +var ReactMount = require('ReactMount'); var mergeInto = require('mergeInto'); @@ -222,10 +222,10 @@ var ReactTestUtils = { ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( topLevelType ); - var node = ReactID.getNode(reactRootID); + var node = ReactMount.getNode(reactRootID); fakeNativeEvent.target = node; /* jsdom is returning nodes without id's - fixing that issue here. */ - ReactID.setID(node, reactRootID); + ReactMount.setID(node, reactRootID); virtualHandler(fakeNativeEvent); },