@samitouri / QOS-React-2 / commits / 83a840656c
Fork 0

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>'