diff --git a/src/renderers/dom/client/wrappers/ReactDOMInput.js b/src/renderers/dom/client/wrappers/ReactDOMInput.js index 682a34961a63c..c9f8cacd50fe7 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMInput.js +++ b/src/renderers/dom/client/wrappers/ReactDOMInput.js @@ -70,7 +70,7 @@ var ReactDOMInput = { var nativeProps = assign({}, props, { defaultChecked: undefined, defaultValue: undefined, - value: value != null ? value : inst._wrapperState.initialValue, + value: value != null ? value : props.defaultValue, checked: checked != null ? checked : inst._wrapperState.initialChecked, onChange: inst._wrapperState.onChange, }); @@ -103,10 +103,8 @@ var ReactDOMInput = { warnIfValueIsNull(props); } - var defaultValue = props.defaultValue; inst._wrapperState = { initialChecked: props.defaultChecked || false, - initialValue: defaultValue != null ? defaultValue : null, listeners: null, onChange: _handleChange.bind(inst), }; @@ -140,13 +138,16 @@ var ReactDOMInput = { var value = LinkedValueUtils.getValue(props); if (value != null) { + var node = ReactDOMComponentTree.getNodeFromInstance(inst); + // Cast `value` to a string to ensure the value is set correctly. While // browsers typically do this as necessary, jsdom doesn't. - DOMPropertyOperations.setValueForProperty( - ReactDOMComponentTree.getNodeFromInstance(inst), - 'value', - '' + value - ); + var newValue = '' + value; + + // To avoid side effects (such as losing text selection), only set value if changed + if (newValue !== node.value) { + node.value = newValue; + } } }, }; diff --git a/src/renderers/dom/client/wrappers/ReactDOMTextarea.js b/src/renderers/dom/client/wrappers/ReactDOMTextarea.js index b206c57d6c1d0..d1b38252e0198 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMTextarea.js +++ b/src/renderers/dom/client/wrappers/ReactDOMTextarea.js @@ -11,7 +11,6 @@ 'use strict'; -var DOMPropertyOperations = require('DOMPropertyOperations'); var LinkedValueUtils = require('LinkedValueUtils'); var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactUpdates = require('ReactUpdates'); @@ -143,13 +142,16 @@ var ReactDOMTextarea = { var value = LinkedValueUtils.getValue(props); if (value != null) { + var node = ReactDOMComponentTree.getNodeFromInstance(inst); + // Cast `value` to a string to ensure the value is set correctly. While // browsers typically do this as necessary, jsdom doesn't. - DOMPropertyOperations.setValueForProperty( - ReactDOMComponentTree.getNodeFromInstance(inst), - 'value', - '' + value - ); + var newValue = '' + value; + + // To avoid side effects (such as losing text selection), only set value if changed + if (newValue !== node.value) { + node.value = newValue; + } } }, }; diff --git a/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js b/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js index ffa90a059d4cc..75c4289e2c287 100644 --- a/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js +++ b/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js @@ -36,6 +36,7 @@ describe('ReactDOMInput', function() { stub = ReactTestUtils.renderIntoDocument(stub); var node = ReactDOM.findDOMNode(stub); + expect(node.getAttribute('value')).toBe('0'); expect(node.value).toBe('0'); }); @@ -55,6 +56,68 @@ describe('ReactDOMInput', function() { expect(node.value).toBe('false'); }); + it('should update `defaultValue` for uncontrolled input', function() { + var container = document.createElement('div'); + + var el = ReactDOM.render(, container); + var node = ReactDOM.findDOMNode(el); + + expect(node.value).toBe('0'); + + ReactDOM.render(, container); + + expect(node.value).toBe('1'); + }); + + it('should take `defaultValue` for a controlled input', function() { + var container = document.createElement('div'); + + var el = ReactDOM.render(, container); + var node = ReactDOM.findDOMNode(el); + + expect(node.value).toBe('0'); + + ReactDOM.render(, container); + + expect(node.value).toBe('1'); + + ReactDOM.render(, container); + + expect(node.value).toBe('3'); + + ReactDOM.render(, container); + + expect(node.value).toBe('5'); + expect(node.getAttribute('value')).toBe('5'); + expect(node.defaultValue).toBe('5'); + }); + + it('should update `value` when changing to controlled input', function() { + var container = document.createElement('div'); + + var el = ReactDOM.render(, container); + var node = ReactDOM.findDOMNode(el); + + expect(node.value).toBe('0'); + + ReactDOM.render(, container); + + expect(node.value).toBe('1'); + }); + + it('should take `defaultValue` when changing to uncontrolled input', function() { + var container = document.createElement('div'); + + var el = ReactDOM.render(, container); + var node = ReactDOM.findDOMNode(el); + + expect(node.value).toBe('0'); + + ReactDOM.render(, container); + + expect(node.value).toBe('1'); + }); + it('should display "foobar" for `defaultValue` of `objToString`', function() { var objToString = { toString: function() { @@ -127,6 +190,29 @@ describe('ReactDOMInput', function() { expect(node.value).toEqual('foobar'); }); + it('should not incur unnecessary DOM mutations', function() { + var container = document.createElement('div'); + ReactDOM.render(, container); + + var node = container.firstChild; + var nodeValue = 'a'; // node.value always returns undefined + var nodeValueSetter = jest.genMockFn(); + Object.defineProperty(node, 'value', { + get: function() { + return nodeValue; + }, + set: nodeValueSetter.mockImplementation(function(newValue) { + nodeValue = newValue; + }), + }); + + ReactDOM.render(, container); + expect(nodeValueSetter.mock.calls.length).toBe(0); + + ReactDOM.render(, container); + expect(nodeValueSetter.mock.calls.length).toBe(1); + }); + it('should properly control a value of number `0`', function() { var stub = ; stub = ReactTestUtils.renderIntoDocument(stub); diff --git a/src/renderers/dom/client/wrappers/__tests__/ReactDOMTextarea-test.js b/src/renderers/dom/client/wrappers/__tests__/ReactDOMTextarea-test.js index 1512d25eaac90..340cc7e59cb61 100644 --- a/src/renderers/dom/client/wrappers/__tests__/ReactDOMTextarea-test.js +++ b/src/renderers/dom/client/wrappers/__tests__/ReactDOMTextarea-test.js @@ -163,6 +163,42 @@ describe('ReactDOMTextarea', function() { expect(node.value).toEqual('foo'); }); + it('should not take updates to `defaultValue` for uncontrolled textarea', function() { + var container = document.createElement('div'); + + var el = ReactDOM.render(