@samitouri / QOS-React / commits / 3bc64327f0

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.

Sebastian Markbåge committed Aug 11, 2017 at 19:02 UTC 3bc64327f01a2c9fd1529934ebf4a484c55d7e94
3 files changed +106 -4
fixtures/dom/src/components/fixtures/selects/index.js
+33 -1
@@ -1,10 +1,34 @@
1 const React = window.React;
2 +const ReactDOM = window.ReactDOM;
3
4 class SelectFixture extends React.Component {
5 state = {value: ''};
6 + _nestedDOMNode = null;
7 +
8 onChange = event => {
9 this.setState({value: event.target.value});
10 };
11 +
12 + componentDidMount() {
13 + this._renderNestedSelect();
14 + }
15 +
16 + componentDidUpdate() {
17 + this._renderNestedSelect();
18 + }
19 +
20 + _renderNestedSelect() {
21 + ReactDOM.render(
22 + <select value={this.state.value} onChange={this.onChange}>
23 + <option value="">Select a color</option>
24 + <option value="red">Red</option>
25 + <option value="blue">Blue</option>
26 + <option value="green">Green</option>
27 + </select>,
28 + this._nestedDOMNode
29 + );
30 + }
31 +
32 render() {
33 return (
34 <form>
@@ -24,8 +48,16 @@ class SelectFixture extends React.Component {
48 <option value="">Select a color</option>
49 <option value="red">Red</option>
50 <option value="blue">Blue</option>
27 - <option value="gree">Green</option>
51 + <option value="green">Green</option>
52 </select>
53 + <span className="hint" />
54 + </fieldset>
55 + <fieldset>
56 + <legend>Controlled in nested subtree</legend>
57 + <div ref={node => (this._nestedDOMNode = node)} />
58 + <span className="hint">
59 + This should synchronize in both direction with the one above.
60 + </span>
61 </fieldset>
62 </form>
63 );
src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js
+9 -3
@@ -47,16 +47,21 @@ function shouldUseChangeEvent(elem) {
47 );
48 }
49
50 -function createAndAccumulateChangeEvent(inst, nativeEvent, target) {
50 +function createAndAccumulateChangeEvent(
51 + inst,
52 + nativeEvent,
53 + nativeEventTarget,
54 + virtualTarget,
55 +) {
56 var event = SyntheticEvent.getPooled(
57 eventTypes.change,
58 inst,
59 nativeEvent,
55 - target,
60 + nativeEventTarget,
61 );
62 event.type = 'change';
63 // Flag this event loop as needing state restore.
59 - ReactControlledComponent.enqueueStateRestore(target);
64 + ReactControlledComponent.enqueueStateRestore(virtualTarget);
65 EventPropagators.accumulateTwoPhaseDispatches(event);
66 return event;
67 }
@@ -175,6 +180,7 @@ var ChangeEventPlugin = {
180 inst,
181 nativeEvent,
182 nativeEventTarget,
183 + targetNode,
184 );
185 return event;
186 }
src/renderers/dom/shared/wrappers/__tests__/ReactDOMSelect-test.js
+64
@@ -589,4 +589,68 @@ describe('ReactDOMSelect', () => {
589 expect(node.options[1].selected).toBe(true); // b
590 expect(node.options[2].selected).toBe(false); // c
591 });
592 +
593 + it('should allow controlling `value` in a nested render', () => {
594 + var selectNode;
595 +
596 + class Parent extends React.Component {
597 + state = {
598 + value: 'giraffe',
599 + };
600 +
601 + componentDidMount() {
602 + this._renderNested();
603 + }
604 +
605 + componentDidUpdate() {
606 + this._renderNested();
607 + }
608 +
609 + _handleChange(event) {
610 + this.setState({value: event.target.value});
611 + }
612 +
613 + _renderNested() {
614 + ReactDOM.render(
615 + <select
616 + onChange={this._handleChange.bind(this)}
617 + ref={n => (selectNode = n)}
618 + value={this.state.value}>
619 + <option value="monkey">A monkey!</option>
620 + <option value="giraffe">A giraffe!</option>
621 + <option value="gorilla">A gorilla!</option>
622 + </select>,
623 + this._nestingContainer,
624 + );
625 + }
626 +
627 + render() {
628 + return <div ref={n => (this._nestingContainer = n)} />;
629 + }
630 + }
631 +
632 + var container = document.createElement('div');
633 +
634 + document.body.appendChild(container);
635 +
636 + ReactDOM.render(<Parent />, container);
637 +
638 + expect(selectNode.value).toBe('giraffe');
639 +
640 + selectNode.value = 'gorilla';
641 +
642 + let nativeEvent = document.createEvent('Event');
643 + nativeEvent.initEvent('input', true, true);
644 + selectNode.dispatchEvent(nativeEvent);
645 +
646 + expect(selectNode.value).toEqual('gorilla');
647 +
648 + nativeEvent = document.createEvent('Event');
649 + nativeEvent.initEvent('change', true, true);
650 + selectNode.dispatchEvent(nativeEvent);
651 +
652 + expect(selectNode.value).toEqual('gorilla');
653 +
654 + document.body.removeChild(container);
655 + });
656 });