UNPKG

@maskito/react

Version:

The React-specific Maskito's library

120 lines (114 loc) 4.55 kB
'use strict'; var core = require('@maskito/core'); var react = require('react'); /** * React adds `_valueTracker` property to every textfield elements for its internal logic with controlled inputs. * Also, React monkey-patches `value`-setter of the native textfield elements to update state inside its `_valueTracker`. * @see https://github.com/facebook/react/blob/ee76351917106c6146745432a52e9a54a41ee181/packages/react-dom-bindings/src/client/inputValueTracking.js#L12-L19 * * React depends on `_valueTracker` to know if the value was changed to decide: * - should it revert state for controlled input (if its state handler does not update value) * - should it dispatch its synthetic (not native!) `change` event * * When Maskito patches textfield with a valid value (using setter of `value` property), * it also updates `_valueTracker` state and React mistakenly decides that nothing has happened. * React should update `_valueTracker` state by itself! * ___ * @see https://github.com/facebook/react/blob/ee76351917106c6146745432a52e9a54a41ee181/packages/react-dom-bindings/src/client/inputValueTracking.js#L173-L177 */ function adaptReactControlledElement(element) { var _a; const valueSetter = (_a = Object.getOwnPropertyDescriptor(getPrototype(element), 'value')) === null || _a === void 0 ? void 0 : _a.set; if (!valueSetter) { return element; } const adapter = { set value(value) { /** * Mimics exactly what happens when a browser silently changes the value property. * Bypass the React monkey-patching. */ valueSetter.call(element, value); } }; return new Proxy(element, { get(target, prop) { const nativeProperty = target[prop]; return typeof nativeProperty === 'function' ? nativeProperty.bind(target) : nativeProperty; }, // eslint-disable-next-line @typescript-eslint/max-params set(target, prop, val, receiver) { return Reflect.set(prop in adapter ? adapter : target, prop, val, receiver); } }); } function getPrototype(element) { var _a, _b; switch (element.nodeName) { case 'INPUT': return (_a = globalThis.HTMLInputElement) === null || _a === void 0 ? void 0 : _a.prototype; case 'TEXTAREA': return (_b = globalThis.HTMLTextAreaElement) === null || _b === void 0 ? void 0 : _b.prototype; default: return null; } } const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? react.useLayoutEffect : react.useEffect; function isThenable(x) { return x && typeof x === 'object' && 'then' in x; } /** * Hook for convenient use of Maskito in React * @description For controlled inputs use `onInput` event * @param options options used for creating Maskito * @param elementPredicate function that can help find nested Input or TextArea * @returns ref callback to pass it in React Element * @example * // To avoid unnecessary hook runs with Maskito recreation pass named variables * // good example ✅ * useMaskito({ options: maskitoOptions, elementPredicate: maskitoPredicate }) * * // bad example ❌ * useMaskito({ options: { mask: /^.*$/ }, elementPredicate: () => e.querySelector('input') }) */ const useMaskito = ({ options = null, elementPredicate = core.MASKITO_DEFAULT_ELEMENT_PREDICATE } = {}) => { const [hostElement, setHostElement] = react.useState(null); const [element, setElement] = react.useState(null); const onRefChange = react.useCallback(node => { setHostElement(node); }, []); const latestPredicateRef = react.useRef(elementPredicate); const latestOptionsRef = react.useRef(options); latestPredicateRef.current = elementPredicate; latestOptionsRef.current = options; useIsomorphicLayoutEffect(() => { if (!hostElement) { return; } const elementOrPromise = elementPredicate(hostElement); if (isThenable(elementOrPromise)) { void elementOrPromise.then(el => { if (latestPredicateRef.current === elementPredicate && latestOptionsRef.current === options) { setElement(el); } }); } else { setElement(elementOrPromise); } }, [hostElement, elementPredicate, latestPredicateRef, options, latestOptionsRef]); useIsomorphicLayoutEffect(() => { if (!element || !options) { return; } const maskedElement = new core.Maskito(adaptReactControlledElement(element), options); return () => { maskedElement.destroy(); setElement(null); }; }, [options, element]); return onRefChange; }; exports.useMaskito = useMaskito;