Skip to content

Commit 467e5b5

Browse files
committed
Controlled inputs do not synchronize value or checked attribute
This commit is a follow up to prior work to resolve issues with number inputs in React. Inputs keep their value/checked attribute in sync with the value/checked property. This is a React behavior. Traditionally browser DOM manipulation does not rely on keeping the value/checked attribute in sync. It's also very problematic for number inputs. After discussion, it was decided to make a breaking change to no longer sync up the value/checked attribute with it's assoicated property. For this to work, I made the following changes: - The value, defaultValue, checked and defaultChecked properties are now maintained within the HTML property config. - This required adding a filter to strip out the value property on selects and textareas - The logic to defer assignment of the value attribute has been removed form ChangeEventPlugin - defaultValue and defaultChecked are aliased to `value` and `checked` so that uncontrolled input attribute assignment works as intended.
1 parent 2e2f503 commit 467e5b5

File tree

11 files changed

+36
-155
lines changed

11 files changed

+36
-155
lines changed

scripts/fiber/tests-passing.txt

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -717,8 +717,6 @@ src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js
717717
* should set className to empty string instead of null
718718
* should remove property properly for boolean properties
719719
* should remove property properly even with different name
720-
* should update an empty attribute to zero
721-
* should always assign the value attribute for non-inputs
722720
* should remove attributes for normal properties
723721
* should not remove attributes for special properties
724722
* should not leave all options selected when deleting multiple
@@ -1805,7 +1803,7 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
18051803
* should control values in reentrant events with different targets
18061804
* does change the number 2 to "2.0" with no change handler
18071805
* does change the string "2" to "2.0" with no change handler
1808-
* changes the number 2 to "2.0" using a change handler
1806+
* changes the value property from number 2 to "2.0" using a change handler
18091807
* does change the string ".98" to "0.98" with no change handler
18101808
* distinguishes precision for extra zeroes in string number values
18111809
* should display `defaultValue` of number 0
@@ -1867,11 +1865,7 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
18671865
* sets value properly with type coming later in props
18681866
* does not raise a validation warning when it switches types
18691867
* resets value of date/time input to fix bugs in iOS Safari
1870-
* always sets the attribute when values change on text inputs
1871-
* does not set the value attribute on number inputs if focused
1872-
* sets the value attribute on number inputs on blur
1873-
* an uncontrolled number input will not update the value attribute on blur
1874-
* an uncontrolled text input will not update the value attribute on blur
1868+
* does not set the attribute when values change on text inputs
18751869

18761870
src/renderers/dom/shared/wrappers/__tests__/ReactDOMOption-test.js
18771871
* should flatten children to a string

src/renderers/dom/fiber/ReactDOMFiberComponent.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,7 @@ var ReactDOMFiberComponent = {
811811
break;
812812
case 'select':
813813
ReactDOMFiberSelect.initWrapperState(domElement, rawProps);
814+
rawProps = ReactDOMFiberSelect.getHostProps(domElement, rawProps);
814815
trapBubbledEventsLocal(domElement, tag);
815816
// For controlled components we always need to ensure we're listening
816817
// to onChange. Even if there is no listener.
@@ -846,8 +847,10 @@ var ReactDOMFiberComponent = {
846847
// Controlled attributes are not validated
847848
// TODO: Only ignore them on controlled tags.
848849
case 'value':
850+
case 'defaultValue':
849851
break;
850852
case 'checked':
853+
case 'defaultChecked':
851854
break;
852855
case 'selected':
853856
break;
@@ -901,7 +904,9 @@ var ReactDOMFiberComponent = {
901904
// Controlled attributes are not validated
902905
// TODO: Only ignore them on controlled tags.
903906
propKey === 'value' ||
907+
propKey === 'defaultValue' ||
904908
propKey === 'checked' ||
909+
propKey === 'defaultChecked' ||
905910
propKey === 'selected'
906911
) {
907912
// Noop

src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,14 @@ var ReactDOMInput = {
7979
},
8080
props,
8181
{
82-
defaultChecked: undefined,
83-
defaultValue: undefined,
84-
value: value != null ? value : node._wrapperState.initialValue,
85-
checked: checked != null ? checked : node._wrapperState.initialChecked,
82+
defaultValue: value == null
83+
? props.defaultValue
84+
: node._wrapperState.initialValue,
85+
defaultChecked: checked == null
86+
? props.defaultChecked
87+
: node._wrapperState.initialChecked,
88+
value: undefined,
89+
checked: undefined,
8690
},
8791
);
8892

src/renderers/dom/fiber/wrappers/ReactDOMFiberSelect.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ var ReactDOMSelect = {
133133
getHostProps: function(element: Element, props: Object) {
134134
return Object.assign({}, props, {
135135
value: undefined,
136+
defaultValue: undefined,
136137
});
137138
},
138139

src/renderers/dom/shared/HTMLDOMPropertyConfig.js

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ var HTMLDOMPropertyConfig = {
4949
charSet: 0,
5050
challenge: 0,
5151
checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
52+
defaultChecked: HAS_BOOLEAN_VALUE,
5253
cite: 0,
5354
classID: 0,
5455
className: 0,
@@ -159,6 +160,7 @@ var HTMLDOMPropertyConfig = {
159160
type: 0,
160161
useMap: 0,
161162
value: 0,
163+
defaultValue: 0,
162164
width: 0,
163165
wmode: 0,
164166
wrap: 0,
@@ -211,36 +213,11 @@ var HTMLDOMPropertyConfig = {
211213
className: 'class',
212214
htmlFor: 'for',
213215
httpEquiv: 'http-equiv',
216+
defaultValue: 'value',
217+
defaultChecked: 'checked',
214218
},
215219
DOMPropertyNames: {},
216-
DOMMutationMethods: {
217-
value: function(node, value) {
218-
if (value == null) {
219-
return node.removeAttribute('value');
220-
}
221-
222-
// Number inputs get special treatment due to some edge cases in
223-
// Chrome. Let everything else assign the value attribute as normal.
224-
// https://github.com/facebook/react/issues/7253#issuecomment-236074326
225-
if (node.type !== 'number' || node.hasAttribute('value') === false) {
226-
node.setAttribute('value', '' + value);
227-
} else if (
228-
node.validity &&
229-
!node.validity.badInput &&
230-
node.ownerDocument.activeElement !== node
231-
) {
232-
// Don't assign an attribute if validation reports bad
233-
// input. Chrome will clear the value. Additionally, don't
234-
// operate on inputs that have focus, otherwise Chrome might
235-
// strip off trailing decimal places and cause the user's
236-
// cursor position to jump to the beginning of the input.
237-
//
238-
// In ReactDOMInput, we have an onBlur event that will trigger
239-
// this function again when focus is lost.
240-
node.setAttribute('value', '' + value);
241-
}
242-
},
243-
},
220+
DOMMutationMethods: {},
244221
};
245222

246223
module.exports = HTMLDOMPropertyConfig;

src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -297,35 +297,6 @@ describe('DOMPropertyOperations', () => {
297297
});
298298
});
299299

300-
describe('value mutation method', function() {
301-
it('should update an empty attribute to zero', function() {
302-
var stubNode = document.createElement('input');
303-
var stubInstance = {_debugID: 1};
304-
ReactDOMComponentTree.precacheNode(stubInstance, stubNode);
305-
306-
stubNode.setAttribute('type', 'radio');
307-
308-
DOMPropertyOperations.setValueForProperty(stubNode, 'value', '');
309-
spyOn(stubNode, 'setAttribute');
310-
DOMPropertyOperations.setValueForProperty(stubNode, 'value', 0);
311-
312-
expect(stubNode.setAttribute.calls.count()).toBe(1);
313-
});
314-
315-
it('should always assign the value attribute for non-inputs', function() {
316-
var stubNode = document.createElement('progress');
317-
var stubInstance = {_debugID: 1};
318-
ReactDOMComponentTree.precacheNode(stubInstance, stubNode);
319-
320-
spyOn(stubNode, 'setAttribute');
321-
322-
DOMPropertyOperations.setValueForProperty(stubNode, 'value', 30);
323-
DOMPropertyOperations.setValueForProperty(stubNode, 'value', '30');
324-
325-
expect(stubNode.setAttribute.calls.count()).toBe(2);
326-
});
327-
});
328-
329300
describe('deleteValueForProperty', () => {
330301
var stubNode;
331302
var stubInstance;

src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -226,26 +226,6 @@ function getTargetInstForInputOrChangeEvent(topLevelType, targetInst) {
226226
}
227227
}
228228

229-
function handleControlledInputBlur(inst, node) {
230-
// TODO: In IE, inst is occasionally null. Why?
231-
if (inst == null) {
232-
return;
233-
}
234-
235-
// Fiber and ReactDOM keep wrapper state in separate places
236-
let state = inst._wrapperState || node._wrapperState;
237-
238-
if (!state || !state.controlled || node.type !== 'number') {
239-
return;
240-
}
241-
242-
// If controlled, assign the value attribute to the current value on blur
243-
let value = '' + node.value;
244-
if (node.getAttribute('value') !== value) {
245-
node.setAttribute('value', value);
246-
}
247-
}
248-
249229
/**
250230
* This plugin creates an `onChange` event that normalizes change events
251231
* across form elements. This event fires at a time when it's possible to
@@ -300,11 +280,6 @@ var ChangeEventPlugin = {
300280
if (handleEventFunc) {
301281
handleEventFunc(topLevelType, targetNode, targetInst);
302282
}
303-
304-
// When blurring, set the value attribute for number inputs
305-
if (topLevelType === 'topBlur') {
306-
handleControlledInputBlur(targetInst, targetNode);
307-
}
308283
},
309284
};
310285

src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ describe('ReactDOMInput', () => {
207207
expect(node.value).toBe('2');
208208
});
209209

210-
it('changes the number 2 to "2.0" using a change handler', () => {
210+
it('changes the value property from number 2 to "2.0" using a change handler', () => {
211211
class Stub extends React.Component {
212212
state = {
213213
value: 2,
@@ -229,7 +229,7 @@ describe('ReactDOMInput', () => {
229229

230230
ReactTestUtils.Simulate.change(node);
231231

232-
expect(node.getAttribute('value')).toBe('2.0');
232+
expect(node.getAttribute('value')).toBe('2');
233233
expect(node.value).toBe('2.0');
234234
});
235235
});
@@ -1283,67 +1283,14 @@ describe('ReactDOMInput', () => {
12831283
};
12841284
}
12851285

1286-
it('always sets the attribute when values change on text inputs', function() {
1286+
it('does not set the attribute when values change on text inputs', function() {
12871287
var Input = getTestInput();
12881288
var stub = ReactTestUtils.renderIntoDocument(<Input type="text" />);
12891289
var node = ReactDOM.findDOMNode(stub);
12901290

12911291
ReactTestUtils.Simulate.change(node, {target: {value: '2'}});
12921292

1293-
expect(node.getAttribute('value')).toBe('2');
1294-
});
1295-
1296-
it('does not set the value attribute on number inputs if focused', () => {
1297-
var Input = getTestInput();
1298-
var stub = ReactTestUtils.renderIntoDocument(
1299-
<Input type="number" value="1" />,
1300-
);
1301-
var node = ReactDOM.findDOMNode(stub);
1302-
1303-
node.focus();
1304-
1305-
ReactTestUtils.Simulate.change(node, {target: {value: '2'}});
1306-
1307-
expect(node.getAttribute('value')).toBe('1');
1308-
});
1309-
1310-
it('sets the value attribute on number inputs on blur', () => {
1311-
var Input = getTestInput();
1312-
var stub = ReactTestUtils.renderIntoDocument(
1313-
<Input type="number" value="1" />,
1314-
);
1315-
var node = ReactDOM.findDOMNode(stub);
1316-
1317-
ReactTestUtils.Simulate.change(node, {target: {value: '2'}});
1318-
ReactTestUtils.SimulateNative.blur(node);
1319-
1320-
expect(node.getAttribute('value')).toBe('2');
1321-
});
1322-
1323-
it('an uncontrolled number input will not update the value attribute on blur', () => {
1324-
var stub = ReactTestUtils.renderIntoDocument(
1325-
<input type="number" defaultValue="1" />,
1326-
);
1327-
var node = ReactDOM.findDOMNode(stub);
1328-
1329-
node.value = 4;
1330-
1331-
ReactTestUtils.SimulateNative.blur(node);
1332-
1333-
expect(node.getAttribute('value')).toBe('1');
1334-
});
1335-
1336-
it('an uncontrolled text input will not update the value attribute on blur', () => {
1337-
var stub = ReactTestUtils.renderIntoDocument(
1338-
<input type="text" defaultValue="1" />,
1339-
);
1340-
var node = ReactDOM.findDOMNode(stub);
1341-
1342-
node.value = 4;
1343-
1344-
ReactTestUtils.SimulateNative.blur(node);
1345-
1346-
expect(node.getAttribute('value')).toBe('1');
1293+
expect(node.getAttribute('value')).toBe('');
13471294
});
13481295
});
13491296
});

src/renderers/dom/stack/client/wrappers/ReactDOMInput.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,14 @@ var ReactDOMInput = {
7070
},
7171
props,
7272
{
73-
defaultChecked: undefined,
74-
defaultValue: undefined,
75-
value: value != null ? value : inst._wrapperState.initialValue,
76-
checked: checked != null ? checked : inst._wrapperState.initialChecked,
73+
defaultValue: value == null
74+
? props.defaultValue
75+
: inst._wrapperState.initialValue,
76+
defaultChecked: checked == null
77+
? props.defaultChecked
78+
: inst._wrapperState.initialChecked,
79+
value: undefined,
80+
checked: undefined,
7781
},
7882
);
7983

src/renderers/dom/stack/client/wrappers/ReactDOMSelect.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ function updateOptions(inst, multiple, propValue) {
127127
var ReactDOMSelect = {
128128
getHostProps: function(inst, props) {
129129
return Object.assign({}, props, {
130+
defaultValue: undefined,
130131
value: undefined,
131132
});
132133
},

src/renderers/shared/server/ReactPartialRenderer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ class ReactDOMServerRenderer {
534534

535535
props = Object.assign({}, props, {
536536
value: undefined,
537+
defaultValue: undefined,
537538
children: '' + initialValue,
538539
});
539540
} else if (tag === 'select') {
@@ -590,6 +591,7 @@ class ReactDOMServerRenderer {
590591
: props.defaultValue;
591592
props = Object.assign({}, props, {
592593
value: undefined,
594+
defaultValue: undefined,
593595
});
594596
} else if (tag === 'option') {
595597
var selected = null;

0 commit comments

Comments
 (0)