@samitouri / QOS-React / commits / 045f1a791c

Only fire input value change events when the value changes (#5746)

* Only fire input value change events when the value changes fixes #554, fixes #1471, fixes #2185 (still trying to figure out why) * catch programmatic value changes * move value tracking to seperate module

Jason Quense committed Apr 15, 2016 at 17:37 UTC 045f1a791c6e17253e9d927ffca70ae5d00b4fe5
7 files changed +606 -90
src/renderers/dom/client/__tests__/inputValueTracking-test.js new
+165
@@ -0,0 +1,165 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @emails react-core
10 + */
11 +'use strict';
12 +
13 +var React = require('React');
14 +var ReactTestUtils = require('ReactTestUtils');
15 +var inputValueTracking = require('inputValueTracking');
16 +
17 +describe('inputValueTracking', function() {
18 + var input, checkbox, mockComponent;
19 +
20 + beforeEach(function() {
21 + input = document.createElement('input');
22 + input.type = 'text';
23 + checkbox = document.createElement('input');
24 + checkbox.type = 'checkbox';
25 + mockComponent = { _nativeNode: input, _wrapperState: {} };
26 + });
27 +
28 + it('should attach tracker to wrapper state', function() {
29 + inputValueTracking.track(mockComponent);
30 +
31 + expect(
32 + mockComponent._wrapperState.hasOwnProperty('valueTracker')
33 + ).toBe(true);
34 + });
35 +
36 + it('should define `value` on the instance node', function() {
37 + inputValueTracking.track(mockComponent);
38 +
39 + expect(
40 + input.hasOwnProperty('value')
41 + ).toBe(true);
42 + });
43 +
44 + it('should define `checked` on the instance node', function() {
45 + mockComponent._nativeNode = checkbox;
46 + inputValueTracking.track(mockComponent);
47 +
48 + expect(checkbox.hasOwnProperty('checked')).toBe(true);
49 + });
50 +
51 + it('should initialize with the current value', function() {
52 + input.value ='foo';
53 +
54 + inputValueTracking.track(mockComponent);
55 +
56 + var tracker = mockComponent._wrapperState.valueTracker;
57 +
58 + expect(tracker.getValue()).toEqual('foo');
59 + });
60 +
61 + it('should initialize with the current `checked`', function() {
62 + mockComponent._nativeNode = checkbox;
63 + checkbox.checked = true;
64 + inputValueTracking.track(mockComponent);
65 +
66 + var tracker = mockComponent._wrapperState.valueTracker;
67 +
68 + expect(tracker.getValue()).toEqual('true');
69 + });
70 +
71 + it('should track value changes', function() {
72 + input.value ='foo';
73 +
74 + inputValueTracking.track(mockComponent);
75 +
76 + var tracker = mockComponent._wrapperState.valueTracker;
77 +
78 + input.value ='bar';
79 + expect(tracker.getValue()).toEqual('bar');
80 + });
81 +
82 + it('should tracked`checked` changes', function() {
83 + mockComponent._nativeNode = checkbox;
84 + checkbox.checked = true;
85 + inputValueTracking.track(mockComponent);
86 +
87 + var tracker = mockComponent._wrapperState.valueTracker;
88 +
89 + checkbox.checked = false;
90 + expect(tracker.getValue()).toEqual('false');
91 + });
92 +
93 + it('should update value manually', function() {
94 + input.value ='foo';
95 + inputValueTracking.track(mockComponent);
96 +
97 + var tracker = mockComponent._wrapperState.valueTracker;
98 +
99 + tracker.setValue('bar');
100 + expect(tracker.getValue()).toEqual('bar');
101 + });
102 +
103 + it('should coerce value to a string', function() {
104 + input.value ='foo';
105 + inputValueTracking.track(mockComponent);
106 +
107 + var tracker = mockComponent._wrapperState.valueTracker;
108 +
109 + tracker.setValue(500);
110 + expect(tracker.getValue()).toEqual('500');
111 + });
112 +
113 + it('should update value if it changed and return result', function() {
114 + inputValueTracking.track(mockComponent);
115 + input.value ='foo';
116 +
117 + var tracker = mockComponent._wrapperState.valueTracker;
118 +
119 + expect(
120 + inputValueTracking.updateValueIfChanged(mockComponent)
121 + ).toBe(false);
122 +
123 + tracker.setValue('bar');
124 +
125 + expect(
126 + inputValueTracking.updateValueIfChanged(mockComponent)
127 + ).toBe(true);
128 +
129 + expect(tracker.getValue()).toEqual('foo');
130 + });
131 +
132 + it('should track value and return true when updating untracked instance', function() {
133 + input.value ='foo';
134 +
135 + expect(
136 + inputValueTracking.updateValueIfChanged(mockComponent)
137 + )
138 + .toBe(true);
139 +
140 + var tracker = mockComponent._wrapperState.valueTracker;
141 + expect(tracker.getValue()).toEqual('foo');
142 + });
143 +
144 + it('should return tracker from node', function() {
145 + var node = ReactTestUtils.renderIntoDocument(<input type="text" defaultValue="foo" />);
146 + var tracker = inputValueTracking._getTrackerFromNode(node);
147 + expect(tracker.getValue()).toEqual('foo');
148 + });
149 +
150 + it('should stop tracking', function() {
151 + inputValueTracking.track(mockComponent);
152 +
153 + expect(
154 + mockComponent._wrapperState.hasOwnProperty('valueTracker')
155 + ).toBe(true);
156 +
157 + inputValueTracking.stopTracking(mockComponent);
158 +
159 + expect(
160 + mockComponent._wrapperState.hasOwnProperty('valueTracker')
161 + ).toBe(false);
162 +
163 + expect(input.hasOwnProperty('value')).toBe(false);
164 + });
165 +});
src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js
+56 -87
@@ -19,6 +19,7 @@ var ReactDOMComponentTree = require('ReactDOMComponentTree');
19 var ReactUpdates = require('ReactUpdates');
20 var SyntheticEvent = require('SyntheticEvent');
21
22 +var inputValueTracking = require('inputValueTracking');
23 var getEventTarget = require('getEventTarget');
24 var isEventSupported = require('isEventSupported');
25 var isTextInputElement = require('isTextInputElement');
@@ -26,6 +27,7 @@ var keyOf = require('keyOf');
27
28 var topLevelTypes = EventConstants.topLevelTypes;
29
30 +
31 var eventTypes = {
32 change: {
33 phasedRegistrationNames: {
@@ -45,13 +47,24 @@ var eventTypes = {
47 },
48 };
49
50 +function createAndAccumulateChangeEvent(inst, nativeEvent, target) {
51 + var event = SyntheticEvent.getPooled(
52 + eventTypes.change,
53 + inst,
54 + nativeEvent,
55 + target
56 + );
57 + event.type = 'change';
58 + EventPropagators.accumulateTwoPhaseDispatches(event);
59 + return event;
60 +}
61 /**
62 * For IE shims
63 */
64 var activeElement = null;
65 var activeElementInst = null;
53 -var activeElementValue = null;
54 -var activeElementValueProp = null;
66 +
67 +
68
69 /**
70 * SECTION: handle `change` event
@@ -73,13 +86,11 @@ if (ExecutionEnvironment.canUseDOM) {
86 }
87
88 function manualDispatchChangeEvent(nativeEvent) {
76 - var event = SyntheticEvent.getPooled(
77 - eventTypes.change,
89 + var event = createAndAccumulateChangeEvent(
90 activeElementInst,
91 nativeEvent,
92 getEventTarget(nativeEvent)
93 );
82 - EventPropagators.accumulateTwoPhaseDispatches(event);
94
95 // If change and propertychange bubbled, we'd just bind to it like all the
96 // other events and have it go through ReactBrowserEventEmitter. Since it
@@ -115,6 +126,12 @@ function stopWatchingForChangeEventIE8() {
126 activeElementInst = null;
127 }
128
129 +function getInstIfValueChanged(targetInst) {
130 + if (inputValueTracking.updateValueIfChanged(targetInst)) {
131 + return targetInst;
132 + }
133 +}
134 +
135 function getTargetInstForChangeEvent(
136 topLevelType,
137 targetInst
@@ -123,6 +140,7 @@ function getTargetInstForChangeEvent(
140 return targetInst;
141 }
142 }
143 +
144 function handleEventsForChangeEventIE8(
145 topLevelType,
146 target,
@@ -146,108 +164,50 @@ var isInputEventSupported = false;
164 if (ExecutionEnvironment.canUseDOM) {
165 // IE9 claims to support the input event but fails to trigger it when
166 // deleting text, so we ignore its input events.
149 - // IE10+ fire input events to often, such when a placeholder
150 - // changes or when an input with a placeholder is focused.
167 isInputEventSupported = isEventSupported('input') && (
152 - !('documentMode' in document) || document.documentMode > 11
168 + !('documentMode' in document) || document.documentMode > 9
169 );
170 }
171
156 -/**
157 - * (For IE <=11) Replacement getter/setter for the `value` property that gets
158 - * set on the active element.
159 - */
160 -var newValueProp = {
161 - get: function() {
162 - return activeElementValueProp.get.call(this);
163 - },
164 - set: function(val) {
165 - // Cast to a string so we can do equality checks.
166 - activeElementValue = '' + val;
167 - activeElementValueProp.set.call(this, val);
168 - },
169 -};
172
173 /**
172 - * (For IE <=11) Starts tracking propertychange events on the passed-in element
174 + * (For IE <=9) Starts tracking propertychange events on the passed-in element
175 * and override the value property so that we can distinguish user events from
176 * value changes in JS.
177 */
178 function startWatchingForValueChange(target, targetInst) {
179 activeElement = target;
180 activeElementInst = targetInst;
179 - activeElementValue = target.value;
180 - activeElementValueProp = Object.getOwnPropertyDescriptor(
181 - target.constructor.prototype,
182 - 'value'
183 - );
184 -
185 - // Not guarded in a canDefineProperty check: IE8 supports defineProperty only
186 - // on DOM elements
187 - Object.defineProperty(activeElement, 'value', newValueProp);
188 - if (activeElement.attachEvent) {
189 - activeElement.attachEvent('onpropertychange', handlePropertyChange);
190 - } else {
191 - activeElement.addEventListener('propertychange', handlePropertyChange, false);
192 - }
181 + activeElement.attachEvent('onpropertychange', handlePropertyChange);
182 }
183
184 /**
196 - * (For IE <=11) Removes the event listeners from the currently-tracked element,
185 + * (For IE <=9) Removes the event listeners from the currently-tracked element,
186 * if any exists.
187 */
188 function stopWatchingForValueChange() {
189 if (!activeElement) {
190 return;
191 }
203 -
204 - // delete restores the original property definition
205 - delete activeElement.value;
206 -
207 - if (activeElement.detachEvent) {
208 - activeElement.detachEvent('onpropertychange', handlePropertyChange);
209 - } else {
210 - activeElement.removeEventListener('propertychange', handlePropertyChange, false);
211 - }
212 -
192 + activeElement.detachEvent('onpropertychange', handlePropertyChange);
193 activeElement = null;
194 activeElementInst = null;
215 - activeElementValue = null;
216 - activeElementValueProp = null;
195 }
196
197 /**
220 - * (For IE <=11) Handles a propertychange event, sending a `change` event if
198 + * (For IE <=9) Handles a propertychange event, sending a `change` event if
199 * the value of the active element has changed.
200 */
201 function handlePropertyChange(nativeEvent) {
202 if (nativeEvent.propertyName !== 'value') {
203 return;
204 }
227 - var value = nativeEvent.srcElement.value;
228 - if (value === activeElementValue) {
229 - return;
205 + if (getInstIfValueChanged(activeElementInst)) {
206 + manualDispatchChangeEvent(nativeEvent);
207 }
231 - activeElementValue = value;
232 -
233 - manualDispatchChangeEvent(nativeEvent);
208 }
209
236 -/**
237 - * If a `change` event should be fired, returns the target's ID.
238 - */
239 -function getTargetInstForInputEvent(
240 - topLevelType,
241 - targetInst
242 -) {
243 - if (topLevelType === topLevelTypes.topInput) {
244 - // In modern browsers (i.e., not IE8 or IE9), the input event is exactly
245 - // what we want so fall through here and trigger an abstract event
246 - return targetInst;
247 - }
248 -}
249 -
250 -function handleEventsForInputEventIE(
210 +function handleEventsForInputEventPolyfill(
211 topLevelType,
212 target,
213 targetInst
@@ -256,7 +216,7 @@ function handleEventsForInputEventIE(
216 // In IE8, we can capture almost all .value changes by adding a
217 // propertychange handler and looking for events with propertyName
218 // equal to 'value'
259 - // In IE9-11, propertychange fires for most input events but is buggy and
219 + // In IE9, propertychange fires for most input events but is buggy and
220 // doesn't fire when text is deleted, but conveniently, selectionchange
221 // appears to fire in all of the remaining cases so we catch those and
222 // forward the event if the value has changed
@@ -274,7 +234,7 @@ function handleEventsForInputEventIE(
234 }
235
236 // For IE8 and IE9.
277 -function getTargetInstForInputEventIE(
237 +function getTargetInstForInputEventPolyfill(
238 topLevelType,
239 targetInst
240 ) {
@@ -291,10 +251,7 @@ function getTargetInstForInputEventIE(
251 // keystroke if user does a key repeat (it'll be a little delayed: right
252 // before the second keystroke). Other input methods (e.g., paste) seem to
253 // fire selectionchange normally.
294 - if (activeElement && activeElement.value !== activeElementValue) {
295 - activeElementValue = activeElement.value;
296 - return activeElementInst;
297 - }
254 + return getInstIfValueChanged(activeElementInst);
255 }
256 }
257
@@ -306,8 +263,9 @@ function shouldUseClickEvent(elem) {
263 // Use the `click` event to detect changes to checkbox and radio inputs.
264 // This approach works across all browsers, whereas `change` does not fire
265 // until `blur` in IE8.
266 + var nodeName = elem.nodeName;
267 return (
310 - (elem.nodeName && elem.nodeName.toLowerCase() === 'input') &&
268 + (nodeName && nodeName.toLowerCase() === 'input') &&
269 (elem.type === 'checkbox' || elem.type === 'radio')
270 );
271 }
@@ -317,7 +275,19 @@ function getTargetInstForClickEvent(
275 targetInst
276 ) {
277 if (topLevelType === topLevelTypes.topClick) {
320 - return targetInst;
278 + return getInstIfValueChanged(targetInst);
279 + }
280 +}
281 +
282 +function getTargetInstForInputOrChangeEvent(
283 + topLevelType,
284 + targetInst
285 +) {
286 + if (
287 + topLevelType === topLevelTypes.topInput ||
288 + topLevelType === topLevelTypes.topChange
289 + ) {
290 + return getInstIfValueChanged(targetInst);
291 }
292 }
293
@@ -335,6 +305,8 @@ var ChangeEventPlugin = {
305
306 eventTypes: eventTypes,
307
308 + _isInputEventSupported: isInputEventSupported,
309 +
310 extractEvents: function(
311 topLevelType,
312 targetInst,
@@ -353,10 +325,10 @@ var ChangeEventPlugin = {
325 }
326 } else if (isTextInputElement(targetNode)) {
327 if (isInputEventSupported) {
356 - getTargetInstFunc = getTargetInstForInputEvent;
328 + getTargetInstFunc = getTargetInstForInputOrChangeEvent;
329 } else {
358 - getTargetInstFunc = getTargetInstForInputEventIE;
359 - handleEventFunc = handleEventsForInputEventIE;
330 + getTargetInstFunc = getTargetInstForInputEventPolyfill;
331 + handleEventFunc = handleEventsForInputEventPolyfill;
332 }
333 } else if (shouldUseClickEvent(targetNode)) {
334 getTargetInstFunc = getTargetInstForClickEvent;
@@ -365,14 +337,11 @@ var ChangeEventPlugin = {
337 if (getTargetInstFunc) {
338 var inst = getTargetInstFunc(topLevelType, targetInst);
339 if (inst) {
368 - var event = SyntheticEvent.getPooled(
369 - eventTypes.change,
340 + var event = createAndAccumulateChangeEvent(
341 inst,
342 nativeEvent,
343 nativeEventTarget
344 );
374 - event.type = 'change';
375 - EventPropagators.accumulateTwoPhaseDispatches(event);
345 return event;
346 }
347 }
src/renderers/dom/client/eventPlugins/__tests__/ChangeEventPlugin-test.js
+185
@@ -12,7 +12,32 @@
12 'use strict';
13
14 var React = require('React');
15 +var ReactDOM = require('ReactDOM');
16 var ReactTestUtils = require('ReactTestUtils');
17 +var ChangeEventPlugin = require('ChangeEventPlugin');
18 +var inputValueTracking = require('inputValueTracking');
19 +
20 +function getTrackedValue(elem) {
21 + var tracker = inputValueTracking._getTrackerFromNode(elem);
22 + return tracker.getValue();
23 +}
24 +
25 +function setTrackedValue(elem, value) {
26 + var tracker = inputValueTracking._getTrackerFromNode(elem);
27 + tracker.setValue(value);
28 +}
29 +
30 +function setUntrackedValue(elem, value) {
31 + var tracker = inputValueTracking._getTrackerFromNode(elem);
32 + var current = tracker.getValue();
33 +
34 + if (elem.type === 'checkbox' || elem.type === 'radio') {
35 + elem.checked = value;
36 + } else {
37 + elem.value = value;
38 + }
39 + tracker.setValue(current);
40 +}
41
42 describe('ChangeEventPlugin', function() {
43 it('should fire change for checkbox input', function() {
@@ -24,7 +49,167 @@ describe('ChangeEventPlugin', function() {
49 }
50
51 var input = ReactTestUtils.renderIntoDocument(<input type="checkbox" onChange={cb}/>);
52 +
53 + setUntrackedValue(input, true);
54 + ReactTestUtils.SimulateNative.click(input);
55 +
56 + expect(called).toBe(1);
57 + });
58 +
59 + it('should catch setting the value programmatically', function() {
60 + var input = ReactTestUtils.renderIntoDocument(
61 + <input type="text" defaultValue="foo"/>
62 + );
63 +
64 + input.value = 'bar';
65 + expect(getTrackedValue(input)).toBe('bar');
66 + });
67 +
68 + it('should not fire change when setting the value programmatically', function() {
69 + var called = 0;
70 +
71 + function cb(e) {
72 + called += 1;
73 + expect(e.type).toBe('change');
74 + }
75 +
76 + var input = ReactTestUtils.renderIntoDocument(
77 + <input type="text" onChange={cb} defaultValue="foo"/>
78 + );
79 +
80 + input.value = 'bar';
81 + ReactTestUtils.SimulateNative.change(input);
82 + expect(called).toBe(0);
83 +
84 + setUntrackedValue(input, 'foo');
85 + ReactTestUtils.SimulateNative.change(input);
86 +
87 + expect(called).toBe(1);
88 + });
89 +
90 + it('should not fire change when setting checked programmatically', function() {
91 + var called = 0;
92 +
93 + function cb(e) {
94 + called += 1;
95 + expect(e.type).toBe('change');
96 + }
97 +
98 + var input = ReactTestUtils.renderIntoDocument(
99 + <input type="checkbox" onChange={cb} defaultChecked={true} />
100 + );
101 +
102 + input.checked = true;
103 ReactTestUtils.SimulateNative.click(input);
104 + expect(called).toBe(0);
105 +
106 + input.checked = false;
107 + setTrackedValue(input, undefined);
108 + ReactTestUtils.SimulateNative.click(input);
109 +
110 expect(called).toBe(1);
111 });
112 +
113 + it('should unmount', function() {
114 + var container = document.createElement('div');
115 + var input = ReactDOM.render(<input />, container);
116 +
117 + ReactDOM.unmountComponentAtNode(container);
118 + });
119 +
120 + it('should only fire change for checked radio button once', function() {
121 + var called = 0;
122 +
123 + function cb(e) {
124 + called += 1;
125 + }
126 +
127 + var input = ReactTestUtils.renderIntoDocument(<input type="radio" onChange={cb}/>);
128 + setUntrackedValue(input, true);
129 + ReactTestUtils.SimulateNative.click(input);
130 + ReactTestUtils.SimulateNative.click(input);
131 + expect(called).toBe(1);
132 + });
133 +
134 + it('should deduplicate input value change events', function() {
135 + var input;
136 + var called = 0;
137 +
138 + function cb(e) {
139 + called += 1;
140 + expect(e.type).toBe('change');
141 + }
142 +
143 + [
144 + <input type="text" onChange={cb}/>,
145 + <input type="number" onChange={cb}/>,
146 + <input type="range" onChange={cb}/>,
147 + ].forEach(function(element) {
148 + called = 0;
149 + input = ReactTestUtils.renderIntoDocument(element);
150 +
151 + setUntrackedValue(input, '40');
152 + ReactTestUtils.SimulateNative.change(input);
153 + ReactTestUtils.SimulateNative.change(input);
154 + expect(called).toBe(1);
155 +
156 + called = 0;
157 + input = ReactTestUtils.renderIntoDocument(element);
158 + setUntrackedValue(input, '40');
159 + ReactTestUtils.SimulateNative.input(input);
160 + ReactTestUtils.SimulateNative.input(input);
161 + expect(called).toBe(1);
162 +
163 + called = 0;
164 + input = ReactTestUtils.renderIntoDocument(element);
165 + setUntrackedValue(input, '40');
166 + ReactTestUtils.SimulateNative.input(input);
167 + ReactTestUtils.SimulateNative.change(input);
168 + expect(called).toBe(1);
169 + });
170 + });
171 +
172 + it('should listen for both change and input events when supported', function() {
173 + var called = 0;
174 +
175 + function cb(e) {
176 + called += 1;
177 + expect(e.type).toBe('change');
178 + }
179 +
180 + if (!ChangeEventPlugin._isInputEventSupported) {
181 + return;
182 + }
183 +
184 + var input = ReactTestUtils.renderIntoDocument(<input type="range" onChange={cb}/>);
185 + setUntrackedValue(input, 'bar');
186 +
187 + ReactTestUtils.SimulateNative.input(input);
188 +
189 + setUntrackedValue(input, 'foo');
190 +
191 + ReactTestUtils.SimulateNative.change(input);
192 +
193 + expect(called).toBe(2);
194 + });
195 +
196 + it('should only fire events when the value changes for range inputs', function() {
197 + var called = 0;
198 +
199 + function cb(e) {
200 + called += 1;
201 + expect(e.type).toBe('change');
202 + }
203 +
204 + var input = ReactTestUtils.renderIntoDocument(<input type="range" onChange={cb}/>);
205 + setUntrackedValue(input, '40');
206 + ReactTestUtils.SimulateNative.input(input);
207 + ReactTestUtils.SimulateNative.change(input);
208 +
209 + setUntrackedValue(input, 'foo');
210 +
211 + ReactTestUtils.SimulateNative.input(input);
212 + ReactTestUtils.SimulateNative.change(input);
213 + expect(called).toBe(2);
214 + });
215 });
src/renderers/dom/client/inputValueTracking.js new
+133
@@ -0,0 +1,133 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule inputValueTracking
10 + */
11 +
12 +'use strict';
13 +var ReactDOMComponentTree = require('ReactDOMComponentTree');
14 +
15 +function isCheckable(elem) {
16 + var type = elem.type;
17 + var nodeName = elem.nodeName;
18 + return (
19 + (nodeName && nodeName.toLowerCase() === 'input') &&
20 + (type === 'checkbox' || type === 'radio')
21 + );
22 +}
23 +
24 +function getTracker(inst) {
25 + return inst._wrapperState.valueTracker;
26 +}
27 +
28 +function attachTracker(inst, tracker) {
29 + inst._wrapperState.valueTracker = tracker;
30 +}
31 +
32 +function detachTracker(inst) {
33 + delete inst._wrapperState.valueTracker;
34 +}
35 +
36 +function getValueFromNode(node) {
37 + var value;
38 + if (node) {
39 + value = isCheckable(node)
40 + ? '' + node.checked
41 + : node.value;
42 + }
43 + return value;
44 +}
45 +
46 +var inputValueTracking = {
47 + // exposed for testing
48 + _getTrackerFromNode(node) {
49 + return getTracker(
50 + ReactDOMComponentTree.getInstanceFromNode(node)
51 + );
52 + },
53 +
54 + track: function(inst) {
55 + if (getTracker(inst)) {
56 + return;
57 + }
58 +
59 + var node = ReactDOMComponentTree.getNodeFromInstance(inst);
60 + var valueField = isCheckable(node) ? 'checked' : 'value';
61 + var descriptor = Object.getOwnPropertyDescriptor(
62 + node.constructor.prototype,
63 + valueField
64 + );
65 +
66 + var currentValue = '' + node[valueField];
67 +
68 + // if someone has already defined a value bail and don't track value
69 + // will cause over reporting of changes, but it's better then a hard failure
70 + // (needed for certain tests that spyOn input values)
71 + if (node.hasOwnProperty(valueField)) {
72 + return;
73 + }
74 +
75 + Object.defineProperty(node, valueField, {
76 + enumerable: descriptor.enumerable,
77 + configurable: true,
78 + get: function() {
79 + return descriptor.get.call(this);
80 + },
81 + set: function(value) {
82 + currentValue = '' + value;
83 + descriptor.set.call(this, value);
84 + },
85 + });
86 +
87 + attachTracker(inst, {
88 + getValue() {
89 + return currentValue;
90 + },
91 + setValue(value) {
92 + currentValue = '' + value;
93 + },
94 + stopTracking() {
95 + detachTracker(inst);
96 + delete node[valueField];
97 + },
98 + });
99 + },
100 +
101 + updateValueIfChanged(inst) {
102 + if (!inst) {
103 + return false;
104 + }
105 + var tracker = getTracker(inst);
106 +
107 + if (!tracker) {
108 + inputValueTracking.track(inst);
109 + return true;
110 + }
111 +
112 + var lastValue = tracker.getValue();
113 + var nextValue = getValueFromNode(
114 + ReactDOMComponentTree.getNodeFromInstance(inst)
115 + );
116 +
117 + if (nextValue !== lastValue) {
118 + tracker.setValue(nextValue);
119 + return true;
120 + }
121 +
122 + return false;
123 + },
124 +
125 + stopTracking(inst) {
126 + var tracker = getTracker(inst);
127 + if (tracker) {
128 + tracker.stopTracking();
129 + }
130 + },
131 +};
132 +
133 +module.exports = inputValueTracking;
src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js
+10 -1
@@ -21,6 +21,14 @@ describe('ReactDOMInput', function() {
21 var ReactDOMFeatureFlags;
22 var ReactLink;
23 var ReactTestUtils;
24 + var inputValueTracking;
25 +
26 + function setUntrackedValue(elem, value) {
27 + var tracker = inputValueTracking._getTrackerFromNode(elem);
28 + var current = tracker.getValue();
29 + elem.value = value;
30 + tracker.setValue(current);
31 + }
32
33 beforeEach(function() {
34 jest.resetModuleRegistry();
@@ -30,6 +38,7 @@ describe('ReactDOMInput', function() {
38 ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
39 ReactLink = require('ReactLink');
40 ReactTestUtils = require('ReactTestUtils');
41 + inputValueTracking = require('inputValueTracking');
42 spyOn(console, 'error');
43 });
44
@@ -149,7 +158,7 @@ describe('ReactDOMInput', function() {
158 var container = document.createElement('div');
159 var node = ReactDOM.render(stub, container);
160
152 - node.value = 'giraffe';
161 + setUntrackedValue(node, 'giraffe');
162
163 var fakeNativeEvent = new function() {};
164 fakeNativeEvent.target = node;
src/renderers/dom/shared/ReactDOMComponent.js
+13 -2
@@ -40,6 +40,7 @@ var invariant = require('invariant');
40 var isEventSupported = require('isEventSupported');
41 var keyOf = require('keyOf');
42 var shallowEqual = require('shallowEqual');
43 +var inputValueTracking = require('inputValueTracking');
44 var validateDOMNesting = require('validateDOMNesting');
45 var warning = require('warning');
46
@@ -272,6 +273,10 @@ var mediaEvents = {
273 topWaiting: 'waiting',
274 };
275
276 +function trackInputValue() {
277 + inputValueTracking.track(this);
278 +}
279 +
280 function trapBubbledEventsLocal() {
281 var inst = this;
282 // If a component renders to null or if another component fatals and causes
@@ -496,6 +501,7 @@ ReactDOMComponent.Mixin = {
501 case 'input':
502 ReactDOMInput.mountWrapper(this, props, nativeParent);
503 props = ReactDOMInput.getNativeProps(this, props);
504 + transaction.getReactMountReady().enqueue(trackInputValue, this);
505 transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
506 break;
507 case 'option':
@@ -510,6 +516,7 @@ ReactDOMComponent.Mixin = {
516 case 'textarea':
517 ReactDOMTextarea.mountWrapper(this, props, nativeParent);
518 props = ReactDOMTextarea.getNativeProps(this, props);
519 + transaction.getReactMountReady().enqueue(trackInputValue, this);
520 transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
521 break;
522 }
@@ -598,10 +605,10 @@ ReactDOMComponent.Mixin = {
605 }
606
607 switch (this._tag) {
601 - case 'button':
608 case 'input':
603 - case 'select':
609 case 'textarea':
610 + case 'select':
611 + case 'button':
612 if (props.autoFocus) {
613 transaction.getReactMountReady().enqueue(
614 AutoFocusUtils.focusDOMComponent,
@@ -1036,6 +1043,10 @@ ReactDOMComponent.Mixin = {
1043 }
1044 }
1045 break;
1046 + case 'input':
1047 + case 'textarea':
1048 + inputValueTracking.stopTracking(this);
1049 + break;
1050 case 'html':
1051 case 'head':
1052 case 'body':
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
+44
@@ -16,12 +16,14 @@ describe('ReactDOMComponent', function() {
16 var React;
17 var ReactDOM;
18 var ReactDOMServer;
19 + var inputValueTracking;
20
21 beforeEach(function() {
22 jest.resetModuleRegistry();
23 React = require('React');
24 ReactDOM = require('ReactDOM');
25 ReactDOMServer = require('ReactDOMServer');
26 + inputValueTracking = require('inputValueTracking');
27 });
28
29 describe('updateDOM', function() {
@@ -816,6 +818,24 @@ describe('ReactDOMComponent', function() {
818 );
819 });
820
821 + it('should track input values', function() {
822 + var container = document.createElement('div');
823 + var inst = ReactDOM.render(<input type="text" defaultValue="foo"/>, container);
824 +
825 + var tracker = inputValueTracking._getTrackerFromNode(inst);
826 +
827 + expect(tracker.getValue()).toEqual('foo');
828 + });
829 +
830 + it('should track textarea values', function() {
831 + var container = document.createElement('div');
832 + var inst = ReactDOM.render(<textarea defaultValue="foo"/>, container);
833 +
834 + var tracker = inputValueTracking._getTrackerFromNode(inst);
835 +
836 + expect(tracker.getValue()).toEqual('foo');
837 + });
838 +
839 it('should execute custom event plugin listening behavior', function() {
840 var SimpleEventPlugin = require('SimpleEventPlugin');
841
@@ -1015,6 +1035,30 @@ describe('ReactDOMComponent', function() {
1035 ).toBe(undefined);
1036 });
1037
1038 + it('should clean up input value tracking', function() {
1039 + var container = document.createElement('div');
1040 + var node = ReactDOM.render(<input type="text" defaultValue="foo"/>, container);
1041 + var tracker = inputValueTracking._getTrackerFromNode(node);
1042 +
1043 + spyOn(tracker, 'stopTracking');
1044 +
1045 + ReactDOM.unmountComponentAtNode(container);
1046 +
1047 + expect(tracker.stopTracking.calls.length).toBe(1);
1048 + });
1049 +
1050 + it('should clean up input textarea tracking', function() {
1051 + var container = document.createElement('div');
1052 + var node = ReactDOM.render(<textarea defaultValue="foo"/>, container);
1053 + var tracker = inputValueTracking._getTrackerFromNode(node);
1054 +
1055 + spyOn(tracker, 'stopTracking');
1056 +
1057 + ReactDOM.unmountComponentAtNode(container);
1058 +
1059 + expect(tracker.stopTracking.calls.length).toBe(1);
1060 + });
1061 +
1062 it('unmounts children before unsetting DOM node info', function() {
1063 var Inner = React.createClass({
1064 render: function() {