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
JavaScript
;
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