From 68347c998571afa4fcce2fe7f4bab8cff6694980 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Sat, 20 May 2017 14:15:12 -0400 Subject: [PATCH] Backport input fix (#8575) * Only fire input value change events when the value changes (#5746) * Allow simulated native events to propagate fixes #7211 fixes #6822 fixes #6614 we should make sure it doesn't break #3926 any worse (or works with #8438) --- .../__tests__/inputValueTracking-test.js | 165 +++++++++++++++ .../client/eventPlugins/ChangeEventPlugin.js | 189 ++++++++--------- .../__tests__/ChangeEventPlugin-test.js | 193 +++++++++++++++++- .../dom/client/inputValueTracking.js | 133 ++++++++++++ .../wrappers/__tests__/ReactDOMInput-test.js | 1 + src/renderers/dom/shared/ReactDOMComponent.js | 11 + .../__tests__/ReactDOMComponent-test.js | 50 ++++- src/test/ReactTestUtils.js | 1 + 8 files changed, 640 insertions(+), 103 deletions(-) create mode 100644 src/renderers/dom/client/__tests__/inputValueTracking-test.js create mode 100644 src/renderers/dom/client/inputValueTracking.js 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(