diff --git a/src/browser/ui/ReactDOMComponent.js b/src/browser/ui/ReactDOMComponent.js index 9b528def2726c..e11a03e4bc73d 100644 --- a/src/browser/ui/ReactDOMComponent.js +++ b/src/browser/ui/ReactDOMComponent.js @@ -57,8 +57,11 @@ function assertValidProps(props) { } // Note the use of `==` which checks for null or undefined. invariant( - props.children == null || props.dangerouslySetInnerHTML == null, - 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.' + props.children == null || + props.dangerouslySetInnerHTML == null && + props.dangerouslySetDefaultInnerHTML == null, + 'Can only set `children` or `props.dangerouslySetInnerHTML` and ' + + '`dangerouslySetDefaultInnerHTML`.' ); if (__DEV__) { if (props.contentEditable && props.children != null) { @@ -69,6 +72,12 @@ function assertValidProps(props) { 'intentional.' ); } + if (props.contentEditable && props.dangerouslySetInnerHTML != null) { + console.warn( + 'A component is `contentEditable` and has `dangerouslySetInnerHTML` ' + + 'set. It is preferable to use `dangerouslySetDefaultInnerHTML` instead.' + ); + } } invariant( props.style == null || typeof props.style === 'object', @@ -205,6 +214,9 @@ ReactDOMComponent.Mixin = { _createContentMarkup: function(transaction) { // Intentional use of != to avoid catching zero/false. var innerHTML = this.props.dangerouslySetInnerHTML; + if (innerHTML == null) { + innerHTML = this.props.dangerouslySetDefaultInnerHTML; + } if (innerHTML != null) { if (innerHTML.__html != null) { return innerHTML.__html; @@ -387,17 +399,26 @@ ReactDOMComponent.Mixin = { nextProps.dangerouslySetInnerHTML && nextProps.dangerouslySetInnerHTML.__html; + var lastDefaultHtml = + lastProps.dangerouslySetDefaultInnerHTML && + lastProps.dangerouslySetDefaultInnerHTML.__html; + var nextDefaultHtml = + nextProps.dangerouslySetDefaultInnerHTML && + nextProps.dangerouslySetDefaultInnerHTML.__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; - // 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; + var lastHasHtml = lastHtml != null || lastDefaultHtml != null; + var nextHasHtml = nextHtml != null || nextDefaultHtml != null; + if (lastChildren != null && nextChildren == null) { this.updateChildren(null, transaction); - } else if (lastHasContentOrHtml && !nextHasContentOrHtml) { + } else if (lastContent != null && nextContent == null || + lastHasHtml && !nextHasHtml) { + // If we're switching from children to content/html or vice versa, remove + // the old content this.updateTextContent(''); } diff --git a/src/browser/ui/ReactDOMIDOperations.js b/src/browser/ui/ReactDOMIDOperations.js index ea206cd065b90..53a48d5b21adb 100644 --- a/src/browser/ui/ReactDOMIDOperations.js +++ b/src/browser/ui/ReactDOMIDOperations.js @@ -39,6 +39,9 @@ var setInnerHTML = require('setInnerHTML'); var INVALID_PROPERTY_ERRORS = { dangerouslySetInnerHTML: '`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.', + dangerouslySetDefaultInnerHTML: + '`dangerouslySetDefaultInnerHTML` must be set using ' + + '`updateInnerHTMLByID()`.', style: '`style` must be set using `updateStylesByID()`.' }; diff --git a/src/browser/ui/__tests__/ReactDOMComponent-test.js b/src/browser/ui/__tests__/ReactDOMComponent-test.js index 8eb0e105a65c4..09041bc27570f 100644 --- a/src/browser/ui/__tests__/ReactDOMComponent-test.js +++ b/src/browser/ui/__tests__/ReactDOMComponent-test.js @@ -341,11 +341,20 @@ describe('ReactDOMComponent', function() { expect(function() { mountComponent({ children: '', dangerouslySetInnerHTML: '' }); }).toThrow( - 'Invariant Violation: Can only set one of `children` or ' + - '`props.dangerouslySetInnerHTML`.' + 'Invariant Violation: Can only set `children` or ' + + '`props.dangerouslySetInnerHTML` and `dangerouslySetDefaultInnerHTML`.' ); }); + it("should validate for both default and set innerHTML", function() { + expect(function() { + mountComponent({ + dangerouslySetInnerHTML: '', + dangerouslySetDefaultInnerHTML: '' + }); + }).not.toThrow(); + }); + it("should warn about contentEditable and children", function() { spyOn(console, 'warn'); mountComponent({ contentEditable: true, children: '' }); @@ -381,9 +390,81 @@ describe('ReactDOMComponent', function() { container ); }).toThrow( - 'Invariant Violation: Can only set one of `children` or ' + - '`props.dangerouslySetInnerHTML`.' + 'Invariant Violation: Can only set `children` or ' + + '`props.dangerouslySetInnerHTML` and `dangerouslySetDefaultInnerHTML`.' + ); + }); + + it("should validate for both default and set innerHTML", function() { + React.renderComponent(
, container); + + expect(function() { + React.renderComponent( +
, + container + ); + }).not.toThrow() + }); + + it("should not update children when adding dangerouslySetDefaultInnerHTML", function() { + var stub = React.renderComponent( +
, + container + ); + + React.renderComponent( +
, + container + ); + + expect(stub.getDOMNode().innerHTML).toBe(''); + }); + + it("should not update children when updating dangerouslySetDefaultInnerHTML", function() { + var stub = React.renderComponent( +
, + container ); + + expect(stub.getDOMNode().innerHTML).toBe('default'); + + React.renderComponent( +
, + container + ); + + expect(stub.getDOMNode().innerHTML).toBe('default'); + }); + + it("should not clear children when removing dangerouslySetInnerHTML and dangerouslySetDefaultInnerHTML is set", function() { + var stub = React.renderComponent( +
, + container + ); + + React.renderComponent( +
, + container + ); + + expect(stub.getDOMNode().innerHTML).toBe('set'); + }); + + it("should clear children when removing dangerouslySetDefaultInnerHTML", function() { + var stub = React.renderComponent( +
, + container + ); + + React.renderComponent( +
, + container + ); + + expect(stub.getDOMNode().innerHTML).toBe(''); }); it("should warn about contentEditable and children", function() { diff --git a/src/browser/ui/dom/DOMPropertyOperations.js b/src/browser/ui/dom/DOMPropertyOperations.js index e4d562af856c7..ded82a52903f0 100644 --- a/src/browser/ui/dom/DOMPropertyOperations.js +++ b/src/browser/ui/dom/DOMPropertyOperations.js @@ -41,6 +41,7 @@ if (__DEV__) { var reactProps = { children: true, dangerouslySetInnerHTML: true, + dangerouslySetDefaultInnerHTML: true, key: true, ref: true }; diff --git a/src/browser/ui/dom/components/ReactDOMTextarea.js b/src/browser/ui/dom/components/ReactDOMTextarea.js index 07b1377c952ce..8dffea4d1ce0a 100644 --- a/src/browser/ui/dom/components/ReactDOMTextarea.js +++ b/src/browser/ui/dom/components/ReactDOMTextarea.js @@ -106,8 +106,10 @@ var ReactDOMTextarea = ReactCompositeComponent.createClass({ var props = merge(this.props); invariant( - props.dangerouslySetInnerHTML == null, - '`dangerouslySetInnerHTML` does not make sense on