Add more context to DOM nesting warning
Better version of #3578.
Ben Alpert committed
Apr 21, 2015 at 21:55 UTC
64b1f05e7518e46c13ca9dbe8d2792054dd9a515
6 files changed
+327
-141
src/browser/__tests__/validateDOMNesting-test.js
+10
-4
@@ -11,7 +11,7 @@
11
12
'use strict';
13
14
-var isTagValidInContext;
14
+var validateDOMNesting;
15
16
// https://html.spec.whatwg.org/multipage/syntax.html#special
17
var specialTags = [
@@ -34,10 +34,13 @@ var formattingTags = [
34
];
35
36
function isTagStackValid(stack) {
37
+ var ancestorInfo = null;
38
for (var i = 0; i < stack.length; i++) {
38
- if (!isTagValidInContext(stack[i], stack.slice(0, i))) {
39
+ if (!validateDOMNesting.isTagValidInContext(stack[i], ancestorInfo)) {
40
return false;
41
}
42
+ ancestorInfo =
43
+ validateDOMNesting.updatedAncestorInfo(ancestorInfo, stack[i], null);
44
}
45
return true;
46
}
@@ -46,14 +49,14 @@ describe('ReactContextValidator', function() {
49
beforeEach(function() {
50
require('mock-modules').dumpCache();
51
49
- isTagValidInContext = require('validateDOMNesting').isTagValidInContext;
52
+ validateDOMNesting = require('validateDOMNesting');
53
});
54
55
it('allows any tag with no context', function() {
56
// With renderToString (for example), we don't know where we're mounting the
57
// tag so we must err on the side of leniency.
58
specialTags.concat(formattingTags, ['mysterytag']).forEach(function(tag) {
56
- expect(isTagValidInContext(tag, [])).toBe(true);
59
+ expect(validateDOMNesting.isTagValidInContext(tag, null)).toBe(true);
60
});
61
});
62
@@ -67,6 +70,8 @@ describe('ReactContextValidator', function() {
70
// Invalid, but not changed by browser parsing so we allow them
71
expect(isTagStackValid(['div', 'ul', 'ul', 'li'])).toBe(true);
72
expect(isTagStackValid(['div', 'label', 'div'])).toBe(true);
73
+ expect(isTagStackValid(['div', 'ul', 'li', 'section', 'li'])).toBe(true);
74
+ expect(isTagStackValid(['div', 'ul', 'li', 'dd', 'li'])).toBe(true);
75
});
76
77
it('prevents problematic nestings', function() {
@@ -74,5 +79,6 @@ describe('ReactContextValidator', function() {
79
expect(isTagStackValid(['form', 'form'])).toBe(false);
80
expect(isTagStackValid(['p', 'p'])).toBe(false);
81
expect(isTagStackValid(['table', 'tr'])).toBe(false);
82
+ expect(isTagStackValid(['div', 'ul', 'li', 'div', 'li'])).toBe(false);
83
});
84
});
src/browser/ui/ReactDOMComponent.js
+9
-8
@@ -222,12 +222,13 @@ function validateDangerousTag(tag) {
222
}
223
}
224
225
-function processChildContext(context, tagName) {
225
+function processChildContext(context, inst) {
226
if (__DEV__) {
227
// Pass down our tag name to child components for validation purposes
228
context = assign({}, context);
229
- var stack = context[validateDOMNesting.tagStackContextKey] || [];
230
- context[validateDOMNesting.tagStackContextKey] = stack.concat([tagName]);
229
+ var info = context[validateDOMNesting.ancestorInfoContextKey];
230
+ context[validateDOMNesting.ancestorInfoContextKey] =
231
+ validateDOMNesting.updatedAncestorInfo(info, inst._tag, inst);
232
}
233
return context;
234
}
@@ -278,11 +279,11 @@ ReactDOMComponent.Mixin = {
279
280
assertValidProps(this, this._currentElement.props);
281
if (__DEV__) {
281
- if (context[validateDOMNesting.tagStackContextKey]) {
282
+ if (context[validateDOMNesting.ancestorInfoContextKey]) {
283
validateDOMNesting(
283
- context[validateDOMNesting.tagStackContextKey],
284
this._tag,
285
- this._currentElement
285
+ this,
286
+ context[validateDOMNesting.ancestorInfoContextKey]
287
);
288
}
289
}
@@ -379,7 +380,7 @@ ReactDOMComponent.Mixin = {
380
var mountImages = this.mountChildren(
381
childrenToUse,
382
transaction,
382
- processChildContext(context, this._tag)
383
+ processChildContext(context, this)
384
);
385
ret = mountImages.join('');
386
}
@@ -431,7 +432,7 @@ ReactDOMComponent.Mixin = {
432
this._updateDOMChildren(
433
prevElement.props,
434
transaction,
434
- processChildContext(context, this._tag)
435
+ processChildContext(context, this)
436
);
437
},
438
src/browser/ui/ReactDOMTextComponent.js
+3
-3
@@ -67,11 +67,11 @@ assign(ReactDOMTextComponent.prototype, {
67
*/
68
mountComponent: function(rootID, transaction, context) {
69
if (__DEV__) {
70
- if (context[validateDOMNesting.tagStackContextKey]) {
70
+ if (context[validateDOMNesting.ancestorInfoContextKey]) {
71
validateDOMNesting(
72
- context[validateDOMNesting.tagStackContextKey],
72
'span',
74
- null
73
+ null,
74
+ context[validateDOMNesting.ancestorInfoContextKey]
75
);
76
}
77
}
src/browser/ui/ReactMount.js
+3
-2
@@ -268,8 +268,9 @@ function mountComponentIntoNode(
268
if (context === emptyObject) {
269
context = {};
270
}
271
- context[validateDOMNesting.tagStackContextKey] =
272
- [container.nodeName.toLowerCase()];
271
+ var tag = container.nodeName.toLowerCase();
272
+ context[validateDOMNesting.ancestorInfoContextKey] =
273
+ validateDOMNesting.updatedAncestorInfo(null, tag, null);
274
}
275
var markup = ReactReconciler.mountComponent(
276
componentInstance, rootID, transaction, context
src/browser/ui/__tests__/ReactDOMComponent-test.js
+77
-6
@@ -740,7 +740,7 @@ describe('ReactDOMComponent', function() {
740
expect(console.error.calls.length).toBe(1);
741
expect(console.error.calls[0].args[0]).toBe(
742
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
743
- '<div> in this context (div > div).'
743
+ '<div>. See div > tr.'
744
);
745
});
746
@@ -752,15 +752,20 @@ describe('ReactDOMComponent', function() {
752
expect(console.error.calls.length).toBe(1);
753
expect(console.error.calls[0].args[0]).toBe(
754
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
755
- '<p> in this context (p).'
755
+ '<p>. See p > tr.'
756
);
757
});
758
759
it('warns nicely for table rows', () => {
760
spyOn(console, 'error');
761
+ var Row = React.createClass({
762
+ render: function() {
763
+ return <tr />;
764
+ }
765
+ });
766
var Foo = React.createClass({
767
render: function() {
763
- return <table><tr /></table>;
768
+ return <table><Row /></table>;
769
}
770
});
771
ReactTestUtils.renderIntoDocument(<Foo />);
@@ -768,9 +773,75 @@ describe('ReactDOMComponent', function() {
773
expect(console.error.calls.length).toBe(1);
774
expect(console.error.calls[0].args[0]).toBe(
775
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
771
- '<table> in this context (div > table). Add a <tbody> to your code ' +
772
- 'to match the DOM tree generated by the browser. Check the render ' +
773
- 'method of `Foo`.'
776
+ '<table>. See Foo > table > Row > tr. Add a <tbody> to your code to ' +
777
+ 'match the DOM tree generated by the browser.'
778
+ );
779
+ });
780
+
781
+ it('gives useful context in warnings', () => {
782
+ spyOn(console, 'error');
783
+ var Row = React.createClass({
784
+ render: () => <tr />
785
+ });
786
+ var FancyRow = React.createClass({
787
+ render: () => <Row />
788
+ });
789
+ var Table = React.createClass({
790
+ render: function() { return <table>{this.props.children}</table>; }
791
+ });
792
+ var FancyTable = React.createClass({
793
+ render: function() { return <Table>{this.props.children}</Table>; }
794
+ });
795
+
796
+ var Viz1 = React.createClass({
797
+ render: () => <table><FancyRow /></table>
798
+ });
799
+ var App1 = React.createClass({
800
+ render: () => <Viz1 />
801
+ });
802
+ ReactTestUtils.renderIntoDocument(<App1 />);
803
+ expect(console.error.calls.length).toBe(1);
804
+ expect(console.error.calls[0].args[0]).toContain(
805
+ 'See Viz1 > table > FancyRow > Row > tr.'
806
+ );
807
+
808
+ var Viz2 = React.createClass({
809
+ render: () => <FancyTable><FancyRow /></FancyTable>
810
+ });
811
+ var App2 = React.createClass({
812
+ render: () => <Viz2 />
813
+ });
814
+ ReactTestUtils.renderIntoDocument(<App2 />);
815
+ expect(console.error.calls.length).toBe(2);
816
+ expect(console.error.calls[1].args[0]).toContain(
817
+ 'See Viz2 > FancyTable > Table > table > FancyRow > Row > tr.'
818
+ );
819
+
820
+ ReactTestUtils.renderIntoDocument(<FancyTable><FancyRow /></FancyTable>);
821
+ expect(console.error.calls.length).toBe(3);
822
+ expect(console.error.calls[2].args[0]).toContain(
823
+ 'See FancyTable > Table > table > FancyRow > Row > tr.'
824
+ );
825
+
826
+ ReactTestUtils.renderIntoDocument(<table><FancyRow /></table>);
827
+ expect(console.error.calls.length).toBe(4);
828
+ expect(console.error.calls[3].args[0]).toContain(
829
+ 'See table > FancyRow > Row > tr.'
830
+ );
831
+
832
+ ReactTestUtils.renderIntoDocument(<FancyTable><tr /></FancyTable>);
833
+ expect(console.error.calls.length).toBe(5);
834
+ expect(console.error.calls[4].args[0]).toContain(
835
+ 'See FancyTable > Table > table > tr.'
836
+ );
837
+
838
+ var Link = React.createClass({
839
+ render: function() { return <a>{this.props.children}</a>; }
840
+ });
841
+ ReactTestUtils.renderIntoDocument(<Link><div><Link /></div></Link>);
842
+ expect(console.error.calls.length).toBe(6);
843
+ expect(console.error.calls[5].args[0]).toContain(
844
+ 'See Link > a > ... > Link > a.'
845
);
846
});
847
});
src/browser/validateDOMNesting.js
+225
-118
@@ -11,6 +11,7 @@
11
12
'use strict';
13
14
+var assign = require('Object.assign');
15
var emptyFunction = require('emptyFunction');
16
var warning = require('warning');
17
@@ -43,88 +44,93 @@ if (__DEV__) {
44
'xmp'
45
];
46
46
- /**
47
- * Return whether `stack` contains `tag` and the last occurrence of `tag` is
48
- * deeper than any element in the `scope` array.
49
- *
50
- * https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-the-specific-scope
51
- *
52
- * Examples:
53
- * stackHasTagInSpecificScope(['p', 'quote'], 'p', ['button']) is true
54
- * stackHasTagInSpecificScope(['p', 'button'], 'p', ['button']) is false
55
- *
56
- * @param {Array<string>} stack
57
- * @param {string} tag
58
- * @param {Array<string>} scope
59
- */
60
- var stackHasTagInSpecificScope = function(stack, tag, scope) {
61
- for (var i = stack.length - 1; i >= 0; i--) {
62
- if (stack[i] === tag) {
63
- return true;
64
- }
65
- if (scope.indexOf(stack[i]) !== -1) {
66
- return false;
67
- }
68
- }
69
- return false;
70
- };
71
-
47
// https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
48
var inScopeTags = [
49
'applet', 'caption', 'html', 'table', 'td', 'th', 'marquee', 'object',
50
'template',
51
52
// https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
78
- // TODO: Distinguish by namespace here
53
+ // TODO: Distinguish by namespace here -- for <title>, including it here
54
+ // errs on the side of fewer warnings
55
'foreignObject', 'desc', 'title'
56
];
81
- var stackHasTagInScope = function(stack, tag) {
82
- return stackHasTagInSpecificScope(stack, tag, inScopeTags);
83
- };
57
58
// https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
59
var buttonScopeTags = inScopeTags.concat(['button']);
87
- var stackHasTagInButtonScope = function(stack, tag) {
88
- return stackHasTagInSpecificScope(stack, tag, buttonScopeTags);
89
- };
90
-
91
- // See rules for 'li', 'dd', 'dt' start tags in
92
- // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
93
- var listItemTagAllowed = function(tags, stack) {
94
- // tags is ['li'] or ['dd, 'dt']
95
- for (var i = stack.length - 1; i >= 0; i--) {
96
- if (tags.indexOf(stack[i]) !== -1) {
97
- return false;
98
- } else if (
99
- specialTags.indexOf(stack[i]) !== -1 &&
100
- stack[i] !== 'address' && stack[i] !== 'div' && stack[i] !== 'p'
101
- ) {
102
- return true;
103
- }
104
- }
105
- return true;
106
- };
60
61
// https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
62
var impliedEndTags =
63
['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
64
65
+ var emptyAncestorInfo = {
66
+ parentTag: null,
67
+
68
+ formTag: null,
69
+ aTagInScope: null,
70
+ buttonTagInScope: null,
71
+ nobrTagInScope: null,
72
+ pTagInButtonScope: null,
73
+
74
+ listItemTagAutoclosing: null,
75
+ dlItemTagAutoclosing: null
76
+ };
77
+
78
+ var updatedAncestorInfo = function(oldInfo, tag, instance) {
79
+ var ancestorInfo = assign({}, oldInfo || emptyAncestorInfo);
80
+ var info = {tag: tag, instance: instance};
81
+
82
+ if (inScopeTags.indexOf(tag) !== -1) {
83
+ ancestorInfo.aTagInScope = null;
84
+ ancestorInfo.buttonTagInScope = null;
85
+ ancestorInfo.nobrTagInScope = null;
86
+ }
87
+ if (buttonScopeTags.indexOf(tag) !== -1) {
88
+ ancestorInfo.pTagInButtonScope = null;
89
+ }
90
+
91
+ // See rules for 'li', 'dd', 'dt' start tags in
92
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
93
+ if (
94
+ specialTags.indexOf(tag) !== -1 &&
95
+ tag !== 'address' && tag !== 'div' && tag !== 'p'
96
+ ) {
97
+ ancestorInfo.listItemTagAutoclosing = null;
98
+ ancestorInfo.dlItemTagAutoclosing = null;
99
+ }
100
+
101
+ ancestorInfo.parentTag = info;
102
+
103
+ if (tag === 'form') {
104
+ ancestorInfo.formTag = info;
105
+ }
106
+ if (tag === 'a') {
107
+ ancestorInfo.aTagInScope = info;
108
+ }
109
+ if (tag === 'button') {
110
+ ancestorInfo.buttonTagInScope = info;
111
+ }
112
+ if (tag === 'nobr') {
113
+ ancestorInfo.nobrTagInScope = info;
114
+ }
115
+ if (tag === 'p') {
116
+ ancestorInfo.pTagInButtonScope = info;
117
+ }
118
+ if (tag === 'li') {
119
+ ancestorInfo.listItemTagAutoclosing = info;
120
+ }
121
+ if (tag === 'dd' || tag === 'dt') {
122
+ ancestorInfo.dlItemTagAutoclosing = info;
123
+ }
124
+
125
+ return ancestorInfo;
126
+ };
127
+
128
/**
113
- * Returns whether we allow putting `tag` in the document if the current stack
114
- * of open tags is `openTagStack`.
115
- *
116
- * Examples:
117
- * isTagValidInContext('tr', [..., 'table', 'tbody']) is true
118
- * isTagValidInContext('tr', [..., 'table']) is false
119
- *
120
- * @param {string} tag Lowercase HTML tag name or node name like '#text'
121
- * @param {Array<string>} openTagStack
129
+ * Returns whether
130
*/
123
- var isTagValidInContext = function(tag, openTagStack) {
124
- var currentTag = openTagStack[openTagStack.length - 1];
125
-
131
+ var isTagValidWithParent = function(tag, parentTag) {
132
// First, let's check if we're in an unusual parsing mode...
127
- switch (currentTag) {
133
+ switch (parentTag) {
134
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
135
case 'select':
136
return tag === 'option' || tag === 'optgroup' || tag === '#text';
@@ -185,6 +191,47 @@ if (__DEV__) {
191
// Probably in the "in body" parsing mode, so we outlaw only tag combos
192
// where the parsing rules cause implicit opens or closes to be added.
193
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
194
+ switch (tag) {
195
+ case 'h1':
196
+ case 'h2':
197
+ case 'h3':
198
+ case 'h4':
199
+ case 'h5':
200
+ case 'h6':
201
+ return (
202
+ parentTag !== 'h1' && parentTag !== 'h2' && parentTag !== 'h3' &&
203
+ parentTag !== 'h4' && parentTag !== 'h5' && parentTag !== 'h6'
204
+ );
205
+
206
+ case 'rp':
207
+ case 'rt':
208
+ return impliedEndTags.indexOf(parentTag) === -1;
209
+
210
+ case 'caption':
211
+ case 'col':
212
+ case 'colgroup':
213
+ case 'frame':
214
+ case 'head':
215
+ case 'tbody':
216
+ case 'td':
217
+ case 'tfoot':
218
+ case 'th':
219
+ case 'thead':
220
+ case 'tr':
221
+ // These tags are only valid with a few parents that have special child
222
+ // parsing rules -- if we're down here, then none of those matched and
223
+ // so we allow it only if we don't know what the parent is, as all other
224
+ // cases are invalid.
225
+ return parentTag == null;
226
+ }
227
+
228
+ return true;
229
+ };
230
+
231
+ /**
232
+ * Returns whether
233
+ */
234
+ var findInvalidAncestorForTag = function(tag, ancestorInfo) {
235
switch (tag) {
236
case 'address':
237
case 'article':
@@ -219,7 +266,6 @@ if (__DEV__) {
266
case 'hr':
267
268
case 'xmp':
222
- return !stackHasTagInButtonScope(openTagStack, 'p');
269
270
case 'h1':
271
case 'h2':
@@ -227,90 +273,151 @@ if (__DEV__) {
273
case 'h4':
274
case 'h5':
275
case 'h6':
230
- return (
231
- !stackHasTagInButtonScope(openTagStack, 'p') &&
232
- currentTag !== 'h1' && currentTag !== 'h2' && currentTag !== 'h3' &&
233
- currentTag !== 'h4' && currentTag !== 'h5' && currentTag !== 'h6'
234
- );
276
+ return ancestorInfo.pTagInButtonScope;
277
278
case 'form':
237
- return (
238
- openTagStack.indexOf('form') === -1 &&
239
- !stackHasTagInButtonScope(openTagStack, 'p')
240
- );
279
+ return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;
280
281
case 'li':
243
- return listItemTagAllowed(['li'], openTagStack);
282
+ return ancestorInfo.listItemTagAutoclosing;
283
284
case 'dd':
285
case 'dt':
247
- return listItemTagAllowed(['dd', 'dt'], openTagStack);
286
+ return ancestorInfo.dlItemTagAutoclosing;
287
288
case 'button':
250
- return !stackHasTagInScope(openTagStack, 'button');
289
+ return ancestorInfo.buttonTagInScope;
290
291
case 'a':
292
// Spec says something about storing a list of markers, but it sounds
293
// equivalent to this check.
255
- return !stackHasTagInScope(openTagStack, 'a');
294
+ return ancestorInfo.aTagInScope;
295
296
case 'nobr':
258
- return !stackHasTagInScope(openTagStack, 'nobr');
297
+ return ancestorInfo.nobrTagInScope;
298
+ }
299
260
- case 'rp':
261
- case 'rt':
262
- return impliedEndTags.indexOf(currentTag) === -1;
300
+ return null;
301
+ };
302
264
- case 'caption':
265
- case 'col':
266
- case 'colgroup':
267
- case 'frame':
268
- case 'head':
269
- case 'tbody':
270
- case 'td':
271
- case 'tfoot':
272
- case 'th':
273
- case 'thead':
274
- case 'tr':
275
- return currentTag === undefined;
303
+ /**
304
+ * Given a ReactCompositeComponent instance, return a list of its recursive
305
+ * owners, starting at the root and ending with the instance itself.
306
+ */
307
+ var findOwnerStack = function(instance) {
308
+ if (!instance) {
309
+ return [];
310
}
311
278
- return true;
312
+ var stack = [];
313
+ /*eslint-disable space-after-keywords */
314
+ do {
315
+ /*eslint-enable space-after-keywords */
316
+ stack.push(instance);
317
+ } while ((instance = instance._currentElement._owner));
318
+ stack.reverse();
319
+ return stack;
320
};
321
281
- validateDOMNesting = function(parentStack, childTag, element) {
282
- if (!isTagValidInContext(childTag, parentStack)) {
283
- var info = '';
284
- var parentTag = parentStack[parentStack.length - 1];
285
- if (parentTag === 'table' && childTag === 'tr') {
286
- info +=
287
- ' Add a <tbody> to your code to match the DOM tree generated by ' +
288
- 'the browser.';
289
- }
290
- if (element && element._owner) {
291
- var name = element._owner.getName();
292
- if (name) {
293
- info += ` Check the render method of \`${name}\`.`;
322
+ validateDOMNesting = function(childTag, childInstance, ancestorInfo) {
323
+ ancestorInfo = ancestorInfo || emptyAncestorInfo;
324
+ var parentInfo = ancestorInfo.parentTag;
325
+ var parentTag = parentInfo && parentInfo.tag;
326
+
327
+ var invalidParent =
328
+ isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
329
+ var invalidAncestor =
330
+ invalidParent ? null : findInvalidAncestorForTag(childTag, ancestorInfo);
331
+ var problematic = invalidParent || invalidAncestor;
332
+
333
+ if (problematic) {
334
+ var ancestorTag = problematic.tag;
335
+ var ancestorInstance = problematic.instance;
336
+
337
+ var childOwner = childInstance && childInstance._currentElement._owner;
338
+ var ancestorOwner =
339
+ ancestorInstance && ancestorInstance._currentElement._owner;
340
+
341
+ var childOwners = findOwnerStack(childOwner);
342
+ var ancestorOwners = findOwnerStack(ancestorOwner);
343
+
344
+ var minStackLen = Math.min(childOwners.length, ancestorOwners.length);
345
+ var i;
346
+
347
+ var deepestCommon = -1;
348
+ for (i = 0; i < minStackLen; i++) {
349
+ if (childOwners[i] === ancestorOwners[i]) {
350
+ deepestCommon = i;
351
+ } else {
352
+ break;
353
}
354
}
355
297
- warning(
298
- false,
299
- 'validateDOMNesting(...): <%s> cannot appear as a child of <%s> ' +
300
- 'in this context (%s).%s',
301
- childTag,
302
- parentTag,
303
- parentStack.join(' > '),
304
- info
356
+ var UNKNOWN = '(unknown)';
357
+ var childOwnerNames = childOwners.slice(deepestCommon + 1).map(
358
+ (inst) => inst.getName() || UNKNOWN
359
+ );
360
+ var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map(
361
+ (inst) => inst.getName() || UNKNOWN
362
);
363
+ var ownerInfo = [].concat(
364
+ // If the parent and child instances have a common owner ancestor, start
365
+ // with that -- otherwise we just start with the parent's owners.
366
+ deepestCommon !== -1 ?
367
+ childOwners[deepestCommon].getName() || UNKNOWN :
368
+ [],
369
+ ancestorOwnerNames,
370
+ ancestorTag,
371
+ // If we're warning about an invalid (non-parent) ancestry, add '...'
372
+ invalidAncestor ? ['...'] : [],
373
+ childOwnerNames,
374
+ childTag
375
+ ).join(' > ');
376
+
377
+ if (invalidParent) {
378
+ var info = '';
379
+ if (ancestorTag === 'table' && childTag === 'tr') {
380
+ info +=
381
+ ' Add a <tbody> to your code to match the DOM tree generated by ' +
382
+ 'the browser.';
383
+ }
384
+ warning(
385
+ false,
386
+ 'validateDOMNesting(...): <%s> cannot appear as a child of <%s>. ' +
387
+ 'See %s.%s',
388
+ childTag,
389
+ ancestorTag,
390
+ ownerInfo,
391
+ info
392
+ );
393
+ } else {
394
+ warning(
395
+ false,
396
+ 'validateDOMNesting(...): <%s> cannot appear as a descendant of ' +
397
+ '<%s>. See %s.',
398
+ childTag,
399
+ ancestorTag,
400
+ ownerInfo
401
+ );
402
+ }
403
}
404
};
405
309
- validateDOMNesting.tagStackContextKey =
310
- '__validateDOMNesting_tagStack$' + Math.random().toString(36).slice(2);
406
+ validateDOMNesting.ancestorInfoContextKey =
407
+ '__validateDOMNesting_ancestorInfo$' + Math.random().toString(36).slice(2);
408
+
409
+ validateDOMNesting.updatedAncestorInfo = updatedAncestorInfo;
410
411
// For testing
313
- validateDOMNesting.isTagValidInContext = isTagValidInContext;
412
+ validateDOMNesting.isTagValidInContext = function(tag, ancestorInfo) {
413
+ ancestorInfo = ancestorInfo || emptyAncestorInfo;
414
+ var parentInfo = ancestorInfo.parentTag;
415
+ var parentTag = parentInfo && parentInfo.tag;
416
+ return (
417
+ isTagValidWithParent(tag, parentTag) &&
418
+ !findInvalidAncestorForTag(tag, ancestorInfo)
419
+ );
420
+ };
421
}
422
423
module.exports = validateDOMNesting;