diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index b08e4ff1b5..ce04e18793 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -16,17 +16,11 @@ * @providesModule ReactComponent */ -/*jslint evil: true */ - "use strict"; -var getReactRootElementInContainer = require('getReactRootElementInContainer'); +var ReactComponentEnvironment = require('ReactComponentEnvironment'); var ReactCurrentOwner = require('ReactCurrentOwner'); -var ReactDOMIDOperations = require('ReactDOMIDOperations'); -var ReactMarkupChecksum = require('ReactMarkupChecksum'); -var ReactMount = require('ReactMount'); var ReactOwner = require('ReactOwner'); -var ReactReconcileTransaction = require('ReactReconcileTransaction'); var ReactUpdates = require('ReactUpdates'); var invariant = require('invariant'); @@ -195,12 +189,33 @@ var ReactComponent = { LifeCycle: ComponentLifeCycle, /** - * React references `ReactDOMIDOperations` using this property in order to - * allow dependency injection. + * Injected module that provides ability to mutate individual properties. + * Injected into the base class because many different subclasses need access + * to this. * * @internal */ - DOMIDOperations: ReactDOMIDOperations, + DOMIDOperations: ReactComponentEnvironment.DOMIDOperations, + + /** + * Optionally injectable environment dependent cleanup hook. (server vs. + * browser etc). Example: A browser system caches DOM nodes based on component + * ID and must remove that cache entry when this instance is unmounted. + * + * @private + */ + unmountIDFromEnvironment: ReactComponentEnvironment.unmountIDFromEnvironment, + + /** + * The "image" of a component tree, is the platform specific (typically + * serialized) data that represents a tree of lower level UI building blocks. + * On the web, this "image" is HTML markup which describes a construction of + * low level `div` and `span` nodes. Other platforms may have different + * encoding of this "image". This must be injected. + * + * @private + */ + mountImageIntoNode: ReactComponentEnvironment.mountImageIntoNode, /** * React references `ReactReconcileTransaction` using this property in order @@ -208,30 +223,16 @@ var ReactComponent = { * * @internal */ - ReactReconcileTransaction: ReactReconcileTransaction, + ReactReconcileTransaction: + ReactComponentEnvironment.ReactReconcileTransaction, /** - * @param {object} DOMIDOperations - * @final - */ - setDOMOperations: function(DOMIDOperations) { - ReactComponent.DOMIDOperations = DOMIDOperations; - }, - - /** - * @param {Transaction} ReactReconcileTransaction - * @final - */ - setReactReconcileTransaction: function(ReactReconcileTransaction) { - ReactComponent.ReactReconcileTransaction = ReactReconcileTransaction; - }, - - /** - * Base functionality for every ReactComponent constructor. + * Base functionality for every ReactComponent constructor. Mixed into the + * `ReactComponent` prototype, but exposed statically for easy access. * * @lends {ReactComponent.prototype} */ - Mixin: { + Mixin: merge(ReactComponentEnvironment.Mixin, { /** * Checks whether or not this component is mounted. @@ -244,21 +245,6 @@ var ReactComponent = { return this._lifeCycleState === ComponentLifeCycle.MOUNTED; }, - /** - * Returns the DOM node rendered by this component. - * - * @return {DOMElement} The root node of this component. - * @final - * @protected - */ - getDOMNode: function() { - invariant( - this.isMounted(), - 'getDOMNode(): A component must be mounted to have a DOM node.' - ); - return ReactMount.getNode(this._rootNodeID); - }, - /** * Sets a subset of the props. * @@ -382,7 +368,7 @@ var ReactComponent = { if (props.ref != null) { ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]); } - ReactMount.purgeID(this._rootNodeID); + ReactComponent.unmountIDFromEnvironment(this._rootNodeID); this._rootNodeID = null; this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; }, @@ -495,63 +481,8 @@ var ReactComponent = { container, transaction, shouldReuseMarkup) { - invariant( - container && container.nodeType === 1, - 'mountComponentIntoNode(...): Target container is not a DOM element.' - ); var markup = this.mountComponent(rootID, transaction); - - if (shouldReuseMarkup) { - if (ReactMarkupChecksum.canReuseMarkup( - markup, - getReactRootElementInContainer(container))) { - return; - } else { - if (__DEV__) { - console.warn( - 'React attempted to use reuse markup in a container but the ' + - 'checksum was invalid. This generally means that you are using ' + - 'server rendering and the markup generated on the server was ' + - 'not what the client was expecting. React injected new markup ' + - 'to compensate which works but you have lost many of the ' + - 'benefits of server rendering. Instead, figure out why the ' + - 'markup being generated is different on the client or server.' - ); - } - } - } - - // Asynchronously inject markup by ensuring that the container is not in - // the document when settings its `innerHTML`. - var parent = container.parentNode; - if (parent) { - var next = container.nextSibling; - parent.removeChild(container); - container.innerHTML = markup; - if (next) { - parent.insertBefore(container, next); - } else { - parent.appendChild(container); - } - } else { - container.innerHTML = markup; - } - }, - - /** - * Unmounts this component and removes it from the DOM. - * - * @param {DOMElement} container DOM element to unmount from. - * @final - * @internal - * @see {ReactMount.unmountAndReleaseReactRootNode} - */ - unmountComponentFromNode: function(container) { - this.unmountComponent(); - // http://jsperf.com/emptying-a-node - while (container.lastChild) { - container.removeChild(container.lastChild); - } + ReactComponent.mountImageIntoNode(markup, container, shouldReuseMarkup); }, /** @@ -581,9 +512,7 @@ var ReactComponent = { } return owner.refs[ref]; } - - } - + }) }; module.exports = ReactComponent; diff --git a/src/core/ReactComponentBrowserEnvironment.js b/src/core/ReactComponentBrowserEnvironment.js new file mode 100644 index 0000000000..26a3e27739 --- /dev/null +++ b/src/core/ReactComponentBrowserEnvironment.js @@ -0,0 +1,122 @@ +/** + * 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 ReactComponentBrowserEnvironment + */ + +/*jslint evil: true */ + +"use strict"; + +var ReactDOMIDOperations = require('ReactDOMIDOperations'); +var ReactMarkupChecksum = require('ReactMarkupChecksum'); +var ReactMount = require('ReactMount'); +var ReactReconcileTransaction = require('ReactReconcileTransaction'); + +var getReactRootElementInContainer = require('getReactRootElementInContainer'); +var invariant = require('invariant'); + + + +/** + * Abstracts away all functionality of `ReactComponent` requires knowledge of + * the browser context. + */ +var ReactComponentBrowserEnvironment = { + /** + * Mixed into every component instance. + */ + Mixin: { + /** + * Returns the DOM node rendered by this component. + * + * @return {DOMElement} The root node of this component. + * @final + * @protected + */ + getDOMNode: function() { + invariant( + this.isMounted(), + 'getDOMNode(): A component must be mounted to have a DOM node.' + ); + return ReactMount.getNode(this._rootNodeID); + } + }, + + ReactReconcileTransaction: ReactReconcileTransaction, + + DOMIDOperations: ReactDOMIDOperations, + + /** + * If a particular environment requires that some resources be cleaned up, + * specify this in the injected Mixin. In the DOM, we would likely want to + * purge any cached node ID lookups. + * + * @private + */ + unmountIDFromEnvironment: function(rootNodeID) { + ReactMount.purgeID(rootNodeID); + }, + + /** + * @param {string} markup Markup string to place into the DOM Element. + * @param {DOMElement} container DOM Element to insert markup into. + * @param {boolean} shouldReuseMarkup Should reuse the existing markup in the + * container if possible. + */ + mountImageIntoNode: function(markup, container, shouldReuseMarkup) { + invariant( + container && container.nodeType === 1, + 'mountComponentIntoNode(...): Target container is not a DOM element.' + ); + if (shouldReuseMarkup) { + if (ReactMarkupChecksum.canReuseMarkup( + markup, + getReactRootElementInContainer(container))) { + return; + } else { + if (__DEV__) { + console.warn( + 'React attempted to use reuse markup in a container but the ' + + 'checksum was invalid. This generally means that you are using ' + + 'server rendering and the markup generated on the server was ' + + 'not what the client was expecting. React injected new markup ' + + 'to compensate which works but you have lost many of the ' + + 'benefits of server rendering. Instead, figure out why the ' + + 'markup being generated is different on the client or server.' + ); + } + } + } + + // Asynchronously inject markup by ensuring that the container is not in + // the document when settings its `innerHTML`. + var parent = container.parentNode; + if (parent) { + var next = container.nextSibling; + parent.removeChild(container); + container.innerHTML = markup; + if (next) { + parent.insertBefore(container, next); + } else { + parent.appendChild(container); + } + } else { + container.innerHTML = markup; + } + } +}; + +module.exports = ReactComponentBrowserEnvironment; diff --git a/src/core/ReactComponentEnvironment.js b/src/core/ReactComponentEnvironment.js new file mode 100644 index 0000000000..5af6a268cd --- /dev/null +++ b/src/core/ReactComponentEnvironment.js @@ -0,0 +1,24 @@ +/** + * 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 ReactComponentEnvironment + */ + +var ReactComponentBrowserEnvironment = + require('ReactComponentBrowserEnvironment'); + +var ReactComponentEnvironment = ReactComponentBrowserEnvironment; + +module.exports = ReactComponentEnvironment; diff --git a/src/core/ReactEventEmitterMixin.js b/src/core/ReactEventEmitterMixin.js index b93d58a001..ac635f87d7 100644 --- a/src/core/ReactEventEmitterMixin.js +++ b/src/core/ReactEventEmitterMixin.js @@ -44,7 +44,7 @@ var ReactEventEmitterMixin = { * there's a touch top-level listeners, anchors don't receive clicks for some * reason, and only in some cases). * - * @param {*} Configuration passed through to `listenAtTopLevel`. + * @param {*} config Configuration passed through to `listenAtTopLevel`. */ ensureListening: function(config) { if (!this._isListening) { diff --git a/src/core/ReactMount.js b/src/core/ReactMount.js index ed1def0d4c..dc1fd92aa1 100644 --- a/src/core/ReactMount.js +++ b/src/core/ReactMount.js @@ -225,7 +225,7 @@ var ReactMount = { * * @private */ - prepareTopLevelEvents: function() { + prepareEnvironmentForDOM: function() { ReactEventEmitter.ensureListening(ReactMount.useTouchEvents); }, @@ -262,7 +262,7 @@ var ReactMount = { * @return {string} reactRoot ID prefix */ _registerComponent: function(nextComponent, container) { - ReactMount.prepareTopLevelEvents(); + ReactMount.prepareEnvironmentForDOM(); var reactRootID = ReactMount.registerContainer(container); instanceByReactRootID[reactRootID] = nextComponent; @@ -400,7 +400,7 @@ var ReactMount = { if (!component) { return false; } - component.unmountComponentFromNode(container); + ReactMount.unmountComponentFromNode(component, container); delete instanceByReactRootID[reactRootID]; delete containersByReactRootID[reactRootID]; if (__DEV__) { @@ -409,6 +409,24 @@ var ReactMount = { 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.unmountAndReleaseReactRootNode} + */ + unmountComponentFromNode: function(instance, container) { + instance.unmountComponent(); + + // 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.