UNPKG

react-phone-number-input

Version:

Telephone number input React component

335 lines (322 loc) 16.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = usePhoneDigits; var _react = require("react"); var _core = require("libphonenumber-js/core"); var _getInternationalPhoneNumberPrefix = _interopRequireDefault(require("./helpers/getInternationalPhoneNumberPrefix.js")); var _isE164Number = require("./helpers/isE164Number.js"); var _inputValuePrefix = require("./helpers/inputValuePrefix.js"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } /** * Returns `[phoneDigits, setPhoneDigits]`. * "Phone digits" includes not only "digits" but also a `+` sign. */ function usePhoneDigits(_ref) { var _this = this; var value = _ref.value, onChange = _ref.onChange, country = _ref.country, defaultCountry = _ref.defaultCountry, international = _ref.international, withCountryCallingCode = _ref.withCountryCallingCode, useNationalFormatForDefaultCountryValue = _ref.useNationalFormatForDefaultCountryValue, metadata = _ref.metadata; // Validate the use of `withCountryCallingCode` property. if (typeof withCountryCallingCode === 'boolean' && !(country && international)) { console.error('[react-phone-number-input] `withCountryCallingCode` property can only be used together with `country` and `international` properties'); } // Validate the use of `country` and `defaultCountry` properties. if (country && defaultCountry) { console.error('[react-phone-number-input] When `country` property is passed, `defaultCountry` property has no effect and therefore shouldn\'t be passed'); } // Validate the use of `international` property. if (typeof international === 'boolean' && !country) { console.error('[react-phone-number-input] `international` property can only be used together with `country` property'); } var inputFormat = getInputFormat({ international: international, country: country, defaultCountry: defaultCountry, withCountryCallingCode: withCountryCallingCode }); var countryMismatchDetected = (0, _react.useRef)(); var onCountryMismatch = function onCountryMismatch(value, country, actualCountry) { console.error("[react-phone-number-input] Expected phone number ".concat(value, " to correspond to country ").concat(country, " but ").concat(actualCountry ? 'in reality it corresponds to country ' + actualCountry : 'it doesn\'t', ".")); countryMismatchDetected.current = true; }; var getInitialPhoneDigits = function getInitialPhoneDigits(options) { // Validate that the initially-supplied `value` is in `E.164` format. // Because sometimes people attempt to supply a `value` like "+1 (879) 490-8676". // https://gitlab.com/catamphetamine/react-phone-number-input/-/issues/231#note_2016334796 if (value) { (0, _isE164Number.validateE164Number)(value); } return getPhoneDigitsForValue(value, country, defaultCountry, inputFormat, useNationalFormatForDefaultCountryValue, metadata, function () { if (options && options.onCountryMismatch) { options.onCountryMismatch(); } for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } onCountryMismatch.apply(_this, args); }); }; // This is only used to detect `country` property change. var _useState = (0, _react.useState)(country), _useState2 = _slicedToArray(_useState, 2), prevCountry = _useState2[0], setPrevCountry = _useState2[1]; // This is only used to detect `defaultCountry` property change. var _useState3 = (0, _react.useState)(defaultCountry), _useState4 = _slicedToArray(_useState3, 2), prevDefaultCountry = _useState4[0], setPrevDefaultCountry = _useState4[1]; // `phoneDigits` is the a property that gets passed to the `<input/>` component as its "value": // * `phoneDigits` is the `<input value/>` property. // * `value` is the `<PhoneInput value/>` property. var _useState5 = (0, _react.useState)(getInitialPhoneDigits()), _useState6 = _slicedToArray(_useState5, 2), phoneDigits = _useState6[0], setPhoneDigits = _useState6[1]; // This is only used to detect `value` property changes. var _useState7 = (0, _react.useState)(value), _useState8 = _slicedToArray(_useState7, 2), valueForPhoneDigits = _useState8[0], setValueForPhoneDigits = _useState8[1]; // Rerender hack. var _useState9 = (0, _react.useState)(), _useState10 = _slicedToArray(_useState9, 2), rerenderTrigger = _useState10[0], setRerenderTrigger = _useState10[1]; var rerender = (0, _react.useCallback)(function () { return setRerenderTrigger({}); }, [setRerenderTrigger]); function getValueForPhoneDigits(phoneDigits) { // If the user hasn't input any digits then `value` is `undefined`. if (!phoneDigits) { return; } if (inputFormat === 'NATIONAL_PART_OF_INTERNATIONAL') { phoneDigits = "+".concat((0, _core.getCountryCallingCode)(country, metadata)).concat(phoneDigits); } // Return the E.164 phone number value. // // Even if no "national (significant) number" digits have been input, // still return a non-`undefined` value. // https://gitlab.com/catamphetamine/react-phone-number-input/-/issues/113 // // For example, if the user has selected country `US` and entered `"1"` // then that `"1"` is just a "national prefix" and no "national (significant) number" // digits have been input yet. Still, return `"+1"` as `value` in such cases, // because otherwise the app would think that the input is empty and mark it as such // while in reality it isn't empty, which might be thought of as a "bug", or just // a "weird" behavior. // // The only case when there's any input and `getNumberValue()` still returns `undefined` // is when that input is `"+"`. // var asYouType = new _core.AsYouType(country || defaultCountry, metadata); asYouType.input(phoneDigits); return asYouType.getNumberValue(); } // If `value` property has been changed externally // then re-initialize the component. (0, _react.useEffect)(function () { if (value !== valueForPhoneDigits) { setValueForPhoneDigits(value); setPhoneDigits(getInitialPhoneDigits()); } }, [value]); // If the `country` has been changed then re-initialize the component. (0, _react.useEffect)(function () { if (country !== prevCountry) { setPrevCountry(country); var _countryMismatchDetected; var _phoneDigits = getInitialPhoneDigits({ onCountryMismatch: function onCountryMismatch() { _countryMismatchDetected = true; } }); setPhoneDigits(_phoneDigits); if (_countryMismatchDetected) { setValueForPhoneDigits(getValueForPhoneDigits(_phoneDigits)); } } }, [country]); // If the `defaultCountry` has been changed then re-initialize the component. (0, _react.useEffect)(function () { if (defaultCountry !== prevDefaultCountry) { setPrevDefaultCountry(defaultCountry); setPhoneDigits(getInitialPhoneDigits()); } }, [defaultCountry]); // Update the `value` after `valueForPhoneDigits` has been updated. (0, _react.useEffect)(function () { if (valueForPhoneDigits !== value) { onChange(valueForPhoneDigits); } }, [valueForPhoneDigits]); var onSetPhoneDigits = (0, _react.useCallback)(function (phoneDigits) { var value; if (country) { if (inputFormat === 'INTERNATIONAL') { // The `<input/>` value must start with the country calling code. var prefix = (0, _getInternationalPhoneNumberPrefix["default"])(country, metadata); if (phoneDigits.indexOf(prefix) !== 0) { // If a user tabs into a phone number input field // that is in `withCountryCallingCode` mode, // and then starts inputting local phone number digits, // the first digit would get "swallowed" if the fix below wasn't implemented. // https://gitlab.com/catamphetamine/react-phone-number-input/-/issues/43 if (phoneDigits && phoneDigits[0] !== '+') { phoneDigits = prefix + phoneDigits; } else { // // Reset phone digits if they don't start with the correct prefix. // // Undo the `<input/>` value change if it doesn't. if (countryMismatchDetected.current) { // In case of a `country`/`value` mismatch, // if it performed an "undo" here, then // it wouldn't let a user edit their phone number at all, // so this special case at least allows phone number editing // when `value` already doesn't match the `country`. } else { // If it simply did `phoneDigits = prefix` here, // then it could have no effect when erasing phone number // via Backspace, because `phoneDigits` in `state` wouldn't change // as a result, because it was `prefix` and it became `prefix`, // so the component wouldn't rerender, and the user would be able // to erase the country calling code part, and that part is // assumed to be non-eraseable. That's why the component is // forcefully rerendered here. setPhoneDigits(prefix); setValueForPhoneDigits(undefined); // Force a re-render of the `<input/>` with previous `phoneDigits` value. return rerender(); } } } } else { // Entering phone number either in "NATIONAL" or in "NATIONAL_PART_OF_INTERNATIONAL" format. // Therefore, `+` is not allowed. if (phoneDigits && phoneDigits[0] === '+') { // Remove the `+`. phoneDigits = phoneDigits.slice(1); } } } else if (!defaultCountry) { // Force a `+` in the beginning of a `value` // when no `country` and `defaultCountry` have been specified. if (phoneDigits && phoneDigits[0] !== '+') { // Prepend a `+`. phoneDigits = '+' + phoneDigits; } } // Convert `phoneDigits` to `value`. if (phoneDigits) { value = getValueForPhoneDigits(phoneDigits); } setPhoneDigits(phoneDigits); setValueForPhoneDigits(value); }, [country, inputFormat, defaultCountry, metadata, setPhoneDigits, setValueForPhoneDigits, rerender, countryMismatchDetected]); return { phoneDigits: phoneDigits, setPhoneDigits: onSetPhoneDigits, inputFormat: inputFormat }; } /** * Returns phone number input field value for a E.164 phone number `value`. * @param {string} [value] * @param {string} [country] * @param {string} [inputFormat] * @param {string} [defaultCountry] * @param {boolean} [useNationalFormatForDefaultCountryValue] * @param {object} metadata * @return {string} */ function getPhoneDigitsForValue(value, country, defaultCountry, inputFormat, useNationalFormatForDefaultCountryValue, metadata, onCountryMismatch) { if (country && inputFormat === 'INTERNATIONAL') { var prefix = (0, _getInternationalPhoneNumberPrefix["default"])(country, metadata); if (value) { if (value.indexOf(prefix) !== 0) { onCountryMismatch(value, country); } return value; } return prefix; } if (!value) { return ''; } if (!country && !defaultCountry) { return value; } var asYouType = new _core.AsYouType(undefined, metadata); asYouType.input(value); var phoneNumber = asYouType.getNumber(); if (phoneNumber) { if (country) { // Check for `country` property mismatch for the actual `value`. if (phoneNumber.country && phoneNumber.country !== country) { onCountryMismatch(value, country, phoneNumber.country); } else if (phoneNumber.countryCallingCode !== (0, _core.getCountryCallingCode)(country, metadata)) { onCountryMismatch(value, country); } switch (inputFormat) { case 'NATIONAL': return (0, _core.parseDigits)(phoneNumber.formatNational()); case 'NATIONAL_PART_OF_INTERNATIONAL': return (0, _core.parseDigits)((0, _inputValuePrefix.removePrefixFromFormattedPhoneNumber)(phoneNumber.formatInternational(), (0, _getInternationalPhoneNumberPrefix["default"])(country, metadata))); case 'INTERNATIONAL': throw new Error('`inputFormat: "INTERNATIONAL"` case should\'ve already been handled earlier in the code'); case 'INTERNATIONAL_OR_NATIONAL': throw new Error('`inputFormat: "INTERNATIONAL_OR_NATIONAL"` is not possible when `country` is fixed'); default: throw new Error("Unknown `inputFormat`: ".concat(inputFormat)); } } else { // `phoneNumber.countryCallingCode` is compared here instead of // `phoneNumber.country`, because, for example, a person could have // previously input a phone number (in "national" format) that isn't // 100% valid for the `defaultCountry`, and if `phoneNumber.country` // was compared, then it wouldn't match, and such phone number // wouldn't be formatted as a "national" one, and instead would be // formatted as an "international" one, confusing the user. // Comparing `phoneNumber.countryCallingCode` works around such issues. // // Example: `defaultCountry="US"` and the `<input/>` is empty. // The user inputs: "222 333 4444", which gets formatted to "(222) 333-4444". // The user then clicks "Save", the page is refreshed, and the user sees // that the `<input/>` value is now "+1 222 333 4444" which confuses the user: // the user expected the `<input/>` value to be "(222) 333-4444", same as it // was when they've just typed it in. The cause of the issue is that "222 333 4444" // is not a valid national number for US, and `phoneNumber.country` is compared // instead of `phoneNumber.countryCallingCode`. After the `phoneNumber.country` // comparison is replaced with `phoneNumber.countryCallingCode` one, the issue // is no longer the case. // if (phoneNumber.countryCallingCode && phoneNumber.countryCallingCode === (0, _core.getCountryCallingCode)(defaultCountry, metadata) && useNationalFormatForDefaultCountryValue) { return (0, _core.parseDigits)(phoneNumber.formatNational()); } return value; } } else { return ''; } } function getInputFormat(_ref2) { var international = _ref2.international, country = _ref2.country, defaultCountry = _ref2.defaultCountry, withCountryCallingCode = _ref2.withCountryCallingCode; return country ? international ? withCountryCallingCode ? 'INTERNATIONAL' : 'NATIONAL_PART_OF_INTERNATIONAL' : 'NATIONAL' : defaultCountry ? 'INTERNATIONAL_OR_NATIONAL' : 'INTERNATIONAL'; } //# sourceMappingURL=usePhoneDigits.js.map