Refactor some controlled component stuff (#26573)
This is mainly renaming some stuff. The behavior change is hasOwnProperty to nullish check. I had a bigger refactor that was a dead-end but might as well land this part and see if I can pick it up later.
This commit is contained in:
parent
657698e48d
commit
e5146cb525
|
@ -7,8 +7,6 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import type {InputWithWrapperState} from './ReactDOMInput';
|
||||
|
||||
import {
|
||||
registrationNameDependencies,
|
||||
possibleRegistrationNames,
|
||||
|
@ -17,6 +15,7 @@ import {
|
|||
import {canUseDOM} from 'shared/ExecutionEnvironment';
|
||||
import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
|
||||
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
|
||||
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
|
||||
|
||||
import {
|
||||
getValueForAttribute,
|
||||
|
@ -27,27 +26,24 @@ import {
|
|||
setValueForNamespacedAttribute,
|
||||
} from './DOMPropertyOperations';
|
||||
import {
|
||||
initWrapperState as ReactDOMInputInitWrapperState,
|
||||
postMountWrapper as ReactDOMInputPostMountWrapper,
|
||||
updateChecked as ReactDOMInputUpdateChecked,
|
||||
updateWrapper as ReactDOMInputUpdateWrapper,
|
||||
restoreControlledState as ReactDOMInputRestoreControlledState,
|
||||
validateInputProps,
|
||||
initInput,
|
||||
updateInputChecked,
|
||||
updateInput,
|
||||
restoreControlledInputState,
|
||||
} from './ReactDOMInput';
|
||||
import {initOption, validateOptionProps} from './ReactDOMOption';
|
||||
import {
|
||||
postMountWrapper as ReactDOMOptionPostMountWrapper,
|
||||
validateProps as ReactDOMOptionValidateProps,
|
||||
} from './ReactDOMOption';
|
||||
import {
|
||||
initWrapperState as ReactDOMSelectInitWrapperState,
|
||||
postMountWrapper as ReactDOMSelectPostMountWrapper,
|
||||
restoreControlledState as ReactDOMSelectRestoreControlledState,
|
||||
postUpdateWrapper as ReactDOMSelectPostUpdateWrapper,
|
||||
validateSelectProps,
|
||||
initSelect,
|
||||
restoreControlledSelectState,
|
||||
updateSelect,
|
||||
} from './ReactDOMSelect';
|
||||
import {
|
||||
initWrapperState as ReactDOMTextareaInitWrapperState,
|
||||
postMountWrapper as ReactDOMTextareaPostMountWrapper,
|
||||
updateWrapper as ReactDOMTextareaUpdateWrapper,
|
||||
restoreControlledState as ReactDOMTextareaRestoreControlledState,
|
||||
validateTextareaProps,
|
||||
initTextarea,
|
||||
updateTextarea,
|
||||
restoreControlledTextareaState,
|
||||
} from './ReactDOMTextarea';
|
||||
import {track} from './inputValueTracking';
|
||||
import setInnerHTML from './setInnerHTML';
|
||||
|
@ -79,6 +75,8 @@ import {
|
|||
listenToNonDelegatedEvent,
|
||||
} from '../events/DOMPluginEventSystem';
|
||||
|
||||
let didWarnControlledToUncontrolled = false;
|
||||
let didWarnUncontrolledToControlled = false;
|
||||
let didWarnInvalidHydration = false;
|
||||
let canDiffStyleForHydrationWarning;
|
||||
if (__DEV__) {
|
||||
|
@ -805,7 +803,9 @@ export function setInitialProperties(
|
|||
break;
|
||||
}
|
||||
case 'input': {
|
||||
ReactDOMInputInitWrapperState(domElement, props);
|
||||
if (__DEV__) {
|
||||
checkControlledValueProps('input', props);
|
||||
}
|
||||
// We listen to this event in case to ensure emulated bubble
|
||||
// listeners still fire for the invalid event.
|
||||
listenToNonDelegatedEvent('invalid', domElement);
|
||||
|
@ -834,10 +834,10 @@ export function setInitialProperties(
|
|||
break;
|
||||
}
|
||||
case 'checked': {
|
||||
const node = ((domElement: any): InputWithWrapperState);
|
||||
const checked =
|
||||
propValue != null ? propValue : node._wrapperState.initialChecked;
|
||||
node.checked =
|
||||
propValue != null ? propValue : props.defaultChecked;
|
||||
const inputElement: HTMLInputElement = (domElement: any);
|
||||
inputElement.checked =
|
||||
!!checked &&
|
||||
typeof checked !== 'function' &&
|
||||
checked !== 'symbol';
|
||||
|
@ -866,11 +866,14 @@ export function setInitialProperties(
|
|||
// TODO: Make sure we check if this is still unmounted or do any clean
|
||||
// up necessary since we never stop tracking anymore.
|
||||
track((domElement: any));
|
||||
ReactDOMInputPostMountWrapper(domElement, props, false);
|
||||
validateInputProps(domElement, props);
|
||||
initInput(domElement, props, false);
|
||||
return;
|
||||
}
|
||||
case 'select': {
|
||||
ReactDOMSelectInitWrapperState(domElement, props);
|
||||
if (__DEV__) {
|
||||
checkControlledValueProps('select', props);
|
||||
}
|
||||
// We listen to this event in case to ensure emulated bubble
|
||||
// listeners still fire for the invalid event.
|
||||
listenToNonDelegatedEvent('invalid', domElement);
|
||||
|
@ -893,11 +896,14 @@ export function setInitialProperties(
|
|||
}
|
||||
}
|
||||
}
|
||||
ReactDOMSelectPostMountWrapper(domElement, props);
|
||||
validateSelectProps(domElement, props);
|
||||
initSelect(domElement, props);
|
||||
return;
|
||||
}
|
||||
case 'textarea': {
|
||||
ReactDOMTextareaInitWrapperState(domElement, props);
|
||||
if (__DEV__) {
|
||||
checkControlledValueProps('textarea', props);
|
||||
}
|
||||
// We listen to this event in case to ensure emulated bubble
|
||||
// listeners still fire for the invalid event.
|
||||
listenToNonDelegatedEvent('invalid', domElement);
|
||||
|
@ -936,11 +942,12 @@ export function setInitialProperties(
|
|||
// TODO: Make sure we check if this is still unmounted or do any clean
|
||||
// up necessary since we never stop tracking anymore.
|
||||
track((domElement: any));
|
||||
ReactDOMTextareaPostMountWrapper(domElement, props);
|
||||
validateTextareaProps(domElement, props);
|
||||
initTextarea(domElement, props);
|
||||
return;
|
||||
}
|
||||
case 'option': {
|
||||
ReactDOMOptionValidateProps(domElement, props);
|
||||
validateOptionProps(domElement, props);
|
||||
for (const propKey in props) {
|
||||
if (!props.hasOwnProperty(propKey)) {
|
||||
continue;
|
||||
|
@ -963,7 +970,7 @@ export function setInitialProperties(
|
|||
}
|
||||
}
|
||||
}
|
||||
ReactDOMOptionPostMountWrapper(domElement, props);
|
||||
initOption(domElement, props);
|
||||
return;
|
||||
}
|
||||
case 'dialog': {
|
||||
|
@ -1213,17 +1220,17 @@ export function updateProperties(
|
|||
// In the middle of an update, it is possible to have multiple checked.
|
||||
// When a checked radio tries to change name, browser makes another radio's checked false.
|
||||
if (nextProps.type === 'radio' && nextProps.name != null) {
|
||||
ReactDOMInputUpdateChecked(domElement, nextProps);
|
||||
updateInputChecked(domElement, nextProps);
|
||||
}
|
||||
for (let i = 0; i < updatePayload.length; i += 2) {
|
||||
const propKey = updatePayload[i];
|
||||
const propValue = updatePayload[i + 1];
|
||||
switch (propKey) {
|
||||
case 'checked': {
|
||||
const node = ((domElement: any): InputWithWrapperState);
|
||||
const checked =
|
||||
propValue != null ? propValue : node._wrapperState.initialChecked;
|
||||
node.checked =
|
||||
propValue != null ? propValue : nextProps.defaultChecked;
|
||||
const inputElement: HTMLInputElement = (domElement: any);
|
||||
inputElement.checked =
|
||||
!!checked &&
|
||||
typeof checked !== 'function' &&
|
||||
checked !== 'symbol';
|
||||
|
@ -1249,10 +1256,50 @@ export function updateProperties(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
const wasControlled =
|
||||
lastProps.type === 'checkbox' || lastProps.type === 'radio'
|
||||
? lastProps.checked != null
|
||||
: lastProps.value != null;
|
||||
const isControlled =
|
||||
nextProps.type === 'checkbox' || nextProps.type === 'radio'
|
||||
? nextProps.checked != null
|
||||
: nextProps.value != null;
|
||||
|
||||
if (
|
||||
!wasControlled &&
|
||||
isControlled &&
|
||||
!didWarnUncontrolledToControlled
|
||||
) {
|
||||
console.error(
|
||||
'A component is changing an uncontrolled input to be controlled. ' +
|
||||
'This is likely caused by the value changing from undefined to ' +
|
||||
'a defined value, which should not happen. ' +
|
||||
'Decide between using a controlled or uncontrolled input ' +
|
||||
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components',
|
||||
);
|
||||
didWarnUncontrolledToControlled = true;
|
||||
}
|
||||
if (
|
||||
wasControlled &&
|
||||
!isControlled &&
|
||||
!didWarnControlledToUncontrolled
|
||||
) {
|
||||
console.error(
|
||||
'A component is changing a controlled input to be uncontrolled. ' +
|
||||
'This is likely caused by the value changing from a defined to ' +
|
||||
'undefined, which should not happen. ' +
|
||||
'Decide between using a controlled or uncontrolled input ' +
|
||||
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components',
|
||||
);
|
||||
didWarnControlledToUncontrolled = true;
|
||||
}
|
||||
}
|
||||
// Update the wrapper around inputs *after* updating props. This has to
|
||||
// happen after updating the rest of props. Otherwise HTML5 input validations
|
||||
// raise warnings and prevent the new value from being assigned.
|
||||
ReactDOMInputUpdateWrapper(domElement, nextProps);
|
||||
updateInput(domElement, nextProps);
|
||||
return;
|
||||
}
|
||||
case 'select': {
|
||||
|
@ -1272,7 +1319,7 @@ export function updateProperties(
|
|||
}
|
||||
// <select> value update needs to occur after <option> children
|
||||
// reconciliation
|
||||
ReactDOMSelectPostUpdateWrapper(domElement, nextProps);
|
||||
updateSelect(domElement, lastProps, nextProps);
|
||||
return;
|
||||
}
|
||||
case 'textarea': {
|
||||
|
@ -1303,7 +1350,7 @@ export function updateProperties(
|
|||
}
|
||||
}
|
||||
}
|
||||
ReactDOMTextareaUpdateWrapper(domElement, nextProps);
|
||||
updateTextarea(domElement, nextProps);
|
||||
return;
|
||||
}
|
||||
case 'option': {
|
||||
|
@ -2263,38 +2310,47 @@ export function diffHydratedProperties(
|
|||
listenToNonDelegatedEvent('toggle', domElement);
|
||||
break;
|
||||
case 'input':
|
||||
ReactDOMInputInitWrapperState(domElement, props);
|
||||
if (__DEV__) {
|
||||
checkControlledValueProps('input', props);
|
||||
}
|
||||
// We listen to this event in case to ensure emulated bubble
|
||||
// listeners still fire for the invalid event.
|
||||
listenToNonDelegatedEvent('invalid', domElement);
|
||||
// TODO: Make sure we check if this is still unmounted or do any clean
|
||||
// up necessary since we never stop tracking anymore.
|
||||
track((domElement: any));
|
||||
validateInputProps(domElement, props);
|
||||
// For input and textarea we current always set the value property at
|
||||
// post mount to force it to diverge from attributes. However, for
|
||||
// option and select we don't quite do the same thing and select
|
||||
// is not resilient to the DOM state changing so we don't do that here.
|
||||
// TODO: Consider not doing this for input and textarea.
|
||||
ReactDOMInputPostMountWrapper(domElement, props, true);
|
||||
initInput(domElement, props, true);
|
||||
break;
|
||||
case 'option':
|
||||
ReactDOMOptionValidateProps(domElement, props);
|
||||
validateOptionProps(domElement, props);
|
||||
break;
|
||||
case 'select':
|
||||
ReactDOMSelectInitWrapperState(domElement, props);
|
||||
if (__DEV__) {
|
||||
checkControlledValueProps('select', props);
|
||||
}
|
||||
// We listen to this event in case to ensure emulated bubble
|
||||
// listeners still fire for the invalid event.
|
||||
listenToNonDelegatedEvent('invalid', domElement);
|
||||
validateSelectProps(domElement, props);
|
||||
break;
|
||||
case 'textarea':
|
||||
ReactDOMTextareaInitWrapperState(domElement, props);
|
||||
if (__DEV__) {
|
||||
checkControlledValueProps('textarea', props);
|
||||
}
|
||||
// We listen to this event in case to ensure emulated bubble
|
||||
// listeners still fire for the invalid event.
|
||||
listenToNonDelegatedEvent('invalid', domElement);
|
||||
// TODO: Make sure we check if this is still unmounted or do any clean
|
||||
// up necessary since we never stop tracking anymore.
|
||||
track((domElement: any));
|
||||
ReactDOMTextareaPostMountWrapper(domElement, props);
|
||||
validateTextareaProps(domElement, props);
|
||||
initTextarea(domElement, props);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -2472,13 +2528,13 @@ export function restoreControlledState(
|
|||
): void {
|
||||
switch (tag) {
|
||||
case 'input':
|
||||
ReactDOMInputRestoreControlledState(domElement, props);
|
||||
restoreControlledInputState(domElement, props);
|
||||
return;
|
||||
case 'textarea':
|
||||
ReactDOMTextareaRestoreControlledState(domElement, props);
|
||||
restoreControlledTextareaState(domElement, props);
|
||||
return;
|
||||
case 'select':
|
||||
ReactDOMSelectRestoreControlledState(domElement, props);
|
||||
restoreControlledSelectState(domElement, props);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCur
|
|||
|
||||
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
|
||||
import {getToStringValue, toString} from './ToStringValue';
|
||||
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
|
||||
import {updateValueIfChanged} from './inputValueTracking';
|
||||
import getActiveElement from './getActiveElement';
|
||||
import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
|
||||
|
@ -20,29 +19,8 @@ import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
|
|||
|
||||
import type {ToStringValue} from './ToStringValue';
|
||||
|
||||
export type InputWithWrapperState = HTMLInputElement & {
|
||||
_wrapperState: {
|
||||
initialValue: ToStringValue,
|
||||
initialChecked: ?boolean,
|
||||
controlled?: boolean,
|
||||
...
|
||||
},
|
||||
checked: boolean,
|
||||
value: string,
|
||||
defaultChecked: boolean,
|
||||
defaultValue: string,
|
||||
...
|
||||
};
|
||||
|
||||
let didWarnValueDefaultValue = false;
|
||||
let didWarnCheckedDefaultChecked = false;
|
||||
let didWarnControlledToUncontrolled = false;
|
||||
let didWarnUncontrolledToControlled = false;
|
||||
|
||||
function isControlled(props: any) {
|
||||
const usesChecked = props.type === 'checkbox' || props.type === 'radio';
|
||||
return usesChecked ? props.checked != null : props.value != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements an <input> host component that allows setting these optional
|
||||
|
@ -61,10 +39,11 @@ function isControlled(props: any) {
|
|||
* See http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
|
||||
*/
|
||||
|
||||
export function initWrapperState(element: Element, props: Object) {
|
||||
export function validateInputProps(element: Element, props: Object) {
|
||||
if (__DEV__) {
|
||||
checkControlledValueProps('input', props);
|
||||
|
||||
// Normally we check for undefined and null the same, but explicitly specifying both
|
||||
// properties, at all is probably worth warning for. We could move this either direction
|
||||
// and just make it ok to pass null or just check hasOwnProperty.
|
||||
if (
|
||||
props.checked !== undefined &&
|
||||
props.defaultChecked !== undefined &&
|
||||
|
@ -100,71 +79,65 @@ export function initWrapperState(element: Element, props: Object) {
|
|||
didWarnValueDefaultValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
const node = ((element: any): InputWithWrapperState);
|
||||
const defaultValue = props.defaultValue == null ? '' : props.defaultValue;
|
||||
const initialChecked =
|
||||
props.checked != null ? props.checked : props.defaultChecked;
|
||||
node._wrapperState = {
|
||||
initialChecked:
|
||||
typeof initialChecked !== 'function' &&
|
||||
typeof initialChecked !== 'symbol' &&
|
||||
!!initialChecked,
|
||||
initialValue: getToStringValue(
|
||||
props.value != null ? props.value : defaultValue,
|
||||
),
|
||||
controlled: isControlled(props),
|
||||
};
|
||||
}
|
||||
|
||||
export function updateChecked(element: Element, props: Object) {
|
||||
const node = ((element: any): InputWithWrapperState);
|
||||
export function updateInputChecked(element: Element, props: Object) {
|
||||
const node: HTMLInputElement = (element: any);
|
||||
const checked = props.checked;
|
||||
if (checked != null) {
|
||||
node.checked = checked;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateWrapper(element: Element, props: Object) {
|
||||
const node = ((element: any): InputWithWrapperState);
|
||||
if (__DEV__) {
|
||||
const controlled = isControlled(props);
|
||||
|
||||
if (
|
||||
!node._wrapperState.controlled &&
|
||||
controlled &&
|
||||
!didWarnUncontrolledToControlled
|
||||
) {
|
||||
console.error(
|
||||
'A component is changing an uncontrolled input to be controlled. ' +
|
||||
'This is likely caused by the value changing from undefined to ' +
|
||||
'a defined value, which should not happen. ' +
|
||||
'Decide between using a controlled or uncontrolled input ' +
|
||||
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components',
|
||||
);
|
||||
didWarnUncontrolledToControlled = true;
|
||||
}
|
||||
if (
|
||||
node._wrapperState.controlled &&
|
||||
!controlled &&
|
||||
!didWarnControlledToUncontrolled
|
||||
) {
|
||||
console.error(
|
||||
'A component is changing a controlled input to be uncontrolled. ' +
|
||||
'This is likely caused by the value changing from a defined to ' +
|
||||
'undefined, which should not happen. ' +
|
||||
'Decide between using a controlled or uncontrolled input ' +
|
||||
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components',
|
||||
);
|
||||
didWarnControlledToUncontrolled = true;
|
||||
}
|
||||
}
|
||||
|
||||
updateChecked(element, props);
|
||||
export function updateInput(element: Element, props: Object) {
|
||||
const node: HTMLInputElement = (element: any);
|
||||
|
||||
const value = getToStringValue(props.value);
|
||||
const type = props.type;
|
||||
|
||||
if (disableInputAttributeSyncing) {
|
||||
// When not syncing the value attribute, React only assigns a new value
|
||||
// whenever the defaultValue React prop has changed. When not present,
|
||||
// React does nothing
|
||||
if (props.defaultValue != null) {
|
||||
setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
|
||||
} else {
|
||||
node.removeAttribute('value');
|
||||
}
|
||||
} else {
|
||||
// When syncing the value attribute, the value comes from a cascade of
|
||||
// properties:
|
||||
// 1. The value React property
|
||||
// 2. The defaultValue React property
|
||||
// 3. Otherwise there should be no change
|
||||
if (props.value != null) {
|
||||
setDefaultValue(node, props.type, value);
|
||||
} else if (props.defaultValue != null) {
|
||||
setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
|
||||
} else {
|
||||
node.removeAttribute('value');
|
||||
}
|
||||
}
|
||||
|
||||
if (disableInputAttributeSyncing) {
|
||||
// When not syncing the checked attribute, the attribute is directly
|
||||
// controllable from the defaultValue React property. It needs to be
|
||||
// updated as new props come in.
|
||||
if (props.defaultChecked == null) {
|
||||
node.removeAttribute('checked');
|
||||
} else {
|
||||
node.defaultChecked = !!props.defaultChecked;
|
||||
}
|
||||
} else {
|
||||
// When syncing the checked attribute, it only changes when it needs
|
||||
// to be removed, such as transitioning from a checkbox into a text input
|
||||
if (props.checked == null && props.defaultChecked != null) {
|
||||
node.defaultChecked = !!props.defaultChecked;
|
||||
}
|
||||
}
|
||||
|
||||
updateInputChecked(element, props);
|
||||
|
||||
if (value != null) {
|
||||
if (type === 'number') {
|
||||
if (
|
||||
|
@ -185,55 +158,16 @@ export function updateWrapper(element: Element, props: Object) {
|
|||
node.removeAttribute('value');
|
||||
return;
|
||||
}
|
||||
|
||||
if (disableInputAttributeSyncing) {
|
||||
// When not syncing the value attribute, React only assigns a new value
|
||||
// whenever the defaultValue React prop has changed. When not present,
|
||||
// React does nothing
|
||||
if (props.hasOwnProperty('defaultValue')) {
|
||||
setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
|
||||
}
|
||||
} else {
|
||||
// When syncing the value attribute, the value comes from a cascade of
|
||||
// properties:
|
||||
// 1. The value React property
|
||||
// 2. The defaultValue React property
|
||||
// 3. Otherwise there should be no change
|
||||
if (props.hasOwnProperty('value')) {
|
||||
setDefaultValue(node, props.type, value);
|
||||
} else if (props.hasOwnProperty('defaultValue')) {
|
||||
setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
|
||||
}
|
||||
}
|
||||
|
||||
if (disableInputAttributeSyncing) {
|
||||
// When not syncing the checked attribute, the attribute is directly
|
||||
// controllable from the defaultValue React property. It needs to be
|
||||
// updated as new props come in.
|
||||
if (props.defaultChecked == null) {
|
||||
node.removeAttribute('checked');
|
||||
} else {
|
||||
node.defaultChecked = !!props.defaultChecked;
|
||||
}
|
||||
} else {
|
||||
// When syncing the checked attribute, it only changes when it needs
|
||||
// to be removed, such as transitioning from a checkbox into a text input
|
||||
if (props.checked == null && props.defaultChecked != null) {
|
||||
node.defaultChecked = !!props.defaultChecked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function postMountWrapper(
|
||||
export function initInput(
|
||||
element: Element,
|
||||
props: Object,
|
||||
isHydrating: boolean,
|
||||
) {
|
||||
const node = ((element: any): InputWithWrapperState);
|
||||
const node: HTMLInputElement = (element: any);
|
||||
|
||||
// Do not assign value if it is already set. This prevents user text input
|
||||
// from being lost during SSR hydration.
|
||||
if (props.hasOwnProperty('value') || props.hasOwnProperty('defaultValue')) {
|
||||
if (props.value != null || props.defaultValue != null) {
|
||||
const type = props.type;
|
||||
const isButton = type === 'submit' || type === 'reset';
|
||||
|
||||
|
@ -243,7 +177,14 @@ export function postMountWrapper(
|
|||
return;
|
||||
}
|
||||
|
||||
const initialValue = toString(node._wrapperState.initialValue);
|
||||
const defaultValue =
|
||||
props.defaultValue != null
|
||||
? toString(getToStringValue(props.defaultValue))
|
||||
: '';
|
||||
const initialValue =
|
||||
props.value != null
|
||||
? toString(getToStringValue(props.value))
|
||||
: defaultValue;
|
||||
|
||||
// Do not assign value if it is already set. This prevents user text input
|
||||
// from being lost during SSR hydration.
|
||||
|
@ -282,9 +223,8 @@ export function postMountWrapper(
|
|||
if (disableInputAttributeSyncing) {
|
||||
// When not syncing the value attribute, assign the value attribute
|
||||
// directly from the defaultValue React property (when present)
|
||||
const defaultValue = getToStringValue(props.defaultValue);
|
||||
if (defaultValue != null) {
|
||||
node.defaultValue = toString(defaultValue);
|
||||
if (props.defaultValue != null) {
|
||||
node.defaultValue = defaultValue;
|
||||
}
|
||||
} else {
|
||||
// Otherwise, the value attribute is synchronized to the property,
|
||||
|
@ -304,20 +244,27 @@ export function postMountWrapper(
|
|||
node.name = '';
|
||||
}
|
||||
|
||||
const defaultChecked =
|
||||
props.checked != null ? props.checked : props.defaultChecked;
|
||||
const initialChecked =
|
||||
typeof defaultChecked !== 'function' &&
|
||||
typeof defaultChecked !== 'symbol' &&
|
||||
!!defaultChecked;
|
||||
|
||||
// The checked property never gets assigned. It must be manually set.
|
||||
// We don't want to do this when hydrating so that existing user input isn't
|
||||
// modified
|
||||
// TODO: I'm pretty sure this is a bug because initialValueTracking won't be
|
||||
// correct for the hydration case then.
|
||||
if (!isHydrating) {
|
||||
node.checked = !!node._wrapperState.initialChecked;
|
||||
node.checked = !!initialChecked;
|
||||
}
|
||||
|
||||
if (disableInputAttributeSyncing) {
|
||||
// Only assign the checked attribute if it is defined. This saves
|
||||
// a DOM write when controlling the checked attribute isn't needed
|
||||
// (text inputs, submit/reset)
|
||||
if (props.hasOwnProperty('defaultChecked')) {
|
||||
if (props.defaultChecked != null) {
|
||||
node.defaultChecked = !node.defaultChecked;
|
||||
node.defaultChecked = !!props.defaultChecked;
|
||||
}
|
||||
|
@ -329,7 +276,7 @@ export function postMountWrapper(
|
|||
// 2. The defaultChecked React property when present
|
||||
// 3. Otherwise, false
|
||||
node.defaultChecked = !node.defaultChecked;
|
||||
node.defaultChecked = !!node._wrapperState.initialChecked;
|
||||
node.defaultChecked = !!initialChecked;
|
||||
}
|
||||
|
||||
if (name !== '') {
|
||||
|
@ -337,13 +284,13 @@ export function postMountWrapper(
|
|||
}
|
||||
}
|
||||
|
||||
export function restoreControlledState(element: Element, props: Object) {
|
||||
const node = ((element: any): InputWithWrapperState);
|
||||
updateWrapper(node, props);
|
||||
export function restoreControlledInputState(element: Element, props: Object) {
|
||||
const node: HTMLInputElement = (element: any);
|
||||
updateInput(node, props);
|
||||
updateNamedCousins(node, props);
|
||||
}
|
||||
|
||||
function updateNamedCousins(rootNode: InputWithWrapperState, props: any) {
|
||||
function updateNamedCousins(rootNode: HTMLInputElement, props: any) {
|
||||
const name = props.name;
|
||||
if (props.type === 'radio' && name != null) {
|
||||
let queryRoot: Element = rootNode;
|
||||
|
@ -391,7 +338,7 @@ function updateNamedCousins(rootNode: InputWithWrapperState, props: any) {
|
|||
// If this is a controlled radio button group, forcing the input that
|
||||
// was previously checked to update will cause it to be come re-checked
|
||||
// as appropriate.
|
||||
updateWrapper(otherNode, otherProps);
|
||||
updateInput(otherNode, otherProps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -405,7 +352,7 @@ function updateNamedCousins(rootNode: InputWithWrapperState, props: any) {
|
|||
//
|
||||
// https://github.com/facebook/react/issues/7253
|
||||
export function setDefaultValue(
|
||||
node: InputWithWrapperState,
|
||||
node: HTMLInputElement,
|
||||
type: ?string,
|
||||
value: ToStringValue,
|
||||
) {
|
||||
|
@ -414,9 +361,7 @@ export function setDefaultValue(
|
|||
type !== 'number' ||
|
||||
getActiveElement(node.ownerDocument) !== node
|
||||
) {
|
||||
if (value == null) {
|
||||
node.defaultValue = toString(node._wrapperState.initialValue);
|
||||
} else if (node.defaultValue !== toString(value)) {
|
||||
if (node.defaultValue !== toString(value)) {
|
||||
node.defaultValue = toString(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ let didWarnInvalidInnerHTML = false;
|
|||
* Implements an <option> host component that warns when `selected` is set.
|
||||
*/
|
||||
|
||||
export function validateProps(element: Element, props: Object) {
|
||||
export function validateOptionProps(element: Element, props: Object) {
|
||||
if (__DEV__) {
|
||||
// If a value is not provided, then the children must be simple.
|
||||
if (props.value == null) {
|
||||
|
@ -60,7 +60,7 @@ export function validateProps(element: Element, props: Object) {
|
|||
}
|
||||
}
|
||||
|
||||
export function postMountWrapper(element: Element, props: Object) {
|
||||
export function initOption(element: Element, props: Object) {
|
||||
// value="" should make a value attribute (#6219)
|
||||
if (props.value != null) {
|
||||
element.setAttribute('value', toString(getToStringValue(props.value)));
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
// TODO: direct imports like some-package/src/* are bad. Fix me.
|
||||
import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber';
|
||||
|
||||
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
|
||||
import {getToStringValue, toString} from './ToStringValue';
|
||||
import isArray from 'shared/isArray';
|
||||
|
||||
|
@ -20,10 +19,6 @@ if (__DEV__) {
|
|||
didWarnValueDefaultValue = false;
|
||||
}
|
||||
|
||||
type SelectWithWrapperState = HTMLSelectElement & {
|
||||
_wrapperState: {wasMultiple: boolean},
|
||||
};
|
||||
|
||||
function getDeclarationErrorAddendum() {
|
||||
const ownerName = getCurrentFiberOwnerNameInDevOrNull();
|
||||
if (ownerName) {
|
||||
|
@ -39,8 +34,6 @@ const valuePropNames = ['value', 'defaultValue'];
|
|||
*/
|
||||
function checkSelectPropTypes(props: any) {
|
||||
if (__DEV__) {
|
||||
checkControlledValueProps('select', props);
|
||||
|
||||
for (let i = 0; i < valuePropNames.length; i++) {
|
||||
const propName = valuePropNames[i];
|
||||
if (props[propName] == null) {
|
||||
|
@ -129,17 +122,9 @@ function updateOptions(
|
|||
* selected.
|
||||
*/
|
||||
|
||||
export function initWrapperState(element: Element, props: Object) {
|
||||
const node = ((element: any): SelectWithWrapperState);
|
||||
export function validateSelectProps(element: Element, props: Object) {
|
||||
if (__DEV__) {
|
||||
checkSelectPropTypes(props);
|
||||
}
|
||||
|
||||
node._wrapperState = {
|
||||
wasMultiple: !!props.multiple,
|
||||
};
|
||||
|
||||
if (__DEV__) {
|
||||
if (
|
||||
props.value !== undefined &&
|
||||
props.defaultValue !== undefined &&
|
||||
|
@ -157,8 +142,8 @@ export function initWrapperState(element: Element, props: Object) {
|
|||
}
|
||||
}
|
||||
|
||||
export function postMountWrapper(element: Element, props: Object) {
|
||||
const node = ((element: any): SelectWithWrapperState);
|
||||
export function initSelect(element: Element, props: Object) {
|
||||
const node: HTMLSelectElement = (element: any);
|
||||
node.multiple = !!props.multiple;
|
||||
const value = props.value;
|
||||
if (value != null) {
|
||||
|
@ -168,10 +153,13 @@ export function postMountWrapper(element: Element, props: Object) {
|
|||
}
|
||||
}
|
||||
|
||||
export function postUpdateWrapper(element: Element, props: Object) {
|
||||
const node = ((element: any): SelectWithWrapperState);
|
||||
const wasMultiple = node._wrapperState.wasMultiple;
|
||||
node._wrapperState.wasMultiple = !!props.multiple;
|
||||
export function updateSelect(
|
||||
element: Element,
|
||||
prevProps: Object,
|
||||
props: Object,
|
||||
) {
|
||||
const node: HTMLSelectElement = (element: any);
|
||||
const wasMultiple = !!prevProps.multiple;
|
||||
|
||||
const value = props.value;
|
||||
if (value != null) {
|
||||
|
@ -187,8 +175,8 @@ export function postUpdateWrapper(element: Element, props: Object) {
|
|||
}
|
||||
}
|
||||
|
||||
export function restoreControlledState(element: Element, props: Object) {
|
||||
const node = ((element: any): SelectWithWrapperState);
|
||||
export function restoreControlledSelectState(element: Element, props: Object) {
|
||||
const node: HTMLSelectElement = (element: any);
|
||||
const value = props.value;
|
||||
|
||||
if (value != null) {
|
||||
|
|
|
@ -9,18 +9,12 @@
|
|||
|
||||
import isArray from 'shared/isArray';
|
||||
|
||||
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
|
||||
import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber';
|
||||
import {getToStringValue, toString} from './ToStringValue';
|
||||
import type {ToStringValue} from './ToStringValue';
|
||||
import {disableTextareaChildren} from 'shared/ReactFeatureFlags';
|
||||
|
||||
let didWarnValDefaultVal = false;
|
||||
|
||||
export type TextAreaWithWrapperState = HTMLTextAreaElement & {
|
||||
_wrapperState: {initialValue: ToStringValue},
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a <textarea> host component that allows setting `value`, and
|
||||
* `defaultValue`. This differs from the traditional DOM API because value is
|
||||
|
@ -37,10 +31,8 @@ export type TextAreaWithWrapperState = HTMLTextAreaElement & {
|
|||
* `defaultValue` if specified, or the children content (deprecated).
|
||||
*/
|
||||
|
||||
export function initWrapperState(element: Element, props: Object) {
|
||||
const node = ((element: any): TextAreaWithWrapperState);
|
||||
export function validateTextareaProps(element: Element, props: Object) {
|
||||
if (__DEV__) {
|
||||
checkControlledValueProps('textarea', props);
|
||||
if (
|
||||
props.value !== undefined &&
|
||||
props.defaultValue !== undefined &&
|
||||
|
@ -57,7 +49,41 @@ export function initWrapperState(element: Element, props: Object) {
|
|||
);
|
||||
didWarnValDefaultVal = true;
|
||||
}
|
||||
if (props.children != null && props.value == null) {
|
||||
console.error(
|
||||
'Use the `defaultValue` or `value` props instead of setting ' +
|
||||
'children on <textarea>.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTextarea(element: Element, props: Object) {
|
||||
const node: HTMLTextAreaElement = (element: any);
|
||||
const value = getToStringValue(props.value);
|
||||
const defaultValue = getToStringValue(props.defaultValue);
|
||||
if (defaultValue != null) {
|
||||
node.defaultValue = toString(defaultValue);
|
||||
} else {
|
||||
node.defaultValue = '';
|
||||
}
|
||||
if (value != null) {
|
||||
// Cast `value` to a string to ensure the value is set correctly. While
|
||||
// browsers typically do this as necessary, jsdom doesn't.
|
||||
const newValue = toString(value);
|
||||
// To avoid side effects (such as losing text selection), only set value if changed
|
||||
if (newValue !== node.value) {
|
||||
node.value = newValue;
|
||||
}
|
||||
// TOOO: This should respect disableInputAttributeSyncing flag.
|
||||
if (props.defaultValue == null && node.defaultValue !== newValue) {
|
||||
node.defaultValue = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function initTextarea(element: Element, props: Object) {
|
||||
const node: HTMLTextAreaElement = (element: any);
|
||||
|
||||
let initialValue = props.value;
|
||||
|
||||
|
@ -65,12 +91,6 @@ export function initWrapperState(element: Element, props: Object) {
|
|||
if (initialValue == null) {
|
||||
let {children, defaultValue} = props;
|
||||
if (children != null) {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'Use the `defaultValue` or `value` props instead of setting ' +
|
||||
'children on <textarea>.',
|
||||
);
|
||||
}
|
||||
if (!disableTextareaChildren) {
|
||||
if (defaultValue != null) {
|
||||
throw new Error(
|
||||
|
@ -97,34 +117,7 @@ export function initWrapperState(element: Element, props: Object) {
|
|||
|
||||
const stringValue = getToStringValue(initialValue);
|
||||
node.defaultValue = (stringValue: any); // This will be toString:ed.
|
||||
node._wrapperState = {
|
||||
initialValue: stringValue,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateWrapper(element: Element, props: Object) {
|
||||
const node = ((element: any): TextAreaWithWrapperState);
|
||||
const value = getToStringValue(props.value);
|
||||
const defaultValue = getToStringValue(props.defaultValue);
|
||||
if (value != null) {
|
||||
// Cast `value` to a string to ensure the value is set correctly. While
|
||||
// browsers typically do this as necessary, jsdom doesn't.
|
||||
const newValue = toString(value);
|
||||
// To avoid side effects (such as losing text selection), only set value if changed
|
||||
if (newValue !== node.value) {
|
||||
node.value = newValue;
|
||||
}
|
||||
if (props.defaultValue == null && node.defaultValue !== newValue) {
|
||||
node.defaultValue = newValue;
|
||||
}
|
||||
}
|
||||
if (defaultValue != null) {
|
||||
node.defaultValue = toString(defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
export function postMountWrapper(element: Element, props: Object) {
|
||||
const node = ((element: any): TextAreaWithWrapperState);
|
||||
// This is in postMount because we need access to the DOM node, which is not
|
||||
// available until after the component has mounted.
|
||||
const textContent = node.textContent;
|
||||
|
@ -133,14 +126,17 @@ export function postMountWrapper(element: Element, props: Object) {
|
|||
// initial value. In IE10/IE11 there is a bug where the placeholder attribute
|
||||
// will populate textContent as well.
|
||||
// https://developer.microsoft.com/microsoft-edge/platform/issues/101525/
|
||||
if (textContent === node._wrapperState.initialValue) {
|
||||
if (textContent === stringValue) {
|
||||
if (textContent !== '' && textContent !== null) {
|
||||
node.value = textContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function restoreControlledState(element: Element, props: Object) {
|
||||
export function restoreControlledTextareaState(
|
||||
element: Element,
|
||||
props: Object,
|
||||
) {
|
||||
// DOM component is still mounted; update
|
||||
updateWrapper(element, props);
|
||||
updateTextarea(element, props);
|
||||
}
|
||||
|
|
|
@ -123,7 +123,6 @@ export function track(node: ElementWithValueTracker) {
|
|||
return;
|
||||
}
|
||||
|
||||
// TODO: Once it's just Fiber we can move this to node._wrapperState
|
||||
node._valueTracker = trackValueOnNode(node);
|
||||
}
|
||||
|
||||
|
|
|
@ -263,16 +263,17 @@ function getTargetInstForInputOrChangeEvent(
|
|||
}
|
||||
}
|
||||
|
||||
function handleControlledInputBlur(node: HTMLInputElement) {
|
||||
const state = (node: any)._wrapperState;
|
||||
|
||||
if (!state || !state.controlled || node.type !== 'number') {
|
||||
function handleControlledInputBlur(node: HTMLInputElement, props: any) {
|
||||
if (node.type !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!disableInputAttributeSyncing) {
|
||||
// If controlled, assign the value attribute to the current value on blur
|
||||
setDefaultValue((node: any), 'number', (node: any).value);
|
||||
const isControlled = props.value != null;
|
||||
if (isControlled) {
|
||||
// If controlled, assign the value attribute to the current value on blur
|
||||
setDefaultValue((node: any), 'number', (node: any).value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -335,8 +336,12 @@ function extractEvents(
|
|||
}
|
||||
|
||||
// When blurring, set the value attribute for number inputs
|
||||
if (domEventName === 'focusout') {
|
||||
handleControlledInputBlur(((targetNode: any): HTMLInputElement));
|
||||
if (domEventName === 'focusout' && targetInst) {
|
||||
// These props aren't necessarily the most current but we warn for changing
|
||||
// between controlled and uncontrolled, so it doesn't matter and the previous
|
||||
// code was also broken for changes.
|
||||
const props = targetInst.memoizedProps;
|
||||
handleControlledInputBlur(((targetNode: any): HTMLInputElement), props);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1166,11 +1166,7 @@ describe('DOMPropertyOperations', () => {
|
|||
).toErrorDev(
|
||||
'A component is changing a controlled input to be uncontrolled',
|
||||
);
|
||||
if (disableInputAttributeSyncing) {
|
||||
expect(container.firstChild.hasAttribute('value')).toBe(false);
|
||||
} else {
|
||||
expect(container.firstChild.getAttribute('value')).toBe('foo');
|
||||
}
|
||||
expect(container.firstChild.hasAttribute('value')).toBe(false);
|
||||
expect(container.firstChild.value).toBe('foo');
|
||||
});
|
||||
|
||||
|
|
|
@ -1083,10 +1083,17 @@ describe('ReactDOMComponent', () => {
|
|||
);
|
||||
expect(nodeValueSetter).toHaveBeenCalledTimes(1);
|
||||
|
||||
ReactDOM.render(
|
||||
<input type="checkbox" onChange={onChange} checked={false} />,
|
||||
container,
|
||||
expect(() => {
|
||||
ReactDOM.render(
|
||||
<input type="checkbox" onChange={onChange} checked={false} />,
|
||||
container,
|
||||
);
|
||||
}).toErrorDev(
|
||||
' A component is changing an uncontrolled input to be controlled. This is likely caused by ' +
|
||||
'the value changing from undefined to a defined value, which should not happen. Decide between ' +
|
||||
'using a controlled or uncontrolled input element for the lifetime of the component.',
|
||||
);
|
||||
|
||||
// TODO: Non-null values are updated twice on inputs. This is should ideally be fixed.
|
||||
expect(nodeValueSetter).toHaveBeenCalledTimes(3);
|
||||
|
||||
|
|
|
@ -1952,11 +1952,7 @@ describe('ReactDOMInput', () => {
|
|||
expect(renderInputWithStringThenWithUndefined).toErrorDev(
|
||||
'A component is changing a controlled input to be uncontrolled.',
|
||||
);
|
||||
if (disableInputAttributeSyncing) {
|
||||
expect(input.getAttribute('value')).toBe(null);
|
||||
} else {
|
||||
expect(input.getAttribute('value')).toBe('first');
|
||||
}
|
||||
expect(input.getAttribute('value')).toBe(null);
|
||||
});
|
||||
|
||||
it('preserves the value property', () => {
|
||||
|
@ -2002,11 +1998,7 @@ describe('ReactDOMInput', () => {
|
|||
'or `undefined` for uncontrolled components.',
|
||||
'A component is changing a controlled input to be uncontrolled.',
|
||||
]);
|
||||
if (disableInputAttributeSyncing) {
|
||||
expect(input.hasAttribute('value')).toBe(false);
|
||||
} else {
|
||||
expect(input.getAttribute('value')).toBe('first');
|
||||
}
|
||||
expect(input.hasAttribute('value')).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves the value property', () => {
|
||||
|
@ -2165,4 +2157,30 @@ describe('ReactDOMInput', () => {
|
|||
expect(node.hasAttribute('value')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove previous `defaultValue`', () => {
|
||||
const node = ReactDOM.render(
|
||||
<input type="text" defaultValue="0" />,
|
||||
container,
|
||||
);
|
||||
|
||||
expect(node.value).toBe('0');
|
||||
expect(node.defaultValue).toBe('0');
|
||||
|
||||
ReactDOM.render(<input type="text" />, container);
|
||||
expect(node.defaultValue).toBe('');
|
||||
});
|
||||
|
||||
it('should treat `defaultValue={null}` as missing', () => {
|
||||
const node = ReactDOM.render(
|
||||
<input type="text" defaultValue="0" />,
|
||||
container,
|
||||
);
|
||||
|
||||
expect(node.value).toBe('0');
|
||||
expect(node.defaultValue).toBe('0');
|
||||
|
||||
ReactDOM.render(<input type="text" defaultValue={null} />, container);
|
||||
expect(node.defaultValue).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -736,4 +736,26 @@ describe('ReactDOMTextarea', () => {
|
|||
expect(node.value).toBe('foo');
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove previous `defaultValue`', () => {
|
||||
const container = document.createElement('div');
|
||||
const node = ReactDOM.render(<textarea defaultValue="0" />, container);
|
||||
|
||||
expect(node.value).toBe('0');
|
||||
expect(node.defaultValue).toBe('0');
|
||||
|
||||
ReactDOM.render(<textarea />, container);
|
||||
expect(node.defaultValue).toBe('');
|
||||
});
|
||||
|
||||
it('should treat `defaultValue={null}` as missing', () => {
|
||||
const container = document.createElement('div');
|
||||
const node = ReactDOM.render(<textarea defaultValue="0" />, container);
|
||||
|
||||
expect(node.value).toBe('0');
|
||||
expect(node.defaultValue).toBe('0');
|
||||
|
||||
ReactDOM.render(<textarea defaultValue={null} />, container);
|
||||
expect(node.defaultValue).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue