Skip to content

Commit 67a19c6

Browse files
committed
Validate node nesting structure
This solution feels a little gross to me, but I haven't been able to come up with anything better. Fixes facebook#101.
1 parent 153b75f commit 67a19c6

13 files changed

+240
-37
lines changed

src/core/React.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ var ReactTextComponent = require('ReactTextComponent');
3636
ReactDefaultInjection.inject();
3737

3838
var React = {
39+
EventPluginRegistry: require('EventPluginRegistry'),
3940
DOM: ReactDOM,
4041
PropTypes: ReactPropTypes,
4142
initializeTouchEvents: function(shouldUseTouch) {

src/core/ReactComponent.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ var ReactComponent = {
331331
* @param {string} rootID DOM ID of the root node.
332332
* @param {ReactReconcileTransaction} transaction
333333
* @param {number} mountDepth number of components in the owner hierarchy.
334-
* @return {?string} Rendered markup to be inserted into the DOM.
334+
* @return {?ReactDOMMountImage} Mount image to be inserted into the DOM.
335335
* @internal
336336
*/
337337
mountComponent: function(rootID, transaction, mountDepth) {
@@ -486,8 +486,12 @@ var ReactComponent = {
486486
container,
487487
transaction,
488488
shouldReuseMarkup) {
489-
var markup = this.mountComponent(rootID, transaction, 0);
490-
ReactComponent.mountImageIntoNode(markup, container, shouldReuseMarkup);
489+
var mountImage = this.mountComponent(rootID, transaction, 0);
490+
ReactComponent.mountImageIntoNode(
491+
mountImage,
492+
container,
493+
shouldReuseMarkup
494+
);
491495
},
492496

493497
/**

src/core/ReactComponentBrowserEnvironment.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"use strict";
2222

2323
var ReactDOMIDOperations = require('ReactDOMIDOperations');
24+
var ReactDOMMountImage = require('ReactDOMMountImage');
2425
var ReactMarkupChecksum = require('ReactMarkupChecksum');
2526
var ReactMount = require('ReactMount');
2627
var ReactReconcileTransaction = require('ReactReconcileTransaction');
@@ -29,6 +30,10 @@ var getReactRootElementInContainer = require('getReactRootElementInContainer');
2930
var invariant = require('invariant');
3031
var mutateHTMLNodeWithMarkup = require('mutateHTMLNodeWithMarkup');
3132

33+
if (__DEV__) {
34+
var validateNodeNesting = require('validateNodeNesting');
35+
}
36+
3237

3338
var ELEMENT_NODE_TYPE = 1;
3439
var DOC_NODE_TYPE = 9;
@@ -75,12 +80,12 @@ var ReactComponentBrowserEnvironment = {
7580
},
7681

7782
/**
78-
* @param {string} markup Markup string to place into the DOM Element.
83+
* @param {ReactDOMMountImage} mountImage Markup to put into the DOM Element.
7984
* @param {DOMElement} container DOM Element to insert markup into.
8085
* @param {boolean} shouldReuseMarkup Should reuse the existing markup in the
8186
* container if possible.
8287
*/
83-
mountImageIntoNode: function(markup, container, shouldReuseMarkup) {
88+
mountImageIntoNode: function(mountImage, container, shouldReuseMarkup) {
8489
invariant(
8590
container && (
8691
container.nodeType === ELEMENT_NODE_TYPE ||
@@ -90,7 +95,7 @@ var ReactComponentBrowserEnvironment = {
9095
);
9196
if (shouldReuseMarkup) {
9297
if (ReactMarkupChecksum.canReuseMarkup(
93-
markup,
98+
mountImage.markup,
9499
getReactRootElementInContainer(container))) {
95100
return;
96101
} else {
@@ -112,25 +117,31 @@ var ReactComponentBrowserEnvironment = {
112117
// to mutate documentElement which requires doing some crazy tricks. See
113118
// mutateHTMLNodeWithMarkup()
114119
if (container.nodeType === DOC_NODE_TYPE) {
115-
mutateHTMLNodeWithMarkup(container.documentElement, markup);
120+
mutateHTMLNodeWithMarkup(container.documentElement, mountImage.markup);
116121
return;
117122
}
118123

124+
if (__DEV__) {
125+
validateNodeNesting(container.nodeName, mountImage.nodeName);
126+
}
127+
119128
// Asynchronously inject markup by ensuring that the container is not in
120129
// the document when settings its `innerHTML`.
121130
var parent = container.parentNode;
122131
if (parent) {
123132
var next = container.nextSibling;
124133
parent.removeChild(container);
125-
container.innerHTML = markup;
134+
container.innerHTML = mountImage.markup;
126135
if (next) {
127136
parent.insertBefore(container, next);
128137
} else {
129138
parent.appendChild(container);
130139
}
131140
} else {
132-
container.innerHTML = markup;
141+
container.innerHTML = mountImage.markup;
133142
}
143+
144+
ReactDOMMountImage.release(mountImage);
134145
}
135146
};
136147

src/core/ReactCompositeComponent.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
var ReactComponent = require('ReactComponent');
2222
var ReactContext = require('ReactContext');
2323
var ReactCurrentOwner = require('ReactCurrentOwner');
24+
var ReactDOMMountImage = require('ReactDOMMountImage');
2425
var ReactErrorUtils = require('ReactErrorUtils');
2526
var ReactOwner = require('ReactOwner');
27+
var ReactMount = require('ReactMount');
2628
var ReactPerf = require('ReactPerf');
2729
var ReactPropTransferer = require('ReactPropTransferer');
2830
var ReactPropTypeLocations = require('ReactPropTypeLocations');
@@ -36,6 +38,10 @@ var mixInto = require('mixInto');
3638
var objMap = require('objMap');
3739
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
3840

41+
if (__DEV__) {
42+
var validateNodeNesting = require('validateNodeNesting');
43+
}
44+
3945
/**
4046
* Policies that describe methods in `ReactCompositeComponentInterface`.
4147
*/
@@ -659,7 +665,7 @@ var ReactCompositeComponentMixin = {
659665
* @param {string} rootID DOM ID of the root node.
660666
* @param {ReactReconcileTransaction} transaction
661667
* @param {number} mountDepth number of components in the owner hierarchy
662-
* @return {?string} Rendered markup to be inserted into the DOM.
668+
* @return {ReactDOMMountImage} Mount image to be inserted into the DOM.
663669
* @final
664670
* @internal
665671
*/
@@ -700,15 +706,15 @@ var ReactCompositeComponentMixin = {
700706

701707
// Done with mounting, `setState` will now trigger UI changes.
702708
this._compositeLifeCycleState = null;
703-
var markup = this._renderedComponent.mountComponent(
709+
var mountImage = this._renderedComponent.mountComponent(
704710
rootID,
705711
transaction,
706712
mountDepth + 1
707713
);
708714
if (this.componentDidMount) {
709715
transaction.getReactMountReady().enqueue(this, this.componentDidMount);
710716
}
711-
return markup;
717+
return mountImage;
712718
}
713719
),
714720

@@ -1052,15 +1058,20 @@ var ReactCompositeComponentMixin = {
10521058
var prevComponentID = prevComponent._rootNodeID;
10531059
prevComponent.unmountComponent();
10541060
this._renderedComponent = nextComponent;
1055-
var nextMarkup = nextComponent.mountComponent(
1061+
var nextMountImage = nextComponent.mountComponent(
10561062
thisID,
10571063
transaction,
10581064
this._mountDepth + 1
10591065
);
1066+
if (__DEV__) {
1067+
var parentNode = ReactMount.getNode(prevComponentID).parentNode;
1068+
validateNodeNesting(parentNode.nodeName, nextMountImage.nodeName);
1069+
}
10601070
ReactComponent.DOMIDOperations.dangerouslyReplaceNodeWithMarkupByID(
10611071
prevComponentID,
1062-
nextMarkup
1072+
nextMountImage.markup
10631073
);
1074+
ReactDOMMountImage.release(nextMountImage);
10641075
}
10651076
}
10661077
),

src/core/ReactDOMComponent.js

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var CSSPropertyOperations = require('CSSPropertyOperations');
2323
var DOMProperty = require('DOMProperty');
2424
var DOMPropertyOperations = require('DOMPropertyOperations');
2525
var ReactComponent = require('ReactComponent');
26+
var ReactDOMMountImage = require('ReactDOMMountImage');
2627
var ReactEventEmitter = require('ReactEventEmitter');
2728
var ReactMultiChild = require('ReactMultiChild');
2829
var ReactMount = require('ReactMount');
@@ -34,6 +35,10 @@ var keyOf = require('keyOf');
3435
var merge = require('merge');
3536
var mixInto = require('mixInto');
3637

38+
if (__DEV__) {
39+
var validateNodeNesting = require('validateNodeNesting');
40+
}
41+
3742
var putListener = ReactEventEmitter.putListener;
3843
var deleteListener = ReactEventEmitter.deleteListener;
3944
var registrationNames = ReactEventEmitter.registrationNames;
@@ -83,7 +88,7 @@ ReactDOMComponent.Mixin = {
8388
* @param {string} rootID The root DOM ID for this node.
8489
* @param {ReactReconcileTransaction} transaction
8590
* @param {number} mountDepth number of components in the owner hierarchy
86-
* @return {string} The computed markup.
91+
* @return {ReactDOMMountImage} The computed mount image.
8792
*/
8893
mountComponent: ReactPerf.measure(
8994
'ReactDOMComponent',
@@ -96,9 +101,21 @@ ReactDOMComponent.Mixin = {
96101
mountDepth
97102
);
98103
assertValidProps(this.props);
99-
return (
104+
var contentImages = this._createContentMountImages(transaction);
105+
var contentMarkup = contentImages.join('');
106+
for (var i = 0; i < contentImages.length; i++) {
107+
var image = contentImages[i];
108+
if (__DEV__) {
109+
if (image.nodeName != null) {
110+
validateNodeNesting(this.tagName, image.nodeName);
111+
}
112+
}
113+
ReactDOMMountImage.release(image);
114+
}
115+
return ReactDOMMountImage.getPooled(
116+
this.tagName,
100117
this._createOpenTagMarkup() +
101-
this._createContentMarkup(transaction) +
118+
contentMarkup +
102119
this._tagClose
103120
);
104121
}
@@ -153,30 +170,33 @@ ReactDOMComponent.Mixin = {
153170
*
154171
* @private
155172
* @param {ReactReconcileTransaction} transaction
156-
* @return {string} Content markup.
173+
* @return {array} List of mount images
157174
*/
158-
_createContentMarkup: function(transaction) {
175+
_createContentMountImages: function(transaction) {
159176
// Intentional use of != to avoid catching zero/false.
160177
var innerHTML = this.props.dangerouslySetInnerHTML;
161178
if (innerHTML != null) {
162179
if (innerHTML.__html != null) {
163-
return innerHTML.__html;
180+
return [ReactDOMMountImage.getPooled(null, innerHTML.__html)];
164181
}
165182
} else {
166183
var contentToUse =
167184
CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
168185
var childrenToUse = contentToUse != null ? null : this.props.children;
169186
if (contentToUse != null) {
170-
return escapeTextForBrowser(contentToUse);
187+
return [ReactDOMMountImage.getPooled(
188+
'#text',
189+
escapeTextForBrowser(contentToUse)
190+
)];
171191
} else if (childrenToUse != null) {
172192
var mountImages = this.mountChildren(
173193
childrenToUse,
174194
transaction
175195
);
176-
return mountImages.join('');
196+
return mountImages;
177197
}
178198
}
179-
return '';
199+
return [];
180200
},
181201

182202
receiveComponent: function(nextComponent, transaction) {

src/core/ReactDOMMountImage.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Copyright 2013 Facebook, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* @providesModule ReactDOMMountImage
17+
*/
18+
19+
"use strict";
20+
21+
var PooledClass = require('PooledClass');
22+
23+
var mixInto = require('mixInto');
24+
25+
/**
26+
* A class to keep track of strings of markup alongside
27+
*
28+
* This implements `PooledClass`, so you should never need to instantiate this.
29+
* Instead, use `ReactDOMMountImage.getPooled()`.
30+
*
31+
* @param {?string} nodeName Tag name, '#text', or null if unknown
32+
* @param {string} markup HTML to be mounted into the DOM
33+
* @class ReactDOMMountImage
34+
* @implements PooledClass
35+
* @internal
36+
*/
37+
function ReactDOMMountImage(nodeName, markup) {
38+
this.nodeName = nodeName;
39+
this.markup = markup;
40+
}
41+
42+
mixInto(ReactDOMMountImage, {
43+
toString: function() {
44+
// Allow using .join('') on an array of mount images
45+
return this.markup;
46+
},
47+
48+
/**
49+
* `PooledClass` looks for this.
50+
*/
51+
destructor: function() {
52+
this.nodeName = null;
53+
this.markup = null;
54+
}
55+
});
56+
57+
PooledClass.addPoolingTo(ReactDOMMountImage, PooledClass.twoArgumentPooler);
58+
59+
module.exports = ReactDOMMountImage;

src/core/ReactMultiChild.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"use strict";
2121

2222
var ReactComponent = require('ReactComponent');
23+
var ReactDOMMountImage = require('ReactDOMMountImage');
2324
var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
2425

2526
var flattenChildren = require('flattenChildren');
@@ -199,7 +200,6 @@ var ReactMultiChild = {
199200
transaction,
200201
this._mountDepth + 1
201202
);
202-
child._mountImage = mountImage;
203203
child._mountIndex = index;
204204
mountImages.push(mountImage);
205205
index++;
@@ -350,11 +350,12 @@ var ReactMultiChild = {
350350
/**
351351
* Creates a child component.
352352
*
353-
* @param {ReactComponent} child Component to create.
353+
* @param {ReactDOMMountImage} mountImage Markup to insert.
354+
* @param {number} mountIndex Destination index of markup.
354355
* @protected
355356
*/
356-
createChild: function(child) {
357-
enqueueMarkup(this._rootNodeID, child._mountImage, child._mountIndex);
357+
createChild: function(mountImage, mountIndex) {
358+
enqueueMarkup(this._rootNodeID, mountImage.markup, mountIndex);
358359
},
359360

360361
/**
@@ -396,9 +397,10 @@ var ReactMultiChild = {
396397
transaction,
397398
this._mountDepth + 1
398399
);
399-
child._mountImage = mountImage;
400400
child._mountIndex = index;
401-
this.createChild(child);
401+
// TODO: Use validateNodeNesting to check DOM structure
402+
this.createChild(mountImage, index);
403+
ReactDOMMountImage.release(mountImage);
402404
this._renderedChildren = this._renderedChildren || {};
403405
this._renderedChildren[name] = child;
404406
},
@@ -415,7 +417,6 @@ var ReactMultiChild = {
415417
_unmountChildByName: function(child, name) {
416418
if (ReactComponent.isValidComponent(child)) {
417419
this.removeChild(child);
418-
child._mountImage = null;
419420
child._mountIndex = null;
420421
child.unmountComponent();
421422
delete this._renderedChildren[name];

0 commit comments

Comments
 (0)