Skip to content

Commit d25f3a7

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 d25f3a7

File tree

12 files changed

+95
-214
lines changed

12 files changed

+95
-214
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

scripts/rollup/results.json

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -25,36 +25,36 @@
2525
"gzip": 7214
2626
},
2727
"react-dom.development.js (UMD_DEV)": {
28-
"size": 613141,
29-
"gzip": 140395
28+
"size": 621770,
29+
"gzip": 142030
3030
},
3131
"react-dom.production.min.js (UMD_PROD)": {
32-
"size": 126584,
33-
"gzip": 39853
32+
"size": 125762,
33+
"gzip": 39791
3434
},
3535
"react-dom.development.js (NODE_DEV)": {
36-
"size": 570841,
37-
"gzip": 130520
36+
"size": 580431,
37+
"gzip": 132387
3838
},
3939
"react-dom.production.min.js (NODE_PROD)": {
40-
"size": 122880,
41-
"gzip": 38546
40+
"size": 122127,
41+
"gzip": 38480
4242
},
4343
"ReactDOMFiber-dev.js (FB_DEV)": {
44-
"size": 570125,
45-
"gzip": 130563
44+
"size": 579715,
45+
"gzip": 132422
4646
},
4747
"ReactDOMFiber-prod.js (FB_PROD)": {
48-
"size": 428502,
49-
"gzip": 96996
48+
"size": 425463,
49+
"gzip": 96331
5050
},
5151
"react-dom-test-utils.development.js (NODE_DEV)": {
52-
"size": 53025,
53-
"gzip": 13685
52+
"size": 53430,
53+
"gzip": 13782
5454
},
5555
"ReactTestUtils-dev.js (FB_DEV)": {
56-
"size": 52904,
57-
"gzip": 13646
56+
"size": 53309,
57+
"gzip": 13741
5858
},
5959
"ReactDOMServerStack-dev.js (FB_DEV)": {
6060
"size": 460810,
@@ -65,20 +65,20 @@
6565
"gzip": 81957
6666
},
6767
"react-dom-server.development.js (UMD_DEV)": {
68-
"size": 308329,
69-
"gzip": 77617
68+
"size": 307012,
69+
"gzip": 76850
7070
},
7171
"react-dom-server.production.min.js (UMD_PROD)": {
72-
"size": 66111,
73-
"gzip": 22613
72+
"size": 65184,
73+
"gzip": 22155
7474
},
7575
"react-dom-server.development.js (NODE_DEV)": {
76-
"size": 266194,
77-
"gzip": 67866
76+
"size": 265858,
77+
"gzip": 67373
7878
},
7979
"react-dom-server.production.min.js (NODE_PROD)": {
80-
"size": 62380,
81-
"gzip": 21260
80+
"size": 61522,
81+
"gzip": 20857
8282
},
8383
"ReactDOMServerStream-dev.js (FB_DEV)": {
8484
"size": 264750,
@@ -89,64 +89,64 @@
8989
"gzip": 51047
9090
},
9191
"react-art.development.js (UMD_DEV)": {
92-
"size": 362062,
93-
"gzip": 80236
92+
"size": 359303,
93+
"gzip": 79940
9494
},
9595
"react-art.production.min.js (UMD_PROD)": {
96-
"size": 99126,
97-
"gzip": 30132
96+
"size": 97521,
97+
"gzip": 29904
9898
},
9999
"react-art.development.js (NODE_DEV)": {
100-
"size": 283458,
101-
"gzip": 60201
100+
"size": 280721,
101+
"gzip": 59867
102102
},
103103
"react-art.production.min.js (NODE_PROD)": {
104-
"size": 60504,
105-
"gzip": 18189
104+
"size": 58905,
105+
"gzip": 17961
106106
},
107107
"ReactARTFiber-dev.js (FB_DEV)": {
108-
"size": 282891,
109-
"gzip": 60125
108+
"size": 280154,
109+
"gzip": 59786
110110
},
111111
"ReactARTFiber-prod.js (FB_PROD)": {
112-
"size": 220185,
113-
"gzip": 45704
112+
"size": 215532,
113+
"gzip": 44949
114114
},
115115
"ReactNativeStack-dev.js (RN_DEV)": {
116116
"size": 197039,
117-
"gzip": 36193
117+
"gzip": 36189
118118
},
119119
"ReactNativeStack-prod.js (RN_PROD)": {
120120
"size": 136606,
121121
"gzip": 25990
122122
},
123123
"ReactNativeFiber-dev.js (RN_DEV)": {
124-
"size": 301278,
125-
"gzip": 51431
124+
"size": 298654,
125+
"gzip": 51338
126126
},
127127
"ReactNativeFiber-prod.js (RN_PROD)": {
128-
"size": 221863,
129-
"gzip": 38015
128+
"size": 218380,
129+
"gzip": 37833
130130
},
131131
"react-test-renderer.development.js (NODE_DEV)": {
132-
"size": 280651,
133-
"gzip": 59110
132+
"size": 277864,
133+
"gzip": 58787
134134
},
135135
"ReactTestRendererFiber-dev.js (FB_DEV)": {
136-
"size": 280075,
137-
"gzip": 59030
136+
"size": 277288,
137+
"gzip": 58707
138138
},
139139
"react-test-renderer-shallow.development.js (NODE_DEV)": {
140-
"size": 8179,
141-
"gzip": 2288
140+
"size": 8437,
141+
"gzip": 2308
142142
},
143143
"ReactShallowRenderer-dev.js (FB_DEV)": {
144-
"size": 8080,
145-
"gzip": 2237
144+
"size": 8338,
145+
"gzip": 2255
146146
},
147147
"react-noop-renderer.development.js (NODE_DEV)": {
148-
"size": 274713,
149-
"gzip": 57491
148+
"size": 272012,
149+
"gzip": 57213
150150
},
151151
"ReactHTMLString-dev.js (FB_DEV)": {
152152
"size": 265654,
@@ -181,20 +181,20 @@
181181
"gzip": 50920
182182
},
183183
"ReactDOMServer-dev.js (FB_DEV)": {
184-
"size": 265645,
185-
"gzip": 67788
184+
"size": 265309,
185+
"gzip": 67297
186186
},
187187
"ReactDOMServer-prod.js (FB_PROD)": {
188-
"size": 197859,
189-
"gzip": 51191
188+
"size": 195492,
189+
"gzip": 50325
190190
},
191191
"react-dom-node-stream.development.js (NODE_DEV)": {
192-
"size": 265427,
193-
"gzip": 67670
192+
"size": 267552,
193+
"gzip": 67871
194194
},
195195
"react-dom-node-stream.production.min.js (NODE_PROD)": {
196-
"size": 62695,
197-
"gzip": 21279
196+
"size": 62459,
197+
"gzip": 21187
198198
},
199199
"ReactDOMNodeStream-dev.js (FB_DEV)": {
200200
"size": 264918,

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;

0 commit comments

Comments
 (0)