UNPKG

react-magic-search-params

Version:

#️⃣ A React Hook for advanced, typed management of URL search parameters, providing built-in TypeScript autocomplete.

497 lines (491 loc) 22.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var reactRouterDom = require('react-router-dom'); var react = require('react'); function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _createForOfIteratorHelperLoose(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (t) return (t = t.call(r)).next.bind(t); if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var o = 0; return function () { return o >= r.length ? { done: !0 } : { done: !1, value: r[o++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } /** Generic hook to handle search parameters in the URL @param mandatory - Mandatory parameters (e.g., page=1, page_size=10, etc.) @param optional - Optional parameters (e.g., order, search, etc.) @param defaultParams - Default parameters sent in the URL on initialization @param forceParams - Parameters forced into the URL regardless of user input @param omitParamsByValues - Parameters omitted if they have specific values */ var useMagicSearchParams = function useMagicSearchParams(_ref) { var _ref$mandatory = _ref.mandatory, mandatory = _ref$mandatory === void 0 ? {} : _ref$mandatory, _ref$optional = _ref.optional, optional = _ref$optional === void 0 ? {} : _ref$optional, _ref$defaultParams = _ref.defaultParams, defaultParams = _ref$defaultParams === void 0 ? {} : _ref$defaultParams, _ref$arraySerializati = _ref.arraySerialization, arraySerialization = _ref$arraySerializati === void 0 ? 'csv' : _ref$arraySerializati, _ref$forceParams = _ref.forceParams, forceParams = _ref$forceParams === void 0 ? {} : _ref$forceParams, _ref$omitParamsByValu = _ref.omitParamsByValues, omitParamsByValues = _ref$omitParamsByValu === void 0 ? [] : _ref$omitParamsByValu; var _useSearchParams = reactRouterDom.useSearchParams(), searchParams = _useSearchParams[0], setSearchParams = _useSearchParams[1]; // Ref to store subscriptions: { paramName: [callback1, callback2, ...] } var subscriptionsRef = react.useRef({}); var previousParamsRef = react.useRef({}); var TOTAL_PARAMS_PAGE = react.useMemo(function () { return _extends({}, mandatory, optional); }, [mandatory, optional]); var PARAM_ORDER = react.useMemo(function () { return Array.from(Object.keys(TOTAL_PARAMS_PAGE)); }, [TOTAL_PARAMS_PAGE]); // we get the keys that are arrays according to TOTAL_PARAMS_PAGE since these require special treatment in the URL due to serialization mode var ARRAY_KEYS = react.useMemo(function () { return Object.keys(TOTAL_PARAMS_PAGE).filter(function (key) { return Array.isArray(TOTAL_PARAMS_PAGE[key]); }); }, [TOTAL_PARAMS_PAGE]); var appendArrayValues = function appendArrayValues(finallyParams, newParams) { // Note: We cannot modify the object of the final parameters directly, as immutability must be maintained var updatedParams = _extends({}, finallyParams); if (ARRAY_KEYS.length === 0) return updatedParams; ARRAY_KEYS.forEach(function (key) { // We use the current values directly from searchParams (source of truth) // This avoids depending on finallyParams in which the arrays have been omitted var currentValues = []; switch (arraySerialization) { case 'csv': { var raw = searchParams.get(key) || ''; // For csv we expect "value1,value2,..." (no prefix) currentValues = raw.split(',').map(function (v) { return v.trim(); }).filter(Boolean); break; } case 'repeat': { // Here we get all ocurrences of key var urlParams = searchParams.getAll(key); currentValues = urlParams.length > 0 ? urlParams : []; break; } case 'brackets': { // Build URLSearchParams from current parameters (to ensure no serialized values are taken previously) var _urlParams = searchParams.getAll(key + "[]"); currentValues = _urlParams.length > 0 ? _urlParams : []; break; } default: { var _searchParams$get; // Mode by default works as csv var _raw = (_searchParams$get = searchParams.get(key)) != null ? _searchParams$get : ''; currentValues = _raw.split(',').map(function (v) { return v.trim(); }).filter(Boolean); } break; } // Update array values with new ones if (newParams[key] !== undefined) { var incoming = newParams[key]; var combined = []; if (typeof incoming === 'string') { // If it is a string, it is toggled (add/remove) combined = currentValues.includes(incoming) ? currentValues.filter(function (v) { return v !== incoming; }) : [].concat(currentValues, [incoming]); } else if (Array.isArray(incoming)) { // if an array is passed, repeated values are merged into a single value // Note: Set is used to remove duplicates combined = Array.from(new Set([].concat(incoming))); } else { combined = currentValues; } updatedParams[key] = combined; } }); return updatedParams; }; var transformParamsToURLSearch = function transformParamsToURLSearch(params) { console.log({ PARAMS_RECIBIDOS_TRANSFORM: params }); var newParam = new URLSearchParams(); var paramsKeys = Object.keys(params); for (var _i = 0, _paramsKeys = paramsKeys; _i < _paramsKeys.length; _i++) { var key = _paramsKeys[_i]; if (Array.isArray(TOTAL_PARAMS_PAGE[key])) { var arrayValue = params[key]; console.log({ arrayValue: arrayValue }); switch (arraySerialization) { case 'csv': { var csvValue = arrayValue.join(','); // set ensure that the previous value is replaced newParam.set(key, csvValue); break; } case 'repeat': { for (var _iterator = _createForOfIteratorHelperLoose(arrayValue), _step; !(_step = _iterator()).done;) { var item = _step.value; // add new value to the key, instead of replacing it newParam.append(key, item); } break; } case 'brackets': { for (var _iterator2 = _createForOfIteratorHelperLoose(arrayValue), _step2; !(_step2 = _iterator2()).done;) { var _item = _step2.value; newParam.append(key + "[]", _item); } break; } default: { var _csvValue = arrayValue.join(','); newParam.set(key, _csvValue); } } } else { newParam.set(key, params[key]); } } return newParam; }; // @ts-ignore var hasForcedParamsValues = function hasForcedParamsValues(_ref2) { var paramsForced = _ref2.paramsForced, compareParams = _ref2.compareParams; // Iterates over the forced parameters and verifies that they exist in the URL and match their values // Ej: { page: 1, page_size: 10 } === { page: 1, page_size: 10 } => true var allParamsMatch = Object.entries(paramsForced).every(function (_ref3) { var key = _ref3[0], value = _ref3[1]; return compareParams[key] === value; }); return allParamsMatch; }; react.useEffect(function () { var keysDefaultParams = Object.keys(defaultParams); var keysForceParams = Object.keys(forceParams); if (keysDefaultParams.length === 0 && keysForceParams.length === 0) return; function handleStartingParams() { var defaultParamsString = transformParamsToURLSearch(defaultParams).toString(); var paramsUrl = getParams(); var paramsUrlString = transformParamsToURLSearch(paramsUrl).toString(); var forceParamsString = transformParamsToURLSearch(forceParams).toString(); console.log({ defaultParamsString: defaultParamsString }); var isForcedParams = hasForcedParamsValues({ paramsForced: forceParams, compareParams: paramsUrl }); if (!isForcedParams) { // In this case, the forced parameters take precedence over the default parameters and the parameters of the current URL (which could have been modified by the user, e.g., page_size=1000) updateParams({ newParams: _extends({}, defaultParams, forceParams) }); return; } // In this way it will be validated that the forced parameters keys and values are in the current URL var isIncludesForcedParams = hasForcedParamsValues({ paramsForced: forceParamsString, compareParams: defaultParams }); if (keysDefaultParams.length > 0 && isIncludesForcedParams) { if (defaultParamsString === paramsUrlString) return; // this means that the URL already has the default parameters updateParams({ newParams: defaultParams }); } } handleStartingParams(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); /** * Convert a string value to its original type (number, boolean, array) according to TOTAL_PARAMS_PAGE * @param value - Chain obtained from the URL * @param key - Key of the parameter */ var convertOriginalType = function convertOriginalType(value, key) { // Given that the parameters of a URL are recieved as strings, they are converted to their original type if (typeof TOTAL_PARAMS_PAGE[key] === 'number') { return parseInt(value); } else if (typeof TOTAL_PARAMS_PAGE[key] === 'boolean') { return value === 'true'; } else if (Array.isArray(TOTAL_PARAMS_PAGE[key])) { // The result will be a valid array represented in the URL ej: tags=tag1,tag2,tag3 to ['tag1', 'tag2', 'tag3'], useful to combine the values of the arrays with the new ones if (arraySerialization === 'csv') { return searchParams.getAll(key).join('').split(','); } else if (arraySerialization === 'repeat') { console.log({ SEARCH_PARAMS: searchParams.getAll(key) }); return searchParams.getAll(key); } else if (arraySerialization === 'brackets') { return searchParams.getAll(key + "[]"); } } // Note: dates are not converted as it is better to handle them directly in the component that receives them, using a library like < date-fns > return value; }; /** * Gets the current parameters from the URL and converts them to their original type if desired * @param convert - If true, converts from string to the inferred type (number, boolean, ...) */ var getStringUrl = function getStringUrl(key, paramsUrl) { var isKeyArray = Array.isArray(TOTAL_PARAMS_PAGE[key]); if (isKeyArray) { var _transformParamsToURL3; if (arraySerialization === 'brackets') { var _transformParamsToURL; var arrayUrl = searchParams.getAll(key + "[]"); var encodedQueryArray = transformParamsToURLSearch((_transformParamsToURL = {}, _transformParamsToURL[key] = arrayUrl, _transformParamsToURL)).toString(); // in this way the array of the URL is decoded to its original form ej: tags[]=tag1&tags[]=tag2&tags[]=tag3 var unencodeQuery = decodeURIComponent(encodedQueryArray); return unencodeQuery; } else if (arraySerialization === 'csv') { var _transformParamsToURL2; var _arrayValue = searchParams.getAll(key); var _encodedQueryArray = transformParamsToURLSearch((_transformParamsToURL2 = {}, _transformParamsToURL2[key] = _arrayValue, _transformParamsToURL2)).toString(); var _unencodeQuery = decodeURIComponent(_encodedQueryArray); return _unencodeQuery; } var arrayValue = searchParams.getAll(key); var stringResult = transformParamsToURLSearch((_transformParamsToURL3 = {}, _transformParamsToURL3[key] = arrayValue, _transformParamsToURL3)).toString(); return stringResult; } else { return paramsUrl[key]; } }; var getParamsObj = function getParamsObj(searchParams) { var paramsObj = {}; // @ts-ignore for (var _iterator3 = _createForOfIteratorHelperLoose(searchParams.entries()), _step3; !(_step3 = _iterator3()).done;) { var _step3$value = _step3.value, key = _step3$value[0], value = _step3$value[1]; if (key.endsWith('[]')) { var bareKey = key.replace('[]', ''); if (paramsObj[bareKey]) { paramsObj[bareKey].push(value); } else { paramsObj[bareKey] = [value]; } } else { // If the key already exists, it is a repeated parameter if (paramsObj[key]) { if (Array.isArray(paramsObj[key])) { paramsObj[key].push(value); } else { paramsObj[key] = [paramsObj[key], value]; } } else { paramsObj[key] = value; } } } return paramsObj; }; // Optimization: While the parameters are not updated, the current parameters of the URL are not recalculated var CURRENT_PARAMS_URL = react.useMemo(function () { return arraySerialization === 'brackets' ? getParamsObj(searchParams) : Object.fromEntries(searchParams.entries()); }, [searchParams, arraySerialization]); /** * Gets the current parameters from the URL and converts them to their original type if desired * @param convert - If true, converts from string to the inferred type (number, boolean, ...) * @returns - Returns the current parameters of the URL */ var getParams = function getParams(_temp) { var _ref4 = _temp === void 0 ? {} : _temp, _ref4$convert = _ref4.convert, convert = _ref4$convert === void 0 ? true : _ref4$convert; // All the paramteres are extracted from the URL and converted into an object var params = Object.keys(CURRENT_PARAMS_URL).reduce(function (acc, key) { if (Object.prototype.hasOwnProperty.call(TOTAL_PARAMS_PAGE, key)) { var realKey = arraySerialization === 'brackets' ? key.replace('[]', '') : key; // @ts-ignore acc[realKey] = convert === true ? convertOriginalType(CURRENT_PARAMS_URL[key], key) : getStringUrl(key, CURRENT_PARAMS_URL); } return acc; }, {}); return params; }; /** * Gets the value of a parameter from the URL and converts it to its original type if desired * @param key - Key of the parameter * @param options - Options to convert the value to its original type, default is true * @returns - Returns the value of the parameter */ var getParam = function getParam(key, options) { var keyStr = String(key); // @ts-ignore var value = (options == null ? void 0 : options.convert) === true ? convertOriginalType(searchParams.get(keyStr), keyStr) : getStringUrl(keyStr, CURRENT_PARAMS_URL); return value; }; var calculateOmittedParameters = function calculateOmittedParameters(newParams, keepParams) { // Calculate the ommited parameters, that is, the parameters that have not been sent in the request var params = getParams(); // hasOw // Note: it will be necessary to omit the parameters that are arrays because the idea is not to replace them but to add or remove some values var newParamsWithoutArray = Object.entries(newParams).filter(function (_ref5) { var key = _ref5[0]; return !Array.isArray(TOTAL_PARAMS_PAGE[key]); }); var result = Object.assign(_extends({}, params, Object.fromEntries(newParamsWithoutArray), forceParams)); var paramsFiltered = Object.keys(result).reduce(function (acc, key) { // for default no parameters are omitted unless specified in the keepParams object if (Object.prototype.hasOwnProperty.call(keepParams, key) && keepParams[key] === false) { return acc; // Note: They array of parameters omitted by values (e.g., ['all', 'default']) are omitted since they are usually a default value that is not desired to be sent } else if (!!result[key] !== false && !omitParamsByValues.includes(result[key])) { // @ts-ignore acc[key] = result[key]; } return acc; }, {}); return _extends({}, mandatory, paramsFiltered); }; // @ts-ignore var sortParameters = function sortParameters(paramsFiltered) { // sort the parameters according to the structure so that it persists with each change in the URL, eg: localhost:3000/?page=1&page_size=10 // Note: this visibly improves the user experience var orderedParams = PARAM_ORDER.reduce(function (acc, key) { if (Object.prototype.hasOwnProperty.call(paramsFiltered, key)) { // @ts-ignore acc[key] = paramsFiltered[key]; } return acc; }, {}); return orderedParams; }; var mandatoryParameters = function mandatoryParameters() { // Note: in case there are arrays in the URL, they are converted to their original form ej: tags=['tag1', 'tag2'] otherwise the parameters are extracted without converting to optimize performance var isNecessaryConvert = ARRAY_KEYS.length > 0 ? true : false; var totalParametros = getParams({ convert: isNecessaryConvert }); var paramsUrlFound = Object.keys(totalParametros).reduce(function (acc, key) { if (Object.prototype.hasOwnProperty.call(mandatory, key)) { // @ts-ignore acc[key] = totalParametros[key]; } return acc; }, {}); return paramsUrlFound; }; /** clears the parameters of the URL, keeping the mandatory parameters * @param keepMandatoryParams - If true, the mandatory parameters are kept in the URL */ var clearParams = function clearParams(_temp2) { var _ref6 = _temp2 === void 0 ? {} : _temp2, _ref6$keepMandatoryPa = _ref6.keepMandatoryParams, keepMandatoryParams = _ref6$keepMandatoryPa === void 0 ? true : _ref6$keepMandatoryPa; // for default, the mandatory parameters are not cleared since the current pagination would be lost var paramsTransformed = transformParamsToURLSearch(_extends({}, mandatory, keepMandatoryParams && _extends({}, mandatoryParameters()), forceParams)); setSearchParams(paramsTransformed); }; /** Merges the new parameters with the current ones, omits the parameters that are not sent and sorts them according to the structure * @param newParams - New parameters to be sent in the URL * @param keepParams - Parameters to keep in the URL, default is true */ var updateParams = function updateParams(_temp3) { var _ref7 = _temp3 === void 0 ? {} : _temp3, _ref7$newParams = _ref7.newParams, newParams = _ref7$newParams === void 0 ? {} : _ref7$newParams, _ref7$keepParams = _ref7.keepParams, keepParams = _ref7$keepParams === void 0 ? {} : _ref7$keepParams; if (Object.keys(newParams).length === 0 && Object.keys(keepParams).length === 0) { clearParams(); return; } // @ts-ignore var finallyParamters = calculateOmittedParameters(newParams, keepParams); var convertedArrayValues = appendArrayValues(finallyParamters, newParams); var paramsSorted = sortParameters(convertedArrayValues); setSearchParams(transformParamsToURLSearch(paramsSorted)); }; /** * @param paramName - Name of the parameter to subscribe to * @param callbacks - Callbacks to be executed when the parameter changes * @returns - Returns the function to unsubscribe */ var onChange = react.useCallback(function (paramName, callbacks) { var paramNameStr = String(paramName); // replace the previous callbacks with the new ones so as not to accumulate callbacks subscriptionsRef.current[paramNameStr] = callbacks; }, []); // each time searchParams changes, we notify the subscribers react.useEffect(function () { for (var _i2 = 0, _Object$entries = Object.entries(subscriptionsRef.current); _i2 < _Object$entries.length; _i2++) { var _CURRENT_PARAMS_URL$k, _previousParamsRef$cu; var _Object$entries$_i = _Object$entries[_i2], key = _Object$entries$_i[0], value = _Object$entries$_i[1]; var newValue = (_CURRENT_PARAMS_URL$k = CURRENT_PARAMS_URL[key]) != null ? _CURRENT_PARAMS_URL$k : null; var oldValue = (_previousParamsRef$cu = previousParamsRef.current[key]) != null ? _previousParamsRef$cu : null; if (newValue !== oldValue) { for (var _iterator4 = _createForOfIteratorHelperLoose(value), _step4; !(_step4 = _iterator4()).done;) { var callback = _step4.value; callback(); } } // once the callback is executed, the previous value is updated to ensure that the next time the value changes, the callback is executed previousParamsRef.current[key] = newValue; } }, [CURRENT_PARAMS_URL]); return { searchParams: searchParams, updateParams: updateParams, clearParams: clearParams, getParams: getParams, getParam: getParam, onChange: onChange }; }; exports.useMagicSearchParams = useMagicSearchParams; //# sourceMappingURL=react-magic-search-params.cjs.development.js.map