@samitouri / QOS-React / commits / 4338c8db4b

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 });