blade
Version:
React at the edge.
282 lines (280 loc) • 9.47 kB
JavaScript
import { _ as RootClientContext, a as useLocation, c as usePopulatePathname, l as useRedirect, n as usePrivateLocationRef, r as useReduce, t as usePageTransition } from "./hooks-BCUm8YC_.js";
import { t as generateUniqueId } from "./utils-BIjILkWh.js";
import { v as isStorableObject, y as processStorableObjects } from "./utils-BeDKhSpT.js";
import { r as QUERY_SYMBOLS } from "./dist-C7NzDGd9.js";
import { n as getSyntaxProxy, t as getBatchProxy } from "./queries-CO8sEzJ9.js";
import { t as deserializeError } from "./errors-B8jB1JUF.js";
import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
//#region public/client/hooks.ts
const useMutation = () => {
const clientContext = useContext(RootClientContext);
if (!clientContext) throw new Error("Missing client context in `useMutation`");
const { deferredPromises, collected } = clientContext;
const privateLocationRef = usePrivateLocationRef();
const { transitionPage } = usePageTransition();
const populatePathname = usePopulatePathname();
for (const [promiseID, promise] of Object.entries(deferredPromises.current)) {
let rejection;
const results = collected.queries.filter(({ hookHash, result, error }) => {
if (hookHash !== promiseID) return false;
if (typeof error !== "undefined") rejection = error;
return typeof result !== "undefined";
}).map(({ result }) => result);
if (rejection) promise.reject(deserializeError(rejection));
else if (results.length > 0) promise.resolve(results);
}
const queryHandler = async (queries, options) => {
const privateLocation = privateLocationRef.current;
const currentPathnameWithQuery = privateLocation.pathname + privateLocation.search + privateLocation.hash;
const destination = options?.redirect ? populatePathname(options.redirect) : currentPathnameWithQuery;
const hookHash = generateUniqueId();
const files = /* @__PURE__ */ new Map();
const updatedQueries = await processStorableObjects(queries, (objects) => {
return objects.map(({ value: file }) => {
const id = generateUniqueId();
files.set(id, file);
return {
name: file.name,
key: id,
src: file.name,
meta: {
size: file.size,
type: file.type
},
placeholder: null
};
});
});
transitionPage(destination, {
queries: queries.map((_, index) => ({
query: JSON.stringify(updatedQueries[index]),
type: "write",
database: options?.database,
hookHash
})),
errorFallback: currentPathnameWithQuery,
files
});
return new Promise((resolve, reject) => {
const clear = () => {
delete deferredPromises.current[hookHash];
};
deferredPromises.current[hookHash] = {
resolve: (result) => {
resolve(result);
clear();
},
reject: (error) => {
reject(error);
clear();
}
};
});
};
const callback = async (defaultQuery, options) => {
return (await queryHandler([defaultQuery[QUERY_SYMBOLS.QUERY]], options))[0];
};
const replacer = (value) => isStorableObject(value) ? value : void 0;
return {
add: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.add`,
callback,
replacer
}),
set: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.set`,
callback,
replacer
}),
remove: getSyntaxProxy({
root: `${QUERY_SYMBOLS.QUERY}.remove`,
callback,
replacer
}),
batch: (operations, options) => {
return queryHandler(getBatchProxy(operations).map(({ structure }) => structure), options);
}
};
};
const useLinkEvents = (destination) => {
const { primePageCache, transitionPage } = usePageTransition();
const populatePathname = usePopulatePathname();
const activeTransition = useRef();
if (!destination) return {
onClick: void 0,
onMouseEnter: void 0,
onTouchStart: void 0
};
const populatedPathname = populatePathname(destination);
const primePage = () => {
primePageCache(populatedPathname);
};
return {
onMouseEnter: () => primePage(),
onTouchStart: () => primePage(),
onClick: (event) => {
if (event.defaultPrevented) return;
if (event.metaKey) return;
event.preventDefault();
if (!activeTransition.current) primePage();
transitionPage(populatedPathname, { acceptCache: true });
}
};
};
/**
* Hook for paginating a list of records intelligently.
*
* @param moreAfter - The pagination identifier provided for a list of records by `use`.
* In the case of `const accounts = use.accounts();`, for example, `accounts.moreAfter`
* should be passed.
* @param options - A list of options for customizing the pagination behavior.
* @param options.updateAddressBar - By default, a `?page` parameter will be added to the
* URL, which allows for sharing the current pagination status of the page with other
* people. Setting this argument to `false` will avoid that.
*
* @returns A function that can be invoked to load the next page.
*/
const usePagination = (moreAfter, options) => {
const { transitionPage } = usePageTransition();
const privateLocationRef = usePrivateLocationRef();
const loadingMore = useRef(false);
useEffect(() => {
if (!loadingMore.current) return;
loadingMore.current = false;
}, [moreAfter]);
const resetPagination = () => {
const privateLocation = privateLocationRef.current;
if (!privateLocation.searchParams.has("page")) {
console.debug(`Cannot reset pagination because it isn't active.`);
return;
}
console.debug("Pagination was reset");
const newSearchParams = new URLSearchParams(privateLocation.searchParams);
newSearchParams.delete("page");
const params = newSearchParams.toString();
transitionPage(privateLocation.pathname + (params ? `?${params}` : ""));
};
if (!moreAfter) return {
paginate: () => {
console.debug("Pagination did not occur because no further records are available.");
},
resetPagination
};
const paginate = () => {
const privateLocation = privateLocationRef.current;
if (loadingMore.current) {
console.debug("Pagination did not occur again because it is already ongoing.");
return;
}
const newSearchParams = new URLSearchParams(privateLocation.searchParams);
newSearchParams.set("page", moreAfter);
const newPath = `${privateLocation.pathname}?${newSearchParams.toString()}`;
loadingMore.current = true;
transitionPage(newPath, { updateAddressBar: options?.updateAddressBar });
};
return {
paginate,
resetPagination
};
};
/**
* Concatenates arrays based on pagination. Whenever the current paginated page changes,
* the provided items will be concatenated with the previously provided list of items.
*
* @param items - An array of items (of any type) that should be accumulated. For example,
* this could be a list of React children.
* @param options.moreBefore - The `moreBefore` property of the record list that
* should be paginated.
* @param options.allowUpdates - Controls whether changes to the provided items should be
* allowed, or if they should instead just be ignored.
*
* @returns The concatenated array of items, and a function for updating it.
*/
const usePaginationBuffer = (items, options) => {
const { allowUpdates = true, moreBefore: page } = options;
const [renderedChildren, setRenderedChildren] = useReduce((existing) => {
const storedPreviousPage = existing?.page;
if (!storedPreviousPage && page || storedPreviousPage && page && storedPreviousPage !== page) return {
buffered: true,
items: [...existing?.items || [], ...items],
page
};
if (!allowUpdates && existing?.buffered) return existing;
if (storedPreviousPage && page && storedPreviousPage === page) {
const buffered = existing?.buffered || false;
return {
buffered,
items: buffered ? existing?.items || [] : items,
page
};
}
return {
buffered: false,
items,
page
};
}, [items]);
const setValue = (factory) => {
setRenderedChildren((prevState) => ({
...prevState,
items: factory(prevState.items)
}));
};
return [renderedChildren.items, setValue];
};
/**
* A hook for search / query parameter state management.
*
* @param key - The key of the query parameter to manage.
* @param options - Options for the query parameter.
*
* @returns A tuple containing the current value of the query parameter and a function to set it.
*
* @example
* ```tsx
* import { useQueryState } from 'blade/client/hooks';
*
* export default () => {
* const [hello, setHello] = useQueryState('hello');
*
* return (
* <>
* <input
* onChange={(e) => setHello(e.target.value)}
* value={hello}
* />
* <p>Hello, {hello || 'world'}!</p>
* </>
* )
* }
* ```
*/
const useQueryState = (key, options) => {
const { defaultValue = null, parse } = options ?? {};
const location = useLocation();
const redirect = useRedirect();
const queryState = useMemo(() => {
const rawParam = location.searchParams.get(key);
if (rawParam === null) return defaultValue;
if (parse) return parse({
defaultValue,
value: rawParam
});
return rawParam;
}, [location.searchParams]);
return [queryState, useCallback((value) => {
if (value === null) {
location.searchParams.delete(key);
redirect(location.href, { immediatelyUpdateQueryParams: true });
return;
}
location.searchParams.set(key, String(value));
redirect(location.href, { immediatelyUpdateQueryParams: true });
}, [
queryState,
location.searchParams,
redirect
])];
};
//#endregion
export { useQueryState as a, usePaginationBuffer as i, useMutation as n, usePagination as r, useLinkEvents as t };