import React, { forwardRef, useCallback } from 'react' import { Link, useLocation, useNavigate, generatePath, Params, NavigateOptions, LinkProps } from 'react-router-dom' import { parseSearch, stringifySearch } from '../util/url-search-params' export type AppNavigationTarget = { /** * path to target, for example `"/posts"` or `"/posts/:id"` */ path?: string /** * dictionary of param keys with their values, if needed, for example `{ id: "some-id" }` */ params?: Params /** * function in the form of `(currentSearchRecord) => newSearchRecord` to set link search value, for example * - `(s) => s` preserves current search value, * - `(s) => ({ ...s, calendar: !s.calendar })` toggles the value for calendar search parameter, * - `() => ({ page: 5 })` sets the search to `?page=5`, * - `undefined` empties the search */ search?: (search: Record) => Record } const getNavigateToOptions = ( currentSearchString: string, { path, params, search }: AppNavigationTarget ) => { const searchRecord = parseSearch(currentSearchString) const updatedSearchRecord = search && search(searchRecord) const updatedPath = path && generatePath(path, params) return { pathname: updatedPath, search: updatedSearchRecord && stringifySearch(updatedSearchRecord) } } export const useGetNavigateOptions = () => { const location = useLocation() const getToOptions = useCallback( ({ path, params, search }: AppNavigationTarget) => { return getNavigateToOptions(location.search, { path, params, search }) }, [location.search] ) return getToOptions } export const useAppNavigate = () => { const _navigate = useNavigate() const getToOptions = useGetNavigateOptions() const navigate = useCallback( ({ path, params, search, ...options }: AppNavigationTarget & NavigateOptions) => { // Event dispatched for handling by LiveView dashboard via hook. // Necessary to emulate navigation events in LiveView with pushState // manipulation disabled. window.dispatchEvent( new CustomEvent('dashboard:live-navigate-back', { detail: { search: window.location.search } }) ) return _navigate(getToOptions({ path, params, search }), options) }, [getToOptions, _navigate] ) return navigate } export type AppNavigationLinkProps = AppNavigationTarget & Omit export const AppNavigationLink = forwardRef< HTMLAnchorElement | null, AppNavigationLinkProps >(({ path, params, search, ...options }, ref) => { const getToOptions = useGetNavigateOptions() return ( ) }) AppNavigationLink.displayName = 'AppNavigationLink'