@samitouri / QOS-React / commits / 6a65960641

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