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.
This commit is contained in:
Tim Yung
2013-07-16 11:33:42 -07:00
committed by Paul O’Shannessy
parent ed54fff204
commit 83a840656c
4 changed files with 156 additions and 17 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ function assertValidProps(props) {
* @extends ReactMultiChild
*/
function ReactNativeComponent(tag, omitClose) {
this._tagOpen = '<' + tag + ' ';
this._tagOpen = '<' + tag;
this._tagClose = omitClose ? '' : '</' + tag + '>';
this.tagName = tag.toUpperCase();
}
+82 -12
View File
@@ -16,7 +16,7 @@
* @providesModule Danger
*/
/*jslint evil: true */
/*jslint evil: true, sub: true */
"use strict";
@@ -50,12 +50,86 @@ if (__DEV__) {
};
}
var dummies = {};
/**
* Dummy container used to render all markup.
*/
var dummyNode = ExecutionEnvironment.canUseDOM ?
document.createElement('div') :
null;
function getParentDummy(parent) {
var parentTag = parent.tagName;
return dummies[parentTag] ||
(dummies[parentTag] = document.createElement(parentTag));
/**
* Some browsers cannot use `innerHTML` to render certain elements standalone,
* so we wrap them, render the wrapped nodes, then extract the desired node.
*/
var markupWrap = {
'option': [1, '<select multiple="true">', '</select>'],
'legend': [1, '<fieldset>', '</fieldset>'],
'area': [1, '<map>', '</map>'],
'param': [1, '<object>', '</object>'],
'thead': [1, '<table>', '</table>'],
'tr': [2, '<table><tbody>', '</tbody></table>'],
'col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
'td': [3, '<table><tbody><tr>', '</tr></tbody></table>']
};
markupWrap['optgroup'] = markupWrap['option'];
markupWrap['tbody'] = markupWrap['thead'];
markupWrap['tfoot'] = markupWrap['thead'];
markupWrap['colgroup'] = markupWrap['thead'];
markupWrap['caption'] = markupWrap['thead'];
markupWrap['th'] = markupWrap['td'];
/**
* In IE8, certain elements cannot render alone, so wrap all elements.
*/
var defaultWrap = [1, '?<div>', '</div>'];
/**
* Feature detection, remove wraps that are unnecessary for the current browser.
*/
if (dummyNode) {
for (var nodeName in markupWrap) {
if (!markupWrap.hasOwnProperty(nodeName)) {
continue;
}
dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';
if (dummyNode.firstChild) {
markupWrap[nodeName] = null;
}
}
dummyNode.innerHTML = '<link />';
if (dummyNode.firstChild) {
defaultWrap = null;
}
}
/**
* Renders markup into nodes. The returned HTMLCollection is live and should be
* used immediately (or at least before the next invocation to `renderMarkup`).
*
* NOTE: Extracting the `nodeName` does not require a regular expression match
* because we make assumptions about React-generated markup (i.e. there are no
* spaces surrounding the opening tag and there is at least one attribute).
* @see http://jsperf.com/extract-nodename
*
* @param {string} markup
* @return {*} An HTMLCollection.
*/
function renderMarkup(markup) {
var node = dummyNode;
var nodeName = markup.substring(1, markup.indexOf(' '));
var wrap = markupWrap[nodeName.toLowerCase()] || defaultWrap;
if (wrap) {
node.innerHTML = wrap[1] + markup + wrap[2];
var wrapDepth = wrap[0];
while (wrapDepth--) {
node = node.lastChild;
}
} else {
node.innerHTML = markup;
}
return node.childNodes;
}
/**
@@ -122,9 +196,7 @@ function dangerouslyInsertMarkupAt(parentNode, markup, index) {
if (__DEV__) {
validateMarkupParams(parentNode, markup);
}
var parentDummy = getParentDummy(parentNode);
parentDummy.innerHTML = markup;
var htmlCollection = parentDummy.childNodes;
var htmlCollection = renderMarkup(markup);
var afterNode = index ? parentNode.childNodes[index - 1] : null;
inefficientlyInsertHTMLCollectionAfter(parentNode, htmlCollection, afterNode);
}
@@ -143,9 +215,7 @@ function dangerouslyReplaceNodeWithMarkup(childNode, markup) {
if (__DEV__) {
validateMarkupParams(parentNode, markup);
}
var parentDummy = getParentDummy(parentNode);
parentDummy.innerHTML = markup;
var htmlCollection = parentDummy.childNodes;
var htmlCollection = renderMarkup(markup);
if (__DEV__) {
throwIf(htmlCollection.length !== 1, NO_MULTI_MARKUP);
}
+69
View File
@@ -0,0 +1,69 @@
/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @jsx React.DOM
* @emails react-core
*/
/*jslint evil: true */
var React = require('React');
describe('Danger', function() {
describe('dangerouslyInsertMarkupAt', function() {
var Danger;
var transaction;
beforeEach(function() {
require('mock-modules').dumpCache();
Danger = require('Danger');
var ReactReconcileTransaction = require('ReactReconcileTransaction');
transaction = new ReactReconcileTransaction();
});
it('should render markup', function() {
var markup = (<div />).mountComponent('.rX', transaction);
var parent = document.createElement('div');
Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
expect(parent.innerHTML).toBe('<div data-reactid=".rX"></div>');
});
it('should render markup with props', function() {
var markup = (<div className="foo" />).mountComponent('.rX', transaction);
var parent = document.createElement('div');
Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
expect(parent.innerHTML).toBe(
'<div class="foo" data-reactid=".rX"></div>'
);
});
it('should render wrapped markup', function() {
var markup = (<th />).mountComponent('.rX', transaction);
var parent = document.createElement('div');
Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
expect(parent.innerHTML).toBe('<th data-reactid=".rX"></th>');
});
});
});
@@ -54,7 +54,7 @@ describe('ReactServerRendering', function() {
}
);
expect(response).toMatch(
'<span ' + ReactID.ATTR_NAME + '="[^"]+">hello world</span>'
'<span ' + ReactID.ATTR_NAME + '="[^"]+">hello world</span>'
);
});
@@ -77,8 +77,8 @@ describe('ReactServerRendering', function() {
}
);
expect(response).toMatch(
'<div ' + ReactID.ATTR_NAME + '="[^"]+">' +
'<span ' + ReactID.ATTR_NAME + '="[^"]+">' +
'<div ' + ReactID.ATTR_NAME + '="[^"]+">' +
'<span ' + ReactID.ATTR_NAME + '="[^"]+">' +
'<span ' + ReactID.ATTR_NAME + '="[^"]+">My name is </span>' +
'<span ' + ReactID.ATTR_NAME + '="[^"]+">child</span>' +
'</span>' +
@@ -130,7 +130,7 @@ describe('ReactServerRendering', function() {
);
expect(response).toMatch(
'<span ' + ReactID.ATTR_NAME + '="[^"]+">' +
'<span ' + ReactID.ATTR_NAME + '="[^"]+">' +
'<span ' + ReactID.ATTR_NAME + '="[^"]+">Component name: </span>' +
'<span ' + ReactID.ATTR_NAME + '="[^"]+">TestComponent</span>' +
'</span>'