| 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 | } |