Improve validateDOMNesting message for whitespace (#7515)
For #5071.
Ben Alpert committed
Aug 19, 2016 at 11:45 UTC
6a659606415d4853026df6e6d525274fdc7d35ea
4 files changed
+49
-18
src/renderers/dom/client/validateDOMNesting.js
+27
-3
@@ -322,11 +322,24 @@ if (__DEV__) {
322
323
var didWarn = {};
324
325
- validateDOMNesting = function(childTag, childInstance, ancestorInfo) {
325
+ validateDOMNesting = function(
326
+ childTag,
327
+ childText,
328
+ childInstance,
329
+ ancestorInfo
330
+ ) {
331
ancestorInfo = ancestorInfo || emptyAncestorInfo;
332
var parentInfo = ancestorInfo.current;
333
var parentTag = parentInfo && parentInfo.tag;
334
335
+ if (childText != null) {
336
+ warning(
337
+ childTag == null,
338
+ 'validateDOMNesting: when childText is passed, childTag should be null'
339
+ );
340
+ childTag = '#text';
341
+ }
342
+
343
var invalidParent =
344
isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
345
var invalidAncestor =
@@ -385,7 +398,17 @@ if (__DEV__) {
398
didWarn[warnKey] = true;
399
400
var tagDisplayName = childTag;
388
- if (childTag !== '#text') {
401
+ var whitespaceInfo = '';
402
+ if (childTag === '#text') {
403
+ if (/\S/.test(childText)) {
404
+ tagDisplayName = 'Text nodes';
405
+ } else {
406
+ tagDisplayName = 'Whitespace text nodes';
407
+ whitespaceInfo =
408
+ ' Make sure you don\'t have any extra whitespace between tags on ' +
409
+ 'each line of your source code.';
410
+ }
411
+ } else {
412
tagDisplayName = '<' + childTag + '>';
413
}
414
@@ -398,10 +421,11 @@ if (__DEV__) {
421
}
422
warning(
423
false,
401
- 'validateDOMNesting(...): %s cannot appear as a child of <%s>. ' +
424
+ 'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s ' +
425
'See %s.%s',
426
tagDisplayName,
427
ancestorTag,
428
+ whitespaceInfo,
429
ownerInfo,
430
info
431
);
src/renderers/dom/shared/ReactDOMComponent.js
+10
-9
@@ -254,9 +254,9 @@ function optionPostMount() {
254
ReactDOMOption.postMountWrapper(inst);
255
}
256
257
-var setContentChildForInstrumentation = emptyFunction;
257
+var setAndValidateContentChildDev = emptyFunction;
258
if (__DEV__) {
259
- setContentChildForInstrumentation = function(content) {
259
+ setAndValidateContentChildDev = function(content) {
260
var hasExistingContent = this._contentDebugID != null;
261
var debugID = this._debugID;
262
// This ID represents the inlined child that has no backing instance:
@@ -270,6 +270,7 @@ if (__DEV__) {
270
return;
271
}
272
273
+ validateDOMNesting(null, String(content), this, this._ancestorInfo);
274
this._contentDebugID = contentDebugID;
275
if (hasExistingContent) {
276
ReactInstrumentation.debugTool.onBeforeUpdateComponent(contentDebugID, content);
@@ -497,7 +498,7 @@ function ReactDOMComponent(element) {
498
this._flags = 0;
499
if (__DEV__) {
500
this._ancestorInfo = null;
500
- setContentChildForInstrumentation.call(this, null);
501
+ setAndValidateContentChildDev.call(this, null);
502
}
503
}
504
@@ -605,7 +606,7 @@ ReactDOMComponent.Mixin = {
606
if (parentInfo) {
607
// parentInfo should always be present except for the top-level
608
// component when server rendering
608
- validateDOMNesting(this._tag, this, parentInfo);
609
+ validateDOMNesting(this._tag, null, this, parentInfo);
610
}
611
this._ancestorInfo =
612
validateDOMNesting.updatedAncestorInfo(parentInfo, this._tag, this);
@@ -801,7 +802,7 @@ ReactDOMComponent.Mixin = {
802
// TODO: Validate that text is allowed as a child of this node
803
ret = escapeTextContentForBrowser(contentToUse);
804
if (__DEV__) {
804
- setContentChildForInstrumentation.call(this, contentToUse);
805
+ setAndValidateContentChildDev.call(this, contentToUse);
806
}
807
} else if (childrenToUse != null) {
808
var mountImages = this.mountChildren(
@@ -843,7 +844,7 @@ ReactDOMComponent.Mixin = {
844
if (contentToUse != null) {
845
// TODO: Validate that text is allowed as a child of this node
846
if (__DEV__) {
846
- setContentChildForInstrumentation.call(this, contentToUse);
847
+ setAndValidateContentChildDev.call(this, contentToUse);
848
}
849
DOMLazyTree.queueText(lazyTree, contentToUse);
850
} else if (childrenToUse != null) {
@@ -1117,7 +1118,7 @@ ReactDOMComponent.Mixin = {
1118
if (lastContent !== nextContent) {
1119
this.updateTextContent('' + nextContent);
1120
if (__DEV__) {
1120
- setContentChildForInstrumentation.call(this, nextContent);
1121
+ setAndValidateContentChildDev.call(this, nextContent);
1122
}
1123
}
1124
} else if (nextHtml != null) {
@@ -1129,7 +1130,7 @@ ReactDOMComponent.Mixin = {
1130
}
1131
} else if (nextChildren != null) {
1132
if (__DEV__) {
1132
- setContentChildForInstrumentation.call(this, null);
1133
+ setAndValidateContentChildDev.call(this, null);
1134
}
1135
1136
this.updateChildren(nextChildren, transaction, context);
@@ -1196,7 +1197,7 @@ ReactDOMComponent.Mixin = {
1197
this._wrapperState = null;
1198
1199
if (__DEV__) {
1199
- setContentChildForInstrumentation.call(this, null);
1200
+ setAndValidateContentChildDev.call(this, null);
1201
}
1202
},
1203
src/renderers/dom/shared/ReactDOMTextComponent.js
+1
-1
@@ -75,7 +75,7 @@ Object.assign(ReactDOMTextComponent.prototype, {
75
if (parentInfo) {
76
// parentInfo should always be present except for the top-level
77
// component when server rendering
78
- validateDOMNesting('#text', this, parentInfo);
78
+ validateDOMNesting(null, this._stringText, this, parentInfo);
79
}
80
}
81
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
+11
-5
@@ -736,7 +736,7 @@ describe('ReactDOMComponent', function() {
736
});
737
738
it('should work error event on <source> element', function() {
739
- spyOn(console, 'error');
739
+ spyOn(console, 'error');
740
var container = document.createElement('div');
741
ReactDOM.render(
742
<video>
@@ -1225,7 +1225,7 @@ describe('ReactDOMComponent', function() {
1225
1226
class Row extends React.Component {
1227
render() {
1228
- return <tr />;
1228
+ return <tr>x</tr>;
1229
}
1230
}
1231
@@ -1237,15 +1237,21 @@ describe('ReactDOMComponent', function() {
1237
1238
ReactTestUtils.renderIntoDocument(<Foo />);
1239
1240
- expect(console.error.calls.count()).toBe(2);
1240
+ expect(console.error.calls.count()).toBe(3);
1241
expect(console.error.calls.argsFor(0)[0]).toBe(
1242
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1243
'<table>. See Foo > table > Row > tr. Add a <tbody> to your code to ' +
1244
'match the DOM tree generated by the browser.'
1245
);
1246
expect(console.error.calls.argsFor(1)[0]).toBe(
1247
- 'Warning: validateDOMNesting(...): #text cannot appear as a child ' +
1248
- 'of <table>. See Foo > table > #text.'
1247
+ 'Warning: validateDOMNesting(...): Text nodes cannot appear as a ' +
1248
+ 'child of <tr>. See Row > tr > #text.'
1249
+ );
1250
+ expect(console.error.calls.argsFor(2)[0]).toBe(
1251
+ 'Warning: validateDOMNesting(...): Whitespace text nodes cannot ' +
1252
+ 'appear as a child of <table>. Make sure you don\'t have any extra ' +
1253
+ 'whitespace between tags on each line of your source code. See Foo > ' +
1254
+ 'table > #text.'
1255
);
1256
});
1257