import {checkFormFieldValueStringCoercion} from 'shared/CheckStringCoercion';
type ValueTracker = {
getValue(): string,
setValue(value: string): void,
stopTracking(): void,
};
interface ElementWithValueTracker extends HTMLInputElement {
_valueTracker?: ?ValueTracker;
}
function isCheckable(elem: HTMLInputElement) {
const type = elem.type;
const nodeName = elem.nodeName;
return (
nodeName &&
nodeName.toLowerCase() === 'input' &&
(type === 'checkbox' || type === 'radio')
);
}
function getTracker(node: ElementWithValueTracker) {
return node._valueTracker;
}
function detachTracker(node: ElementWithValueTracker) {
node._valueTracker = null;
}
function getValueFromNode(node: HTMLInputElement): string {
let value = '';
if (!node) {
return value;
}
if (isCheckable(node)) {
value = node.checked ? 'true' : 'false';
} else {
value = node.value;
}
return value;
}
function trackValueOnNode(
node: any,
valueField: 'checked' | 'value',
currentValue: string,
): ?ValueTracker {
const descriptor = Object.getOwnPropertyDescriptor(
node.constructor.prototype,
valueField,
);
if (
node.hasOwnProperty(valueField) ||
typeof descriptor === 'undefined' ||
typeof descriptor.get !== 'function' ||
typeof descriptor.set !== 'function'
) {
return;
}
const {get, set} = descriptor;
Object.defineProperty(node, valueField, {
configurable: true,
get: function () {
return get.call(this);
},
set: function (value) {
if (__DEV__) {
checkFormFieldValueStringCoercion(value);
}
currentValue = '' + value;
set.call(this, value);
},
});
Object.defineProperty(node, valueField, {
enumerable: descriptor.enumerable,
});
const tracker = {
getValue() {
return currentValue;
},
setValue(value: string) {
if (__DEV__) {
checkFormFieldValueStringCoercion(value);
}
currentValue = '' + value;
},
stopTracking() {
detachTracker(node);
delete node[valueField];
},
};
return tracker;
}
export function track(node: ElementWithValueTracker) {
if (getTracker(node)) {
return;
}
const valueField = isCheckable(node) ? 'checked' : 'value';
const initialValue = '' + (node[valueField]: any);
node._valueTracker = trackValueOnNode(node, valueField, initialValue);
}
export function trackHydrated(
node: ElementWithValueTracker,
initialValue: string,
initialChecked: boolean,
): boolean {
if (getTracker(node)) {
return false;
}
let valueField;
let expectedValue;
if (isCheckable(node)) {
valueField = 'checked';
expectedValue = '' + (initialChecked: any);
} else {
valueField = 'value';
expectedValue = initialValue;
}
const currentValue = '' + (node[valueField]: any);
node._valueTracker = trackValueOnNode(node, valueField, expectedValue);
return currentValue !== expectedValue;
}
export function updateValueIfChanged(node: ElementWithValueTracker): boolean {
if (!node) {
return false;
}
const tracker = getTracker(node);
if (!tracker) {
return true;
}
const lastValue = tracker.getValue();
const nextValue = getValueFromNode(node);
if (nextValue !== lastValue) {
tracker.setValue(nextValue);
return true;
}
return false;
}
export function stopTracking(node: ElementWithValueTracker) {
const tracker = getTracker(node);
if (tracker) {
tracker.stopTracking();
}
}