From 3bc64327f01a2c9fd1529934ebf4a484c55d7e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 11 Aug 2017 19:02:10 -0700 Subject: [PATCH] Use the virtual target for change events to avoid restoring controlled state on the real target (#10444) * Add test for nested controlled selects The failure repros only when two events are fired sequentially, where the second event is what updates the value. * Add failure case to DOM select fixture * Use virtual event target for change event instead of native target Ensures that we use the same node to make decisions instead of two like we do now. This will cause restore to be called or not called consistently. * Pass native event target to the event We normally pass the native event target to the event object. This ensures that we do the same thing here. We still want to schedule a "restore" on the virtual target though since those are the only nodes known to React. --- .../src/components/fixtures/selects/index.js | 34 +++++++++- .../shared/eventPlugins/ChangeEventPlugin.js | 12 +++- .../wrappers/__tests__/ReactDOMSelect-test.js | 64 +++++++++++++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/fixtures/dom/src/components/fixtures/selects/index.js b/fixtures/dom/src/components/fixtures/selects/index.js index ddf5d735fc8eb..b89e63314b2dd 100644 --- a/fixtures/dom/src/components/fixtures/selects/index.js +++ b/fixtures/dom/src/components/fixtures/selects/index.js @@ -1,10 +1,34 @@ const React = window.React; +const ReactDOM = window.ReactDOM; class SelectFixture extends React.Component { state = {value: ''}; + _nestedDOMNode = null; + onChange = event => { this.setState({value: event.target.value}); }; + + componentDidMount() { + this._renderNestedSelect(); + } + + componentDidUpdate() { + this._renderNestedSelect(); + } + + _renderNestedSelect() { + ReactDOM.render( + , + this._nestedDOMNode + ); + } + render() { return (
@@ -24,8 +48,16 @@ class SelectFixture extends React.Component { - + + + +
+ Controlled in nested subtree +
(this._nestedDOMNode = node)} /> + + This should synchronize in both direction with the one above. +
); diff --git a/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js b/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js index 6381eb54eded7..ba1d7b0461659 100644 --- a/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js +++ b/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js @@ -47,16 +47,21 @@ function shouldUseChangeEvent(elem) { ); } -function createAndAccumulateChangeEvent(inst, nativeEvent, target) { +function createAndAccumulateChangeEvent( + inst, + nativeEvent, + nativeEventTarget, + virtualTarget, +) { var event = SyntheticEvent.getPooled( eventTypes.change, inst, nativeEvent, - target, + nativeEventTarget, ); event.type = 'change'; // Flag this event loop as needing state restore. - ReactControlledComponent.enqueueStateRestore(target); + ReactControlledComponent.enqueueStateRestore(virtualTarget); EventPropagators.accumulateTwoPhaseDispatches(event); return event; } @@ -175,6 +180,7 @@ var ChangeEventPlugin = { inst, nativeEvent, nativeEventTarget, + targetNode, ); return event; } diff --git a/src/renderers/dom/shared/wrappers/__tests__/ReactDOMSelect-test.js b/src/renderers/dom/shared/wrappers/__tests__/ReactDOMSelect-test.js index 5aa28eff34ecd..95c547b7b9e8d 100644 --- a/src/renderers/dom/shared/wrappers/__tests__/ReactDOMSelect-test.js +++ b/src/renderers/dom/shared/wrappers/__tests__/ReactDOMSelect-test.js @@ -589,4 +589,68 @@ describe('ReactDOMSelect', () => { expect(node.options[1].selected).toBe(true); // b expect(node.options[2].selected).toBe(false); // c }); + + it('should allow controlling `value` in a nested render', () => { + var selectNode; + + class Parent extends React.Component { + state = { + value: 'giraffe', + }; + + componentDidMount() { + this._renderNested(); + } + + componentDidUpdate() { + this._renderNested(); + } + + _handleChange(event) { + this.setState({value: event.target.value}); + } + + _renderNested() { + ReactDOM.render( + , + this._nestingContainer, + ); + } + + render() { + return
(this._nestingContainer = n)} />; + } + } + + var container = document.createElement('div'); + + document.body.appendChild(container); + + ReactDOM.render(, container); + + expect(selectNode.value).toBe('giraffe'); + + selectNode.value = 'gorilla'; + + let nativeEvent = document.createEvent('Event'); + nativeEvent.initEvent('input', true, true); + selectNode.dispatchEvent(nativeEvent); + + expect(selectNode.value).toEqual('gorilla'); + + nativeEvent = document.createEvent('Event'); + nativeEvent.initEvent('change', true, true); + selectNode.dispatchEvent(nativeEvent); + + expect(selectNode.value).toEqual('gorilla'); + + document.body.removeChild(container); + }); });