@maskito/react
Version:
The React-specific Maskito's library
120 lines (114 loc) • 4.55 kB
JavaScript
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;
;