@samitouri / QOS-React / commits / 171e0b7d44

Fix “no onChange handler” warning to fire on falsy values ("", 0, false) too (#12628)

* throw warning for falsey `value` prop * add nop onChange handler to tests for `value` prop * prettier * check for falsey checked * fix tests for `checked` prop * new tests for `value` prop * test formatting * forgot 0 (: * test for falsey `checked` prop * add null check * Update ReactDOMInput-test.js * revert unneeded change * prettier * Update DOMPropertyOperations-test.js * Update ReactDOMInput-test.js * Update ReactDOMSelect-test.js * Fixes and tests * Remove unnecessary changes

Nicole Levy committed Jul 17, 2018 at 17:46 UTC 171e0b7d447a20b7cc90808a95478c6e5b8b6af0
5 files changed +128 -36
packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
+1 -1
@@ -79,7 +79,7 @@ describe('DOMPropertyOperations', () => {
79
80 it('should not remove empty attributes for special properties', () => {
81 const container = document.createElement('div');
82 - ReactDOM.render(<input value="" />, container);
82 + ReactDOM.render(<input value="" onChange={() => {}} />, container);
83 expect(container.firstChild.getAttribute('value')).toBe('');
84 expect(container.firstChild.value).toBe('');
85 });
packages/react-dom/src/__tests__/ReactDOMInput-test.js
+106 -30
@@ -47,6 +47,71 @@ describe('ReactDOMInput', () => {
47 document.body.removeChild(container);
48 });
49
50 + it('should warn for controlled value of 0 with missing onChange', () => {
51 + expect(() => {
52 + ReactDOM.render(<input type="text" value={0} />, container);
53 + }).toWarnDev(
54 + 'Failed prop type: You provided a `value` prop to a form field without an `onChange` handler.',
55 + );
56 + });
57 +
58 + it('should warn for controlled value of "" with missing onChange', () => {
59 + expect(() => {
60 + ReactDOM.render(<input type="text" value="" />, container);
61 + }).toWarnDev(
62 + 'Failed prop type: You provided a `value` prop to a form field without an `onChange` handler.',
63 + );
64 + });
65 +
66 + it('should warn for controlled value of "0" with missing onChange', () => {
67 + expect(() => {
68 + ReactDOM.render(<input type="text" value="0" />, container);
69 + }).toWarnDev(
70 + 'Failed prop type: You provided a `value` prop to a form field without an `onChange` handler.',
71 + );
72 + });
73 +
74 + it('should warn for controlled value of false with missing onChange', () => {
75 + expect(() =>
76 + ReactDOM.render(<input type="checkbox" checked={false} />, container),
77 + ).toWarnDev(
78 + 'Failed prop type: You provided a `checked` prop to a form field without an `onChange` handler.',
79 + );
80 + });
81 +
82 + it('should warn with checked and no onChange handler with readOnly specified', () => {
83 + ReactDOM.render(
84 + <input type="checkbox" checked={false} readOnly={true} />,
85 + container,
86 + );
87 + ReactDOM.unmountComponentAtNode(container);
88 +
89 + expect(() =>
90 + ReactDOM.render(
91 + <input type="checkbox" checked={false} readOnly={false} />,
92 + container,
93 + ),
94 + ).toWarnDev(
95 + 'Failed prop type: You provided a `checked` prop to a form field without an `onChange` handler. ' +
96 + 'This will render a read-only field. If the field should be mutable use `defaultChecked`. ' +
97 + 'Otherwise, set either `onChange` or `readOnly`.',
98 + );
99 + });
100 +
101 + it('should not warn about missing onChange in uncontrolled inputs', () => {
102 + ReactDOM.render(<input />, container);
103 + ReactDOM.unmountComponentAtNode(container);
104 + ReactDOM.render(<input value={undefined} />, container);
105 + ReactDOM.unmountComponentAtNode(container);
106 + ReactDOM.render(<input type="text" />, container);
107 + ReactDOM.unmountComponentAtNode(container);
108 + ReactDOM.render(<input type="text" value={undefined} />, container);
109 + ReactDOM.unmountComponentAtNode(container);
110 + ReactDOM.render(<input type="checkbox" />, container);
111 + ReactDOM.unmountComponentAtNode(container);
112 + ReactDOM.render(<input type="checkbox" checked={undefined} />, container);
113 + });
114 +
115 it('should properly control a value even if no event listener exists', () => {
116 let node;
117
@@ -452,7 +517,7 @@ describe('ReactDOMInput', () => {
517 });
518
519 it('should display `value` of number 0', () => {
455 - const stub = <input type="text" value={0} />;
520 + const stub = <input type="text" value={0} onChange={emptyFunction} />;
521 const node = ReactDOM.render(stub, container);
522
523 expect(node.value).toBe('0');
@@ -590,8 +655,14 @@ describe('ReactDOMInput', () => {
655 });
656
657 it('should properly transition from an empty value to 0', function() {
593 - ReactDOM.render(<input type="text" value="" />, container);
594 - ReactDOM.render(<input type="text" value={0} />, container);
658 + ReactDOM.render(
659 + <input type="text" value="" onChange={emptyFunction} />,
660 + container,
661 + );
662 + ReactDOM.render(
663 + <input type="text" value={0} onChange={emptyFunction} />,
664 + container,
665 + );
666
667 const node = container.firstChild;
668
@@ -600,8 +671,14 @@ describe('ReactDOMInput', () => {
671 });
672
673 it('should properly transition from 0 to an empty value', function() {
603 - ReactDOM.render(<input type="text" value={0} />, container);
604 - ReactDOM.render(<input type="text" value="" />, container);
674 + ReactDOM.render(
675 + <input type="text" value={0} onChange={emptyFunction} />,
676 + container,
677 + );
678 + ReactDOM.render(
679 + <input type="text" value="" onChange={emptyFunction} />,
680 + container,
681 + );
682
683 const node = container.firstChild;
684
@@ -610,8 +687,14 @@ describe('ReactDOMInput', () => {
687 });
688
689 it('should properly transition a text input from 0 to an empty 0.0', function() {
613 - ReactDOM.render(<input type="text" value={0} />, container);
614 - ReactDOM.render(<input type="text" value="0.0" />, container);
690 + ReactDOM.render(
691 + <input type="text" value={0} onChange={emptyFunction} />,
692 + container,
693 + );
694 + ReactDOM.render(
695 + <input type="text" value="0.0" onChange={emptyFunction} />,
696 + container,
697 + );
698
699 const node = container.firstChild;
700
@@ -620,8 +703,14 @@ describe('ReactDOMInput', () => {
703 });
704
705 it('should properly transition a number input from "" to 0', function() {
623 - ReactDOM.render(<input type="number" value="" />, container);
624 - ReactDOM.render(<input type="number" value={0} />, container);
706 + ReactDOM.render(
707 + <input type="number" value="" onChange={emptyFunction} />,
708 + container,
709 + );
710 + ReactDOM.render(
711 + <input type="number" value={0} onChange={emptyFunction} />,
712 + container,
713 + );
714
715 const node = container.firstChild;
716
@@ -630,8 +719,14 @@ describe('ReactDOMInput', () => {
719 });
720
721 it('should properly transition a number input from "" to "0"', function() {
633 - ReactDOM.render(<input type="number" value="" />, container);
634 - ReactDOM.render(<input type="number" value="0" />, container);
722 + ReactDOM.render(
723 + <input type="number" value="" onChange={emptyFunction} />,
724 + container,
725 + );
726 + ReactDOM.render(
727 + <input type="number" value="0" onChange={emptyFunction} />,
728 + container,
729 + );
730
731 const node = container.firstChild;
732
@@ -874,25 +969,6 @@ describe('ReactDOMInput', () => {
969 dispatchEventOnNode(node, 'input');
970 });
971
877 - it('should warn with checked and no onChange handler with readOnly specified', () => {
878 - ReactDOM.render(
879 - <input type="checkbox" checked="false" readOnly={true} />,
880 - container,
881 - );
882 - ReactDOM.unmountComponentAtNode(container);
883 -
884 - expect(() =>
885 - ReactDOM.render(
886 - <input type="checkbox" checked="false" readOnly={false} />,
887 - container,
888 - ),
889 - ).toWarnDev(
890 - 'Failed prop type: You provided a `checked` prop to a form field without an `onChange` handler. ' +
891 - 'This will render a read-only field. If the field should be mutable use `defaultChecked`. ' +
892 - 'Otherwise, set either `onChange` or `readOnly`.',
893 - );
894 - });
895 -
972 it('should update defaultValue to empty string', () => {
973 ReactDOM.render(<input type="text" defaultValue={'foo'} />, container);
974 ReactDOM.render(<input type="text" defaultValue={''} />, container);
packages/react-dom/src/__tests__/ReactDOMSelect-test.js
+7
@@ -654,6 +654,13 @@ describe('ReactDOMSelect', () => {
654 );
655 });
656
657 + it('should not warn about missing onChange in uncontrolled textareas', () => {
658 + const container = document.createElement('div');
659 + ReactDOM.render(<select />, container);
660 + ReactDOM.unmountComponentAtNode(container);
661 + ReactDOM.render(<select value={undefined} />, container);
662 + });
663 +
664 it('should be able to safely remove select onChange', () => {
665 function changeView() {
666 ReactDOM.unmountComponentAtNode(container);
packages/react-dom/src/__tests__/ReactDOMTextarea-test.js
+8 -1
@@ -97,7 +97,7 @@ describe('ReactDOMTextarea', () => {
97 });
98
99 it('should display `value` of number 0', () => {
100 - const stub = <textarea value={0} />;
100 + const stub = <textarea value={0} onChange={emptyFunction} />;
101 const node = renderTextarea(stub);
102
103 expect(node.value).toBe('0');
@@ -416,4 +416,11 @@ describe('ReactDOMTextarea', () => {
416 <textarea value="foo" defaultValue="bar" readOnly={true} />,
417 );
418 });
419 +
420 + it('should not warn about missing onChange in uncontrolled textareas', () => {
421 + const container = document.createElement('div');
422 + ReactDOM.render(<textarea />, container);
423 + ReactDOM.unmountComponentAtNode(container);
424 + ReactDOM.render(<textarea value={undefined} />, container);
425 + });
426 });
packages/react-dom/src/shared/ReactControlledValuePropTypes.js
+6 -4
@@ -30,11 +30,12 @@ if (__DEV__) {
30 const propTypes = {
31 value: function(props, propName, componentName) {
32 if (
33 - !props[propName] ||
33 + !(propName in props) ||
34 hasReadOnlyValue[props.type] ||
35 props.onChange ||
36 props.readOnly ||
37 - props.disabled
37 + props.disabled ||
38 + props[propName] == null
39 ) {
40 return null;
41 }
@@ -47,10 +48,11 @@ if (__DEV__) {
48 },
49 checked: function(props, propName, componentName) {
50 if (
50 - !props[propName] ||
51 + !(propName in props) ||
52 props.onChange ||
53 props.readOnly ||
53 - props.disabled
54 + props.disabled ||
55 + props[propName] == null
56 ) {
57 return null;
58 }