Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions __tests__/router.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,15 @@ describe('Router', () => {
expect(router.currentRoute.value.hash).toBe('#two')
})

it('cast number params to string', async () => {
const { router } = await newRouter()
expect(router.resolve({ name: 'Param', params: { p: 1 } })).toMatchObject({
name: 'Param',
path: '/p/1',
params: { p: '1' },
})
})

it('fails if required params are missing', async () => {
const { router } = await newRouter()
expect(() => router.resolve({ name: 'Param', params: {} })).toThrowError(
Expand Down
4 changes: 2 additions & 2 deletions src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function encodeQueryProperty(text: string | number): string {
* @param text - string to encode
* @returns encoded string
*/
export function encodePath(text: string): string {
export function encodePath(text: string | number): string {
return commonEncode(text).replace(HASH_RE, '%23').replace(IM_RE, '%3F')
}

Expand All @@ -96,7 +96,7 @@ export function encodePath(text: string): string {
* @param text - string to encode
* @returns encoded string
*/
export function encodeParam(text: string): string {
export function encodeParam(text: string | number): string {
return encodePath(text).replace(SLASH_RE, '%2F')
}

Expand Down
26 changes: 22 additions & 4 deletions src/matcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import {
MatcherLocation,
isRouteName,
RouteRecordName,
RouteParams,
LocationAsRelative,
} from '../types'
import {
normalizeParams,
isRouteLocationPath,
isRouteLocationRelative,
isRouteLocationName,
} from '../utils'
import { createRouterError, ErrorTypes, MatcherError } from '../errors'
import { createRouteRecordMatcher, RouteRecordMatcher } from './pathMatcher'
import { RouteRecordRedirect, RouteRecordNormalized } from './types'
Expand Down Expand Up @@ -200,7 +208,14 @@ export function createRouterMatcher(
let path: MatcherLocation['path']
let name: MatcherLocation['name']

if ('name' in location && location.name) {
if (isRouteLocationRelative(location)) {
location = {
...location,
params: normalizeParams(location.params),
}
}

if (isRouteLocationName(location)) {
matcher = matcherMap.get(location.name)

if (!matcher)
Expand All @@ -214,11 +229,11 @@ export function createRouterMatcher(
currentLocation.params,
matcher.keys.map(k => k.name)
),
...location.params,
...(location.params as RouteParams),
}
// throws if cannot be stringified
path = matcher.stringify(params)
} else if ('path' in location) {
} else if (isRouteLocationPath(location)) {
// no need to resolve the path with the matcher as it was provided
// this also allows the user to control the encoding
path = location.path
Expand Down Expand Up @@ -251,7 +266,10 @@ export function createRouterMatcher(
name = matcher.record.name
// since we are navigating to the same location, we don't need to pick the
// params like when `name` is provided
params = { ...currentLocation.params, ...location.params }
params = {
...currentLocation.params,
...((location as LocationAsRelative).params as RouteParams),
}
path = matcher.stringify(params)
}

Expand Down
45 changes: 31 additions & 14 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isRouteName,
NavigationGuardWithThis,
RouteLocationOptions,
RouteParams,
} from './types'
import { RouterHistory, HistoryState } from './history/common'
import {
Expand All @@ -30,7 +31,13 @@ import {
NavigationFailure,
NavigationRedirectError,
} from './errors'
import { applyToParams, isBrowser } from './utils'
import {
applyToParams,
isBrowser,
normalizeParams,
isRouteLocationPath,
isRouteLocationRelative,
} from './utils'
import { useCallbacks } from './utils/callbacks'
import { encodeParam, decode, encodeHash } from './encoding'
import {
Expand Down Expand Up @@ -180,7 +187,10 @@ export function createRouter(options: RouterOptions): Router {
history.scrollRestoration = 'manual'
}

const encodeParams = applyToParams.bind(null, encodeParam)
const encodeParams = applyToParams.bind(null, encodeParam) as (
// bind doesn't infer the correct generic type
params: Record<string, string | number | (string | number)[]> | undefined
) => Record<string, string | string[]>
const decodeParams = applyToParams.bind(null, decode)

function addRoute(
Expand Down Expand Up @@ -250,17 +260,16 @@ export function createRouter(options: RouterOptions): Router {
}

// path could be relative in object as well
if ('path' in rawLocation) {
if (isRouteLocationPath(rawLocation)) {
if (
__DEV__ &&
'params' in rawLocation &&
isRouteLocationRelative(rawLocation) &&
// 'params' in rawLocation &&
!('name' in rawLocation) &&
Object.keys((rawLocation as any).params).length
) {
warn(
`Path "${
(rawLocation as any).path
}" was passed with params but they will be ignored. Use a named route alongside params instead.`
`Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.`
)
}
rawLocation = {
Expand All @@ -269,23 +278,31 @@ export function createRouter(options: RouterOptions): Router {
}
}

if (isRouteLocationRelative(rawLocation)) {
rawLocation = {
...rawLocation,
params: normalizeParams(rawLocation.params),
}
}

let matchedRoute: MatcherLocation = // relative or named location, path is ignored
// for same reason TS thinks rawLocation.params can be undefined
matcher.resolve(
'params' in rawLocation
? { ...rawLocation, params: encodeParams(rawLocation.params) }
isRouteLocationRelative(rawLocation)
? {
...rawLocation,
params: encodeParams(rawLocation.params),
}
: rawLocation,
currentLocation
)

const hash = encodeHash(rawLocation.hash || '')

// put back the unencoded params as given by the user (avoid the cost of decoding them)
// TODO: normalize params if we accept numbers as raw values
matchedRoute.params =
'params' in rawLocation
? rawLocation.params!
: decodeParams(matchedRoute.params)
matchedRoute.params = isRouteLocationRelative(rawLocation)
? (rawLocation.params as RouteParams) // already normalized
: decodeParams(matchedRoute.params)

const fullPath = stringifyURL(stringifyQuery, {
...rawLocation,
Expand Down
20 changes: 10 additions & 10 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ export type VueUseOptions<T> = {
export type TODO = any

export type RouteParamValue = string
// TODO: should we allow more values like numbers and normalize them to strings?
// type RouteParamValueRaw = RouteParamValue | number
export type RouteParamValueRaw = RouteParamValue | number
export type RouteParams = Record<string, RouteParamValue | RouteParamValue[]>
export type RouteParamsRaw = RouteParams
// export type RouteParamsRaw = Record<
// string,
// RouteParamValueRaw | RouteParamValueRaw[]
// >
export type RouteParamsRaw = Record<
string,
RouteParamValueRaw | RouteParamValueRaw[]
>

export interface RouteQueryAndHash {
query?: LocationQueryRaw
Expand Down Expand Up @@ -61,14 +59,16 @@ export interface RouteLocationOptions {
state?: HistoryState
}

export type RouteLocationAs<T> = RouteQueryAndHash & RouteLocationOptions & T

/**
* User-level route location
*/
export type RouteLocationRaw =
| string
| (RouteQueryAndHash & LocationAsPath & RouteLocationOptions)
| (RouteQueryAndHash & LocationAsName & RouteLocationOptions)
| (RouteQueryAndHash & LocationAsRelative & RouteLocationOptions)
| RouteLocationAs<LocationAsPath>
| RouteLocationAs<LocationAsName>
| RouteLocationAs<LocationAsRelative>

export interface RouteLocationMatched extends RouteRecordNormalized {
// components cannot be Lazy<RouteComponent>
Expand Down
48 changes: 46 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { RouteParams, RouteComponent } from '../types'
import {
RouteParamsRaw,
RouteComponent,
RouteParams,
RouteParamValue,
RouteLocationAs,
LocationAsPath,
LocationAsRelative,
LocationAsName,
} from '../types'
import { hasSymbol } from '../injectionSymbols'

export * from './env'
Expand All @@ -7,9 +16,20 @@ export function isESModule(obj: any): obj is { default: RouteComponent } {
return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module')
}

export function applyToParams<TValue>(
fn: (v: TValue) => string,
params: Record<string, TValue | TValue[]> | undefined
): RouteParams

// overload for `applyToParams.bind`
export function applyToParams(
fn: (v: string) => string,
fn: (v: RouteParamValue) => string,
params: RouteParams | undefined
): RouteParams

export function applyToParams(
fn: (v: any) => string,
params: RouteParams | RouteParamsRaw | undefined
): RouteParams {
const newParams: RouteParams = {}

Expand All @@ -20,3 +40,27 @@ export function applyToParams(

return newParams
}

export const normalizeParams = (
params: RouteParamsRaw | undefined
): RouteParams => {
return applyToParams(x => '' + x, params)
}

export function isRouteLocationPath(
r: any
): r is RouteLocationAs<LocationAsPath> {
return 'path' in r && r.path
}

export function isRouteLocationRelative(
r: any
): r is RouteLocationAs<LocationAsRelative> {
return 'params' in r && r.params
}

export function isRouteLocationName(
r: any
): r is RouteLocationAs<LocationAsName> {
return 'name' in r && r.name
}