The library for web and native user interfaces.
Fix Markup Rendering in IE
This fixes known browser bugs with rendering markup using `innerHTML` in IE ([[http://support.microsoft.com/kb/276228 | here is an example of one]]). This is a subset of what `HTML` (and jQuery) does, and we should eventually consider pulling it out into a separate module to reduce code duplication. For now, this is the minimal set of changes needed to unbreak React in production. We can afford to use a subset of what `HTML` does because we have the luxury of knowing that the markup is generated sanely with proper closing tags, etc.
Tim Yung committed
Jul 16, 2013 at 11:33 UTC
83a840656cecf00f299804bb538639dd2bb5f8e2
4 files changed
+156
-17
src/core/ReactNativeComponent.js
+1
-1
@@ -69,7 +69,7 @@ function assertValidProps(props) {
69
* @extends ReactMultiChild
70
*/
71
function ReactNativeComponent(tag, omitClose) {
72
- this._tagOpen = '<' + tag + ' ';
72
+ this._tagOpen = '<' + tag;
73
this._tagClose = omitClose ? '' : '</' + tag + '>';
74
this.tagName = tag.toUpperCase();
75
}
src/dom/Danger.js
+82
-12
@@ -16,7 +16,7 @@
16
* @providesModule Danger
17
*/
18
19
-/*jslint evil: true */
19
+/*jslint evil: true, sub: true */
20
21
"use strict";
22
@@ -50,12 +50,86 @@ if (__DEV__) {
50
};
51
}
52
53
-var dummies = {};
53
+/**
54
+ * Dummy container used to render all markup.
55
+ */
56
+var dummyNode = ExecutionEnvironment.canUseDOM ?
57
+ document.createElement('div') :
58
+ null;
59
+
60
+/**
61
+ * Some browsers cannot use `innerHTML` to render certain elements standalone,
62
+ * so we wrap them, render the wrapped nodes, then extract the desired node.
63
+ */
64
+var markupWrap = {
65
+ 'option': [1, '<select multiple="true">', '</select>'],
66
+ 'legend': [1, '<fieldset>', '</fieldset>'],
67
+ 'area': [1, '<map>', '</map>'],
68
+ 'param': [1, '<object>', '</object>'],
69
+ 'thead': [1, '<table>', '</table>'],
70
+ 'tr': [2, '<table><tbody>', '</tbody></table>'],
71
+ 'col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
72
+ 'td': [3, '<table><tbody><tr>', '</tr></tbody></table>']
73
+};
74
+markupWrap['optgroup'] = markupWrap['option'];
75
+markupWrap['tbody'] = markupWrap['thead'];
76
+markupWrap['tfoot'] = markupWrap['thead'];
77
+markupWrap['colgroup'] = markupWrap['thead'];
78
+markupWrap['caption'] = markupWrap['thead'];
79
+markupWrap['th'] = markupWrap['td'];
80
+
81
+/**
82
+ * In IE8, certain elements cannot render alone, so wrap all elements.
83
+ */
84
+var defaultWrap = [1, '?<div>', '</div>'];
85
+
86
+/**
87
+ * Feature detection, remove wraps that are unnecessary for the current browser.
88
+ */
89
+if (dummyNode) {
90
+ for (var nodeName in markupWrap) {
91
+ if (!markupWrap.hasOwnProperty(nodeName)) {
92
+ continue;
93
+ }
94
+ dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';
95
+ if (dummyNode.firstChild) {
96
+ markupWrap[nodeName] = null;
97
+ }
98
+ }
99
+ dummyNode.innerHTML = '<link />';
100
+ if (dummyNode.firstChild) {
101
+ defaultWrap = null;
102
+ }
103
+}
104
55
-function getParentDummy(parent) {
56
- var parentTag = parent.tagName;
57
- return dummies[parentTag] ||
58
- (dummies[parentTag] = document.createElement(parentTag));
105
+/**
106
+ * Renders markup into nodes. The returned HTMLCollection is live and should be
107
+ * used immediately (or at least before the next invocation to `renderMarkup`).
108
+ *
109
+ * NOTE: Extracting the `nodeName` does not require a regular expression match
110
+ * because we make assumptions about React-generated markup (i.e. there are no
111
+ * spaces surrounding the opening tag and there is at least one attribute).
112
+ * @see http://jsperf.com/extract-nodename
113
+ *
114
+ * @param {string} markup
115
+ * @return {*} An HTMLCollection.
116
+ */
117
+function renderMarkup(markup) {
118
+ var node = dummyNode;
119
+ var nodeName = markup.substring(1, markup.indexOf(' '));
120
+
121
+ var wrap = markupWrap[nodeName.toLowerCase()] || defaultWrap;
122
+ if (wrap) {
123
+ node.innerHTML = wrap[1] + markup + wrap[2];
124
+
125
+ var wrapDepth = wrap[0];
126
+ while (wrapDepth--) {
127
+ node = node.lastChild;
128
+ }
129
+ } else {
130
+ node.innerHTML = markup;
131
+ }
132
+ return node.childNodes;
133
}
134
135
/**
@@ -122,9 +196,7 @@ function dangerouslyInsertMarkupAt(parentNode, markup, index) {
196
if (__DEV__) {
197
validateMarkupParams(parentNode, markup);
198
}
125
- var parentDummy = getParentDummy(parentNode);
126
- parentDummy.innerHTML = markup;
127
- var htmlCollection = parentDummy.childNodes;
199
+ var htmlCollection = renderMarkup(markup);
200
var afterNode = index ? parentNode.childNodes[index - 1] : null;
201
inefficientlyInsertHTMLCollectionAfter(parentNode, htmlCollection, afterNode);
202
}
@@ -143,9 +215,7 @@ function dangerouslyReplaceNodeWithMarkup(childNode, markup) {
215
if (__DEV__) {
216
validateMarkupParams(parentNode, markup);
217
}
146
- var parentDummy = getParentDummy(parentNode);
147
- parentDummy.innerHTML = markup;
148
- var htmlCollection = parentDummy.childNodes;
218
+ var htmlCollection = renderMarkup(markup);
219
if (__DEV__) {
220
throwIf(htmlCollection.length !== 1, NO_MULTI_MARKUP);
221
}
src/dom/__tests__/Danger-test.js
new
+69
@@ -0,0 +1,69 @@
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
+ * @jsx React.DOM
17
+ * @emails react-core
18
+ */
19
+
20
+/*jslint evil: true */
21
+
22
+var React = require('React');
23
+
24
+describe('Danger', function() {
25
+
26
+ describe('dangerouslyInsertMarkupAt', function() {
27
+ var Danger;
28
+ var transaction;
29
+
30
+ beforeEach(function() {
31
+ require('mock-modules').dumpCache();
32
+ Danger = require('Danger');
33
+
34
+ var ReactReconcileTransaction = require('ReactReconcileTransaction');
35
+ transaction = new ReactReconcileTransaction();
36
+ });
37
+
38
+ it('should render markup', function() {
39
+ var markup = (<div />).mountComponent('.rX', transaction);
40
+ var parent = document.createElement('div');
41
+
42
+ Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
43
+
44
+ expect(parent.innerHTML).toBe('<div data-reactid=".rX"></div>');
45
+ });
46
+
47
+ it('should render markup with props', function() {
48
+ var markup = (<div className="foo" />).mountComponent('.rX', transaction);
49
+ var parent = document.createElement('div');
50
+
51
+ Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
52
+
53
+ expect(parent.innerHTML).toBe(
54
+ '<div class="foo" data-reactid=".rX"></div>'
55
+ );
56
+ });
57
+
58
+ it('should render wrapped markup', function() {
59
+ var markup = (<th />).mountComponent('.rX', transaction);
60
+ var parent = document.createElement('div');
61
+
62
+ Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
63
+
64
+ expect(parent.innerHTML).toBe('<th data-reactid=".rX"></th>');
65
+ });
66
+
67
+ });
68
+
69
+});
src/environment/__tests__/ReactServerRendering-test.js
+4
-4
@@ -54,7 +54,7 @@ describe('ReactServerRendering', function() {
54
}
55
);
56
expect(response).toMatch(
57
- '<span ' + ReactID.ATTR_NAME + '="[^"]+">hello world</span>'
57
+ '<span ' + ReactID.ATTR_NAME + '="[^"]+">hello world</span>'
58
);
59
});
60
@@ -77,8 +77,8 @@ describe('ReactServerRendering', function() {
77
}
78
);
79
expect(response).toMatch(
80
- '<div ' + ReactID.ATTR_NAME + '="[^"]+">' +
81
- '<span ' + ReactID.ATTR_NAME + '="[^"]+">' +
80
+ '<div ' + ReactID.ATTR_NAME + '="[^"]+">' +
81
+ '<span ' + ReactID.ATTR_NAME + '="[^"]+">' +
82
'<span ' + ReactID.ATTR_NAME + '="[^"]+">My name is </span>' +
83
'<span ' + ReactID.ATTR_NAME + '="[^"]+">child</span>' +
84
'</span>' +
@@ -130,7 +130,7 @@ describe('ReactServerRendering', function() {
130
);
131
132
expect(response).toMatch(
133
- '<span ' + ReactID.ATTR_NAME + '="[^"]+">' +
133
+ '<span ' + ReactID.ATTR_NAME + '="[^"]+">' +
134
'<span ' + ReactID.ATTR_NAME + '="[^"]+">Component name: </span>' +
135
'<span ' + ReactID.ATTR_NAME + '="[^"]+">TestComponent</span>' +
136
'</span>'