jotai-location
Version:
137 lines (136 loc) • 5.53 kB
JavaScript
import { atom } from 'jotai/vanilla';
import { atomWithLocation, } from './atomWithLocation.js';
function warning(...data) {
if (process.env.NODE_ENV !== 'production') {
console.warn(...data);
}
}
const applyLocation = (location, options) => {
const url = new URL(window.location.href);
if ('pathname' in location) {
url.pathname = location.pathname;
}
if ('searchParams' in location) {
const existingParams = new URLSearchParams(url.search);
const newParams = location.searchParams;
for (const [key, value] of newParams.entries()) {
existingParams.set(key, value);
}
url.search = existingParams.toString();
}
if ('hash' in location) {
url.hash = location.hash;
}
if (options === null || options === void 0 ? void 0 : options.replace) {
window.history.replaceState(window.history.state, '', url);
}
else {
window.history.pushState(null, '', url);
}
};
/**
* Creates an atom that manages a single search parameter.
*
* The atom automatically infers the type of the search parameter based on the
* type of `defaultValue`.
*
* The atom's read function returns the current value of the search parameter.
* The atom's write function updates the search parameter in the URL.
*
* @param key - The key of the search parameter.
* @param defaultValue - The default value of the search parameter.
* @returns A writable atom that manages the search parameter.
*/
export const atomWithSearchParams = (key, defaultValue, options) => {
var _a;
// Create an atom for managing location state, including search parameters.
const locationAtom = atomWithLocation({
...options,
applyLocation: (_a = options === null || options === void 0 ? void 0 : options.applyLocation) !== null && _a !== void 0 ? _a : applyLocation,
});
/**
* Resolves the value of a search parameter based on the type of `defaultValue`.
*
* @param value - The raw value from the URL (could be `null` or `undefined`).
* @returns The resolved value matching the type of `defaultValue`.
*/
const resolveValue = (value) => {
// If the value is null, undefined, or not a string, return the default value.
if (value === null || value === undefined) {
return defaultValue;
}
// Determine the type of the default value and parse accordingly.
if (typeof defaultValue === 'number') {
if (value === '') {
warning(`Empty string provided for key "${key}". Falling back to default value.`);
return defaultValue;
}
const parsed = Number(value);
if (!Number.isNaN(parsed)) {
return parsed;
}
warning(`Expected a number for key "${key}", got "${value}".`);
return defaultValue;
}
// If the default value is a boolean, check if the value is `true` or `false`.
if (typeof defaultValue === 'boolean') {
if (value === 'true')
return true;
if (value === 'false')
return false;
warning(`Expected a boolean for key "${key}", got "${value}".`);
return defaultValue;
}
if (typeof defaultValue === 'string') {
return value;
}
// Fallback to default value for unsupported types
warning(`Unsupported defaultValue type for key "${key}".`);
return defaultValue;
};
/**
* Converts the value into a string for use in the URL.
*
* Includes runtime type validation to ensure only compatible types are passed.
*
* @param value - The value to be serialized.
* @returns The stringified value.
*/
const parseValue = (value) => {
if (typeof value === 'number' ||
typeof value === 'boolean' ||
typeof value === 'string') {
return String(value);
}
warning(`Unsupported value type for key "${key}":`, typeof value);
throw new Error(`Unsupported value type for key "${key}".`);
};
return atom(
// Read function: Retrieves the current value of the search parameter.
(get) => {
const { searchParams } = get(locationAtom);
// Resolve the value using the parsing logic.
return resolveValue(searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(key));
},
// Write function: Updates the search parameter in the URL.
(_, set, value) => {
set(locationAtom, (prev) => {
// Create a new instance of URLSearchParams to avoid mutating the original.
const newSearchParams = new URLSearchParams(prev.searchParams);
let nextValue;
if (typeof value === 'function') {
// If the new value is a function, compute it based on the current value.
const currentValue = resolveValue(newSearchParams.get(key));
nextValue = value(currentValue);
}
else {
// Otherwise, use the provided value directly.
nextValue = value;
}
// Update the search parameter with the computed value.
newSearchParams.set(key, parseValue(nextValue));
// Return the updated location state with new search parameters.
return { ...prev, searchParams: newSearchParams };
});
});
};