UNPKG

react-addons

Version:

Simple packaging of react addons to avoid fiddly 'react/addons' npm module.

642 lines (564 loc) 21.2 kB
/** * Copyright 2013-2014 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 ReactMount */ "use strict"; var DOMProperty = require("./DOMProperty"); var ReactEventEmitter = require("./ReactEventEmitter"); var ReactInstanceHandles = require("./ReactInstanceHandles"); var ReactPerf = require("./ReactPerf"); var containsNode = require("./containsNode"); var getReactRootElementInContainer = require("./getReactRootElementInContainer"); var invariant = require("./invariant"); var shouldUpdateReactComponent = require("./shouldUpdateReactComponent"); var SEPARATOR = ReactInstanceHandles.SEPARATOR; var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME; var nodeCache = {}; var ELEMENT_NODE_TYPE = 1; var DOC_NODE_TYPE = 9; /** Mapping from reactRootID to React component instance. */ var instancesByReactRootID = {}; /** Mapping from reactRootID to `container` nodes. */ var containersByReactRootID = {}; if ("production" !== process.env.NODE_ENV) { /** __DEV__-only mapping from reactRootID to root elements. */ var rootElementsByReactRootID = {}; } // Used to store breadth-first search state in findComponentRoot. var findComponentRootReusableArray = []; /** * @param {DOMElement} container DOM element that may contain a React component. * @return {?string} A "reactRoot" ID, if a React component is rendered. */ function getReactRootID(container) { var rootElement = getReactRootElementInContainer(container); 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) { ("production" !== process.env.NODE_ENV ? invariant( !isValid(cached, id), 'ReactMount: Two valid but unequal nodes with the same `%s`: %s', ATTR_NAME, id ) : invariant(!isValid(cached, 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) { ("production" !== process.env.NODE_ENV ? invariant( internalGetID(node) === id, 'ReactMount: Unexpected modification of `%s`', ATTR_NAME ) : invariant(internalGetID(node) === id)); 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 = nodeCache[ancestorID]; if (ancestor && isValid(ancestor, ancestorID)) { 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; } /** * Mounting is the process of initializing a React component by creatings its * representative DOM elements and inserting them into a supplied `container`. * Any prior content inside `container` is destroyed in the process. * * ReactMount.renderComponent( * component, * document.getElementById('container') * ); * * <div id="container"> <-- Supplied `container`. * <div data-reactid=".3"> <-- Rendered reactRoot of React * // ... component. * </div> * </div> * * Inside of `container`, the first element rendered is the "reactRoot". */ var ReactMount = { /** Time spent generating markup. */ totalInstantiationTime: 0, /** Time spent inserting markup into the DOM. */ totalInjectionTime: 0, /** Whether support for touch events should be initialized. */ useTouchEvents: false, /** Exposed for debugging purposes **/ _instancesByReactRootID: instancesByReactRootID, /** * This is a hook provided to support rendering React components while * ensuring that the apparent scroll position of its `container` does not * change. * * @param {DOMElement} container The `container` being rendered into. * @param {function} renderCallback This must be called once to do the render. */ scrollMonitor: function(container, renderCallback) { renderCallback(); }, /** * Take a component that's already mounted into the DOM and replace its props * @param {ReactComponent} prevComponent component instance already in the DOM * @param {ReactComponent} nextComponent component instance to render * @param {DOMElement} container container to render into * @param {?function} callback function triggered on completion */ _updateRootComponent: function( prevComponent, nextComponent, container, callback) { var nextProps = nextComponent.props; ReactMount.scrollMonitor(container, function() { prevComponent.replaceProps(nextProps, callback); }); if ("production" !== process.env.NODE_ENV) { // Record the root element in case it later gets transplanted. rootElementsByReactRootID[getReactRootID(container)] = getReactRootElementInContainer(container); } return prevComponent; }, /** * Register a component into the instance map and starts scroll value * monitoring * @param {ReactComponent} nextComponent component instance to render * @param {DOMElement} container container to render into * @return {string} reactRoot ID prefix */ _registerComponent: function(nextComponent, container) { ("production" !== process.env.NODE_ENV ? invariant( container && ( container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE ), '_registerComponent(...): Target container is not a DOM element.' ) : invariant(container && ( container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE ))); ReactEventEmitter.ensureScrollValueMonitoring(); var reactRootID = ReactMount.registerContainer(container); instancesByReactRootID[reactRootID] = nextComponent; return reactRootID; }, /** * Render a new component into the DOM. * @param {ReactComponent} nextComponent component instance to render * @param {DOMElement} container container to render into * @param {boolean} shouldReuseMarkup if we should skip the markup insertion * @return {ReactComponent} nextComponent */ _renderNewRootComponent: ReactPerf.measure( 'ReactMount', '_renderNewRootComponent', function( nextComponent, container, shouldReuseMarkup) { var reactRootID = ReactMount._registerComponent(nextComponent, container); nextComponent.mountComponentIntoNode( reactRootID, container, shouldReuseMarkup ); if ("production" !== process.env.NODE_ENV) { // Record the root element in case it later gets transplanted. rootElementsByReactRootID[reactRootID] = getReactRootElementInContainer(container); } return nextComponent; } ), /** * Renders a React component into the DOM in the supplied `container`. * * If the React component was previously rendered into `container`, this will * perform an update on it and only mutate the DOM as necessary to reflect the * latest React component. * * @param {ReactComponent} nextComponent Component instance to render. * @param {DOMElement} container DOM element to render into. * @param {?function} callback function triggered on completion * @return {ReactComponent} Component instance rendered in `container`. */ renderComponent: function(nextComponent, container, callback) { var prevComponent = instancesByReactRootID[getReactRootID(container)]; if (prevComponent) { if (shouldUpdateReactComponent(prevComponent, nextComponent)) { return ReactMount._updateRootComponent( prevComponent, nextComponent, container, callback ); } else { ReactMount.unmountComponentAtNode(container); } } var reactRootElement = getReactRootElementInContainer(container); var containerHasReactMarkup = reactRootElement && ReactMount.isRenderedByReact(reactRootElement); var shouldReuseMarkup = containerHasReactMarkup && !prevComponent; var component = ReactMount._renderNewRootComponent( nextComponent, container, shouldReuseMarkup ); callback && callback.call(component); return component; }, /** * Constructs a component instance of `constructor` with `initialProps` and * renders it into the supplied `container`. * * @param {function} constructor React component constructor. * @param {?object} props Initial props of the component instance. * @param {DOMElement} container DOM element to render into. * @return {ReactComponent} Component instance rendered in `container`. */ constructAndRenderComponent: function(constructor, props, container) { return ReactMount.renderComponent(constructor(props), container); }, /** * Constructs a component instance of `constructor` with `initialProps` and * renders it into a container node identified by supplied `id`. * * @param {function} componentConstructor React component constructor * @param {?object} props Initial props of the component instance. * @param {string} id ID of the DOM element to render into. * @return {ReactComponent} Component instance rendered in the container node. */ constructAndRenderComponentByID: function(constructor, props, id) { var domNode = document.getElementById(id); ("production" !== process.env.NODE_ENV ? invariant( domNode, 'Tried to get element with id of "%s" but it is not present on the page.', id ) : invariant(domNode)); return ReactMount.constructAndRenderComponent(constructor, props, domNode); }, /** * Registers a container node into which React components will be rendered. * This also creates the "reactRoot" ID that will be assigned to the element * rendered within. * * @param {DOMElement} container DOM element to register as a container. * @return {string} The "reactRoot" ID of elements rendered within. */ registerContainer: function(container) { var reactRootID = getReactRootID(container); if (reactRootID) { // If one exists, make sure it is a valid "reactRoot" ID. reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID); } if (!reactRootID) { // No valid "reactRoot" ID found, create one. reactRootID = ReactInstanceHandles.createReactRootID(); } containersByReactRootID[reactRootID] = container; return reactRootID; }, /** * Unmounts and destroys the React component rendered in the `container`. * * @param {DOMElement} container DOM element containing a React component. * @return {boolean} True if a component was found in and unmounted from * `container` */ unmountComponentAtNode: function(container) { var reactRootID = getReactRootID(container); var component = instancesByReactRootID[reactRootID]; if (!component) { return false; } ReactMount.unmountComponentFromNode(component, container); delete instancesByReactRootID[reactRootID]; delete containersByReactRootID[reactRootID]; if ("production" !== process.env.NODE_ENV) { delete rootElementsByReactRootID[reactRootID]; } return true; }, /** * Unmounts a component and removes it from the DOM. * * @param {ReactComponent} instance React component instance. * @param {DOMElement} container DOM element to unmount from. * @final * @internal * @see {ReactMount.unmountComponentAtNode} */ unmountComponentFromNode: function(instance, container) { instance.unmountComponent(); if (container.nodeType === DOC_NODE_TYPE) { container = container.documentElement; } // http://jsperf.com/emptying-a-node while (container.lastChild) { container.removeChild(container.lastChild); } }, /** * 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 ("production" !== process.env.NODE_ENV) { var rootElement = rootElementsByReactRootID[reactRootID]; if (rootElement && rootElement.parentNode !== container) { ("production" !== process.env.NODE_ENV ? invariant( // Call internalGetID here because getID calls isValid which calls // findReactContainerForID (this function). internalGetID(rootElement) === reactRootID, 'ReactMount: Root element ID differed from reactRootID.' ) : invariant(// Call internalGetID here because getID calls isValid which calls // findReactContainerForID (this function). internalGetID(rootElement) === 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 { console.warn( 'ReactMount: Root element has been removed from its original ' + 'container. New container:', 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); }, /** * 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 `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; 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 `<table>` // element sprouts an extra `<tbody>` 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; ("production" !== process.env.NODE_ENV ? invariant( false, 'findComponentRoot(..., %s): Unable to find element. This probably ' + 'means the DOM was unexpectedly mutated (e.g., by the browser). ' + 'Try inspecting the child nodes of the element with React ID `%s`.', targetID, ReactMount.getID(ancestorNode) ) : invariant(false)); }, /** * React ID utilities. */ getReactRootID: getReactRootID, getID: getID, setID: setID, getNode: getNode, purgeID: purgeID }; module.exports = ReactMount;