-
Notifications
You must be signed in to change notification settings - Fork 46.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Properly set value and defaultValue for input and textarea #6406
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,6 @@ | |
'use strict'; | ||
|
||
var DisabledInputUtils = require('DisabledInputUtils'); | ||
var DOMPropertyOperations = require('DOMPropertyOperations'); | ||
var LinkedValueUtils = require('LinkedValueUtils'); | ||
var ReactDOMComponentTree = require('ReactDOMComponentTree'); | ||
var ReactUpdates = require('ReactUpdates'); | ||
|
@@ -67,11 +66,14 @@ var ReactDOMTextarea = { | |
); | ||
|
||
// Always set children to the same thing. In IE9, the selection range will | ||
// get reset if `textContent` is mutated. | ||
// get reset if `textContent` is mutated. We could add a check in setTextContent | ||
// to only set the value if/when the value differs from the node value (which would | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this still applicable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking into it now. If not, I'll remove the comment as part of my rebase. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looked into it. Appears that IE9 does indeed get upset if you set textContent to the same value it already had. But this was totally the wrong place for a comment to be talking about that; applied a hack in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's only relevant for textareas, isn't it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure. What happens if you have a contenteditable div with text in it? I wouldn't be surprised if there were another edge condition where this applied for React reconciliation, independent of text areas. But regardless, it's the wrong place for the comment. The behavior here shouldn't be dictated by a chain of assumptions where getNativeProps returns a particular value BECAUSE it knows that the reconciler will use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The issue is that no matter how you might update the children, IE loses cursor position in textareas only. Elements with contenteditable should be handled by our ReactInputSelection wrapper on the transaction. I'd like this logic to be maintained as it was, where the children are always passed down as the same value. |
||
// completely solve this IE9 bug), but Sebastian+Ben seemed to like this solution. | ||
// The value can be a boolean or object so that's why it's forced to be a string. | ||
var hostProps = Object.assign({}, DisabledInputUtils.getHostProps(inst, props), { | ||
defaultValue: undefined, | ||
value: undefined, | ||
children: inst._wrapperState.initialValue, | ||
defaultValue: undefined, | ||
children: '' + inst._wrapperState.initialValue, | ||
onChange: inst._wrapperState.onChange, | ||
}); | ||
|
||
|
@@ -110,41 +112,45 @@ var ReactDOMTextarea = { | |
warnIfValueIsNull(props); | ||
} | ||
|
||
var defaultValue = props.defaultValue; | ||
// TODO (yungsters): Remove support for children content in <textarea>. | ||
var children = props.children; | ||
if (children != null) { | ||
if (__DEV__) { | ||
warning( | ||
false, | ||
'Use the `defaultValue` or `value` props instead of setting ' + | ||
'children on <textarea>.' | ||
); | ||
} | ||
invariant( | ||
defaultValue == null, | ||
'If you supply `defaultValue` on a <textarea>, do not pass children.' | ||
); | ||
if (Array.isArray(children)) { | ||
|
||
var value = LinkedValueUtils.getValue(props); | ||
var initialValue = value; | ||
|
||
// Only bother fetching default value if we're going to use it | ||
if (value == null) { | ||
var defaultValue = props.defaultValue; | ||
// TODO (yungsters): Remove support for children content in <textarea>. | ||
var children = props.children; | ||
if (children != null) { | ||
if (__DEV__) { | ||
warning( | ||
false, | ||
'Use the `defaultValue` or `value` props instead of setting ' + | ||
'children on <textarea>.' | ||
); | ||
} | ||
invariant( | ||
children.length <= 1, | ||
'<textarea> can only have at most one child.' | ||
defaultValue == null, | ||
'If you supply `defaultValue` on a <textarea>, do not pass children.' | ||
); | ||
children = children[0]; | ||
if (Array.isArray(children)) { | ||
invariant( | ||
children.length <= 1, | ||
'<textarea> can only have at most one child.' | ||
); | ||
children = children[0]; | ||
} | ||
|
||
defaultValue = '' + children; | ||
} | ||
|
||
defaultValue = '' + children; | ||
} | ||
if (defaultValue == null) { | ||
defaultValue = ''; | ||
if (defaultValue == null) { | ||
defaultValue = ''; | ||
} | ||
initialValue = defaultValue; | ||
} | ||
var value = LinkedValueUtils.getValue(props); | ||
|
||
inst._wrapperState = { | ||
// We save the initial value so that `ReactDOMComponent` doesn't update | ||
// `textContent` (unnecessary since we update value). | ||
// The initial value can be a boolean or object so that's why it's | ||
// forced to be a string. | ||
initialValue: '' + (value != null ? value : defaultValue), | ||
initialValue: '' + initialValue, | ||
listeners: null, | ||
onChange: _handleChange.bind(inst), | ||
}; | ||
|
@@ -157,17 +163,32 @@ var ReactDOMTextarea = { | |
warnIfValueIsNull(props); | ||
} | ||
|
||
var node = ReactDOMComponentTree.getNodeFromInstance(inst); | ||
var value = LinkedValueUtils.getValue(props); | ||
if (value != null) { | ||
// 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; | ||
} | ||
if (props.defaultValue == null) { | ||
node.defaultValue = newValue; | ||
} | ||
} | ||
if (props.defaultValue != null) { | ||
node.defaultValue = props.defaultValue; | ||
} | ||
}, | ||
|
||
postMountWrapper: function(inst) { | ||
// This is in postMount because we need access to the DOM node, which is not | ||
// available until after the component has mounted. | ||
var node = ReactDOMComponentTree.getNodeFromInstance(inst); | ||
node.value = node.value; // Detach value from defaultValue | ||
}, | ||
}; | ||
|
||
function _handleChange(event) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ describe('ReactDOMInput', function() { | |
var EventConstants; | ||
var React; | ||
var ReactDOM; | ||
var ReactDOMServer; | ||
var ReactDOMFeatureFlags; | ||
var ReactLink; | ||
var ReactTestUtils; | ||
|
@@ -35,6 +36,7 @@ describe('ReactDOMInput', function() { | |
EventConstants = require('EventConstants'); | ||
React = require('React'); | ||
ReactDOM = require('ReactDOM'); | ||
ReactDOMServer = require('ReactDOMServer'); | ||
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); | ||
ReactLink = require('ReactLink'); | ||
ReactTestUtils = require('ReactTestUtils'); | ||
|
@@ -47,6 +49,7 @@ describe('ReactDOMInput', function() { | |
stub = ReactTestUtils.renderIntoDocument(stub); | ||
var node = ReactDOM.findDOMNode(stub); | ||
|
||
expect(node.getAttribute('value')).toBe('0'); | ||
expect(node.value).toBe('0'); | ||
}); | ||
|
||
|
@@ -66,6 +69,48 @@ describe('ReactDOMInput', function() { | |
expect(node.value).toBe('false'); | ||
}); | ||
|
||
it('should update `defaultValue` for uncontrolled input', function() { | ||
var container = document.createElement('div'); | ||
|
||
var node = ReactDOM.render(<input type="text" defaultValue="0" />, container); | ||
|
||
expect(node.value).toBe('0'); | ||
|
||
ReactDOM.render(<input type="text" defaultValue="1" />, container); | ||
|
||
expect(node.value).toBe('0'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you want to assert that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
expect(node.defaultValue).toBe('1'); | ||
}); | ||
|
||
it('should take `defaultValue` when changing to uncontrolled input', function() { | ||
var container = document.createElement('div'); | ||
|
||
var node = ReactDOM.render(<input type="text" value="0" readOnly="true" />, container); | ||
|
||
expect(node.value).toBe('0'); | ||
|
||
ReactDOM.render(<input type="text" defaultValue="1" />, container); | ||
|
||
expect(node.value).toBe('0'); | ||
}); | ||
|
||
it('should render defaultValue for SSR', function() { | ||
var markup = ReactDOMServer.renderToString(<input type="text" defaultValue="1" />); | ||
var div = document.createElement('div'); | ||
div.innerHTML = markup; | ||
expect(div.firstChild.getAttribute('value')).toBe('1'); | ||
expect(div.firstChild.getAttribute('defaultValue')).toBe(null); | ||
}); | ||
|
||
it('should render value for SSR', function() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a SSR test where we render with |
||
var element = <input type="text" value="1" onChange={function() {}} />; | ||
var markup = ReactDOMServer.renderToString(element); | ||
var div = document.createElement('div'); | ||
div.innerHTML = markup; | ||
expect(div.firstChild.getAttribute('value')).toBe('1'); | ||
expect(div.firstChild.getAttribute('defaultValue')).toBe(null); | ||
}); | ||
|
||
it('should display "foobar" for `defaultValue` of `objToString`', function() { | ||
var objToString = { | ||
toString: function() { | ||
|
@@ -91,8 +136,7 @@ describe('ReactDOMInput', function() { | |
it('should allow setting `value` to `true`', function() { | ||
var container = document.createElement('div'); | ||
var stub = <input type="text" value="yolo" onChange={emptyFunction} />; | ||
stub = ReactDOM.render(stub, container); | ||
var node = ReactDOM.findDOMNode(stub); | ||
var node = ReactDOM.render(stub, container); | ||
|
||
expect(node.value).toBe('yolo'); | ||
|
||
|
@@ -106,8 +150,7 @@ describe('ReactDOMInput', function() { | |
it('should allow setting `value` to `false`', function() { | ||
var container = document.createElement('div'); | ||
var stub = <input type="text" value="yolo" onChange={emptyFunction} />; | ||
stub = ReactDOM.render(stub, container); | ||
var node = ReactDOM.findDOMNode(stub); | ||
var node = ReactDOM.render(stub, container); | ||
|
||
expect(node.value).toBe('yolo'); | ||
|
||
|
@@ -121,8 +164,7 @@ describe('ReactDOMInput', function() { | |
it('should allow setting `value` to `objToString`', function() { | ||
var container = document.createElement('div'); | ||
var stub = <input type="text" value="foo" onChange={emptyFunction} />; | ||
stub = ReactDOM.render(stub, container); | ||
var node = ReactDOM.findDOMNode(stub); | ||
var node = ReactDOM.render(stub, container); | ||
|
||
expect(node.value).toBe('foo'); | ||
|
||
|
@@ -138,6 +180,29 @@ describe('ReactDOMInput', function() { | |
expect(node.value).toEqual('foobar'); | ||
}); | ||
|
||
it('should not incur unnecessary DOM mutations', function() { | ||
var container = document.createElement('div'); | ||
ReactDOM.render(<input value="a" />, container); | ||
|
||
var node = container.firstChild; | ||
var nodeValue = 'a'; | ||
var nodeValueSetter = jest.genMockFn(); | ||
Object.defineProperty(node, 'value', { | ||
get: function() { | ||
return nodeValue; | ||
}, | ||
set: nodeValueSetter.mockImplementation(function(newValue) { | ||
nodeValue = newValue; | ||
}), | ||
}); | ||
|
||
ReactDOM.render(<input value="a" />, container); | ||
expect(nodeValueSetter.mock.calls.length).toBe(0); | ||
|
||
ReactDOM.render(<input value="b"/>, container); | ||
expect(nodeValueSetter.mock.calls.length).toBe(1); | ||
}); | ||
|
||
it('should properly control a value of number `0`', function() { | ||
var stub = <input type="text" value={0} onChange={emptyFunction} />; | ||
stub = ReactTestUtils.renderIntoDocument(stub); | ||
|
@@ -399,6 +464,13 @@ describe('ReactDOMInput', function() { | |
|
||
}); | ||
|
||
it('should update defaultValue to empty string', function() { | ||
var container = document.createElement('div'); | ||
ReactDOM.render(<input type="text" defaultValue={'foo'} />, container); | ||
ReactDOM.render(<input type="text" defaultValue={''} />, container); | ||
expect(container.firstChild.defaultValue).toBe(''); | ||
}); | ||
|
||
it('should throw if both checkedLink and valueLink are provided', function() { | ||
var node = document.createElement('div'); | ||
var link = new ReactLink(true, jest.fn()); | ||
|
@@ -618,6 +690,11 @@ describe('ReactDOMInput', function() { | |
'set data-reactroot', | ||
'set type', | ||
'set value', | ||
'set value', | ||
'set name', | ||
'set checked', | ||
'set checked', | ||
'set name', | ||
]); | ||
}); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to understand the logic here. Could somebody tell what "detach" means here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I recall from way back when, once you call the setter for
value
, it then ignores changes to thedefaultValue
. In this case, we set the value to the same value, which has this effect.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation