Skip to content

Commit 684ef3e

Browse files
committed
Merge pull request #5753 from mwiencek/no-text-span-2
Don't wrap text in <span> elements
2 parents f818fa3 + 2038500 commit 684ef3e

11 files changed

+297
-55
lines changed

src/renderers/dom/client/ReactDOMComponentTree.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,12 @@ function precacheChildNodes(inst, node) {
8787
}
8888
// We assume the child nodes are in the same order as the child instances.
8989
for (; childNode !== null; childNode = childNode.nextSibling) {
90-
if (childNode.nodeType === 1 &&
91-
childNode.getAttribute(ATTR_NAME) === String(childID) ||
92-
childNode.nodeType === 8 &&
93-
childNode.nodeValue === ' react-empty: ' + childID + ' ') {
90+
if ((childNode.nodeType === 1 &&
91+
childNode.getAttribute(ATTR_NAME) === String(childID)) ||
92+
(childNode.nodeType === 8 &&
93+
childNode.nodeValue === ' react-text: ' + childID + ' ') ||
94+
(childNode.nodeType === 8 &&
95+
childNode.nodeValue === ' react-empty: ' + childID + ' ')) {
9496
precacheNode(childInst, childNode);
9597
continue outer;
9698
}

src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ describe('ReactDOMComponentTree', function() {
100100
expect(renderAndGetInstance('main')._currentElement.type).toBe('main');
101101

102102
// This one's a text component!
103-
expect(renderAndGetInstance('span')._stringText).toBe('goodbye.');
103+
var root = renderAndQuery(null);
104+
var inst = ReactDOMComponentTree.getInstanceFromNode(root.children[0].childNodes[2]);
105+
expect(inst._stringText).toBe('goodbye.');
104106

105107
expect(renderAndGetClosest('b')._currentElement.type).toBe('main');
106108
expect(renderAndGetClosest('img')._currentElement.type).toBe('main');

src/renderers/dom/client/utils/DOMChildrenOperations.js

+82-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ var setInnerHTML = require('setInnerHTML');
2121
var setTextContent = require('setTextContent');
2222

2323
function getNodeAfter(parentNode, node) {
24+
// Special case for text components, which return [open, close] comments
25+
// from getNativeNode.
26+
if (Array.isArray(node)) {
27+
node = node[1];
28+
}
2429
return node ? node.nextSibling : parentNode.firstChild;
2530
}
2631

@@ -45,6 +50,78 @@ function insertLazyTreeChildAt(parentNode, childTree, referenceNode) {
4550
DOMLazyTree.insertTreeBefore(parentNode, childTree, referenceNode);
4651
}
4752

53+
function moveChild(parentNode, childNode, referenceNode) {
54+
if (Array.isArray(childNode)) {
55+
moveDelimitedText(parentNode, childNode[0], childNode[1], referenceNode);
56+
} else {
57+
insertChildAt(parentNode, childNode, referenceNode);
58+
}
59+
}
60+
61+
function removeChild(parentNode, childNode) {
62+
if (Array.isArray(childNode)) {
63+
var closingComment = childNode[1];
64+
childNode = childNode[0];
65+
removeDelimitedText(parentNode, childNode, closingComment);
66+
parentNode.removeChild(closingComment);
67+
}
68+
parentNode.removeChild(childNode);
69+
}
70+
71+
function moveDelimitedText(
72+
parentNode,
73+
openingComment,
74+
closingComment,
75+
referenceNode
76+
) {
77+
var node = openingComment;
78+
while (true) {
79+
var nextNode = node.nextSibling;
80+
insertChildAt(parentNode, node, referenceNode);
81+
if (node === closingComment) {
82+
break;
83+
}
84+
node = nextNode;
85+
}
86+
}
87+
88+
function removeDelimitedText(parentNode, startNode, closingComment) {
89+
while (true) {
90+
var node = startNode.nextSibling;
91+
if (node === closingComment) {
92+
// The closing comment is removed by ReactMultiChild.
93+
break;
94+
} else {
95+
parentNode.removeChild(node);
96+
}
97+
}
98+
}
99+
100+
function replaceDelimitedText(openingComment, closingComment, stringText) {
101+
var parentNode = openingComment.parentNode;
102+
var nodeAfterComment = openingComment.nextSibling;
103+
if (nodeAfterComment === closingComment) {
104+
// There are no text nodes between the opening and closing comments; insert
105+
// a new one if stringText isn't empty.
106+
if (stringText) {
107+
insertChildAt(
108+
parentNode,
109+
document.createTextNode(stringText),
110+
nodeAfterComment
111+
);
112+
}
113+
} else {
114+
if (stringText) {
115+
// Set the text content of the first node after the opening comment, and
116+
// remove all following nodes up until the closing comment.
117+
setTextContent(nodeAfterComment, stringText);
118+
removeDelimitedText(parentNode, nodeAfterComment, closingComment);
119+
} else {
120+
removeDelimitedText(parentNode, openingComment, closingComment);
121+
}
122+
}
123+
}
124+
48125
/**
49126
* Operations for updating with DOM children.
50127
*/
@@ -54,6 +131,8 @@ var DOMChildrenOperations = {
54131

55132
updateTextContent: setTextContent,
56133

134+
replaceDelimitedText: replaceDelimitedText,
135+
57136
/**
58137
* Updates a component's children by processing a series of updates. The
59138
* update configurations are each expected to have a `parentNode` property.
@@ -73,7 +152,7 @@ var DOMChildrenOperations = {
73152
);
74153
break;
75154
case ReactMultiChildUpdateTypes.MOVE_EXISTING:
76-
insertChildAt(
155+
moveChild(
77156
parentNode,
78157
update.fromNode,
79158
getNodeAfter(parentNode, update.afterNode)
@@ -92,7 +171,7 @@ var DOMChildrenOperations = {
92171
);
93172
break;
94173
case ReactMultiChildUpdateTypes.REMOVE_NODE:
95-
parentNode.removeChild(update.fromNode);
174+
removeChild(parentNode, update.fromNode);
96175
break;
97176
}
98177
}
@@ -102,6 +181,7 @@ var DOMChildrenOperations = {
102181

103182
ReactPerf.measureMethods(DOMChildrenOperations, 'DOMChildrenOperations', {
104183
updateTextContent: 'updateTextContent',
184+
replaceDelimitedText: 'replaceDelimitedText',
105185
});
106186

107187
module.exports = DOMChildrenOperations;

src/renderers/dom/client/validateDOMNesting.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,11 @@ if (__DEV__) {
384384
}
385385
didWarn[warnKey] = true;
386386

387+
var tagDisplayName = childTag;
388+
if (childTag !== '#text') {
389+
tagDisplayName = '<' + childTag + '>';
390+
}
391+
387392
if (invalidParent) {
388393
var info = '';
389394
if (ancestorTag === 'table' && childTag === 'tr') {
@@ -393,19 +398,19 @@ if (__DEV__) {
393398
}
394399
warning(
395400
false,
396-
'validateDOMNesting(...): <%s> cannot appear as a child of <%s>. ' +
401+
'validateDOMNesting(...): %s cannot appear as a child of <%s>. ' +
397402
'See %s.%s',
398-
childTag,
403+
tagDisplayName,
399404
ancestorTag,
400405
ownerInfo,
401406
info
402407
);
403408
} else {
404409
warning(
405410
false,
406-
'validateDOMNesting(...): <%s> cannot appear as a descendant of ' +
411+
'validateDOMNesting(...): %s cannot appear as a descendant of ' +
407412
'<%s>. See %s.',
408-
childTag,
413+
tagDisplayName,
409414
ancestorTag,
410415
ownerInfo
411416
);

src/renderers/dom/server/__tests__/ReactServerRendering-test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ describe('ReactServerRendering', function() {
103103
ID_ATTRIBUTE_NAME + '="[^"]+" ' +
104104
ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">' +
105105
'<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">' +
106-
'<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">My name is </span>' +
107-
'<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">child</span>' +
106+
'<!-- react-text: [0-9]+ -->My name is <!-- /react-text -->' +
107+
'<!-- react-text: [0-9]+ -->child<!-- /react-text -->' +
108108
'</span>' +
109109
'</div>'
110110
);
@@ -153,8 +153,8 @@ describe('ReactServerRendering', function() {
153153
'<span ' + ROOT_ATTRIBUTE_NAME + '="" ' +
154154
ID_ATTRIBUTE_NAME + '="[^"]+" ' +
155155
ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">' +
156-
'<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">Component name: </span>' +
157-
'<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">TestComponent</span>' +
156+
'<!-- react-text: [0-9]+ -->Component name: <!-- /react-text -->' +
157+
'<!-- react-text: [0-9]+ -->TestComponent<!-- /react-text -->' +
158158
'</span>'
159159
);
160160
expect(lifecycle).toEqual(

src/renderers/dom/shared/ReactDOMTextComponent.js

+40-18
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,21 @@
1313

1414
var DOMChildrenOperations = require('DOMChildrenOperations');
1515
var DOMLazyTree = require('DOMLazyTree');
16-
var DOMPropertyOperations = require('DOMPropertyOperations');
1716
var ReactDOMComponentTree = require('ReactDOMComponentTree');
1817
var ReactPerf = require('ReactPerf');
1918

2019
var assign = require('Object.assign');
2120
var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
2221
var validateDOMNesting = require('validateDOMNesting');
2322

24-
var getNode = ReactDOMComponentTree.getNodeFromInstance;
25-
2623
/**
2724
* Text nodes violate a couple assumptions that React makes about components:
2825
*
2926
* - When mounting text into the DOM, adjacent text nodes are merged.
3027
* - Text nodes cannot be assigned a React root ID.
3128
*
32-
* This component is used to wrap strings in elements so that they can undergo
33-
* the same reconciliation that is applied to elements.
29+
* This component is used to wrap strings between comment nodes so that they
30+
* can undergo the same reconciliation that is applied to elements.
3431
*
3532
* TODO: Investigate representing React components in the DOM with text nodes.
3633
*
@@ -49,6 +46,8 @@ var ReactDOMTextComponent = function(text) {
4946
// Properties
5047
this._domID = null;
5148
this._mountIndex = 0;
49+
this._openingComment = null;
50+
this._commentNodes = null;
5251
};
5352

5453
assign(ReactDOMTextComponent.prototype, {
@@ -77,34 +76,44 @@ assign(ReactDOMTextComponent.prototype, {
7776
if (parentInfo) {
7877
// parentInfo should always be present except for the top-level
7978
// component when server rendering
80-
validateDOMNesting('span', this, parentInfo);
79+
validateDOMNesting('#text', this, parentInfo);
8180
}
8281
}
8382

8483
var domID = nativeContainerInfo._idCounter++;
84+
var openingValue = ' react-text: ' + domID + ' ';
85+
var closingValue = ' /react-text ';
8586
this._domID = domID;
8687
this._nativeParent = nativeParent;
8788
if (transaction.useCreateElement) {
8889
var ownerDocument = nativeContainerInfo._ownerDocument;
89-
var el = ownerDocument.createElement('span');
90-
ReactDOMComponentTree.precacheNode(this, el);
91-
var lazyTree = DOMLazyTree(el);
92-
DOMLazyTree.queueText(lazyTree, this._stringText);
90+
var openingComment = ownerDocument.createComment(openingValue);
91+
var closingComment = ownerDocument.createComment(closingValue);
92+
var lazyTree = DOMLazyTree(ownerDocument.createDocumentFragment());
93+
DOMLazyTree.queueChild(lazyTree, DOMLazyTree(openingComment));
94+
if (this._stringText) {
95+
DOMLazyTree.queueChild(
96+
lazyTree,
97+
DOMLazyTree(ownerDocument.createTextNode(this._stringText))
98+
);
99+
}
100+
DOMLazyTree.queueChild(lazyTree, DOMLazyTree(closingComment));
101+
this._openingComment = openingComment;
102+
ReactDOMComponentTree.precacheNode(this, closingComment);
93103
return lazyTree;
94104
} else {
95105
var escapedText = escapeTextContentForBrowser(this._stringText);
96106

97107
if (transaction.renderToStaticMarkup) {
98-
// Normally we'd wrap this in a `span` for the reasons stated above, but
99-
// since this is a situation where React won't take over (static pages),
100-
// we can simply return the text as it is.
108+
// Normally we'd wrap this between comment nodes for the reasons stated
109+
// above, but since this is a situation where React won't take over
110+
// (static pages), we can simply return the text as it is.
101111
return escapedText;
102112
}
103113

104114
return (
105-
'<span ' + DOMPropertyOperations.createMarkupForID(domID) + '>' +
106-
escapedText +
107-
'</span>'
115+
'<!--' + openingValue + '-->' + escapedText +
116+
'<!--' + closingValue + '-->'
108117
);
109118
}
110119
},
@@ -125,16 +134,29 @@ assign(ReactDOMTextComponent.prototype, {
125134
// and/or updateComponent to do the actual update for consistency with
126135
// other component types?
127136
this._stringText = nextStringText;
128-
DOMChildrenOperations.updateTextContent(getNode(this), nextStringText);
137+
var commentNodes = this.getNativeNode();
138+
DOMChildrenOperations.replaceDelimitedText(
139+
commentNodes[0],
140+
commentNodes[1],
141+
nextStringText
142+
);
129143
}
130144
}
131145
},
132146

133147
getNativeNode: function() {
134-
return getNode(this);
148+
var nativeNode = this._commentNodes;
149+
if (nativeNode) {
150+
return nativeNode;
151+
}
152+
nativeNode = [this._openingComment, this._nativeNode];
153+
this._commentNodes = nativeNode;
154+
return nativeNode;
135155
},
136156

137157
unmountComponent: function() {
158+
this._openingComment = null;
159+
this._commentNodes = null;
138160
ReactDOMComponentTree.uncacheNode(this);
139161
},
140162

src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1260,8 +1260,8 @@ describe('ReactDOMComponent', function() {
12601260
'match the DOM tree generated by the browser.'
12611261
);
12621262
expect(console.error.argsForCall[1][0]).toBe(
1263-
'Warning: validateDOMNesting(...): <span> cannot appear as a child ' +
1264-
'of <table>. See Foo > table > span.'
1263+
'Warning: validateDOMNesting(...): #text cannot appear as a child ' +
1264+
'of <table>. See Foo > table > #text.'
12651265
);
12661266
});
12671267

0 commit comments

Comments
 (0)