Properly set value and defaultValue for input and textarea (#6406)
* Have `defaultValue` reach DOM node for html input box for #4618 * Cleanup and bug fixes for merge.
Jim committed
May 24, 2016 at 18:28 UTC
4338c8db4b03cb1ba5f2e229b1baa5a848fe65ab
8 files changed
+367
-115
src/renderers/dom/client/wrappers/ReactDOMInput.js
+36
-9
@@ -149,8 +149,8 @@ var ReactDOMInput = {
149
150
var defaultValue = props.defaultValue;
151
inst._wrapperState = {
152
- initialChecked: props.defaultChecked || false,
153
- initialValue: defaultValue != null ? defaultValue : null,
152
+ initialChecked: props.checked != null ? props.checked : props.defaultChecked,
153
+ initialValue: props.value != null ? props.value : defaultValue,
154
listeners: null,
155
onChange: _handleChange.bind(inst),
156
};
@@ -166,13 +166,12 @@ var ReactDOMInput = {
166
if (__DEV__) {
167
warnIfValueIsNull(props);
168
169
- var initialValue = inst._wrapperState.initialChecked || inst._wrapperState.initialValue;
169
var defaultValue = props.defaultChecked || props.defaultValue;
170
var controlled = props.checked !== undefined || props.value !== undefined;
171
var owner = inst._currentElement._owner;
172
173
if (
175
- (initialValue || !inst._wrapperState.controlled) &&
174
+ !inst._wrapperState.controlled &&
175
controlled && !didWarnUncontrolledToControlled
176
) {
177
warning(
@@ -214,17 +213,45 @@ var ReactDOMInput = {
213
);
214
}
215
216
+ var node = ReactDOMComponentTree.getNodeFromInstance(inst);
217
var value = LinkedValueUtils.getValue(props);
218
if (value != null) {
219
+
220
// Cast `value` to a string to ensure the value is set correctly. While
221
// browsers typically do this as necessary, jsdom doesn't.
221
- DOMPropertyOperations.setValueForProperty(
222
- ReactDOMComponentTree.getNodeFromInstance(inst),
223
- 'value',
224
- '' + value
225
- );
222
+ var newValue = '' + value;
223
+
224
+ // To avoid side effects (such as losing text selection), only set value if changed
225
+ if (newValue !== node.value) {
226
+ node.value = newValue;
227
+ }
228
+ } else {
229
+ if (props.value == null && props.defaultValue != null) {
230
+ node.defaultValue = '' + props.defaultValue;
231
+ }
232
+ if (props.checked == null && props.defaultChecked != null) {
233
+ node.defaultChecked = !!props.defaultChecked;
234
+ }
235
}
236
},
237
+
238
+ postMountWrapper: function(inst) {
239
+ // This is in postMount because we need access to the DOM node, which is not
240
+ // available until after the component has mounted.
241
+ var node = ReactDOMComponentTree.getNodeFromInstance(inst);
242
+ node.value = node.value; // Detach value from defaultValue
243
+
244
+ // Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug
245
+ // this is needed to work around a chrome bug where setting defaultChecked
246
+ // will sometimes influence the value of checked (even after detachment).
247
+ // Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=608416
248
+ // We need to temporarily unset name to avoid disrupting radio button groups.
249
+ var name = node.name;
250
+ node.name = undefined;
251
+ node.defaultChecked = !node.defaultChecked;
252
+ node.defaultChecked = !node.defaultChecked;
253
+ node.name = name;
254
+ },
255
};
256
257
function _handleChange(event) {
src/renderers/dom/client/wrappers/ReactDOMTextarea.js
+60
-39
@@ -12,7 +12,6 @@
12
'use strict';
13
14
var DisabledInputUtils = require('DisabledInputUtils');
15
-var DOMPropertyOperations = require('DOMPropertyOperations');
15
var LinkedValueUtils = require('LinkedValueUtils');
16
var ReactDOMComponentTree = require('ReactDOMComponentTree');
17
var ReactUpdates = require('ReactUpdates');
@@ -67,11 +66,14 @@ var ReactDOMTextarea = {
66
);
67
68
// Always set children to the same thing. In IE9, the selection range will
70
- // get reset if `textContent` is mutated.
69
+ // get reset if `textContent` is mutated. We could add a check in setTextContent
70
+ // to only set the value if/when the value differs from the node value (which would
71
+ // completely solve this IE9 bug), but Sebastian+Ben seemed to like this solution.
72
+ // The value can be a boolean or object so that's why it's forced to be a string.
73
var hostProps = Object.assign({}, DisabledInputUtils.getHostProps(inst, props), {
72
- defaultValue: undefined,
74
value: undefined,
74
- children: inst._wrapperState.initialValue,
75
+ defaultValue: undefined,
76
+ children: '' + inst._wrapperState.initialValue,
77
onChange: inst._wrapperState.onChange,
78
});
79
@@ -110,41 +112,45 @@ var ReactDOMTextarea = {
112
warnIfValueIsNull(props);
113
}
114
113
- var defaultValue = props.defaultValue;
114
- // TODO (yungsters): Remove support for children content in <textarea>.
115
- var children = props.children;
116
- if (children != null) {
117
- if (__DEV__) {
118
- warning(
119
- false,
120
- 'Use the `defaultValue` or `value` props instead of setting ' +
121
- 'children on <textarea>.'
122
- );
123
- }
124
- invariant(
125
- defaultValue == null,
126
- 'If you supply `defaultValue` on a <textarea>, do not pass children.'
127
- );
128
- if (Array.isArray(children)) {
115
+
116
+ var value = LinkedValueUtils.getValue(props);
117
+ var initialValue = value;
118
+
119
+ // Only bother fetching default value if we're going to use it
120
+ if (value == null) {
121
+ var defaultValue = props.defaultValue;
122
+ // TODO (yungsters): Remove support for children content in <textarea>.
123
+ var children = props.children;
124
+ if (children != null) {
125
+ if (__DEV__) {
126
+ warning(
127
+ false,
128
+ 'Use the `defaultValue` or `value` props instead of setting ' +
129
+ 'children on <textarea>.'
130
+ );
131
+ }
132
invariant(
130
- children.length <= 1,
131
- '<textarea> can only have at most one child.'
133
+ defaultValue == null,
134
+ 'If you supply `defaultValue` on a <textarea>, do not pass children.'
135
);
133
- children = children[0];
136
+ if (Array.isArray(children)) {
137
+ invariant(
138
+ children.length <= 1,
139
+ '<textarea> can only have at most one child.'
140
+ );
141
+ children = children[0];
142
+ }
143
+
144
+ defaultValue = '' + children;
145
}
135
-
136
- defaultValue = '' + children;
137
- }
138
- if (defaultValue == null) {
139
- defaultValue = '';
146
+ if (defaultValue == null) {
147
+ defaultValue = '';
148
+ }
149
+ initialValue = defaultValue;
150
}
141
- var value = LinkedValueUtils.getValue(props);
151
+
152
inst._wrapperState = {
143
- // We save the initial value so that `ReactDOMComponent` doesn't update
144
- // `textContent` (unnecessary since we update value).
145
- // The initial value can be a boolean or object so that's why it's
146
- // forced to be a string.
147
- initialValue: '' + (value != null ? value : defaultValue),
153
+ initialValue: '' + initialValue,
154
listeners: null,
155
onChange: _handleChange.bind(inst),
156
};
@@ -157,17 +163,32 @@ var ReactDOMTextarea = {
163
warnIfValueIsNull(props);
164
}
165
166
+ var node = ReactDOMComponentTree.getNodeFromInstance(inst);
167
var value = LinkedValueUtils.getValue(props);
168
if (value != null) {
169
// Cast `value` to a string to ensure the value is set correctly. While
170
// browsers typically do this as necessary, jsdom doesn't.
164
- DOMPropertyOperations.setValueForProperty(
165
- ReactDOMComponentTree.getNodeFromInstance(inst),
166
- 'value',
167
- '' + value
168
- );
171
+ var newValue = '' + value;
172
+
173
+ // To avoid side effects (such as losing text selection), only set value if changed
174
+ if (newValue !== node.value) {
175
+ node.value = newValue;
176
+ }
177
+ if (props.defaultValue == null) {
178
+ node.defaultValue = newValue;
179
+ }
180
+ }
181
+ if (props.defaultValue != null) {
182
+ node.defaultValue = props.defaultValue;
183
}
184
},
185
+
186
+ postMountWrapper: function(inst) {
187
+ // This is in postMount because we need access to the DOM node, which is not
188
+ // available until after the component has mounted.
189
+ var node = ReactDOMComponentTree.getNodeFromInstance(inst);
190
+ node.value = node.value; // Detach value from defaultValue
191
+ },
192
};
193
194
function _handleChange(event) {
src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js
+83
-6
@@ -18,6 +18,7 @@ describe('ReactDOMInput', function() {
18
var EventConstants;
19
var React;
20
var ReactDOM;
21
+ var ReactDOMServer;
22
var ReactDOMFeatureFlags;
23
var ReactLink;
24
var ReactTestUtils;
@@ -35,6 +36,7 @@ describe('ReactDOMInput', function() {
36
EventConstants = require('EventConstants');
37
React = require('React');
38
ReactDOM = require('ReactDOM');
39
+ ReactDOMServer = require('ReactDOMServer');
40
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
41
ReactLink = require('ReactLink');
42
ReactTestUtils = require('ReactTestUtils');
@@ -47,6 +49,7 @@ describe('ReactDOMInput', function() {
49
stub = ReactTestUtils.renderIntoDocument(stub);
50
var node = ReactDOM.findDOMNode(stub);
51
52
+ expect(node.getAttribute('value')).toBe('0');
53
expect(node.value).toBe('0');
54
});
55
@@ -66,6 +69,48 @@ describe('ReactDOMInput', function() {
69
expect(node.value).toBe('false');
70
});
71
72
+ it('should update `defaultValue` for uncontrolled input', function() {
73
+ var container = document.createElement('div');
74
+
75
+ var node = ReactDOM.render(<input type="text" defaultValue="0" />, container);
76
+
77
+ expect(node.value).toBe('0');
78
+
79
+ ReactDOM.render(<input type="text" defaultValue="1" />, container);
80
+
81
+ expect(node.value).toBe('0');
82
+ expect(node.defaultValue).toBe('1');
83
+ });
84
+
85
+ it('should take `defaultValue` when changing to uncontrolled input', function() {
86
+ var container = document.createElement('div');
87
+
88
+ var node = ReactDOM.render(<input type="text" value="0" readOnly="true" />, container);
89
+
90
+ expect(node.value).toBe('0');
91
+
92
+ ReactDOM.render(<input type="text" defaultValue="1" />, container);
93
+
94
+ expect(node.value).toBe('0');
95
+ });
96
+
97
+ it('should render defaultValue for SSR', function() {
98
+ var markup = ReactDOMServer.renderToString(<input type="text" defaultValue="1" />);
99
+ var div = document.createElement('div');
100
+ div.innerHTML = markup;
101
+ expect(div.firstChild.getAttribute('value')).toBe('1');
102
+ expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
103
+ });
104
+
105
+ it('should render value for SSR', function() {
106
+ var element = <input type="text" value="1" onChange={function() {}} />;
107
+ var markup = ReactDOMServer.renderToString(element);
108
+ var div = document.createElement('div');
109
+ div.innerHTML = markup;
110
+ expect(div.firstChild.getAttribute('value')).toBe('1');
111
+ expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
112
+ });
113
+
114
it('should display "foobar" for `defaultValue` of `objToString`', function() {
115
var objToString = {
116
toString: function() {
@@ -91,8 +136,7 @@ describe('ReactDOMInput', function() {
136
it('should allow setting `value` to `true`', function() {
137
var container = document.createElement('div');
138
var stub = <input type="text" value="yolo" onChange={emptyFunction} />;
94
- stub = ReactDOM.render(stub, container);
95
- var node = ReactDOM.findDOMNode(stub);
139
+ var node = ReactDOM.render(stub, container);
140
141
expect(node.value).toBe('yolo');
142
@@ -106,8 +150,7 @@ describe('ReactDOMInput', function() {
150
it('should allow setting `value` to `false`', function() {
151
var container = document.createElement('div');
152
var stub = <input type="text" value="yolo" onChange={emptyFunction} />;
109
- stub = ReactDOM.render(stub, container);
110
- var node = ReactDOM.findDOMNode(stub);
153
+ var node = ReactDOM.render(stub, container);
154
155
expect(node.value).toBe('yolo');
156
@@ -121,8 +164,7 @@ describe('ReactDOMInput', function() {
164
it('should allow setting `value` to `objToString`', function() {
165
var container = document.createElement('div');
166
var stub = <input type="text" value="foo" onChange={emptyFunction} />;
124
- stub = ReactDOM.render(stub, container);
125
- var node = ReactDOM.findDOMNode(stub);
167
+ var node = ReactDOM.render(stub, container);
168
169
expect(node.value).toBe('foo');
170
@@ -138,6 +180,29 @@ describe('ReactDOMInput', function() {
180
expect(node.value).toEqual('foobar');
181
});
182
183
+ it('should not incur unnecessary DOM mutations', function() {
184
+ var container = document.createElement('div');
185
+ ReactDOM.render(<input value="a" />, container);
186
+
187
+ var node = container.firstChild;
188
+ var nodeValue = 'a';
189
+ var nodeValueSetter = jest.genMockFn();
190
+ Object.defineProperty(node, 'value', {
191
+ get: function() {
192
+ return nodeValue;
193
+ },
194
+ set: nodeValueSetter.mockImplementation(function(newValue) {
195
+ nodeValue = newValue;
196
+ }),
197
+ });
198
+
199
+ ReactDOM.render(<input value="a" />, container);
200
+ expect(nodeValueSetter.mock.calls.length).toBe(0);
201
+
202
+ ReactDOM.render(<input value="b"/>, container);
203
+ expect(nodeValueSetter.mock.calls.length).toBe(1);
204
+ });
205
+
206
it('should properly control a value of number `0`', function() {
207
var stub = <input type="text" value={0} onChange={emptyFunction} />;
208
stub = ReactTestUtils.renderIntoDocument(stub);
@@ -399,6 +464,13 @@ describe('ReactDOMInput', function() {
464
465
});
466
467
+ it('should update defaultValue to empty string', function() {
468
+ var container = document.createElement('div');
469
+ ReactDOM.render(<input type="text" defaultValue={'foo'} />, container);
470
+ ReactDOM.render(<input type="text" defaultValue={''} />, container);
471
+ expect(container.firstChild.defaultValue).toBe('');
472
+ });
473
+
474
it('should throw if both checkedLink and valueLink are provided', function() {
475
var node = document.createElement('div');
476
var link = new ReactLink(true, jest.fn());
@@ -618,6 +690,11 @@ describe('ReactDOMInput', function() {
690
'set data-reactroot',
691
'set type',
692
'set value',
693
+ 'set value',
694
+ 'set name',
695
+ 'set checked',
696
+ 'set checked',
697
+ 'set name',
698
]);
699
});
700
src/renderers/dom/client/wrappers/__tests__/ReactDOMTextarea-test.js
+141
-36
@@ -16,6 +16,7 @@ var emptyFunction = require('emptyFunction');
16
describe('ReactDOMTextarea', function() {
17
var React;
18
var ReactDOM;
19
+ var ReactDOMServer;
20
var ReactLink;
21
var ReactTestUtils;
22
@@ -24,6 +25,7 @@ describe('ReactDOMTextarea', function() {
25
beforeEach(function() {
26
React = require('React');
27
ReactDOM = require('ReactDOM');
28
+ ReactDOMServer = require('ReactDOMServer');
29
ReactLink = require('ReactLink');
30
ReactTestUtils = require('ReactTestUtils');
31
@@ -31,39 +33,41 @@ describe('ReactDOMTextarea', function() {
33
if (!container) {
34
container = document.createElement('div');
35
}
34
- var stub = ReactDOM.render(component, container);
35
- var node = ReactDOM.findDOMNode(stub);
36
+ var node = ReactDOM.render(component, container);
37
+
38
// Fixing jsdom's quirky behavior -- in reality, the parser should strip
39
// off the leading newline but we need to do it by hand here.
38
- node.value = node.innerHTML.replace(/^\n/, '');
39
- return stub;
40
+ node.defaultValue = node.innerHTML.replace(/^\n/, '');
41
+ return node;
42
};
43
});
44
45
it('should allow setting `defaultValue`', function() {
46
var container = document.createElement('div');
45
- var stub = renderTextarea(<textarea defaultValue="giraffe" />, container);
46
- var node = ReactDOM.findDOMNode(stub);
47
+ var node = renderTextarea(<textarea defaultValue="giraffe" />, container);
48
49
expect(node.value).toBe('giraffe');
50
51
// Changing `defaultValue` should do nothing.
51
- stub = renderTextarea(<textarea defaultValue="gorilla" />, container);
52
+ renderTextarea(<textarea defaultValue="gorilla" />, container);
53
expect(node.value).toEqual('giraffe');
54
+
55
+ node.value = 'cat';
56
+
57
+ renderTextarea(<textarea defaultValue="monkey" />, container);
58
+ expect(node.value).toEqual('cat');
59
});
60
61
it('should display `defaultValue` of number 0', function() {
62
var stub = <textarea defaultValue={0} />;
57
- stub = renderTextarea(stub);
58
- var node = ReactDOM.findDOMNode(stub);
63
+ var node = renderTextarea(stub);
64
65
expect(node.value).toBe('0');
66
});
67
68
it('should display "false" for `defaultValue` of `false`', function() {
69
var stub = <textarea defaultValue={false} />;
65
- stub = renderTextarea(stub);
66
- var node = ReactDOM.findDOMNode(stub);
70
+ var node = renderTextarea(stub);
71
72
expect(node.value).toBe('false');
73
});
@@ -76,33 +80,44 @@ describe('ReactDOMTextarea', function() {
80
};
81
82
var stub = <textarea defaultValue={objToString} />;
79
- stub = renderTextarea(stub);
80
- var node = ReactDOM.findDOMNode(stub);
83
+ var node = renderTextarea(stub);
84
85
expect(node.value).toBe('foobar');
86
});
87
88
+ it('should set defaultValue', function() {
89
+ var container = document.createElement('div');
90
+ ReactDOM.render(<textarea defaultValue="foo" />, container);
91
+ ReactDOM.render(<textarea defaultValue="bar" />, container);
92
+ ReactDOM.render(<textarea defaultValue="noise" />, container);
93
+ expect(container.firstChild.defaultValue).toBe('noise');
94
+ });
95
+
96
it('should not render value as an attribute', function() {
97
var stub = <textarea value="giraffe" onChange={emptyFunction} />;
87
- stub = renderTextarea(stub);
88
- var node = ReactDOM.findDOMNode(stub);
98
+ var node = renderTextarea(stub);
99
100
expect(node.getAttribute('value')).toBe(null);
101
});
102
103
it('should display `value` of number 0', function() {
104
var stub = <textarea value={0} />;
95
- stub = renderTextarea(stub);
96
- var node = ReactDOM.findDOMNode(stub);
105
+ var node = renderTextarea(stub);
106
107
expect(node.value).toBe('0');
108
});
109
110
+ it('should update defaultValue to empty string', function() {
111
+ var container = document.createElement('div');
112
+ ReactDOM.render(<textarea defaultValue={'foo'} />, container);
113
+ ReactDOM.render(<textarea defaultValue={''} />, container);
114
+ expect(container.firstChild.defaultValue).toBe('');
115
+ });
116
+
117
it('should allow setting `value` to `giraffe`', function() {
118
var container = document.createElement('div');
119
var stub = <textarea value="giraffe" onChange={emptyFunction} />;
104
- stub = renderTextarea(stub, container);
105
- var node = ReactDOM.findDOMNode(stub);
120
+ var node = renderTextarea(stub, container);
121
122
expect(node.value).toBe('giraffe');
123
@@ -113,11 +128,27 @@ describe('ReactDOMTextarea', function() {
128
expect(node.value).toEqual('gorilla');
129
});
130
131
+ it('should render defaultValue for SSR', function() {
132
+ var markup = ReactDOMServer.renderToString(<textarea defaultValue="1" />);
133
+ var div = document.createElement('div');
134
+ div.innerHTML = markup;
135
+ expect(div.firstChild.innerHTML).toBe('1');
136
+ expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
137
+ });
138
+
139
+ it('should render value for SSR', function() {
140
+ var element = <textarea value="1" onChange={function() {}} />;
141
+ var markup = ReactDOMServer.renderToString(element);
142
+ var div = document.createElement('div');
143
+ div.innerHTML = markup;
144
+ expect(div.firstChild.innerHTML).toBe('1');
145
+ expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
146
+ });
147
+
148
it('should allow setting `value` to `true`', function() {
149
var container = document.createElement('div');
150
var stub = <textarea value="giraffe" onChange={emptyFunction} />;
119
- stub = renderTextarea(stub, container);
120
- var node = ReactDOM.findDOMNode(stub);
151
+ var node = renderTextarea(stub, container);
152
153
expect(node.value).toBe('giraffe');
154
@@ -131,8 +162,7 @@ describe('ReactDOMTextarea', function() {
162
it('should allow setting `value` to `false`', function() {
163
var container = document.createElement('div');
164
var stub = <textarea value="giraffe" onChange={emptyFunction} />;
134
- stub = renderTextarea(stub, container);
135
- var node = ReactDOM.findDOMNode(stub);
165
+ var node = renderTextarea(stub, container);
166
167
expect(node.value).toBe('giraffe');
168
@@ -146,8 +176,7 @@ describe('ReactDOMTextarea', function() {
176
it('should allow setting `value` to `objToString`', function() {
177
var container = document.createElement('div');
178
var stub = <textarea value="giraffe" onChange={emptyFunction} />;
149
- stub = renderTextarea(stub, container);
150
- var node = ReactDOM.findDOMNode(stub);
179
+ var node = renderTextarea(stub, container);
180
181
expect(node.value).toBe('giraffe');
182
@@ -163,10 +192,58 @@ describe('ReactDOMTextarea', function() {
192
expect(node.value).toEqual('foo');
193
});
194
195
+ it('should take updates to `defaultValue` for uncontrolled textarea', function() {
196
+ var container = document.createElement('div');
197
+
198
+ var node = ReactDOM.render(<textarea defaultValue="0" />, container);
199
+
200
+ expect(node.value).toBe('0');
201
+
202
+ ReactDOM.render(<textarea defaultValue="1" />, container);
203
+
204
+ expect(node.value).toBe('0');
205
+ });
206
+
207
+ it('should take updates to children in lieu of `defaultValue` for uncontrolled textarea', function() {
208
+ var container = document.createElement('div');
209
+
210
+ var node = ReactDOM.render(<textarea defaultValue="0" />, container);
211
+
212
+ expect(node.value).toBe('0');
213
+
214
+ spyOn(console, 'error'); // deprecation warning for `children` content
215
+
216
+ ReactDOM.render(<textarea>1</textarea>, container);
217
+
218
+ expect(node.value).toBe('0');
219
+ });
220
+
221
+ it('should not incur unnecessary DOM mutations', function() {
222
+ var container = document.createElement('div');
223
+ ReactDOM.render(<textarea value="a" onChange={emptyFunction} />, container);
224
+
225
+ var node = container.firstChild;
226
+ var nodeValue = 'a';
227
+ var nodeValueSetter = jest.genMockFn();
228
+ Object.defineProperty(node, 'value', {
229
+ get: function() {
230
+ return nodeValue;
231
+ },
232
+ set: nodeValueSetter.mockImplementation(function(newValue) {
233
+ nodeValue = newValue;
234
+ }),
235
+ });
236
+
237
+ ReactDOM.render(<textarea value="a" onChange={emptyFunction} />, container);
238
+ expect(nodeValueSetter.mock.calls.length).toBe(0);
239
+
240
+ ReactDOM.render(<textarea value="b" onChange={emptyFunction} />, container);
241
+ expect(nodeValueSetter.mock.calls.length).toBe(1);
242
+ });
243
+
244
it('should properly control a value of number `0`', function() {
245
var stub = <textarea value={0} onChange={emptyFunction} />;
168
- stub = renderTextarea(stub);
169
- var node = ReactDOM.findDOMNode(stub);
246
+ var node = renderTextarea(stub);
247
248
node.value = 'giraffe';
249
ReactTestUtils.Simulate.change(node);
@@ -178,8 +255,7 @@ describe('ReactDOMTextarea', function() {
255
256
var container = document.createElement('div');
257
var stub = <textarea>giraffe</textarea>;
181
- stub = renderTextarea(stub, container);
182
- var node = ReactDOM.findDOMNode(stub);
258
+ var node = renderTextarea(stub, container);
259
260
expect(console.error.argsForCall.length).toBe(1);
261
expect(node.value).toBe('giraffe');
@@ -189,16 +265,44 @@ describe('ReactDOMTextarea', function() {
265
expect(node.value).toEqual('giraffe');
266
});
267
268
+ it('should keep value when switching to uncontrolled element if not changed', function() {
269
+ var container = document.createElement('div');
270
+
271
+ var node = renderTextarea(<textarea value="kitten" onChange={emptyFunction} />, container);
272
+
273
+ expect(node.value).toBe('kitten');
274
+
275
+ ReactDOM.render(<textarea defaultValue="gorilla"></textarea>, container);
276
+
277
+ expect(node.value).toEqual('kitten');
278
+ });
279
+
280
+ it('should keep value when switching to uncontrolled element if changed', function() {
281
+ var container = document.createElement('div');
282
+
283
+ var node = renderTextarea(<textarea value="kitten" onChange={emptyFunction} />, container);
284
+
285
+ expect(node.value).toBe('kitten');
286
+
287
+ ReactDOM.render(<textarea value="puppies" onChange={emptyFunction}></textarea>, container);
288
+
289
+ expect(node.value).toBe('puppies');
290
+
291
+ ReactDOM.render(<textarea defaultValue="gorilla"></textarea>, container);
292
+
293
+ expect(node.value).toEqual('puppies');
294
+ });
295
+
296
it('should allow numbers as children', function() {
297
spyOn(console, 'error');
194
- var node = ReactDOM.findDOMNode(renderTextarea(<textarea>{17}</textarea>));
298
+ var node = renderTextarea(<textarea>{17}</textarea>);
299
expect(console.error.argsForCall.length).toBe(1);
300
expect(node.value).toBe('17');
301
});
302
303
it('should allow booleans as children', function() {
304
spyOn(console, 'error');
201
- var node = ReactDOM.findDOMNode(renderTextarea(<textarea>{false}</textarea>));
305
+ var node = renderTextarea(<textarea>{false}</textarea>);
306
expect(console.error.argsForCall.length).toBe(1);
307
expect(node.value).toBe('false');
308
});
@@ -210,7 +314,7 @@ describe('ReactDOMTextarea', function() {
314
return 'sharkswithlasers';
315
},
316
};
213
- var node = ReactDOM.findDOMNode(renderTextarea(<textarea>{obj}</textarea>));
317
+ var node = renderTextarea(<textarea>{obj}</textarea>);
318
expect(console.error.argsForCall.length).toBe(1);
319
expect(node.value).toBe('sharkswithlasers');
320
});
@@ -228,7 +332,7 @@ describe('ReactDOMTextarea', function() {
332
333
var node;
334
expect(function() {
231
- node = ReactDOM.findDOMNode(renderTextarea(<textarea><strong /></textarea>));
335
+ node = renderTextarea(<textarea><strong /></textarea>);
336
}).not.toThrow();
337
338
expect(node.value).toBe('[object Object]');
@@ -248,12 +352,12 @@ describe('ReactDOMTextarea', function() {
352
);
353
354
251
- expect(ReactDOM.findDOMNode(instance).value).toBe('yolo');
355
+ expect(instance.value).toBe('yolo');
356
expect(link.value).toBe('yolo');
357
expect(link.requestChange.mock.calls.length).toBe(0);
358
255
- ReactDOM.findDOMNode(instance).value = 'test';
256
- ReactTestUtils.Simulate.change(ReactDOM.findDOMNode(instance));
359
+ instance.value = 'test';
360
+ ReactTestUtils.Simulate.change(instance);
361
362
expect(link.requestChange.mock.calls.length).toBe(1);
363
expect(link.requestChange.mock.calls[0][0]).toEqual('test');
@@ -297,4 +401,5 @@ describe('ReactDOMTextarea', function() {
401
);
402
expect(console.error.argsForCall.length).toBe(1);
403
});
404
+
405
});
src/renderers/dom/shared/HTMLDOMPropertyConfig.js
+1
-2
@@ -15,7 +15,6 @@ var DOMProperty = require('DOMProperty');
15
16
var MUST_USE_PROPERTY = DOMProperty.injection.MUST_USE_PROPERTY;
17
var HAS_BOOLEAN_VALUE = DOMProperty.injection.HAS_BOOLEAN_VALUE;
18
-var HAS_SIDE_EFFECTS = DOMProperty.injection.HAS_SIDE_EFFECTS;
18
var HAS_NUMERIC_VALUE = DOMProperty.injection.HAS_NUMERIC_VALUE;
19
var HAS_POSITIVE_NUMERIC_VALUE =
20
DOMProperty.injection.HAS_POSITIVE_NUMERIC_VALUE;
@@ -153,7 +152,7 @@ var HTMLDOMPropertyConfig = {
152
// Setting .type throws on non-<input> tags
153
type: 0,
154
useMap: 0,
156
- value: MUST_USE_PROPERTY | HAS_SIDE_EFFECTS,
155
+ value: 0,
156
width: 0,
157
wmode: 0,
158
wrap: 0,
src/renderers/dom/shared/ReactDOMComponent.js
+21
@@ -241,6 +241,16 @@ function putListener() {
241
);
242
}
243
244
+function inputPostMount() {
245
+ var inst = this;
246
+ ReactDOMInput.postMountWrapper(inst);
247
+}
248
+
249
+function textareaPostMount() {
250
+ var inst = this;
251
+ ReactDOMTextarea.postMountWrapper(inst);
252
+}
253
+
254
function optionPostMount() {
255
var inst = this;
256
ReactDOMOption.postMountWrapper(inst);
@@ -642,7 +652,17 @@ ReactDOMComponent.Mixin = {
652
653
switch (this._tag) {
654
case 'input':
655
+ transaction.getReactMountReady().enqueue(
656
+ inputPostMount,
657
+ this
658
+ );
659
+ break;
660
case 'textarea':
661
+ transaction.getReactMountReady().enqueue(
662
+ textareaPostMount,
663
+ this
664
+ );
665
+ break;
666
case 'select':
667
case 'button':
668
if (props.autoFocus) {
@@ -657,6 +677,7 @@ ReactDOMComponent.Mixin = {
677
optionPostMount,
678
this
679
);
680
+ break;
681
}
682
683
return mountImage;
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
+24
-22
@@ -498,30 +498,32 @@ describe('ReactDOMComponent', function() {
498
ReactDOM.render(<div value="" />, container);
499
500
var node = container.firstChild;
501
- var nodeValue = ''; // node.value always returns undefined
502
- var nodeValueSetter = jest.fn();
503
- Object.defineProperty(node, 'value', {
504
- get: function() {
505
- return nodeValue;
506
- },
507
- set: nodeValueSetter.mockImplementation(function(newValue) {
508
- nodeValue = newValue;
509
- }),
510
- });
501
512
- function renderWithValueAndExpect(value, expected) {
513
- ReactDOM.render(<div value={value} />, container);
514
- expect(nodeValueSetter.mock.calls.length).toBe(expected);
515
- }
502
+ var nodeValueSetter = jest.genMockFn();
503
+
504
+ var oldSetAttribute = node.setAttribute.bind(node);
505
+ node.setAttribute = function(key, value) {
506
+ oldSetAttribute(key, value);
507
+ nodeValueSetter(key, value);
508
+ };
509
+
510
+ ReactDOM.render(<div value="foo" />, container);
511
+ expect(nodeValueSetter.mock.calls.length).toBe(1);
512
517
- renderWithValueAndExpect(undefined, 0);
518
- renderWithValueAndExpect('', 0);
519
- renderWithValueAndExpect('foo', 1);
520
- renderWithValueAndExpect('foo', 1);
521
- renderWithValueAndExpect(undefined, 2);
522
- renderWithValueAndExpect(null, 2);
523
- renderWithValueAndExpect('', 2);
524
- renderWithValueAndExpect(undefined, 2);
513
+ ReactDOM.render(<div value="foo" />, container);
514
+ expect(nodeValueSetter.mock.calls.length).toBe(1);
515
+
516
+ ReactDOM.render(<div />, container);
517
+ expect(nodeValueSetter.mock.calls.length).toBe(1);
518
+
519
+ ReactDOM.render(<div value={null} />, container);
520
+ expect(nodeValueSetter.mock.calls.length).toBe(1);
521
+
522
+ ReactDOM.render(<div value="" />, container);
523
+ expect(nodeValueSetter.mock.calls.length).toBe(2);
524
+
525
+ ReactDOM.render(<div />, container);
526
+ expect(nodeValueSetter.mock.calls.length).toBe(2);
527
});
528
529
it('should not incur unnecessary DOM mutations for boolean properties', function() {
src/renderers/shared/stack/reconciler/__tests__/ReactStatelessComponent-test.js
+1
-1
@@ -108,7 +108,7 @@ describe('ReactStatelessComponent', function() {
108
}).toThrow();
109
expect(console.error.calls.length).toBe(1);
110
expect(console.error.argsForCall[0][0]).toContain(
111
- 'NotAComponent(...): A valid React element (or null) must be returned. '+
111
+ 'NotAComponent(...): A valid React element (or null) must be returned. ' +
112
'You may have returned undefined, an array or some other invalid object.'
113
);
114
});