+
+
+
+ The field should read "3.", preserving the decimal place
+
+
+
+
+
+ Notes: Chrome and Safari clear trailing
+ decimals on blur. React makes this concession so that the
+ value attribute remains in sync with the value property.
+
+
+
+
+
+
Type "0.01"
+
+
+
+ The field should read "0.01"
+
+
+
+
+
+
+
+
Type "2e"
+
Type 4, to read "2e4"
+
+
+
+ The field should read "2e4". The parsed value should read "20000"
+
+
+
+
+
+
+
+
Type "3.14"
+
Press "e", so that the input reads "3.14e"
+
+
+
+ The field should read "3.14e", the parsed value should be empty
+
+
+
+
+
+
+
+
Type "3.14"
+
Move the text cursor to after the decimal place
+
Press "e" twice, so that the value reads "3.ee14"
+
+
+
+ The field should read "3.ee14"
+
+
+
+
+
+
+
+
Type "3.0"
+
+
+
+ The field should read "3.0"
+
+
+
+
+
+
+
+
Type "300"
+
Move the cursor to after the "3"
+
Type "."
+
+
+
+ The field should read "3.00", not "3"
+
+
+
+
+
+
+
Type "3"
+
Select the entire value"
+
Type '-' to replace '3' with '-'
+
+
+
+ The field should read "-", not be blank.
+
+
+
+
+
+
+
Type "-"
+
Type '3'
+
+
+
+ The field should read "-3".
+
+
+
+
+ );
+ },
+});
+
+export default NumberInputs;
diff --git a/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js b/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js
index 484bb1a40d..e4a1f9b910 100644
--- a/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js
+++ b/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js
@@ -317,6 +317,26 @@ function getTargetInstForClickEvent(
}
}
+function handleControlledInputBlur(inst, node) {
+ // TODO: In IE, inst is occasionally null. Why?
+ if (inst == null) {
+ return;
+ }
+
+ // Fiber and ReactDOM keep wrapper state in separate places
+ let state = inst._wrapperState || node._wrapperState;
+
+ if (!state || !state.controlled || node.type !== 'number') {
+ return;
+ }
+
+ // If controlled, assign the value attribute to the current value on blur
+ let value = '' + node.value;
+ if (node.getAttribute('value') !== value) {
+ node.setAttribute('value', value);
+ }
+}
+
/**
* This plugin creates an `onChange` event that normalizes change events
* across form elements. This event fires at a time when it's possible to
@@ -380,6 +400,11 @@ var ChangeEventPlugin = {
targetInst
);
}
+
+ // When blurring, set the value attribute for number inputs
+ if (topLevelType === 'topBlur') {
+ handleControlledInputBlur(targetInst, targetNode);
+ }
},
};
diff --git a/src/renderers/dom/client/wrappers/ReactDOMInput.js b/src/renderers/dom/client/wrappers/ReactDOMInput.js
index 8d1ad6ec55..07061ef32c 100644
--- a/src/renderers/dom/client/wrappers/ReactDOMInput.js
+++ b/src/renderers/dom/client/wrappers/ReactDOMInput.js
@@ -149,11 +149,8 @@ var ReactDOMInput = {
initialValue: props.value != null ? props.value : defaultValue,
listeners: null,
onChange: _handleChange.bind(inst),
+ controlled: isControlled(props),
};
-
- if (__DEV__) {
- inst._wrapperState.controlled = isControlled(props);
- }
},
updateWrapper: function(inst) {
@@ -202,14 +199,24 @@ var ReactDOMInput = {
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
var value = LinkedValueUtils.getValue(props);
if (value != null) {
+ if (value === 0 && node.value === '') {
+ node.value = '0';
+ // Note: IE9 reports a number inputs as 'text', so check props instead.
+ } else if (props.type === 'number') {
+ // Simulate `input.valueAsNumber`. IE9 does not support it
+ var valueAsNumber = parseFloat(node.value, 10) || 0;
- // Cast `value` to a string to ensure the value is set correctly. While
- // browsers typically do this as necessary, jsdom doesn't.
- var newValue = '' + value;
-
- // To avoid side effects (such as losing text selection), only set value if changed
- if (newValue !== node.value) {
- node.value = newValue;
+ // eslint-disable-next-line
+ if (value != valueAsNumber) {
+ // Cast `value` to a string to ensure the value is set correctly. While
+ // browsers typically do this as necessary, jsdom doesn't.
+ node.value = '' + value;
+ }
+ // eslint-disable-next-line
+ } else if (value != node.value) {
+ // Cast `value` to a string to ensure the value is set correctly. While
+ // browsers typically do this as necessary, jsdom doesn't.
+ node.value = '' + value;
}
} else {
if (props.value == null && props.defaultValue != null) {
diff --git a/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js b/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js
index ea2d6074d0..8ee2478bbd 100644
--- a/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js
+++ b/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js
@@ -268,6 +268,48 @@ describe('ReactDOMInput', () => {
expect(node.value).toBe('0');
});
+ it('should properly control 0.0 for a text input', () => {
+ var stub = ;
+ stub = ReactTestUtils.renderIntoDocument(stub);
+ var node = ReactDOM.findDOMNode(stub);
+
+ node.value = '0.0';
+ ReactTestUtils.Simulate.change(node, {target: {value: '0.0'}});
+ expect(node.value).toBe('0.0');
+ });
+
+ it('should properly control 0.0 for a number input', () => {
+ var stub = ;
+ stub = ReactTestUtils.renderIntoDocument(stub);
+ var node = ReactDOM.findDOMNode(stub);
+
+ node.value = '0.0';
+ ReactTestUtils.Simulate.change(node, {target: {value: '0.0'}});
+ expect(node.value).toBe('0.0');
+ });
+
+ it('should properly transition from an empty value to 0', function() {
+ var container = document.createElement('div');
+
+ ReactDOM.render(, container);
+ ReactDOM.render(, container);
+
+ var node = container.firstChild;
+
+ expect(node.value).toBe('0');
+ });
+
+ it('should properly transition from 0 to an empty value', function() {
+ var container = document.createElement('div');
+
+ ReactDOM.render(, container);
+ ReactDOM.render(, container);
+
+ var node = container.firstChild;
+
+ expect(node.value).toBe('');
+ });
+
it('should have the correct target value', () => {
var handled = false;
var handler = function(event) {
@@ -937,4 +979,88 @@ describe('ReactDOMInput', () => {
'node.setAttribute("checked", "")',
]);
});
+
+ describe('assigning the value attribute on controlled inputs', function() {
+ function getTestInput() {
+ return React.createClass({
+ getInitialState: function() {
+ return {
+ value: this.props.value == null ? '' : this.props.value,
+ };
+ },
+ onChange: function(event) {
+ this.setState({value: event.target.value});
+ },
+ render: function() {
+ var type = this.props.type;
+ var value = this.state.value;
+
+ return ;
+ },
+ });
+ }
+
+ it('always sets the attribute when values change on text inputs', function() {
+ var Input = getTestInput();
+ var stub = ReactTestUtils.renderIntoDocument();
+ var node = ReactDOM.findDOMNode(stub);
+
+ ReactTestUtils.Simulate.change(node, {target: {value: '2'}});
+
+ expect(node.getAttribute('value')).toBe('2');
+ });
+
+ it('does not set the value attribute on number inputs if focused', () => {
+ var Input = getTestInput();
+ var stub = ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ var node = ReactDOM.findDOMNode(stub);
+
+ node.focus();
+
+ ReactTestUtils.Simulate.change(node, {target: {value: '2'}});
+
+ expect(node.getAttribute('value')).toBe('1');
+ });
+
+ it('sets the value attribute on number inputs on blur', () => {
+ var Input = getTestInput();
+ var stub = ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ var node = ReactDOM.findDOMNode(stub);
+
+ ReactTestUtils.Simulate.change(node, {target: {value: '2'}});
+ ReactTestUtils.SimulateNative.blur(node);
+
+ expect(node.getAttribute('value')).toBe('2');
+ });
+
+ it('an uncontrolled number input will not update the value attribute on blur', () => {
+ var stub = ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ var node = ReactDOM.findDOMNode(stub);
+
+ node.value = 4;
+
+ ReactTestUtils.SimulateNative.blur(node);
+
+ expect(node.getAttribute('value')).toBe('1');
+ });
+
+ it('an uncontrolled text input will not update the value attribute on blur', () => {
+ var stub = ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ var node = ReactDOM.findDOMNode(stub);
+
+ node.value = 4;
+
+ ReactTestUtils.SimulateNative.blur(node);
+
+ expect(node.getAttribute('value')).toBe('1');
+ });
+ });
});
diff --git a/src/renderers/dom/shared/HTMLDOMPropertyConfig.js b/src/renderers/dom/shared/HTMLDOMPropertyConfig.js
index 31b2b9cd4a..ef0eda5e25 100644
--- a/src/renderers/dom/shared/HTMLDOMPropertyConfig.js
+++ b/src/renderers/dom/shared/HTMLDOMPropertyConfig.js
@@ -210,7 +210,34 @@ var HTMLDOMPropertyConfig = {
htmlFor: 'for',
httpEquiv: 'http-equiv',
},
- DOMPropertyNames: {
+ DOMPropertyNames: {},
+ DOMMutationMethods: {
+ value: function(node, value) {
+ if (value == null) {
+ return node.removeAttribute('value');
+ }
+
+ // Number inputs get special treatment due to some edge cases in
+ // Chrome. Let everything else assign the value attribute as normal.
+ // https://github.com/facebook/react/issues/7253#issuecomment-236074326
+ if (node.type !== 'number' || node.hasAttribute('value') === false) {
+ node.setAttribute('value', '' + value);
+ } else if (
+ node.validity &&
+ !node.validity.badInput &&
+ node.ownerDocument.activeElement !== node
+ ) {
+ // Don't assign an attribute if validation reports bad
+ // input. Chrome will clear the value. Additionally, don't
+ // operate on inputs that have focus, otherwise Chrome might
+ // strip off trailing decimal places and cause the user's
+ // cursor position to jump to the beginning of the input.
+ //
+ // In ReactDOMInput, we have an onBlur event that will trigger
+ // this function again when focus is lost.
+ node.setAttribute('value', '' + value);
+ }
+ },
},
};
diff --git a/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js b/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js
index 972ef0b71b..d5e755fbdb 100644
--- a/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js
+++ b/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js
@@ -348,6 +348,35 @@ describe('DOMPropertyOperations', () => {
});
+ describe('value mutation method', function() {
+ it('should update an empty attribute to zero', function() {
+ var stubNode = document.createElement('input');
+ var stubInstance = {_debugID: 1};
+ ReactDOMComponentTree.precacheNode(stubInstance, stubNode);
+
+ stubNode.setAttribute('type', 'radio');
+
+ DOMPropertyOperations.setValueForProperty(stubNode, 'value', '');
+ spyOn(stubNode, 'setAttribute');
+ DOMPropertyOperations.setValueForProperty(stubNode, 'value', 0);
+
+ expect(stubNode.setAttribute.calls.count()).toBe(1);
+ });
+
+ it('should always assign the value attribute for non-inputs', function() {
+ var stubNode = document.createElement('progress');
+ var stubInstance = {_debugID: 1};
+ ReactDOMComponentTree.precacheNode(stubInstance, stubNode);
+
+ spyOn(stubNode, 'setAttribute');
+
+ DOMPropertyOperations.setValueForProperty(stubNode, 'value', 30);
+ DOMPropertyOperations.setValueForProperty(stubNode, 'value', '30');
+
+ expect(stubNode.setAttribute.calls.count()).toBe(2);
+ });
+ });
+
describe('deleteValueForProperty', () => {
var stubNode;
var stubInstance;