diff --git a/__tests__/router.spec.ts b/__tests__/router.spec.ts index 957b9b892..6bf25b089 100644 --- a/__tests__/router.spec.ts +++ b/__tests__/router.spec.ts @@ -34,6 +34,7 @@ const routes: RouteRecordRaw[] = [ { path: '/to-foo-query', redirect: '/foo?a=2#b' }, { path: '/to-p/:p', redirect: { name: 'Param' } }, { path: '/p/:p', name: 'Param', component: components.Bar }, + { path: '/optional/:p?', name: 'optional', component: components.Bar }, { path: '/repeat/:r+', name: 'repeat', component: components.Bar }, { path: '/to-p/:p', redirect: to => `/p/${to.params.p}` }, { path: '/redirect-with-param/:p', redirect: () => `/` }, @@ -240,6 +241,23 @@ describe('Router', () => { expect(router.currentRoute.value).toMatchObject({ params: { p: '0' } }) }) + it('casts null/undefined params to empty strings', async () => { + const { router } = await newRouter() + expect( + router.resolve({ name: 'optional', params: { p: undefined } }) + ).toMatchObject({ + params: {}, + }) + expect( + router.resolve({ name: 'optional', params: { p: null } }) + ).toMatchObject({ + params: {}, + }) + await router.push({ name: 'optional', params: { p: null } }) + expect(router.currentRoute.value).toMatchObject({ params: {} }) + await router.push({ name: 'optional', params: {} }) + }) + it('navigates to same route record but different query', async () => { const { router } = await newRouter() await router.push('/?q=1') diff --git a/src/encoding.ts b/src/encoding.ts index 5207d417a..248a75063 100644 --- a/src/encoding.ts +++ b/src/encoding.ts @@ -120,13 +120,14 @@ export function encodePath(text: string | number): string { /** * Encode characters that need to be encoded on the path section of the URL as a * param. This function encodes everything {@link encodePath} does plus the - * slash (`/`) character. + * slash (`/`) character. If `text` is `null` or `undefined`, returns an empty + * string instead. * * @param text - string to encode * @returns encoded string */ -export function encodeParam(text: string | number): string { - return encodePath(text).replace(SLASH_RE, '%2F') +export function encodeParam(text: string | number | null | undefined): string { + return text == null ? '' : encodePath(text).replace(SLASH_RE, '%2F') } /** diff --git a/src/router.ts b/src/router.ts index f2ac74d62..f32fb0e27 100644 --- a/src/router.ts +++ b/src/router.ts @@ -12,6 +12,7 @@ import { NavigationGuardWithThis, RouteLocationOptions, MatcherLocationRaw, + RouteParams, } from './types' import { RouterHistory, HistoryState, NavigationType } from './history/common' import { @@ -380,7 +381,9 @@ export function createRouter(options: RouterOptions): Router { paramValue => '' + paramValue ) const encodeParams = applyToParams.bind(null, encodeParam) - const decodeParams = applyToParams.bind(null, decode) + const decodeParams: (params: RouteParams | undefined) => RouteParams = + // @ts-expect-error: intentionally avoid the type check + applyToParams.bind(null, decode) function addRoute( parentOrRoute: RouteRecordName | RouteRecordRaw, @@ -473,6 +476,13 @@ export function createRouter(options: RouterOptions): Router { path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path, }) } else { + // remove any nullish param + const targetParams = assign({}, rawLocation.params) + for (const key in targetParams) { + if (targetParams[key] == null) { + delete targetParams[key] + } + } // pass encoded values to the matcher so it can produce encoded path and fullPath matcherLocation = assign({}, rawLocation, { params: encodeParams(rawLocation.params), diff --git a/src/types/index.ts b/src/types/index.ts index 9bfad6bc1..49c439915 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -31,11 +31,11 @@ export type RouteParamValue = string /** * @internal */ -export type RouteParamValueRaw = RouteParamValue | number +export type RouteParamValueRaw = RouteParamValue | number | null | undefined export type RouteParams = Record export type RouteParamsRaw = Record< string, - RouteParamValueRaw | RouteParamValueRaw[] + RouteParamValueRaw | Exclude[] > /** diff --git a/src/utils/index.ts b/src/utils/index.ts index 35a2b8d06..3e3b92189 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -10,7 +10,7 @@ export function isESModule(obj: any): obj is { default: RouteComponent } { export const assign = Object.assign export function applyToParams( - fn: (v: string | number) => string, + fn: (v: string | number | null | undefined) => string, params: RouteParamsRaw | undefined ): RouteParams { const newParams: RouteParams = {}