diff --git a/src/core/React.js b/src/core/React.js index b6de699478a..ab65e34dfda 100644 --- a/src/core/React.js +++ b/src/core/React.js @@ -36,6 +36,7 @@ var ReactTextComponent = require('ReactTextComponent'); ReactDefaultInjection.inject(); var React = { + EventPluginRegistry: require('EventPluginRegistry'), DOM: ReactDOM, PropTypes: ReactPropTypes, initializeTouchEvents: function(shouldUseTouch) { diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index f078d659734..21e6cc219af 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -331,7 +331,7 @@ var ReactComponent = { * @param {string} rootID DOM ID of the root node. * @param {ReactReconcileTransaction} transaction * @param {number} mountDepth number of components in the owner hierarchy. - * @return {?string} Rendered markup to be inserted into the DOM. + * @return {?ReactDOMMountImage} Mount image to be inserted into the DOM. * @internal */ mountComponent: function(rootID, transaction, mountDepth) { @@ -486,8 +486,12 @@ var ReactComponent = { container, transaction, shouldReuseMarkup) { - var markup = this.mountComponent(rootID, transaction, 0); - ReactComponent.mountImageIntoNode(markup, container, shouldReuseMarkup); + var mountImage = this.mountComponent(rootID, transaction, 0); + ReactComponent.mountImageIntoNode( + mountImage, + container, + shouldReuseMarkup + ); }, /** diff --git a/src/core/ReactComponentBrowserEnvironment.js b/src/core/ReactComponentBrowserEnvironment.js index 719f91a62d4..f27a78a7442 100644 --- a/src/core/ReactComponentBrowserEnvironment.js +++ b/src/core/ReactComponentBrowserEnvironment.js @@ -21,6 +21,7 @@ "use strict"; var ReactDOMIDOperations = require('ReactDOMIDOperations'); +var ReactDOMMountImage = require('ReactDOMMountImage'); var ReactMarkupChecksum = require('ReactMarkupChecksum'); var ReactMount = require('ReactMount'); var ReactReconcileTransaction = require('ReactReconcileTransaction'); @@ -29,6 +30,10 @@ var getReactRootElementInContainer = require('getReactRootElementInContainer'); var invariant = require('invariant'); var mutateHTMLNodeWithMarkup = require('mutateHTMLNodeWithMarkup'); +if (__DEV__) { + var validateNodeNesting = require('validateNodeNesting'); +} + var ELEMENT_NODE_TYPE = 1; var DOC_NODE_TYPE = 9; @@ -75,12 +80,12 @@ var ReactComponentBrowserEnvironment = { }, /** - * @param {string} markup Markup string to place into the DOM Element. + * @param {ReactDOMMountImage} mountImage Markup to put 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) { + mountImageIntoNode: function(mountImage, container, shouldReuseMarkup) { invariant( container && ( container.nodeType === ELEMENT_NODE_TYPE || @@ -90,7 +95,7 @@ var ReactComponentBrowserEnvironment = { ); if (shouldReuseMarkup) { if (ReactMarkupChecksum.canReuseMarkup( - markup, + mountImage.markup, getReactRootElementInContainer(container))) { return; } else { @@ -112,25 +117,31 @@ var ReactComponentBrowserEnvironment = { // to mutate documentElement which requires doing some crazy tricks. See // mutateHTMLNodeWithMarkup() if (container.nodeType === DOC_NODE_TYPE) { - mutateHTMLNodeWithMarkup(container.documentElement, markup); + mutateHTMLNodeWithMarkup(container.documentElement, mountImage.markup); return; } + if (__DEV__) { + validateNodeNesting(container.nodeName, mountImage.nodeName); + } + // 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; + container.innerHTML = mountImage.markup; if (next) { parent.insertBefore(container, next); } else { parent.appendChild(container); } } else { - container.innerHTML = markup; + container.innerHTML = mountImage.markup; } + + ReactDOMMountImage.release(mountImage); } }; diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index ebb30c3a9ed..1db76e36f43 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -21,8 +21,10 @@ var ReactComponent = require('ReactComponent'); var ReactContext = require('ReactContext'); var ReactCurrentOwner = require('ReactCurrentOwner'); +var ReactDOMMountImage = require('ReactDOMMountImage'); var ReactErrorUtils = require('ReactErrorUtils'); var ReactOwner = require('ReactOwner'); +var ReactMount = require('ReactMount'); var ReactPerf = require('ReactPerf'); var ReactPropTransferer = require('ReactPropTransferer'); var ReactPropTypeLocations = require('ReactPropTypeLocations'); @@ -36,6 +38,10 @@ var mixInto = require('mixInto'); var objMap = require('objMap'); var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); +if (__DEV__) { + var validateNodeNesting = require('validateNodeNesting'); +} + /** * Policies that describe methods in `ReactCompositeComponentInterface`. */ @@ -659,7 +665,7 @@ var ReactCompositeComponentMixin = { * @param {string} rootID DOM ID of the root node. * @param {ReactReconcileTransaction} transaction * @param {number} mountDepth number of components in the owner hierarchy - * @return {?string} Rendered markup to be inserted into the DOM. + * @return {ReactDOMMountImage} Mount image to be inserted into the DOM. * @final * @internal */ @@ -700,7 +706,7 @@ var ReactCompositeComponentMixin = { // Done with mounting, `setState` will now trigger UI changes. this._compositeLifeCycleState = null; - var markup = this._renderedComponent.mountComponent( + var mountImage = this._renderedComponent.mountComponent( rootID, transaction, mountDepth + 1 @@ -708,7 +714,7 @@ var ReactCompositeComponentMixin = { if (this.componentDidMount) { transaction.getReactMountReady().enqueue(this, this.componentDidMount); } - return markup; + return mountImage; } ), @@ -1052,15 +1058,20 @@ var ReactCompositeComponentMixin = { var prevComponentID = prevComponent._rootNodeID; prevComponent.unmountComponent(); this._renderedComponent = nextComponent; - var nextMarkup = nextComponent.mountComponent( + var nextMountImage = nextComponent.mountComponent( thisID, transaction, this._mountDepth + 1 ); + if (__DEV__) { + var parentNode = ReactMount.getNode(prevComponentID).parentNode; + validateNodeNesting(parentNode.nodeName, nextMountImage.nodeName); + } ReactComponent.DOMIDOperations.dangerouslyReplaceNodeWithMarkupByID( prevComponentID, - nextMarkup + nextMountImage.markup ); + ReactDOMMountImage.release(nextMountImage); } } ), diff --git a/src/core/ReactDOMComponent.js b/src/core/ReactDOMComponent.js index 6a744b087a6..dc9cd7f4a8f 100644 --- a/src/core/ReactDOMComponent.js +++ b/src/core/ReactDOMComponent.js @@ -23,6 +23,7 @@ var CSSPropertyOperations = require('CSSPropertyOperations'); var DOMProperty = require('DOMProperty'); var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactComponent = require('ReactComponent'); +var ReactDOMMountImage = require('ReactDOMMountImage'); var ReactEventEmitter = require('ReactEventEmitter'); var ReactMultiChild = require('ReactMultiChild'); var ReactMount = require('ReactMount'); @@ -34,6 +35,10 @@ var keyOf = require('keyOf'); var merge = require('merge'); var mixInto = require('mixInto'); +if (__DEV__) { + var validateNodeNesting = require('validateNodeNesting'); +} + var putListener = ReactEventEmitter.putListener; var deleteListener = ReactEventEmitter.deleteListener; var registrationNames = ReactEventEmitter.registrationNames; @@ -83,7 +88,7 @@ ReactDOMComponent.Mixin = { * @param {string} rootID The root DOM ID for this node. * @param {ReactReconcileTransaction} transaction * @param {number} mountDepth number of components in the owner hierarchy - * @return {string} The computed markup. + * @return {ReactDOMMountImage} The computed mount image. */ mountComponent: ReactPerf.measure( 'ReactDOMComponent', @@ -96,9 +101,21 @@ ReactDOMComponent.Mixin = { mountDepth ); assertValidProps(this.props); - return ( + var contentImages = this._createContentMountImages(transaction); + var contentMarkup = contentImages.join(''); + for (var i = 0; i < contentImages.length; i++) { + var image = contentImages[i]; + if (__DEV__) { + if (image.nodeName != null) { + validateNodeNesting(this.tagName, image.nodeName); + } + } + ReactDOMMountImage.release(image); + } + return ReactDOMMountImage.getPooled( + this.tagName, this._createOpenTagMarkup() + - this._createContentMarkup(transaction) + + contentMarkup + this._tagClose ); } @@ -153,30 +170,33 @@ ReactDOMComponent.Mixin = { * * @private * @param {ReactReconcileTransaction} transaction - * @return {string} Content markup. + * @return {array} List of mount images */ - _createContentMarkup: function(transaction) { + _createContentMountImages: function(transaction) { // Intentional use of != to avoid catching zero/false. var innerHTML = this.props.dangerouslySetInnerHTML; if (innerHTML != null) { if (innerHTML.__html != null) { - return innerHTML.__html; + return [ReactDOMMountImage.getPooled(null, innerHTML.__html)]; } } else { var contentToUse = CONTENT_TYPES[typeof this.props.children] ? this.props.children : null; var childrenToUse = contentToUse != null ? null : this.props.children; if (contentToUse != null) { - return escapeTextForBrowser(contentToUse); + return [ReactDOMMountImage.getPooled( + '#text', + escapeTextForBrowser(contentToUse) + )]; } else if (childrenToUse != null) { var mountImages = this.mountChildren( childrenToUse, transaction ); - return mountImages.join(''); + return mountImages; } } - return ''; + return []; }, receiveComponent: function(nextComponent, transaction) { diff --git a/src/core/ReactDOMMountImage.js b/src/core/ReactDOMMountImage.js new file mode 100644 index 00000000000..eb4de071cb3 --- /dev/null +++ b/src/core/ReactDOMMountImage.js @@ -0,0 +1,59 @@ +/** + * 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 ReactDOMMountImage + */ + +"use strict"; + +var PooledClass = require('PooledClass'); + +var mixInto = require('mixInto'); + +/** + * A class to keep track of strings of markup alongside + * + * This implements `PooledClass`, so you should never need to instantiate this. + * Instead, use `ReactDOMMountImage.getPooled()`. + * + * @param {?string} nodeName Tag name, '#text', or null if unknown + * @param {string} markup HTML to be mounted into the DOM + * @class ReactDOMMountImage + * @implements PooledClass + * @internal + */ +function ReactDOMMountImage(nodeName, markup) { + this.nodeName = nodeName; + this.markup = markup; +} + +mixInto(ReactDOMMountImage, { + toString: function() { + // Allow using .join('') on an array of mount images + return this.markup; + }, + + /** + * `PooledClass` looks for this. + */ + destructor: function() { + this.nodeName = null; + this.markup = null; + } +}); + +PooledClass.addPoolingTo(ReactDOMMountImage, PooledClass.twoArgumentPooler); + +module.exports = ReactDOMMountImage; diff --git a/src/core/ReactMultiChild.js b/src/core/ReactMultiChild.js index 9fe658c4f6c..49b56d7f9b7 100644 --- a/src/core/ReactMultiChild.js +++ b/src/core/ReactMultiChild.js @@ -20,6 +20,7 @@ "use strict"; var ReactComponent = require('ReactComponent'); +var ReactDOMMountImage = require('ReactDOMMountImage'); var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); var flattenChildren = require('flattenChildren'); @@ -199,7 +200,6 @@ var ReactMultiChild = { transaction, this._mountDepth + 1 ); - child._mountImage = mountImage; child._mountIndex = index; mountImages.push(mountImage); index++; @@ -350,11 +350,12 @@ var ReactMultiChild = { /** * Creates a child component. * - * @param {ReactComponent} child Component to create. + * @param {ReactDOMMountImage} mountImage Markup to insert. + * @param {number} mountIndex Destination index of markup. * @protected */ - createChild: function(child) { - enqueueMarkup(this._rootNodeID, child._mountImage, child._mountIndex); + createChild: function(mountImage, mountIndex) { + enqueueMarkup(this._rootNodeID, mountImage.markup, mountIndex); }, /** @@ -396,9 +397,10 @@ var ReactMultiChild = { transaction, this._mountDepth + 1 ); - child._mountImage = mountImage; child._mountIndex = index; - this.createChild(child); + // TODO: Use validateNodeNesting to check DOM structure + this.createChild(mountImage, index); + ReactDOMMountImage.release(mountImage); this._renderedChildren = this._renderedChildren || {}; this._renderedChildren[name] = child; }, @@ -415,7 +417,6 @@ var ReactMultiChild = { _unmountChildByName: function(child, name) { if (ReactComponent.isValidComponent(child)) { this.removeChild(child); - child._mountImage = null; child._mountIndex = null; child.unmountComponent(); delete this._renderedChildren[name]; diff --git a/src/core/ReactTextComponent.js b/src/core/ReactTextComponent.js index 5b1da0f4bdc..727490714d9 100644 --- a/src/core/ReactTextComponent.js +++ b/src/core/ReactTextComponent.js @@ -20,6 +20,7 @@ "use strict"; var ReactComponent = require('ReactComponent'); +var ReactDOMMountImage = require('ReactDOMMountImage'); var ReactMount = require('ReactMount'); var escapeTextForBrowser = require('escapeTextForBrowser'); @@ -54,7 +55,7 @@ mixInto(ReactTextComponent, { * @param {string} rootID DOM ID of the root node. * @param {ReactReconcileTransaction} transaction * @param {number} mountDepth number of components in the owner hierarchy - * @return {string} Markup for this text node. + * @return {ReactDOMMountImage} Mount image for this text node. * @internal */ mountComponent: function(rootID, transaction, mountDepth) { @@ -64,7 +65,8 @@ mixInto(ReactTextComponent, { transaction, mountDepth ); - return ( + return ReactDOMMountImage.getPooled( + 'SPAN', '' + escapeTextForBrowser(this.props.text) + '' diff --git a/src/core/__tests__/ReactDOMComponent-test.js b/src/core/__tests__/ReactDOMComponent-test.js index 973f03cb042..5fb931fa44a 100644 --- a/src/core/__tests__/ReactDOMComponent-test.js +++ b/src/core/__tests__/ReactDOMComponent-test.js @@ -204,6 +204,20 @@ describe('ReactDOMComponent', function() { stub.receiveComponent({props: {}}, transaction); expect(nodeValueSetter.mock.calls.length).toBe(1); }); + + it("should warn on invalid markup nesting", function() { + spyOn(console, 'warn'); + expect(console.warn.argsForCall.length).toBe(0); + var stub = ReactTestUtils.renderIntoDocument( +