diff --git a/CHANGELOG.md b/CHANGELOG.md index c3c3a70..2d2d515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [5.0.0] - UNRELEASED +### Added +- Added support for `useRoutes` to take an array to specify route matching priority ### Changed - Added support for underscore `_` in path part matchers diff --git a/docs/api/useRoutes.md b/docs/api/useRoutes.md index 5ad30e5..353e6ce 100644 --- a/docs/api/useRoutes.md +++ b/docs/api/useRoutes.md @@ -12,7 +12,8 @@ This hook is the main entry point for an application using raviger. Returns the ```typescript function useRoutes( - routes: { [key: string]: (props: { [k: string]: any }) => JSX.Element }, + routes: { [key: string]: (props: { [k: string]: any }) => JSX.Element } + | { path: string, fn: (props: { [k: string]: any }) => JSX.Element }, options?: { basePath?: string routeProps?: { [k: string]: any } @@ -126,3 +127,32 @@ export default function App() { ) } ``` + +## Using an Array to Control Priority + +Raviger will normally match routes in the order they are defined in the routes object, allowing you to control matching priority. However, this behavior is not guaranteed by JS, and if you dynamically construct routes you may have difficulty ordering object keys. + +For this case, raviger supports taking an array of `{ path, fn }` objects, where the priority is determined by position in the array. + +Consider this case: + +```javascript +{ + '/comp1/*': () =>

Just Comp-1

, + '/comp1/view2/*': () =>

Comp-1-view-2

, + '/comp1/view1/*': () =>

Comp-1-view1

, + '/comp2/*': () =>

Comp-2

+} +``` + +If the app tries to route to /comp1/view1, instead of matching route /comp1/view1/* it matches /comp1/*. This can be fixed if we define the routes like this + +```javascript +[ + + { path: '/comp1/view2/*', fn:() =>

Comp-1-view-2

, }, + { path: '/comp1/view1/*', fn:() =>

Comp-1-view1

, }, + { path: '/comp1/*', fn:() =>

Just Comp-1

, }, + { path: '/comp2/*', fn:() =>

Comp-2

}, +] +``` \ No newline at end of file diff --git a/docs/docker-compose.yaml b/docs/docker-compose.yaml index d2150fd..39b705f 100644 --- a/docs/docker-compose.yaml +++ b/docs/docker-compose.yaml @@ -1,4 +1,4 @@ -version: '2' +version: '3.7' services: jekyll: environment: @@ -8,4 +8,4 @@ services: volumes: - .:/srv/jekyll ports: - - 4000:4000 \ No newline at end of file + - 4000:4000 diff --git a/scripts/start-docs b/scripts/start-docs index 9be0145..5e9e86a 100755 --- a/scripts/start-docs +++ b/scripts/start-docs @@ -1,3 +1,3 @@ #!/usr/bin/env bash -cd docs && docker-compose up \ No newline at end of file +cd docs && docker compose up \ No newline at end of file diff --git a/src/router.tsx b/src/router.tsx index ac603ed..d292893 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -31,12 +31,21 @@ type ExtractPathParams> = Parts ex : ExtractPathParams : unknown -export type Routes = { - [P in Path]: ( - params: NonEmptyRecord>, +export type Route = { + path: Path + fn: ( + params: NonEmptyRecord>, ) => JSX.Element } +export type Routes = + | { + [P in Path]: ( + params: NonEmptyRecord>, + ) => JSX.Element + } + | Route[] + export function useRoutes( routes: Routes, { @@ -77,7 +86,7 @@ export function useRoutes( function useMatchRoute( // eslint-disable-next-line @typescript-eslint/no-explicit-any - routes: { [key: string]: (...props: any) => JSX.Element }, + routes: { [key: string]: (...props: any) => JSX.Element } | Route[], path: string | null, { routeProps, @@ -86,14 +95,28 @@ function useMatchRoute( }: Omit & { matchTrailingSlash: boolean }, ) { path = trailingMatch(path, matchTrailingSlash) - const matchers = useMatchers(Object.keys(routes)) + const mappedRoutes = Array.isArray(routes) + ? routes + : Object.entries(routes).reduce((arr, [path, fn]) => { + // console.log('route fn', fn()) + arr.push({ path, fn }) + return arr + }, [] as Route[]) + const matchers = useMatchers(mappedRoutes.map((r) => r.path)) + + // console.log('mapped Routes', mappedRoutes) + // console.log('matchers', matchers) if (path === null) return null - const [routeMatch, props] = getMatchParams(path, matchers) + const [pathMatch, props] = getMatchParams(path, matchers) + + if (!pathMatch) return null + + const routeMatch = mappedRoutes.find((r) => r.path == pathMatch.path) if (!routeMatch) return null - return routes[routeMatch.path]( + return routeMatch.fn( overridePathParams ? { ...props, ...routeProps } : { ...routeProps, ...props }, ) } diff --git a/test/router.spec.tsx b/test/router.spec.tsx index 9170db0..488bfa7 100644 --- a/test/router.spec.tsx +++ b/test/router.spec.tsx @@ -205,6 +205,37 @@ describe('useRoutes', () => { expect(getByTestId('label')).toHaveTextContent('new route') }) + describe('with array routes', () => { + test('matches catch-all first', async () => { + const routes = [ + { path: '/layer*', fn: () => }, + { path: '/layer/under', fn: () => }, + ] + act(() => navigate('/layer')) + const { getByTestId } = render() + + // act(() => navigate('/foo')) + expect(getByTestId('label')).toHaveTextContent('top') + act(() => navigate('/layer/under')) + expect(getByTestId('label')).toHaveTextContent('top') + }) + + test('matches catch-all last', async () => { + const routes = [ + { path: '/layer/under', fn: () => }, + { path: '/layer*', fn: () => }, + ] + + act(() => navigate('/layer')) + const { getByTestId } = render() + + // act(() => navigate('/foo')) + expect(getByTestId('label')).toHaveTextContent('top') + act(() => navigate('/layer/under')) + expect(getByTestId('label')).toHaveTextContent('bottom') + }) + }) + describe('with basePath', () => { const routes = { '/': () => ,