diff --git a/src/renderers/dom/client/__tests__/inputValueTracking-test.js b/src/renderers/dom/client/__tests__/inputValueTracking-test.js
new file mode 100644
index 0000000000000..e9f42a1444c99
--- /dev/null
+++ b/src/renderers/dom/client/__tests__/inputValueTracking-test.js
@@ -0,0 +1,165 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails react-core
+ */
+'use strict';
+
+var React = require('React');
+var ReactTestUtils = require('ReactTestUtils');
+var inputValueTracking = require('inputValueTracking');
+
+describe('inputValueTracking', function() {
+ var input, checkbox, mockComponent;
+
+ beforeEach(function() {
+ input = document.createElement('input');
+ input.type = 'text';
+ checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ mockComponent = { _hostNode: input, _wrapperState: {} };
+ });
+
+ it('should attach tracker to wrapper state', function() {
+ inputValueTracking.track(mockComponent);
+
+ expect(
+ mockComponent._wrapperState.hasOwnProperty('valueTracker')
+ ).toBe(true);
+ });
+
+ it('should define `value` on the instance node', function() {
+ inputValueTracking.track(mockComponent);
+
+ expect(
+ input.hasOwnProperty('value')
+ ).toBe(true);
+ });
+
+ it('should define `checked` on the instance node', function() {
+ mockComponent._hostNode = checkbox;
+ inputValueTracking.track(mockComponent);
+
+ expect(checkbox.hasOwnProperty('checked')).toBe(true);
+ });
+
+ it('should initialize with the current value', function() {
+ input.value ='foo';
+
+ inputValueTracking.track(mockComponent);
+
+ var tracker = mockComponent._wrapperState.valueTracker;
+
+ expect(tracker.getValue()).toEqual('foo');
+ });
+
+ it('should initialize with the current `checked`', function() {
+ mockComponent._hostNode = checkbox;
+ checkbox.checked = true;
+ inputValueTracking.track(mockComponent);
+
+ var tracker = mockComponent._wrapperState.valueTracker;
+
+ expect(tracker.getValue()).toEqual('true');
+ });
+
+ it('should track value changes', function() {
+ input.value ='foo';
+
+ inputValueTracking.track(mockComponent);
+
+ var tracker = mockComponent._wrapperState.valueTracker;
+
+ input.value ='bar';
+ expect(tracker.getValue()).toEqual('bar');
+ });
+
+ it('should tracked`checked` changes', function() {
+ mockComponent._hostNode = checkbox;
+ checkbox.checked = true;
+ inputValueTracking.track(mockComponent);
+
+ var tracker = mockComponent._wrapperState.valueTracker;
+
+ checkbox.checked = false;
+ expect(tracker.getValue()).toEqual('false');
+ });
+
+ it('should update value manually', function() {
+ input.value ='foo';
+ inputValueTracking.track(mockComponent);
+
+ var tracker = mockComponent._wrapperState.valueTracker;
+
+ tracker.setValue('bar');
+ expect(tracker.getValue()).toEqual('bar');
+ });
+
+ it('should coerce value to a string', function() {
+ input.value ='foo';
+ inputValueTracking.track(mockComponent);
+
+ var tracker = mockComponent._wrapperState.valueTracker;
+
+ tracker.setValue(500);
+ expect(tracker.getValue()).toEqual('500');
+ });
+
+ it('should update value if it changed and return result', function() {
+ inputValueTracking.track(mockComponent);
+ input.value ='foo';
+
+ var tracker = mockComponent._wrapperState.valueTracker;
+
+ expect(
+ inputValueTracking.updateValueIfChanged(mockComponent)
+ ).toBe(false);
+
+ tracker.setValue('bar');
+
+ expect(
+ inputValueTracking.updateValueIfChanged(mockComponent)
+ ).toBe(true);
+
+ expect(tracker.getValue()).toEqual('foo');
+ });
+
+ it('should track value and return true when updating untracked instance', function() {
+ input.value ='foo';
+
+ expect(
+ inputValueTracking.updateValueIfChanged(mockComponent)
+ )
+ .toBe(true);
+
+ var tracker = mockComponent._wrapperState.valueTracker;
+ expect(tracker.getValue()).toEqual('foo');
+ });
+
+ it('should return tracker from node', function() {
+ var node = ReactTestUtils.renderIntoDocument();
+ var tracker = inputValueTracking._getTrackerFromNode(node);
+ expect(tracker.getValue()).toEqual('foo');
+ });
+
+ it('should stop tracking', function() {
+ inputValueTracking.track(mockComponent);
+
+ expect(
+ mockComponent._wrapperState.hasOwnProperty('valueTracker')
+ ).toBe(true);
+
+ inputValueTracking.stopTracking(mockComponent);
+
+ expect(
+ mockComponent._wrapperState.hasOwnProperty('valueTracker')
+ ).toBe(false);
+
+ expect(input.hasOwnProperty('value')).toBe(false);
+ });
+});
diff --git a/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js b/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js
index a724f8a6042c6..b65ad6b41e4f3 100644
--- a/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js
+++ b/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js
@@ -18,10 +18,12 @@ var ReactDOMComponentTree = require('ReactDOMComponentTree');
var ReactUpdates = require('ReactUpdates');
var SyntheticEvent = require('SyntheticEvent');
+var inputValueTracking = require('inputValueTracking');
var getEventTarget = require('getEventTarget');
var isEventSupported = require('isEventSupported');
var isTextInputElement = require('isTextInputElement');
+
var eventTypes = {
change: {
phasedRegistrationNames: {
@@ -41,13 +43,24 @@ var eventTypes = {
},
};
+function createAndAccumulateChangeEvent(inst, nativeEvent, target) {
+ var event = SyntheticEvent.getPooled(
+ eventTypes.change,
+ inst,
+ nativeEvent,
+ target
+ );
+ event.type = 'change';
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+}
/**
* For IE shims
*/
var activeElement = null;
var activeElementInst = null;
-var activeElementValue = null;
-var activeElementValueProp = null;
+
+
/**
* SECTION: handle `change` event
@@ -68,13 +81,11 @@ if (ExecutionEnvironment.canUseDOM) {
}
function manualDispatchChangeEvent(nativeEvent) {
- var event = SyntheticEvent.getPooled(
- eventTypes.change,
+ var event = createAndAccumulateChangeEvent(
activeElementInst,
nativeEvent,
getEventTarget(nativeEvent),
);
- EventPropagators.accumulateTwoPhaseDispatches(event);
// If change and propertychange bubbled, we'd just bind to it like all the
// other events and have it go through ReactBrowserEventEmitter. Since it
@@ -110,11 +121,26 @@ function stopWatchingForChangeEventIE8() {
activeElementInst = null;
}
+
+function getInstIfValueChanged(targetInst, nativeEvent) {
+ var updated = inputValueTracking.updateValueIfChanged(targetInst);
+ var simulated = (
+ nativeEvent.simulated === true &&
+ ChangeEventPlugin._allowSimulatedPassThrough
+ );
+
+ if (updated || simulated) {
+ return targetInst;
+ }
+}
+
function getTargetInstForChangeEvent(topLevelType, targetInst) {
+
if (topLevelType === 'topChange') {
return targetInst;
}
}
+
function handleEventsForChangeEventIE8(topLevelType, target, targetInst) {
if (topLevelType === 'topFocus') {
// stopWatching() should be a noop here but we call it just in case we
@@ -133,118 +159,64 @@ var isInputEventSupported = false;
if (ExecutionEnvironment.canUseDOM) {
// IE9 claims to support the input event but fails to trigger it when
// deleting text, so we ignore its input events.
- // IE10+ fire input events to often, such when a placeholder
- // changes or when an input with a placeholder is focused.
- isInputEventSupported =
- isEventSupported('input') &&
- (!document.documentMode || document.documentMode > 11);
+
+ isInputEventSupported = isEventSupported('input') && (
+ !('documentMode' in document) || document.documentMode > 9
+ );
+
}
-/**
- * (For IE <=11) Replacement getter/setter for the `value` property that gets
- * set on the active element.
- */
-var newValueProp = {
- get: function() {
- return activeElementValueProp.get.call(this);
- },
- set: function(val) {
- // Cast to a string so we can do equality checks.
- activeElementValue = '' + val;
- activeElementValueProp.set.call(this, val);
- },
-};
/**
- * (For IE <=11) Starts tracking propertychange events on the passed-in element
+ * (For IE <=9) Starts tracking propertychange events on the passed-in element
* and override the value property so that we can distinguish user events from
* value changes in JS.
*/
function startWatchingForValueChange(target, targetInst) {
activeElement = target;
activeElementInst = targetInst;
- activeElementValue = target.value;
- activeElementValueProp = Object.getOwnPropertyDescriptor(
- target.constructor.prototype,
- 'value',
- );
-
- // Not guarded in a canDefineProperty check: IE8 supports defineProperty only
- // on DOM elements
- Object.defineProperty(activeElement, 'value', newValueProp);
- if (activeElement.attachEvent) {
- activeElement.attachEvent('onpropertychange', handlePropertyChange);
- } else {
- activeElement.addEventListener(
- 'propertychange',
- handlePropertyChange,
- false,
- );
- }
+ activeElement.attachEvent('onpropertychange', handlePropertyChange);
}
/**
- * (For IE <=11) Removes the event listeners from the currently-tracked element,
+ * (For IE <=9) Removes the event listeners from the currently-tracked element,
* if any exists.
*/
function stopWatchingForValueChange() {
if (!activeElement) {
return;
}
-
- // delete restores the original property definition
- delete activeElement.value;
-
- if (activeElement.detachEvent) {
- activeElement.detachEvent('onpropertychange', handlePropertyChange);
- } else {
- activeElement.removeEventListener(
- 'propertychange',
- handlePropertyChange,
- false,
- );
- }
+ activeElement.detachEvent('onpropertychange', handlePropertyChange);
activeElement = null;
activeElementInst = null;
- activeElementValue = null;
- activeElementValueProp = null;
}
/**
- * (For IE <=11) Handles a propertychange event, sending a `change` event if
+ * (For IE <=9) Handles a propertychange event, sending a `change` event if
* the value of the active element has changed.
*/
function handlePropertyChange(nativeEvent) {
if (nativeEvent.propertyName !== 'value') {
return;
}
- var value = nativeEvent.srcElement.value;
- if (value === activeElementValue) {
- return;
+ if (getInstIfValueChanged(activeElementInst, nativeEvent)) {
+ manualDispatchChangeEvent(nativeEvent);
}
- activeElementValue = value;
-
- manualDispatchChangeEvent(nativeEvent);
}
-/**
- * If a `change` event should be fired, returns the target's ID.
- */
-function getTargetInstForInputEvent(topLevelType, targetInst) {
- if (topLevelType === 'topInput') {
- // In modern browsers (i.e., not IE8 or IE9), the input event is exactly
- // what we want so fall through here and trigger an abstract event
- return targetInst;
- }
-}
-function handleEventsForInputEventIE(topLevelType, target, targetInst) {
+function handleEventsForInputEventPolyfill(
+ topLevelType,
+ target,
+ targetInst
+) {
+
if (topLevelType === 'topFocus') {
// In IE8, we can capture almost all .value changes by adding a
// propertychange handler and looking for events with propertyName
// equal to 'value'
- // In IE9-11, propertychange fires for most input events but is buggy and
+ // In IE9, propertychange fires for most input events but is buggy and
// doesn't fire when text is deleted, but conveniently, selectionchange
// appears to fire in all of the remaining cases so we catch those and
// forward the event if the value has changed
@@ -262,12 +234,14 @@ function handleEventsForInputEventIE(topLevelType, target, targetInst) {
}
// For IE8 and IE9.
-function getTargetInstForInputEventIE(topLevelType, targetInst) {
- if (
- topLevelType === 'topSelectionChange' ||
- topLevelType === 'topKeyUp' ||
- topLevelType === 'topKeyDown'
- ) {
+function getTargetInstForInputEventPolyfill(
+ topLevelType,
+ targetInst,
+ nativeEvent
+) {
+ if (topLevelType === 'topSelectionChange' ||
+ topLevelType === 'topKeyUp' ||
+ topLevelType === 'topKeyDown') {
// On the selectionchange event, the target is just document which isn't
// helpful for us so just check activeElement instead.
//
@@ -278,10 +252,7 @@ function getTargetInstForInputEventIE(topLevelType, targetInst) {
// keystroke if user does a key repeat (it'll be a little delayed: right
// before the second keystroke). Other input methods (e.g., paste) seem to
// fire selectionchange normally.
- if (activeElement && activeElement.value !== activeElementValue) {
- activeElementValue = activeElement.value;
- return activeElementInst;
- }
+ return getInstIfValueChanged(activeElementInst, nativeEvent);
}
}
@@ -292,16 +263,34 @@ function shouldUseClickEvent(elem) {
// Use the `click` event to detect changes to checkbox and radio inputs.
// This approach works across all browsers, whereas `change` does not fire
// until `blur` in IE8.
+ var nodeName = elem.nodeName;
return (
- elem.nodeName &&
- elem.nodeName.toLowerCase() === 'input' &&
+ (nodeName && nodeName.toLowerCase() === 'input') &&
(elem.type === 'checkbox' || elem.type === 'radio')
);
}
-function getTargetInstForClickEvent(topLevelType, targetInst) {
+function getTargetInstForClickEvent(
+ topLevelType,
+ targetInst,
+ nativeEvent
+) {
+
if (topLevelType === 'topClick') {
- return targetInst;
+ return getInstIfValueChanged(targetInst, nativeEvent);
+ }
+}
+
+function getTargetInstForInputOrChangeEvent(
+ topLevelType,
+ targetInst,
+ nativeEvent
+) {
+ if (
+ topLevelType === 'topInput' ||
+ topLevelType === 'topChange'
+ ) {
+ return getInstIfValueChanged(targetInst, nativeEvent);
}
}
@@ -338,6 +327,9 @@ function handleControlledInputBlur(inst, node) {
var ChangeEventPlugin = {
eventTypes: eventTypes,
+ _allowSimulatedPassThrough: true,
+ _isInputEventSupported: isInputEventSupported,
+
extractEvents: function(
topLevelType,
targetInst,
@@ -357,26 +349,23 @@ var ChangeEventPlugin = {
}
} else if (isTextInputElement(targetNode)) {
if (isInputEventSupported) {
- getTargetInstFunc = getTargetInstForInputEvent;
+ getTargetInstFunc = getTargetInstForInputOrChangeEvent;
} else {
- getTargetInstFunc = getTargetInstForInputEventIE;
- handleEventFunc = handleEventsForInputEventIE;
+ getTargetInstFunc = getTargetInstForInputEventPolyfill;
+ handleEventFunc = handleEventsForInputEventPolyfill;
}
} else if (shouldUseClickEvent(targetNode)) {
getTargetInstFunc = getTargetInstForClickEvent;
}
if (getTargetInstFunc) {
- var inst = getTargetInstFunc(topLevelType, targetInst);
+ var inst = getTargetInstFunc(topLevelType, targetInst, nativeEvent);
if (inst) {
- var event = SyntheticEvent.getPooled(
- eventTypes.change,
+ var event = createAndAccumulateChangeEvent(
inst,
nativeEvent,
nativeEventTarget,
);
- event.type = 'change';
- EventPropagators.accumulateTwoPhaseDispatches(event);
return event;
}
}
diff --git a/src/renderers/dom/client/eventPlugins/__tests__/ChangeEventPlugin-test.js b/src/renderers/dom/client/eventPlugins/__tests__/ChangeEventPlugin-test.js
index 3601015f65d50..066a66ef548b6 100644
--- a/src/renderers/dom/client/eventPlugins/__tests__/ChangeEventPlugin-test.js
+++ b/src/renderers/dom/client/eventPlugins/__tests__/ChangeEventPlugin-test.js
@@ -12,9 +12,42 @@
'use strict';
var React = require('React');
+var ReactDOM = require('ReactDOM');
var ReactTestUtils = require('ReactTestUtils');
+var ChangeEventPlugin = require('ChangeEventPlugin');
+var inputValueTracking = require('inputValueTracking');
+
+function getTrackedValue(elem) {
+ var tracker = inputValueTracking._getTrackerFromNode(elem);
+ return tracker.getValue();
+}
+
+function setTrackedValue(elem, value) {
+ var tracker = inputValueTracking._getTrackerFromNode(elem);
+ tracker.setValue(value);
+}
+
+function setUntrackedValue(elem, value) {
+ var tracker = inputValueTracking._getTrackerFromNode(elem);
+ var current = tracker.getValue();
+
+ if (elem.type === 'checkbox' || elem.type === 'radio') {
+ elem.checked = value;
+ } else {
+ elem.value = value;
+ }
+ tracker.setValue(current);
+}
describe('ChangeEventPlugin', () => {
+ beforeEach(() => {
+ ChangeEventPlugin._allowSimulatedPassThrough = false;
+ });
+
+ afterEach(() => {
+ ChangeEventPlugin._allowSimulatedPassThrough = true;
+ });
+
it('should fire change for checkbox input', () => {
var called = 0;
@@ -23,10 +56,168 @@ describe('ChangeEventPlugin', () => {
expect(e.type).toBe('change');
}
+ var input = ReactTestUtils.renderIntoDocument();
+
+ setUntrackedValue(input, true);
+ ReactTestUtils.SimulateNative.click(input);
+
+ expect(called).toBe(1);
+ });
+
+ it('should catch setting the value programmatically', function() {
+ var input = ReactTestUtils.renderIntoDocument(
+
+ );
+
+ input.value = 'bar';
+ expect(getTrackedValue(input)).toBe('bar');
+ });
+
+ it('should not fire change when setting the value programmatically', function() {
+ var called = 0;
+
+ function cb(e) {
+ called += 1;
+ expect(e.type).toBe('change');
+ }
+
var input = ReactTestUtils.renderIntoDocument(
- ,
+
);
+
+ input.value = 'bar';
+ ReactTestUtils.SimulateNative.change(input);
+ expect(called).toBe(0);
+
+ setUntrackedValue(input, 'foo');
+ ReactTestUtils.SimulateNative.change(input);
+
+ expect(called).toBe(1);
+ });
+
+ it('should not fire change when setting checked programmatically', function() {
+ var called = 0;
+
+ function cb(e) {
+ called += 1;
+ expect(e.type).toBe('change');
+ }
+
+ var input = ReactTestUtils.renderIntoDocument(
+
+ );
+
+ input.checked = true;
ReactTestUtils.SimulateNative.click(input);
+ expect(called).toBe(0);
+
+ input.checked = false;
+ setTrackedValue(input, undefined);
+ ReactTestUtils.SimulateNative.click(input);
+
expect(called).toBe(1);
});
+
+ it('should unmount', function() {
+ var container = document.createElement('div');
+ var input = ReactDOM.render(, container);
+
+ ReactDOM.unmountComponentAtNode(container);
+ });
+
+ it('should only fire change for checked radio button once', function() {
+ var called = 0;
+
+ function cb(e) {
+ called += 1;
+ }
+
+ var input = ReactTestUtils.renderIntoDocument();
+ setUntrackedValue(input, true);
+ ReactTestUtils.SimulateNative.click(input);
+ ReactTestUtils.SimulateNative.click(input);
+ expect(called).toBe(1);
+ });
+
+ it('should deduplicate input value change events', function() {
+ var input;
+ var called = 0;
+
+ function cb(e) {
+ called += 1;
+ expect(e.type).toBe('change');
+ }
+
+ [
+ ,
+ ,
+ ,
+ ].forEach(function(element) {
+ called = 0;
+ input = ReactTestUtils.renderIntoDocument(element);
+
+ setUntrackedValue(input, '40');
+ ReactTestUtils.SimulateNative.change(input);
+ ReactTestUtils.SimulateNative.change(input);
+ expect(called).toBe(1);
+
+ called = 0;
+ input = ReactTestUtils.renderIntoDocument(element);
+ setUntrackedValue(input, '40');
+ ReactTestUtils.SimulateNative.input(input);
+ ReactTestUtils.SimulateNative.input(input);
+ expect(called).toBe(1);
+
+ called = 0;
+ input = ReactTestUtils.renderIntoDocument(element);
+ setUntrackedValue(input, '40');
+ ReactTestUtils.SimulateNative.input(input);
+ ReactTestUtils.SimulateNative.change(input);
+ expect(called).toBe(1);
+ });
+ });
+
+ it('should listen for both change and input events when supported', function() {
+ var called = 0;
+
+ function cb(e) {
+ called += 1;
+ expect(e.type).toBe('change');
+ }
+
+ if (!ChangeEventPlugin._isInputEventSupported) {
+ return;
+ }
+
+ var input = ReactTestUtils.renderIntoDocument();
+ setUntrackedValue(input, 'bar');
+
+ ReactTestUtils.SimulateNative.input(input);
+
+ setUntrackedValue(input, 'foo');
+
+ ReactTestUtils.SimulateNative.change(input);
+
+ expect(called).toBe(2);
+ });
+
+ it('should only fire events when the value changes for range inputs', function() {
+ var called = 0;
+
+ function cb(e) {
+ called += 1;
+ expect(e.type).toBe('change');
+ }
+
+ var input = ReactTestUtils.renderIntoDocument();
+ setUntrackedValue(input, '40');
+ ReactTestUtils.SimulateNative.input(input);
+ ReactTestUtils.SimulateNative.change(input);
+
+ setUntrackedValue(input, 'foo');
+
+ ReactTestUtils.SimulateNative.input(input);
+ ReactTestUtils.SimulateNative.change(input);
+ expect(called).toBe(2);
+ });
});
diff --git a/src/renderers/dom/client/inputValueTracking.js b/src/renderers/dom/client/inputValueTracking.js
new file mode 100644
index 0000000000000..3b877be63ec73
--- /dev/null
+++ b/src/renderers/dom/client/inputValueTracking.js
@@ -0,0 +1,133 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule inputValueTracking
+ */
+
+'use strict';
+var ReactDOMComponentTree = require('ReactDOMComponentTree');
+
+function isCheckable(elem) {
+ var type = elem.type;
+ var nodeName = elem.nodeName;
+ return (
+ (nodeName && nodeName.toLowerCase() === 'input') &&
+ (type === 'checkbox' || type === 'radio')
+ );
+}
+
+function getTracker(inst) {
+ return inst._wrapperState.valueTracker;
+}
+
+function attachTracker(inst, tracker) {
+ inst._wrapperState.valueTracker = tracker;
+}
+
+function detachTracker(inst) {
+ delete inst._wrapperState.valueTracker;
+}
+
+function getValueFromNode(node) {
+ var value;
+ if (node) {
+ value = isCheckable(node)
+ ? '' + node.checked
+ : node.value;
+ }
+ return value;
+}
+
+var inputValueTracking = {
+ // exposed for testing
+ _getTrackerFromNode(node) {
+ return getTracker(
+ ReactDOMComponentTree.getInstanceFromNode(node)
+ );
+ },
+
+ track: function(inst) {
+ if (getTracker(inst)) {
+ return;
+ }
+
+ var node = ReactDOMComponentTree.getNodeFromInstance(inst);
+ var valueField = isCheckable(node) ? 'checked' : 'value';
+ var descriptor = Object.getOwnPropertyDescriptor(
+ node.constructor.prototype,
+ valueField
+ );
+
+ var currentValue = '' + node[valueField];
+
+ // if someone has already defined a value bail and don't track value
+ // will cause over reporting of changes, but it's better then a hard failure
+ // (needed for certain tests that spyOn input values)
+ if (node.hasOwnProperty(valueField)) {
+ return;
+ }
+
+ Object.defineProperty(node, valueField, {
+ enumerable: descriptor.enumerable,
+ configurable: true,
+ get: function() {
+ return descriptor.get.call(this);
+ },
+ set: function(value) {
+ currentValue = '' + value;
+ descriptor.set.call(this, value);
+ },
+ });
+
+ attachTracker(inst, {
+ getValue() {
+ return currentValue;
+ },
+ setValue(value) {
+ currentValue = '' + value;
+ },
+ stopTracking() {
+ detachTracker(inst);
+ delete node[valueField];
+ },
+ });
+ },
+
+ updateValueIfChanged(inst) {
+ if (!inst) {
+ return false;
+ }
+ var tracker = getTracker(inst);
+
+ if (!tracker) {
+ inputValueTracking.track(inst);
+ return true;
+ }
+
+ var lastValue = tracker.getValue();
+ var nextValue = getValueFromNode(
+ ReactDOMComponentTree.getNodeFromInstance(inst)
+ );
+
+ if (nextValue !== lastValue) {
+ tracker.setValue(nextValue);
+ return true;
+ }
+
+ return false;
+ },
+
+ stopTracking(inst) {
+ var tracker = getTracker(inst);
+ if (tracker) {
+ tracker.stopTracking();
+ }
+ },
+};
+
+module.exports = inputValueTracking;
diff --git a/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js b/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js
index 59eaec42bfc66..0e13423b3db76 100644
--- a/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js
+++ b/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js
@@ -21,6 +21,7 @@ describe('ReactDOMInput', () => {
var ReactLink;
var ReactTestUtils;
+
beforeEach(() => {
jest.resetModuleRegistry();
React = require('React');
diff --git a/src/renderers/dom/shared/ReactDOMComponent.js b/src/renderers/dom/shared/ReactDOMComponent.js
index 090efcaf50916..6c7a1ca8bd012 100644
--- a/src/renderers/dom/shared/ReactDOMComponent.js
+++ b/src/renderers/dom/shared/ReactDOMComponent.js
@@ -37,6 +37,7 @@ var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
var invariant = require('invariant');
var isEventSupported = require('isEventSupported');
var shallowEqual = require('shallowEqual');
+var inputValueTracking = require('inputValueTracking');
var validateDOMNesting = require('validateDOMNesting');
var warning = require('warning');
@@ -316,6 +317,10 @@ var mediaEvents = {
topWaiting: 'waiting',
};
+function trackInputValue() {
+ inputValueTracking.track(this);
+}
+
function trapBubbledEventsLocal() {
var inst = this;
// If a component renders to null or if another component fatals and causes
@@ -522,6 +527,7 @@ ReactDOMComponent.Mixin = {
case 'input':
ReactDOMInput.mountWrapper(this, props, hostParent);
props = ReactDOMInput.getHostProps(this, props);
+ transaction.getReactMountReady().enqueue(trackInputValue, this);
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
break;
case 'option':
@@ -536,6 +542,7 @@ ReactDOMComponent.Mixin = {
case 'textarea':
ReactDOMTextarea.mountWrapper(this, props, hostParent);
props = ReactDOMTextarea.getHostProps(this, props);
+ transaction.getReactMountReady().enqueue(trackInputValue, this);
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
break;
}
@@ -1149,6 +1156,10 @@ ReactDOMComponent.Mixin = {
}
}
break;
+ case 'input':
+ case 'textarea':
+ inputValueTracking.stopTracking(this);
+ break;
case 'html':
case 'head':
case 'body':
diff --git a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
index 8c041dba86827..c685d06a0d85f 100644
--- a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
+++ b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
@@ -16,6 +16,7 @@ describe('ReactDOMComponent', () => {
var ReactDOM;
var ReactDOMFeatureFlags;
var ReactDOMServer;
+ var inputValueTracking;
function normalizeCodeLocInfo(str) {
return str.replace(/\(at .+?:\d+\)/g, '(at **)');
@@ -27,6 +28,7 @@ describe('ReactDOMComponent', () => {
ReactDOM = require('ReactDOM');
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
ReactDOMServer = require('ReactDOMServer');
+ inputValueTracking = require('inputValueTracking');
});
describe('updateDOM', () => {
@@ -914,7 +916,26 @@ describe('ReactDOMComponent', () => {
);
});
- it('should execute custom event plugin listening behavior', () => {
+
+ it('should track input values', function() {
+ var container = document.createElement('div');
+ var inst = ReactDOM.render(, container);
+
+ var tracker = inputValueTracking._getTrackerFromNode(inst);
+
+ expect(tracker.getValue()).toEqual('foo');
+ });
+
+ it('should track textarea values', function() {
+ var container = document.createElement('div');
+ var inst = ReactDOM.render(, container);
+
+ var tracker = inputValueTracking._getTrackerFromNode(inst);
+
+ expect(tracker.getValue()).toEqual('foo');
+ });
+
+ it('should execute custom event plugin listening behavior', function() {
var SimpleEventPlugin = require('SimpleEventPlugin');
SimpleEventPlugin.didPutListener = jest.fn();
@@ -1123,7 +1144,32 @@ describe('ReactDOMComponent', () => {
expect(EventPluginHub.getListener(inst, 'onClick')).toBe(undefined);
});
- it('unmounts children before unsetting DOM node info', () => {
+
+ it('should clean up input value tracking', function() {
+ var container = document.createElement('div');
+ var node = ReactDOM.render(, container);
+ var tracker = inputValueTracking._getTrackerFromNode(node);
+
+ spyOn(tracker, 'stopTracking');
+
+ ReactDOM.unmountComponentAtNode(container);
+
+ expect(tracker.stopTracking.calls.count()).toBe(1);
+ });
+
+ it('should clean up input textarea tracking', function() {
+ var container = document.createElement('div');
+ var node = ReactDOM.render(, container);
+ var tracker = inputValueTracking._getTrackerFromNode(node);
+
+ spyOn(tracker, 'stopTracking');
+
+ ReactDOM.unmountComponentAtNode(container);
+
+ expect(tracker.stopTracking.calls.count()).toBe(1);
+ });
+
+ it('unmounts children before unsetting DOM node info', function() {
class Inner extends React.Component {
render() {
return ;
diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js
index b53528fc136c0..9d068581fcdc5 100644
--- a/src/test/ReactTestUtils.js
+++ b/src/test/ReactTestUtils.js
@@ -332,6 +332,7 @@ var ReactTestUtils = {
*/
simulateNativeEventOnNode: function(topLevelType, node, fakeNativeEvent) {
fakeNativeEvent.target = node;
+ fakeNativeEvent.simulated = true;
ReactBrowserEventEmitter.ReactEventListener.dispatchEvent(
topLevelType,
fakeNativeEvent,