Fix reconciling when switching to/from innerHTML

There's no way that this can work if _updateDOMChildren doesn't know about dangerouslySetInnerHTML, so tell it.

Fixes #377.
This commit is contained in:
Ben Alpert
2013-09-27 16:22:46 -07:00
parent 84dea7e971
commit 3ca507d73f
4 changed files with 64 additions and 39 deletions
+5 -3
View File
@@ -49,6 +49,8 @@ var INVALID_PROPERTY_ERRORS = {
*/
var textContentAccessor = getTextContentAccessor() || 'NA';
var LEADING_SPACE = /^ /;
/**
* Operations used to process updates to DOM nodes. This is made injectable via
* `ReactComponent.DOMIDOperations`.
@@ -133,17 +135,17 @@ var ReactDOMIDOperations = {
},
/**
* Updates a DOM node's innerHTML set by `props.dangerouslySetInnerHTML`.
* Updates a DOM node's innerHTML.
*
* @param {string} id ID of the node to update.
* @param {object} html An HTML object with the `__html` property.
* @param {string} html An HTML string.
* @internal
*/
updateInnerHTMLByID: function(id, html) {
var node = ReactMount.getNode(id);
// HACK: IE8- normalize whitespace in innerHTML, removing leading spaces.
// @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html
node.innerHTML = (html && html.__html || '').replace(/^ /g, ' ');
node.innerHTML = html.replace(LEADING_SPACE, ' ');
},
/**
+34 -34
View File
@@ -42,7 +42,6 @@ var registrationNames = ReactEventEmitter.registrationNames;
// For quickly matching children type, to test if can be treated as content.
var CONTENT_TYPES = {'string': true, 'number': true};
var DANGEROUSLY_SET_INNER_HTML = keyOf({dangerouslySetInnerHTML: null});
var STYLE = keyOf({style: null});
/**
@@ -231,15 +230,11 @@ ReactNativeComponent.Mixin = {
styleUpdates[styleName] = '';
}
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
// http://jsperf.com/emptying-speed
ReactComponent.DOMIDOperations.updateTextContentByID(
this._rootNodeID,
''
);
} else if (registrationNames[propKey]) {
deleteListener(this._rootNodeID, propKey);
} else {
} else if (
DOMProperty.isStandardName[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
ReactComponent.DOMIDOperations.deletePropertyByID(
this._rootNodeID,
propKey
@@ -277,15 +272,6 @@ ReactNativeComponent.Mixin = {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
styleUpdates = nextProp;
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
var lastHtml = lastProp && lastProp.__html;
var nextHtml = nextProp && nextProp.__html;
if (lastHtml !== nextHtml) {
ReactComponent.DOMIDOperations.updateInnerHTMLByID(
this._rootNodeID,
nextProp
);
}
} else if (registrationNames[propKey]) {
putListener(this._rootNodeID, propKey, nextProp);
} else if (
@@ -316,31 +302,45 @@ ReactNativeComponent.Mixin = {
_updateDOMChildren: function(lastProps, transaction) {
var nextProps = this.props;
var lastUsedContent =
var lastContent =
CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
var contentToUse =
var nextContent =
CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
var lastHtml =
lastProps.dangerouslySetInnerHTML &&
lastProps.dangerouslySetInnerHTML.__html;
var nextHtml =
nextProps.dangerouslySetInnerHTML &&
nextProps.dangerouslySetInnerHTML.__html;
// Note the use of `!=` which checks for null or undefined.
var lastChildren = lastContent != null ? null : lastProps.children;
var nextChildren = nextContent != null ? null : nextProps.children;
var lastUsedChildren =
lastUsedContent != null ? null : lastProps.children;
var childrenToUse = contentToUse != null ? null : nextProps.children;
// If we're switching from children to content/html or vice versa, remove
// the old content
var lastHasContentOrHtml = lastContent != null || lastHtml != null;
var nextHasContentOrHtml = nextContent != null || nextHtml != null;
if (lastChildren != null && nextChildren == null) {
this.updateChildren(null, transaction);
} else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
this.updateTextContent('');
}
if (contentToUse != null) {
var childrenRemoved = lastUsedChildren != null && childrenToUse == null;
if (childrenRemoved) {
this.updateChildren(null, transaction);
if (nextContent != null) {
if (lastContent !== nextContent) {
this.updateTextContent('' + nextContent);
}
if (lastUsedContent !== contentToUse) {
this.updateTextContent('' + contentToUse);
} else if (nextHtml != null) {
if (lastHtml !== nextHtml) {
ReactComponent.DOMIDOperations.updateInnerHTMLByID(
this._rootNodeID,
nextHtml
);
}
} else {
var contentRemoved = lastUsedContent != null && contentToUse == null;
if (contentRemoved) {
this.updateTextContent('');
}
this.updateChildren(flattenChildren(nextProps.children), transaction);
} else if (nextChildren != null) {
this.updateChildren(flattenChildren(nextChildren), transaction);
}
},
@@ -53,7 +53,7 @@ describe('ReactDOMIDOperations', function() {
ReactDOMIDOperations.updateInnerHTMLByID(
'testID',
{__html: ' testContent'}
' testContent'
);
expect(
@@ -152,7 +152,7 @@ describe('ReactNativeComponent', function() {
it("should empty element when removing innerHTML", function() {
var stub = ReactTestUtils.renderIntoDocument(
<div dangerouslySetInnerHTML={{__html: ":)"}} />
<div dangerouslySetInnerHTML={{__html: ':)'}} />
);
expect(stub.getDOMNode().innerHTML).toEqual(':)');
@@ -160,6 +160,29 @@ describe('ReactNativeComponent', function() {
expect(stub.getDOMNode().innerHTML).toEqual('');
});
it("should transition from string content to innerHTML", function() {
var stub = ReactTestUtils.renderIntoDocument(
<div>hello</div>
);
expect(stub.getDOMNode().innerHTML).toEqual('hello');
stub.receiveProps(
{dangerouslySetInnerHTML: {__html: 'goodbye'}},
transaction
);
expect(stub.getDOMNode().innerHTML).toEqual('goodbye');
});
it("should transition from innerHTML to string content", function() {
var stub = ReactTestUtils.renderIntoDocument(
<div dangerouslySetInnerHTML={{__html: 'bonjour'}} />
);
expect(stub.getDOMNode().innerHTML).toEqual('bonjour');
stub.receiveProps({children: 'adieu'}, transaction);
expect(stub.getDOMNode().innerHTML).toEqual('adieu');
});
it("should not incur unnecessary DOM mutations", function() {
var stub = ReactTestUtils.renderIntoDocument(<div value="" />);