diff --git a/src/lib/adapters/historyjs/index.tsx b/src/lib/adapters/historyjs/index.tsx index 94855eb..512ee85 100644 --- a/src/lib/adapters/historyjs/index.tsx +++ b/src/lib/adapters/historyjs/index.tsx @@ -22,25 +22,30 @@ export interface Refs { readonly updateFromQuery: (query: string) => void } +const createSearch = (query: Record) => { + const queryString = new URLSearchParams(query).toString() + return queryString === '' ? '' : `?${queryString}` +} + export const GeschichteWithHistory = forwardRef( ({ children, history }, ref) => { const historyInstance: HistoryManagement = useMemo(() => { return { initialSearch: () => history.location.search, - push: (next: string) => { + push: async (query: Record) => { history.push( { hash: history.location.hash, - search: next, + search: createSearch(query), }, { __g__: true } ) }, - replace: (next: string) => + replace: async (query: Record) => history.replace( { hash: history.location.hash, - search: next, + search: createSearch(query), }, { __g__: true } ), diff --git a/src/lib/adapters/nextjs/index.tsx b/src/lib/adapters/nextjs/index.tsx index 4892789..a85ad12 100644 --- a/src/lib/adapters/nextjs/index.tsx +++ b/src/lib/adapters/nextjs/index.tsx @@ -18,6 +18,7 @@ import { import shallow from 'zustand/shallow' import { StoreState } from '../../middleware' import { HistoryManagement, StoreContext, useGeschichte } from '../../store' +import type { UrlObject } from 'url' const split = (url?: string) => url?.split('?') || [] @@ -27,11 +28,25 @@ interface TransitionOptions { readonly scroll?: boolean } +declare type Url = UrlObject | string + interface Props { readonly initialClientOnlyAsPath?: string readonly asPath: string readonly defaultPushOptions?: TransitionOptions readonly defaultReplaceOptions?: TransitionOptions + // tslint:disable-next-line:no-mixed-interface + readonly routerPush?: ( + url: Url, + as: UrlObject, + options?: TransitionOptions + ) => Promise + // tslint:disable-next-line:no-mixed-interface + readonly routerReplace?: ( + url: Url, + as: UrlObject, + options?: TransitionOptions + ) => Promise } // FIXME: Somehow imports are messed up for nextjs when importing from modules (see https://github.com/vercel/next.js/issues/36794) @@ -43,6 +58,8 @@ export const GeschichteForNextjs: FC = ({ initialClientOnlyAsPath, defaultPushOptions, defaultReplaceOptions, + routerPush, + routerReplace, }) => { const lastClientSideQuery = useRef(initialClientOnlyAsPath) const historyInstance: HistoryManagement = useMemo(() => { @@ -54,23 +71,34 @@ export const GeschichteForNextjs: FC = ({ : split(lastClientSideQuery.current || Router.asPath) return `?${query || ''}` }, - push: (next: string, options) => { - const [path] = split(Router.asPath) - Router.push(Router.route, `${path}${next}`, { + push: (query, options) => { + const [pathname] = split(Router.asPath) + const routerOptions = { ...defaultPushOptions, ...options, - }) + } + + if (routerPush) { + return routerPush(Router.route, { pathname, query }, routerOptions) + } + return Router.push(Router.route, { pathname, query }, routerOptions) }, - replace: (next: string, options) => { - const [path] = split(Router.asPath) - Router.replace(Router.route, `${path}${next}`, { + replace: (query, options) => { + const [pathname] = split(Router.asPath) + + const routerOptions = { ...defaultReplaceOptions, ...options, - }) + } + + if (routerReplace) { + return routerReplace(Router.route, { pathname, query }, routerOptions) + } + return Router.replace(Router.route, { pathname, query }, routerOptions) }, } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [routerPush, routerReplace]) const useStore = useMemo( () => useGeschichte(historyInstance), @@ -110,7 +138,10 @@ export const GeschichteForNextjs: FC = ({ type ClientOnlyProps = Pick< Props, - 'defaultPushOptions' | 'defaultReplaceOptions' + | 'defaultPushOptions' + | 'defaultReplaceOptions' + | 'routerPush' + | 'routerReplace' > & { readonly children: ReactNode readonly omitQueries?: boolean diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 975f0ff..2b6c39e 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -33,12 +33,12 @@ export type PushStateFunction = ( ns: string, valueCreator: (state: T) => void, routerOptions?: RouterOptions -) => void +) => Promise export type ReplaceStateFunction = ( ns: string, valueCreator: (state: T) => void, routerOptions?: RouterOptions -) => void +) => Promise export interface InnerNamespace { [ns: string]: NamespaceValues @@ -56,12 +56,12 @@ export interface StoreState extends State { ns: readonly string[], fn: (...valueState: ValueState[]) => void, routerOptions?: RouterOptions - ) => void + ) => Promise readonly batchPushState: ( ns: readonly string[], fn: (...valueState: ValueState[]) => void, routerOptions?: RouterOptions - ) => void + ) => Promise namespaces: InnerNamespace readonly pushState: PushStateFunction readonly replaceState: ReplaceStateFunction @@ -93,13 +93,13 @@ export type NamespaceProducer = ( eventType: HistoryEventType, ns?: string, routerOptions?: RouterOptions -) => void +) => Promise export type GenericConverter = ( stateProducer: InnerNamespaceProducerFunction, eventType: HistoryEventType, ns?: string, routerOptions?: RouterOptions -) => void +) => Promise export type ImmerProducer = ( stateMapper: (changes: Patch[], values: StoreState) => StoreState, @@ -129,94 +129,105 @@ export const historyManagement = ns?: string, options?: RouterOptions ) => { - // we call the `immerWithPatches` middleware - return set( - (changes: Patch[], values: StoreState) => { - if (changes.length === 0) { - return values - } - if (type !== HistoryEventType.REGISTER) { - // if namespace is not given, calculate what namespaces are affected - const affectedNamespaces: string[] = ns - ? [ns] - : changes.reduce((next: string[], change: Patch) => { - const { - path: [, namespace], - } = change - - if (next.indexOf(namespace as string) === -1) { - return [...next, namespace as string] - } - return next - }, []) - - const uniqueQueries: { - [key: string]: any - } = affectedNamespaces.reduce((next, thisNs) => { - const { config, query: currentQuery } = get().namespaces[thisNs] - return { - ...next, - [thisNs]: applyDiffWithCreateQueriesFromPatch( - config, - thisNs, - currentQuery, - changes, - values.namespaces[thisNs].values, - values.namespaces[thisNs].initialValues - ), - } - }, {}) + return new Promise((resolve, reject) => { + set( + (changes: Patch[], values: StoreState) => { + if (changes.length === 0) { + resolve(null) + return values + } + if (type !== HistoryEventType.REGISTER) { + // if namespace is not given, calculate what namespaces are affected + const affectedNamespaces: string[] = ns + ? [ns] + : changes.reduce((next: string[], change: Patch) => { + const { + path: [, namespace], + } = change - const method = type === HistoryEventType.PUSH ? 'push' : 'replace' + if (next.indexOf(namespace as string) === -1) { + return [...next, namespace as string] + } + return next + }, []) - const otherQueries = Object.keys(values.namespaces).reduce( - (next, thisNs) => { - if (affectedNamespaces.indexOf(thisNs) !== -1) { - return next - } + const uniqueQueries: { + [key: string]: any + } = affectedNamespaces.reduce((next, thisNs) => { + const { config, query: currentQuery } = + get().namespaces[thisNs] return { ...next, - ...values.namespaces[thisNs].query, + [thisNs]: applyDiffWithCreateQueriesFromPatch( + config, + thisNs, + currentQuery, + changes, + values.namespaces[thisNs].values, + values.namespaces[thisNs].initialValues + ), } - }, - {} - ) + }, {}) - const reducedQueries = Object.keys(uniqueQueries).reduce( - (next, thisNs) => ({ ...next, ...uniqueQueries[thisNs] }), - {} - ) - - const query = new URLSearchParams({ - ...otherQueries, - ...reducedQueries, - }).toString() - historyInstance[method](query === '' ? '' : `?${query}`, options) + const method = + type === HistoryEventType.PUSH ? 'push' : 'replace' - // We safe the current state of `query` for all affected namespaces - return { - ...values, - namespaces: { - ...values.namespaces, - ...affectedNamespaces.reduce((next: any, thisNs: string) => { + const otherQueries = Object.keys(values.namespaces).reduce( + (next, thisNs) => { + if (affectedNamespaces.indexOf(thisNs) !== -1) { + return next + } return { ...next, - [thisNs]: { - ...values.namespaces[thisNs], - query: uniqueQueries[thisNs], - }, + ...values.namespaces[thisNs].query, } - }, {}), - }, + }, + {} + ) + + const reducedQueries = Object.keys(uniqueQueries).reduce( + (next, thisNs) => ({ ...next, ...uniqueQueries[thisNs] }), + {} + ) + + const queryObject = Object.freeze({ + ...otherQueries, + ...reducedQueries, + }) + + historyInstance[method](queryObject, options) + .then(resolve) + .catch(reject) + + // We save the current state of `query` for all affected namespaces + return { + ...values, + namespaces: { + ...values.namespaces, + ...affectedNamespaces.reduce( + (next: any, thisNs: string) => { + return { + ...next, + [thisNs]: { + ...values.namespaces[thisNs], + query: uniqueQueries[thisNs], + }, + } + }, + {} + ), + }, + } } - } - return values - }, - fn as NamespaceProducerFunction & - InnerNamespaceProducerFunction, - type, - ns - ) + resolve(null) + return values + }, + fn as NamespaceProducerFunction & + InnerNamespaceProducerFunction, + type, + ns + ) + }) // H }, get, @@ -338,7 +349,7 @@ export const converter = fn: (...valueState: T[]) => void, routerOptions ) => { - set( + return set( (state: InnerNamespace) => void fn(...ns.map((thisNs) => (state[thisNs] || {}).values)), HistoryEventType.PUSH, @@ -352,7 +363,7 @@ export const converter = fn: (...valueState: T[]) => void, routerOptions ) => { - set( + return set( (state: InnerNamespace) => void fn(...ns.map((thisNs) => (state[thisNs] || {}).values)), HistoryEventType.REPLACE, diff --git a/src/lib/store.ts b/src/lib/store.ts index 9794097..74d7717 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -67,8 +67,14 @@ export type RouterOptions = Record export interface HistoryManagement { /** the initial search string (e.g. ?query=test), contains the questionsmark */ readonly initialSearch: () => string - readonly push: (next: string, options?: RouterOptions) => void - readonly replace: (next: string, options?: RouterOptions) => void + readonly push: ( + queryObject: Record, + options?: RouterOptions + ) => Promise + readonly replace: ( + queryObject: Record, + options?: RouterOptions + ) => Promise } export const useGeschichte = (