main
js 187 lines 4.79 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import {checkFormFieldValueStringCoercion} from 'shared/CheckStringCoercion';
11
12 type ValueTracker = {
13 getValue(): string,
14 setValue(value: string): void,
15 stopTracking(): void,
16 };
17 interface ElementWithValueTracker extends HTMLInputElement {
18 _valueTracker?: ?ValueTracker;
19 }
20
21 function isCheckable(elem: HTMLInputElement) {
22 const type = elem.type;
23 const nodeName = elem.nodeName;
24 return (
25 nodeName &&
26 nodeName.toLowerCase() === 'input' &&
27 (type === 'checkbox' || type === 'radio')
28 );
29 }
30
31 function getTracker(node: ElementWithValueTracker) {
32 return node._valueTracker;
33 }
34
35 function detachTracker(node: ElementWithValueTracker) {
36 node._valueTracker = null;
37 }
38
39 function getValueFromNode(node: HTMLInputElement): string {
40 let value = '';
41 if (!node) {
42 return value;
43 }
44
45 if (isCheckable(node)) {
46 value = node.checked ? 'true' : 'false';
47 } else {
48 value = node.value;
49 }
50
51 return value;
52 }
53
54 function trackValueOnNode(
55 node: any,
56 valueField: 'checked' | 'value',
57 currentValue: string,
58 ): ?ValueTracker {
59 const descriptor = Object.getOwnPropertyDescriptor(
60 node.constructor.prototype,
61 valueField,
62 );
63
64 // if someone has already defined a value or Safari, then bail
65 // and don't track value will cause over reporting of changes,
66 // but it's better then a hard failure
67 // (needed for certain tests that spyOn input values and Safari)
68 if (
69 node.hasOwnProperty(valueField) ||
70 typeof descriptor === 'undefined' ||
71 typeof descriptor.get !== 'function' ||
72 typeof descriptor.set !== 'function'
73 ) {
74 return;
75 }
76 const {get, set} = descriptor;
77 Object.defineProperty(node, valueField, {
78 configurable: true,
79 // $FlowFixMe[missing-this-annot]
80 get: function () {
81 return get.call(this);
82 },
83 // $FlowFixMe[missing-local-annot]
84 // $FlowFixMe[missing-this-annot]
85 set: function (value) {
86 if (__DEV__) {
87 checkFormFieldValueStringCoercion(value);
88 }
89 currentValue = '' + value;
90 set.call(this, value);
91 },
92 });
93 // We could've passed this the first time
94 // but it triggers a bug in IE11 and Edge 14/15.
95 // Calling defineProperty() again should be equivalent.
96 // https://github.com/facebook/react/issues/11768
97 Object.defineProperty(node, valueField, {
98 enumerable: descriptor.enumerable,
99 });
100
101 const tracker = {
102 getValue() {
103 return currentValue;
104 },
105 setValue(value: string) {
106 if (__DEV__) {
107 checkFormFieldValueStringCoercion(value);
108 }
109 currentValue = '' + value;
110 },
111 stopTracking() {
112 detachTracker(node);
113 delete node[valueField];
114 },
115 };
116 return tracker;
117 }
118
119 export function track(node: ElementWithValueTracker) {
120 if (getTracker(node)) {
121 return;
122 }
123
124 const valueField = isCheckable(node) ? 'checked' : 'value';
125 // This is read from the DOM so always safe to coerce. We really shouldn't
126 // be coercing to a string at all. It's just historical.
127 // eslint-disable-next-line react-internal/safe-string-coercion
128 const initialValue = '' + (node[valueField] as any);
129 node._valueTracker = trackValueOnNode(node, valueField, initialValue);
130 }
131
132 export function trackHydrated(
133 node: ElementWithValueTracker,
134 initialValue: string,
135 initialChecked: boolean,
136 ): boolean {
137 // For hydration, the initial value is not the current value but the value
138 // that we last observed which is what the initial server render was.
139 if (getTracker(node)) {
140 return false;
141 }
142
143 let valueField: 'checked' | 'value';
144 let expectedValue;
145 if (isCheckable(node)) {
146 valueField = 'checked';
147 // eslint-disable-next-line react-internal/safe-string-coercion
148 expectedValue = '' + (initialChecked as any);
149 } else {
150 valueField = 'value';
151 expectedValue = initialValue;
152 }
153 const currentValue =
154 // eslint-disable-next-line react-internal/safe-string-coercion
155 '' + // $FlowFixMe[prop-missing]
156 (node[valueField] as any);
157 node._valueTracker = trackValueOnNode(node, valueField, expectedValue);
158 return currentValue !== expectedValue;
159 }
160
161 export function updateValueIfChanged(node: ElementWithValueTracker): boolean {
162 if (!node) {
163 return false;
164 }
165
166 const tracker = getTracker(node);
167 // if there is no tracker at this point it's unlikely
168 // that trying again will succeed
169 if (!tracker) {
170 return true;
171 }
172
173 const lastValue = tracker.getValue();
174 const nextValue = getValueFromNode(node);
175 if (nextValue !== lastValue) {
176 tracker.setValue(nextValue);
177 return true;
178 }
179 return false;
180 }
181
182 export function stopTracking(node: ElementWithValueTracker) {
183 const tracker = getTracker(node);
184 if (tracker) {
185 tracker.stopTracking();
186 }
187 }