import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber';
import {setValueForProperty} from './DOMPropertyOperations';
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
import {getToStringValue, toString} from './ToStringValue';
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
import {updateValueIfChanged} from './inputValueTracking';
import getActiveElement from './getActiveElement';
import assign from 'shared/assign';
import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import type {ToStringValue} from './ToStringValue';
type InputWithWrapperState = HTMLInputElement & {
_wrapperState: {
initialValue: ToStringValue,
initialChecked: ?boolean,
controlled?: boolean,
...
},
...
};
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;
}
export function getHostProps(element: Element, props: Object): Object {
const node = ((element: any): InputWithWrapperState);
const checked = props.checked;
const hostProps = assign({}, props, {
defaultChecked: undefined,
defaultValue: undefined,
value: undefined,
checked: checked != null ? checked : node._wrapperState.initialChecked,
});
return hostProps;
}
export function initWrapperState(element: Element, props: Object) {
if (__DEV__) {
checkControlledValueProps('input', props);
if (
props.checked !== undefined &&
props.defaultChecked !== undefined &&
!didWarnCheckedDefaultChecked
) {
console.error(
'%s contains an input of type %s with both checked and defaultChecked props. ' +
'Input elements must be either controlled or uncontrolled ' +
'(specify either the checked prop, or the defaultChecked prop, but not ' +
'both). Decide between using a controlled or uncontrolled input ' +
'element and remove one of these props. More info: ' +
'https://reactjs.org/link/controlled-components',
getCurrentFiberOwnerNameInDevOrNull() || 'A component',
props.type,
);
didWarnCheckedDefaultChecked = true;
}
if (
props.value !== undefined &&
props.defaultValue !== undefined &&
!didWarnValueDefaultValue
) {
console.error(
'%s contains an input of type %s with both value and defaultValue props. ' +
'Input elements must be either controlled or uncontrolled ' +
'(specify either the value prop, or the defaultValue prop, but not ' +
'both). Decide between using a controlled or uncontrolled input ' +
'element and remove one of these props. More info: ' +
'https://reactjs.org/link/controlled-components',
getCurrentFiberOwnerNameInDevOrNull() || 'A component',
props.type,
);
didWarnValueDefaultValue = true;
}
}
const node = ((element: any): InputWithWrapperState);
const defaultValue = props.defaultValue == null ? '' : props.defaultValue;
node._wrapperState = {
initialChecked:
props.checked != null ? props.checked : props.defaultChecked,
initialValue: getToStringValue(
props.value != null ? props.value : defaultValue,
),
controlled: isControlled(props),
};
}
export function updateChecked(element: Element, props: Object) {
const node = ((element: any): InputWithWrapperState);
const checked = props.checked;
if (checked != null) {
setValueForProperty(node, 'checked', checked, false);
}
}
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);
const value = getToStringValue(props.value);
const type = props.type;
if (value != null) {
if (type === 'number') {
if (
(value === 0 && node.value === '') ||
node.value != (value: any)
) {
node.value = toString((value: any));
}
} else if (node.value !== toString((value: any))) {
node.value = toString((value: any));
}
} else if (type === 'submit' || type === 'reset') {
node.removeAttribute('value');
return;
}
if (disableInputAttributeSyncing) {
if (props.hasOwnProperty('defaultValue')) {
setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
}
} else {
if (props.hasOwnProperty('value')) {
setDefaultValue(node, props.type, value);
} else if (props.hasOwnProperty('defaultValue')) {
setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
}
}
if (disableInputAttributeSyncing) {
if (props.defaultChecked == null) {
node.removeAttribute('checked');
} else {
node.defaultChecked = !!props.defaultChecked;
}
} else {
if (props.checked == null && props.defaultChecked != null) {
node.defaultChecked = !!props.defaultChecked;
}
}
}
export function postMountWrapper(
element: Element,
props: Object,
isHydrating: boolean,
) {
const node = ((element: any): InputWithWrapperState);
if (props.hasOwnProperty('value') || props.hasOwnProperty('defaultValue')) {
const type = props.type;
const isButton = type === 'submit' || type === 'reset';
if (isButton && (props.value === undefined || props.value === null)) {
return;
}
const initialValue = toString(node._wrapperState.initialValue);
if (!isHydrating) {
if (disableInputAttributeSyncing) {
const value = getToStringValue(props.value);
if (value != null) {
if (isButton || value !== node.value) {
node.value = toString(value);
}
}
} else {
if (initialValue !== node.value) {
node.value = initialValue;
}
}
}
if (disableInputAttributeSyncing) {
const defaultValue = getToStringValue(props.defaultValue);
if (defaultValue != null) {
node.defaultValue = toString(defaultValue);
}
} else {
node.defaultValue = initialValue;
}
}
const name = node.name;
if (name !== '') {
node.name = '';
}
if (disableInputAttributeSyncing) {
if (!isHydrating) {
updateChecked(element, props);
}
if (props.hasOwnProperty('defaultChecked')) {
node.defaultChecked = !node.defaultChecked;
node.defaultChecked = !!props.defaultChecked;
}
} else {
node.defaultChecked = !node.defaultChecked;
node.defaultChecked = !!node._wrapperState.initialChecked;
}
if (name !== '') {
node.name = name;
}
}
export function restoreControlledState(element: Element, props: Object) {
const node = ((element: any): InputWithWrapperState);
updateWrapper(node, props);
updateNamedCousins(node, props);
}
function updateNamedCousins(rootNode: InputWithWrapperState, props: any) {
const name = props.name;
if (props.type === 'radio' && name != null) {
let queryRoot: Element = rootNode;
while (queryRoot.parentNode) {
queryRoot = ((queryRoot.parentNode: any): Element);
}
if (__DEV__) {
checkAttributeStringCoercion(name, 'name');
}
const group = queryRoot.querySelectorAll(
'input[name=' + JSON.stringify('' + name) + '][type="radio"]',
);
for (let i = 0; i < group.length; i++) {
const otherNode = ((group[i]: any): HTMLInputElement);
if (otherNode === rootNode || otherNode.form !== rootNode.form) {
continue;
}
const otherProps = getFiberCurrentPropsFromNode(otherNode);
if (!otherProps) {
throw new Error(
'ReactDOMInput: Mixing React and non-React radio inputs with the ' +
'same `name` is not supported.',
);
}
updateValueIfChanged(otherNode);
updateWrapper(otherNode, otherProps);
}
}
}
export function setDefaultValue(
node: InputWithWrapperState,
type: ?string,
value: ToStringValue,
) {
if (
type !== 'number' ||
getActiveElement(node.ownerDocument) !== node
) {
if (value == null) {
node.defaultValue = toString(node._wrapperState.initialValue);
} else if (node.defaultValue !== toString(value)) {
node.defaultValue = toString(value);
}
}
}