params: <code>{{ route.params }}</code>
<br />
<template v-if="queryPage != null">
- page: <input type="number" v-model.number="queryPage" />
+ page:
+ <input
+ type="number"
+ v-model.number="queryPage"
+ autocomplete="off"
+ data-1p-ignore
+ />
<br />
</template>
meta: <code>{{ route.meta }}</code>
--- /dev/null
+<script setup lang="ts">
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+</script>
+
+<template>
+ <main>
+ <h2>Details</h2>
+ <p>{{ route.fullPath }}</p>
+ <pre>{{ route.params }}</pre>
+ </main>
+</template>
-import { createWebHistory } from 'vue-router'
+import { createWebHistory, type RouteParamValue } from 'vue-router'
import {
experimental_createRouter,
createStaticResolver,
MatcherPatternPathStatic,
+ MatcherPatternPathCustomParams,
normalizeRouteRecord,
} from 'vue-router/experimental'
import type {
EXPERIMENTAL_RouteRecordNormalized_Matchable,
MatcherPatternHash,
MatcherPatternQuery,
+ EmptyParams,
} from 'vue-router/experimental'
import PageHome from '../pages/(home).vue'
-import type { EmptyParams } from 'vue-router/experimental'
// type ExtractMatcherQueryParams<T> =
// T extends MatcherPatternQuery<infer P> ? P : never
path: new MatcherPatternPathStatic('/nested/a'),
})
+const r_profiles_detail = normalizeRouteRecord({
+ name: 'profiles-detail',
+ components: { default: () => import('../pages/profiles/[userId].vue') },
+ parent: r_profiles_layout,
+ path: new MatcherPatternPathCustomParams(
+ /^\/profiles\/(?<userId>[^/]+)$/i,
+ {
+ userId: {
+ parser: {
+ // @ts-expect-error: FIXME: should would with generic class
+ get: (value: string): number => Number(value),
+ // @ts-expect-error: FIXME: should would with generic class
+ set: (value: number): string => String(value),
+ },
+ },
+ },
+ ({ userId }) => {
+ if (typeof userId !== 'number') {
+ throw new Error('userId must be a number')
+ }
+ return `/profiles/${userId}`
+ }
+ ),
+})
+
export const router = experimental_createRouter({
history: createWebHistory(),
resolver: createStaticResolver<EXPERIMENTAL_RouteRecordNormalized_Matchable>([
r_nested,
r_nested_a,
r_profiles_list,
+ r_profiles_detail,
]),
})
MatcherPatternPathDynamic,
MatcherPatternPathStatic,
MatcherPatternPathStar,
+ MatcherPatternPathCustomParams,
} from './route-resolver/matchers/matcher-pattern'
export type {
MatcherPattern,
import {
MatcherPatternPathStatic,
MatcherPatternPathStar,
- MatcherPatternPathCustom,
+ MatcherPatternPathCustomParams,
} from './matcher-pattern'
import { pathEncoded } from '../resolver-abstract'
import { invalid } from './errors'
describe('MatcherPatternPathCustom', () => {
it('single param', () => {
- const pattern = new MatcherPatternPathCustom(
+ const pattern = new MatcherPatternPathCustomParams(
/^\/teams\/([^/]+?)\/b$/i,
{
// all defaults
})
it('decodes single param', () => {
- const pattern = new MatcherPatternPathCustom(
+ const pattern = new MatcherPatternPathCustomParams(
/^\/teams\/([^/]+?)$/i,
{
teamId: {},
})
it('optional param', () => {
- const pattern = new MatcherPatternPathCustom(
+ const pattern = new MatcherPatternPathCustomParams(
/^\/teams(?:\/([^/]+?))?\/b$/i,
{
teamId: { optional: true },
})
it('repeatable param', () => {
- const pattern = new MatcherPatternPathCustom(
+ const pattern = new MatcherPatternPathCustomParams(
/^\/teams\/(.+?)\/b$/i,
{
teamId: { repeat: true },
})
it('repeatable optional param', () => {
- const pattern = new MatcherPatternPathCustom(
+ const pattern = new MatcherPatternPathCustomParams(
/^\/teams(?:\/(.+?))?\/b$/i,
{
teamId: { repeat: true, optional: true },
: never
}
-/**
- * TODO: it should accept a dict of param parsers for each param and if they are repeatable and optional
- * The object order matters, they get matched in that order
- */
-
-interface MatcherPatternPathDynamicParam<
- TIn extends string | string[] | null | undefined =
- | string
- | string[]
- | null
- | undefined,
+interface MatcherPatternPathCustomParamOptions<
+ TIn extends string | string[] | null = string | string[] | null,
TOut = string | string[] | null,
> {
repeat?: boolean
parser?: Param_GetSet<TIn, TOut>
}
-export class MatcherPatternPathCustom implements MatcherPatternPath {
+export const PARAM_NUMBER = {
+ get: (value: string) => {
+ const num = Number(value)
+ if (Number.isFinite(num)) {
+ return num
+ }
+ throw miss()
+ },
+ set: (value: number) => String(value),
+} satisfies Param_GetSet<string, number>
+
+export const PARAM_NUMBER_OPTIONAL = {
+ get: (value: string | null) =>
+ value == null ? null : PARAM_NUMBER.get(value),
+ set: (value: number | null) =>
+ value != null ? PARAM_NUMBER.set(value) : null,
+} satisfies Param_GetSet<string | null, number | null>
+
+export const PARAM_NUMBER_REPEATABLE = {
+ get: (value: string[]) => value.map(PARAM_NUMBER.get),
+ set: (value: number[]) => value.map(PARAM_NUMBER.set),
+} satisfies Param_GetSet<string[], number[]>
+
+export class MatcherPatternPathCustomParams implements MatcherPatternPath {
// private paramsKeys: string[]
constructor(
+ // TODO: make this work with named groups and simplify `params` to be an array of the repeat flag
readonly re: RegExp,
- readonly params: Record<string, MatcherPatternPathDynamicParam>,
+ readonly params: Record<
+ string,
+ // @ts-expect-error: adapt with generic class
+ MatcherPatternPathCustomParamOptions<unknown, unknown>
+ >,
readonly build: (params: MatcherParamsFormatted) => string
// A better version could be using all the parts to join them
// .e.g ['users', 0, 'profile', 1] -> /users/123/profile/456
for (const paramName in this.params) {
const currentParam = this.params[paramName]
// an optional group in the regexp will return undefined
- const currentMatch = match[i++] as string | undefined
+ const currentMatch = (match[i++] as string | undefined) ?? null
if (__DEV__ && !currentParam.optional && !currentMatch) {
warn(
`Unexpected undefined value for param "${paramName}". Regexp: ${String(this.re)}. path: "${path}". This is likely a bug.`
)
: decode(currentMatch)
- console.log(paramName, currentParam, value)
-
- params[paramName] = (currentParam.parser?.get || (v => v ?? null))(value)
+ params[paramName] = (currentParam.parser?.get || (v => v))(value)
}
if (__DEV__ && i !== match.length) {