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() {