Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parameter route string to routes object. #7469

Closed
wants to merge 12 commits into from
160 changes: 158 additions & 2 deletions packages/router/src/__tests__/links.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React from 'react'

import { toHaveClass, toHaveStyle } from '@testing-library/jest-dom/matchers'
import { render } from '@testing-library/react'
import { act, render, waitFor } from '@testing-library/react'

// TODO: Remove when jest configs are in place
// @ts-expect-error - Issue with TS and jest-dom
expect.extend({ toHaveClass, toHaveStyle })

import { navigate, Route, Router, routes as generatedRoutes } from '../'
import { NavLink, useMatch, Link } from '../links'
import { LocationProvider } from '../location'
import { flattenSearchParams } from '../util'
import type { GeneratedRoutesMap } from '../util'
import { flattenSearchParams, RouteParams } from '../util'

function createDummyLocation(pathname: string, search = '') {
return {
Expand Down Expand Up @@ -357,4 +359,158 @@ describe('useMatch', () => {

expect(getByText(/Dunder Mifflin/)).toHaveStyle('color: red')
})

it('returns a match on the same pathname', async () => {
const routes = generatedRoutes as GeneratedRoutesMap

Object.keys(routes).forEach((key) => delete routes[key])

const HomePage = () => {
const matchExactPath = useMatch(
routes.home({
dynamic: 'dunder-mifflin',
path: '1',
})
)
const matchWrongPath = useMatch(
routes.home({
dynamic: 'dunder-mifflin',
path: '0',
})
)
const matchParameterPath = useMatch(
routes.home({
dynamic: RouteParams.LITERAL,
path: RouteParams.LITERAL,
})
)
const matchPartialParameterPath = useMatch(
routes.home({
dynamic: RouteParams.LITERAL,
path: '1',
})
)
const matchWrongPartialParameterPath = useMatch(
routes.home({
dynamic: RouteParams.LITERAL,
path: '0',
})
)

const matchHardcodedStringExact = useMatch('/dunder-mifflin/1')
const matchHardcodedStringPartialDefinition = useMatch(
'/dunder-mifflin/{path}'
)
const matchHardcodedStringDefinition = useMatch('/{dynamic}/{path}')
const matchWrongHardcodedStringDefinition = useMatch('/{another}/{path}')

const pathDefinition = routes.home({
dynamic: RouteParams.LITERAL,
path: RouteParams.LITERAL,
})

// const matchWrongParameterPath = useMatch(routes.anotherHome.path)
return (
<>
<h1>Tests</h1>
<ul>
<li>
Literal route path from route definition in the router:{' '}
{pathDefinition}
</li>
<li>
Matches exact path: {matchExactPath.match ? 'true' : 'false'}
</li>
<li>
Matches wrong exact path:{' '}
{matchWrongPath.match ? 'true' : 'false'}
</li>
<li>
Matches parameter path:{' '}
{matchParameterPath.match ? 'true' : 'false'}
</li>
<li>
Matches partial parameter path:{' '}
{matchPartialParameterPath.match ? 'true' : 'false'}
</li>
<li>
Matches wrong partial parameter path:{' '}
{matchWrongPartialParameterPath.match ? 'true' : 'false'}
</li>
<li>
Matches hardcoded string:{' '}
{matchHardcodedStringExact.match ? 'true' : 'false'}
</li>
<li>
Matches hardcoded string partial definition:{' '}
{matchHardcodedStringPartialDefinition.match ? 'true' : 'false'}
</li>
<li>
Matches hardcoded string definition:{' '}
{matchHardcodedStringDefinition.match ? 'true' : 'false'}
</li>
<li>
Matches wrong hardcoded string definition:{' '}
{matchWrongHardcodedStringDefinition.match ? 'true' : 'false'}
</li>
</ul>
</>
)
}

const TestRouter = () => (
<Router>
<Route path="/{dynamic}/{path}" page={HomePage} name="home" />
{/* <Route path="/{another}/{path}" page={MyPage} name="anotherHome" /> */}
</Router>
)

const screen = render(<TestRouter />)

act(() =>
navigate(
routes.home({
dynamic: 'dunder-mifflin',
path: '1',
})
)
)

await waitFor(() =>
expect(
screen.getByText(
/Literal route path from route definition in the router:\s+\/\{dynamic\}\/\{path\}/
)
)
)
await waitFor(() => expect(screen.getByText(/Matches exact path: true/)))
await waitFor(() =>
expect(screen.getByText(/Matches wrong exact path: false/))
)
await waitFor(() =>
expect(screen.getByText(/Matches parameter path: true/))
)
await waitFor(() =>
expect(screen.getByText(/Matches partial parameter path: true/))
)
await waitFor(() =>
expect(screen.getByText(/Matches wrong partial parameter path: false/))
)
await waitFor(() =>
expect(screen.getByText(/Matches hardcoded string: true/))
)
await waitFor(() =>
expect(
screen.getByText(/Matches hardcoded string partial definition: true/)
)
)
await waitFor(() =>
expect(screen.getByText(/Matches hardcoded string definition: true/))
)
await waitFor(() =>
expect(
screen.getByText(/Matches wrong hardcoded string definition: false/)
)
)
})
})
8 changes: 6 additions & 2 deletions packages/router/src/links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ type UseMatchOptions = {
}

/**
* Returns an object of { match: boolean; params: Record<string, unknown>; }
* if the path matches the current location match will be true.
* Returns an object of `{ match: boolean; params: Record<string, unknown>; }`.
* If the path matches the current location `match` will be true.
* Params will be an object of the matched params, if there are any.
*
* Provide searchParams options to match the current location.search
Expand All @@ -35,6 +35,10 @@ type UseMatchOptions = {
* Match sub paths
* const match = useMatch('/product', { matchSubPaths: true })
*
* Match parameter paths
* const match = useMatch('/post/{id:Int}') // works for all '/post/7', 'post/8' ...
* const match = useMatch(routes.post({id: RouteParams.LITERAL})) // same as above. RouteParams.LITERAL can be imported and equals: '/post/{id:Int}'
* const match = useMatch(routes.post({id: 7})) // works only for '/post/7'
*/
const useMatch = (pathname: string, options?: UseMatchOptions) => {
const location = useLocation()
Expand Down
3 changes: 2 additions & 1 deletion packages/router/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ const LocationAwareRouter: React.FC<RouterProps> = ({
})
}, [location.pathname, children, paramTypes])

// Assign namedRoutes so it can be imported like import {routes} from 'rwjs/router'
// Assign namedRoutes so it can be imported like
// `import { routes } from 'rwjs/router'`
// Note that the value changes at runtime
Object.assign(namedRoutes, namedRoutesMap)

Expand Down
16 changes: 13 additions & 3 deletions packages/router/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export function matchPath(
// Map extracted values to their param name, casting the value if needed
const providedParams = matches[0].slice(1)

// @NOTE: refers to definiton e.g. '/page/{id}', not the actual params
// @NOTE: refers to definition e.g. '/page/{id}', not the actual params
if (routeParamsDefinition.length > 0) {
const params = providedParams.reduce<Record<string, unknown>>(
(acc, value, index) => {
Expand Down Expand Up @@ -301,6 +301,10 @@ export function validatePath(path: string, routeName: string) {
}
}

export const RouteParams = {
LITERAL: Symbol('RouteParams.LITERAL'),
}

/**
* Take a given route path and replace any named parameters with those in the
* given args object. Any extra params not used in the path will be appended
Expand All @@ -310,6 +314,8 @@ export function validatePath(path: string, routeName: string) {
*
* replaceParams('/tags/{tag}', { tag: 'code', extra: 'foo' })
* => '/tags/code?extra=foo
*
* If RouteParams.LITERAL is used as a value { tag: RouteParams.LITERAL }, the param {tag} will be used
*/
export function replaceParams(
route: string,
Expand All @@ -322,8 +328,12 @@ export function replaceParams(
params.forEach((param) => {
const [name, _type, match] = param
const value = args[name]

if (value !== undefined) {
path = path.replace(match, value as string)
// replace {tag} with 'code' only if it is not RouteParams.LITERAL
if (value !== RouteParams.LITERAL) {
path = path.replace(match, value as string)
}
} else {
throw new Error(
`Missing parameter '${name}' for route '${route}' when generating a navigation URL.`
Expand Down Expand Up @@ -451,7 +461,7 @@ type WhileLoadingPage = () => ReactElement | null
// We can't index it correctly in the framework
export type GeneratedRoutesMap = {
[key: string]: (
args?: Record<string | number, string | number | boolean>
args?: Record<string | number, string | number | boolean | symbol>
) => string
}

Expand Down
Loading