@samitouri / QOS-React / commits / 323efbc33c

Ensure value and defaultValue do not assign functions and symbols (#11741)

* Ensure value and defaultValue do not assign functions and symbols * Eliminate assignProperty method from ReactDOMInput * Restore original placement of defaultValue reservedProp * Reduce branching. Make assignment more consistent * Control for warnings in symbol/function tests * Add boolean to readOnly assignments * Tweak the tests * Invalid value attributes should convert to an empty string * Revert ChangeEventPlugin update. See #11746 * Format * Replace shouldSetAttribute call with value specific type check DOMProperty.shouldSetAttribute runs a few other checks that aren't appropriate for determining if a value or defaultValue should be assigned on an input. This commit replaces that call with an input specific check. * Remove unused import * Eliminate unnecessary numeric equality checks (#11751) * Eliminate unnecessary numeric equality checks This commit changes the way numeric equality for number inputs works such that it compares against `input.valueAsNumber`. This eliminates quite a bit of branching around numeric equality. * There is no need to compare valueAsNumber * Add test cases for empty string to 0. * Avoid implicit boolean JSX props * Split up numeric equality test to isolate eslint disable command * Fix typo in ReactDOMInput test * Add todos * Update the attribute table

Nathan Hunzaker committed Dec 4, 2017 at 09:39 UTC 323efbc33c27a602a4aab8519f58feba1e0a216c
5 files changed +234 -42
fixtures/attribute-behavior/AttributeTableSnapshot.md
+7 -7
@@ -2593,8 +2593,8 @@
2593 | `defaultValue=(string 'false')`| (changed)| `"false"` |
2594 | `defaultValue=(string 'on')`| (changed)| `"on"` |
2595 | `defaultValue=(string 'off')`| (changed)| `"off"` |
2596 -| `defaultValue=(symbol)`| (changed, error, warning, ssr error)| `` |
2597 -| `defaultValue=(function)`| (changed, ssr warning)| `"function f() {}"` |
2596 +| `defaultValue=(symbol)`| (initial, ssr error, ssr mismatch)| `<empty string>` |
2597 +| `defaultValue=(function)`| (initial, ssr mismatch)| `<empty string>` |
2598 | `defaultValue=(null)`| (initial, ssr warning)| `<empty string>` |
2599 | `defaultValue=(undefined)`| (initial)| `<empty string>` |
2600
@@ -11768,8 +11768,8 @@
11768 | `value=(string 'false')`| (changed)| `"false"` |
11769 | `value=(string 'on')`| (changed)| `"on"` |
11770 | `value=(string 'off')`| (changed)| `"off"` |
11771 -| `value=(symbol)`| (changed, error, warning, ssr error)| `` |
11772 -| `value=(function)`| (changed, warning, ssr warning)| `"function f() {}"` |
11771 +| `value=(symbol)`| (initial, warning, ssr error, ssr mismatch)| `<empty string>` |
11772 +| `value=(function)`| (initial, warning, ssr mismatch)| `<empty string>` |
11773 | `value=(null)`| (initial, warning, ssr warning)| `<empty string>` |
11774 | `value=(undefined)`| (initial)| `<empty string>` |
11775
@@ -11793,8 +11793,8 @@
11793 | `value=(string 'false')`| (changed)| `"false"` |
11794 | `value=(string 'on')`| (changed)| `"on"` |
11795 | `value=(string 'off')`| (changed)| `"off"` |
11796 -| `value=(symbol)`| (changed, error, warning, ssr error)| `` |
11797 -| `value=(function)`| (changed, warning)| `"function f() {}"` |
11796 +| `value=(symbol)`| (initial, warning, ssr error, ssr mismatch)| `<empty string>` |
11797 +| `value=(function)`| (initial, warning, ssr mismatch)| `<empty string>` |
11798 | `value=(null)`| (initial, warning, ssr warning)| `<empty string>` |
11799 | `value=(undefined)`| (initial)| `<empty string>` |
11800
@@ -11818,7 +11818,7 @@
11818 | `value=(string 'false')`| (initial)| `<empty string>` |
11819 | `value=(string 'on')`| (initial)| `<empty string>` |
11820 | `value=(string 'off')`| (initial)| `<empty string>` |
11821 -| `value=(symbol)`| (changed, error, warning, ssr error)| `` |
11821 +| `value=(symbol)`| (initial, warning, ssr error, ssr mismatch)| `<empty string>` |
11822 | `value=(function)`| (initial, warning)| `<empty string>` |
11823 | `value=(null)`| (initial, warning, ssr warning)| `<empty string>` |
11824 | `value=(undefined)`| (initial)| `<empty string>` |
packages/react-dom/src/__tests__/ReactDOMInput-test.js
+183
@@ -248,6 +248,23 @@ describe('ReactDOMInput', () => {
248 }
249 });
250
251 + it('performs a state change from "" to 0', () => {
252 + class Stub extends React.Component {
253 + state = {
254 + value: '',
255 + };
256 + render() {
257 + return <input type="number" value={this.state.value} readOnly={true} />;
258 + }
259 + }
260 +
261 + var stub = ReactTestUtils.renderIntoDocument(<Stub />);
262 + var node = ReactDOM.findDOMNode(stub);
263 + stub.setState({value: 0});
264 +
265 + expect(node.value).toEqual('0');
266 + });
267 +
268 it('distinguishes precision for extra zeroes in string number values', () => {
269 spyOnDev(console, 'error');
270 class Stub extends React.Component {
@@ -595,6 +612,7 @@ describe('ReactDOMInput', () => {
612 var node = container.firstChild;
613
614 expect(node.value).toBe('0');
615 + expect(node.defaultValue).toBe('0');
616 });
617
618 it('should properly transition from 0 to an empty value', function() {
@@ -606,6 +624,43 @@ describe('ReactDOMInput', () => {
624 var node = container.firstChild;
625
626 expect(node.value).toBe('');
627 + expect(node.defaultValue).toBe('');
628 + });
629 +
630 + it('should properly transition a text input from 0 to an empty 0.0', function() {
631 + var container = document.createElement('div');
632 +
633 + ReactDOM.render(<input type="text" value={0} />, container);
634 + ReactDOM.render(<input type="text" value="0.0" />, container);
635 +
636 + var node = container.firstChild;
637 +
638 + expect(node.value).toBe('0.0');
639 + expect(node.defaultValue).toBe('0.0');
640 + });
641 +
642 + it('should properly transition a number input from "" to 0', function() {
643 + var container = document.createElement('div');
644 +
645 + ReactDOM.render(<input type="number" value="" />, container);
646 + ReactDOM.render(<input type="number" value={0} />, container);
647 +
648 + var node = container.firstChild;
649 +
650 + expect(node.value).toBe('0');
651 + expect(node.defaultValue).toBe('0');
652 + });
653 +
654 + it('should properly transition a number input from "" to "0"', function() {
655 + var container = document.createElement('div');
656 +
657 + ReactDOM.render(<input type="number" value="" />, container);
658 + ReactDOM.render(<input type="number" value="0" />, container);
659 +
660 + var node = container.firstChild;
661 +
662 + expect(node.value).toBe('0');
663 + expect(node.defaultValue).toBe('0');
664 });
665
666 it('should have the correct target value', () => {
@@ -1585,4 +1640,132 @@ describe('ReactDOMInput', () => {
1640 }
1641 });
1642 });
1643 +
1644 + describe('When given a Symbol value', function() {
1645 + it('treats initial Symbol value as an empty string', function() {
1646 + spyOnDev(console, 'error');
1647 + var container = document.createElement('div');
1648 + ReactDOM.render(
1649 + <input value={Symbol('foobar')} onChange={() => {}} />,
1650 + container,
1651 + );
1652 + var node = container.firstChild;
1653 +
1654 + expect(node.value).toBe('');
1655 + expect(node.getAttribute('value')).toBe('');
1656 +
1657 + if (__DEV__) {
1658 + expect(console.error.calls.count()).toBe(1);
1659 + expect(console.error.calls.argsFor(0)[0]).toContain(
1660 + 'Invalid value for prop `value`',
1661 + );
1662 + }
1663 + });
1664 +
1665 + it('treats updated Symbol value as an empty string', function() {
1666 + spyOnDev(console, 'error');
1667 + var container = document.createElement('div');
1668 + ReactDOM.render(<input value="foo" onChange={() => {}} />, container);
1669 + ReactDOM.render(
1670 + <input value={Symbol('foobar')} onChange={() => {}} />,
1671 + container,
1672 + );
1673 + var node = container.firstChild;
1674 +
1675 + expect(node.value).toBe('');
1676 + expect(node.getAttribute('value')).toBe('');
1677 +
1678 + if (__DEV__) {
1679 + expect(console.error.calls.count()).toBe(1);
1680 + expect(console.error.calls.argsFor(0)[0]).toContain(
1681 + 'Invalid value for prop `value`',
1682 + );
1683 + }
1684 + });
1685 +
1686 + it('treats initial Symbol defaultValue as an empty string', function() {
1687 + var container = document.createElement('div');
1688 + ReactDOM.render(<input defaultValue={Symbol('foobar')} />, container);
1689 + var node = container.firstChild;
1690 +
1691 + expect(node.value).toBe('');
1692 + expect(node.getAttribute('value')).toBe('');
1693 + // TODO: we should warn here.
1694 + });
1695 +
1696 + it('treats updated Symbol defaultValue as an empty string', function() {
1697 + var container = document.createElement('div');
1698 + ReactDOM.render(<input defaultValue="foo" />, container);
1699 + ReactDOM.render(<input defaultValue={Symbol('foobar')} />, container);
1700 + var node = container.firstChild;
1701 +
1702 + expect(node.value).toBe('foo');
1703 + expect(node.getAttribute('value')).toBe('');
1704 + // TODO: we should warn here.
1705 + });
1706 + });
1707 +
1708 + describe('When given a function value', function() {
1709 + it('treats initial function value as an empty string', function() {
1710 + spyOnDev(console, 'error');
1711 + var container = document.createElement('div');
1712 + ReactDOM.render(
1713 + <input value={() => {}} onChange={() => {}} />,
1714 + container,
1715 + );
1716 + var node = container.firstChild;
1717 +
1718 + expect(node.value).toBe('');
1719 + expect(node.getAttribute('value')).toBe('');
1720 +
1721 + if (__DEV__) {
1722 + expect(console.error.calls.count()).toBe(1);
1723 + expect(console.error.calls.argsFor(0)[0]).toContain(
1724 + 'Invalid value for prop `value`',
1725 + );
1726 + }
1727 + });
1728 +
1729 + it('treats updated function value as an empty string', function() {
1730 + spyOnDev(console, 'error');
1731 + var container = document.createElement('div');
1732 + ReactDOM.render(<input value="foo" onChange={() => {}} />, container);
1733 + ReactDOM.render(
1734 + <input value={() => {}} onChange={() => {}} />,
1735 + container,
1736 + );
1737 + var node = container.firstChild;
1738 +
1739 + expect(node.value).toBe('');
1740 + expect(node.getAttribute('value')).toBe('');
1741 +
1742 + if (__DEV__) {
1743 + expect(console.error.calls.count()).toBe(1);
1744 + expect(console.error.calls.argsFor(0)[0]).toContain(
1745 + 'Invalid value for prop `value`',
1746 + );
1747 + }
1748 + });
1749 +
1750 + it('treats initial function defaultValue as an empty string', function() {
1751 + var container = document.createElement('div');
1752 + ReactDOM.render(<input defaultValue={() => {}} />, container);
1753 + var node = container.firstChild;
1754 +
1755 + expect(node.value).toBe('');
1756 + expect(node.getAttribute('value')).toBe('');
1757 + // TODO: we should warn here.
1758 + });
1759 +
1760 + it('treats updated function defaultValue as an empty string', function() {
1761 + var container = document.createElement('div');
1762 + ReactDOM.render(<input defaultValue="foo" />, container);
1763 + ReactDOM.render(<input defaultValue={() => {}} />, container);
1764 + var node = container.firstChild;
1765 +
1766 + expect(node.value).toBe('foo');
1767 + expect(node.getAttribute('value')).toBe('');
1768 + // TODO: we should warn here.
1769 + });
1770 + });
1771 });
packages/react-dom/src/client/ReactDOMFiberInput.js
+39 -33
@@ -116,12 +116,15 @@ export function initWrapperState(element: Element, props: Object) {
116 }
117 }
118
119 - var defaultValue = props.defaultValue == null ? '' : props.defaultValue;
119 var node = ((element: any): InputWithWrapperState);
120 + var defaultValue = props.defaultValue == null ? '' : props.defaultValue;
121 +
122 node._wrapperState = {
123 initialChecked:
124 props.checked != null ? props.checked : props.defaultChecked,
124 - initialValue: props.value != null ? props.value : defaultValue,
125 + initialValue: getSafeValue(
126 + props.value != null ? props.value : defaultValue,
127 + ),
128 controlled: isControlled(props),
129 };
130 }
@@ -175,36 +178,26 @@ export function updateWrapper(element: Element, props: Object) {
178
179 updateChecked(element, props);
180
178 - var value = props.value;
179 - if (value != null) {
180 - if (value === 0 && node.value === '') {
181 - node.value = '0';
182 - // Note: IE9 reports a number inputs as 'text', so check props instead.
183 - } else if (props.type === 'number') {
184 - // Simulate `input.valueAsNumber`. IE9 does not support it
185 - var valueAsNumber = parseFloat(node.value) || 0;
181 + var value = getSafeValue(props.value);
182
183 + if (value != null) {
184 + if (props.type === 'number') {
185 if (
186 + (value === 0 && node.value === '') ||
187 // eslint-disable-next-line
189 - value != valueAsNumber ||
190 - // eslint-disable-next-line
191 - (value == valueAsNumber && node.value != value)
188 + node.value != value
189 ) {
193 - // Cast `value` to a string to ensure the value is set correctly. While
194 - // browsers typically do this as necessary, jsdom doesn't.
190 node.value = '' + value;
191 }
192 } else if (node.value !== '' + value) {
198 - // Cast `value` to a string to ensure the value is set correctly. While
199 - // browsers typically do this as necessary, jsdom doesn't.
193 node.value = '' + value;
194 }
202 - synchronizeDefaultValue(node, props.type, value);
203 - } else if (
204 - props.hasOwnProperty('value') ||
205 - props.hasOwnProperty('defaultValue')
206 - ) {
207 - synchronizeDefaultValue(node, props.type, props.defaultValue);
195 + }
196 +
197 + if (props.hasOwnProperty('value')) {
198 + setDefaultValue(node, props.type, value);
199 + } else if (props.hasOwnProperty('defaultValue')) {
200 + setDefaultValue(node, props.type, getSafeValue(props.defaultValue));
201 }
202
203 if (props.checked == null && props.defaultChecked != null) {
@@ -214,19 +207,18 @@ export function updateWrapper(element: Element, props: Object) {
207
208 export function postMountWrapper(element: Element, props: Object) {
209 var node = ((element: any): InputWithWrapperState);
217 - var initialValue = node._wrapperState.initialValue;
210
219 - if (props.value != null || props.defaultValue != null) {
211 + if (props.hasOwnProperty('value') || props.hasOwnProperty('defaultValue')) {
212 // Do not assign value if it is already set. This prevents user text input
213 // from being lost during SSR hydration.
214 if (node.value === '') {
223 - node.value = initialValue;
215 + node.value = '' + node._wrapperState.initialValue;
216 }
217
218 // value must be assigned before defaultValue. This fixes an issue where the
219 // visually displayed value of date inputs disappears on mobile Safari and Chrome:
220 // https://github.com/facebook/react/issues/7233
229 - node.defaultValue = initialValue;
221 + node.defaultValue = '' + node._wrapperState.initialValue;
222 }
223
224 // Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug
@@ -307,20 +299,34 @@ function updateNamedCousins(rootNode, props) {
299 // when the user is inputting text
300 //
301 // https://github.com/facebook/react/issues/7253
310 -export function synchronizeDefaultValue(
302 +export function setDefaultValue(
303 node: InputWithWrapperState,
304 type: ?string,
305 value: *,
306 ) {
307 if (
308 // Focused number inputs synchronize on blur. See ChangeEventPlugin.js
317 - (type !== 'number' || node.ownerDocument.activeElement !== node) &&
318 - node.defaultValue !== '' + value
309 + type !== 'number' ||
310 + node.ownerDocument.activeElement !== node
311 ) {
320 - if (value != null) {
312 + if (value == null) {
313 + node.defaultValue = '' + node._wrapperState.initialValue;
314 + } else if (node.defaultValue !== '' + value) {
315 node.defaultValue = '' + value;
322 - } else {
323 - node.defaultValue = node._wrapperState.initialValue;
316 }
317 }
318 }
319 +
320 +function getSafeValue(value: *): * {
321 + switch (typeof value) {
322 + case 'boolean':
323 + case 'number':
324 + case 'object':
325 + case 'string':
326 + case 'undefined':
327 + return value;
328 + default:
329 + // function, symbol are assigned as empty strings
330 + return '';
331 + }
332 +}
packages/react-dom/src/events/ChangeEventPlugin.js
+2 -2
@@ -17,7 +17,7 @@ import getEventTarget from './getEventTarget';
17 import isEventSupported from './isEventSupported';
18 import {getNodeFromInstance} from '../client/ReactDOMComponentTree';
19 import * as inputValueTracking from '../client/inputValueTracking';
20 -import {synchronizeDefaultValue} from '../client/ReactDOMFiberInput';
20 +import {setDefaultValue} from '../client/ReactDOMFiberInput';
21
22 var eventTypes = {
23 change: {
@@ -236,7 +236,7 @@ function handleControlledInputBlur(inst, node) {
236 }
237
238 // If controlled, assign the value attribute to the current value on blur
239 - synchronizeDefaultValue(node, 'number', node.value);
239 + setDefaultValue(node, 'number', node.value);
240 }
241
242 /**
packages/react-dom/src/shared/DOMProperty.js
+3
@@ -12,6 +12,9 @@ import warning from 'fbjs/lib/warning';
12 var RESERVED_PROPS = {
13 children: true,
14 dangerouslySetInnerHTML: true,
15 + // TODO: This prevents the assignment of defaultValue to regular
16 + // elements (not just inputs). Now that ReactDOMInput assigns to the
17 + // defaultValue property -- do we need this?
18 defaultValue: true,
19 defaultChecked: true,
20 innerHTML: true,