From 8b8cebf8096b3eebe2af142a930e6a6047d4027b Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 4 Aug 2025 14:23:00 +0200 Subject: [PATCH] feat: custom param --- packages/experiments-playground/src/App.vue | 8 ++- .../src/pages/profiles/[userId].vue | 13 +++++ .../src/router/index.ts | 31 ++++++++++- packages/router/src/experimental/index.ts | 1 + .../matchers/matcher-pattern.spec.ts | 12 ++--- .../matchers/matcher-pattern.ts | 51 ++++++++++++------- 6 files changed, 90 insertions(+), 26 deletions(-) create mode 100644 packages/experiments-playground/src/pages/profiles/[userId].vue diff --git a/packages/experiments-playground/src/App.vue b/packages/experiments-playground/src/App.vue index cbd335ec..41779df6 100644 --- a/packages/experiments-playground/src/App.vue +++ b/packages/experiments-playground/src/App.vue @@ -59,7 +59,13 @@ const queryPage = computed({ params: {{ route.params }}
meta: {{ route.meta }} diff --git a/packages/experiments-playground/src/pages/profiles/[userId].vue b/packages/experiments-playground/src/pages/profiles/[userId].vue new file mode 100644 index 00000000..f02e2e6b --- /dev/null +++ b/packages/experiments-playground/src/pages/profiles/[userId].vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/experiments-playground/src/router/index.ts b/packages/experiments-playground/src/router/index.ts index b32576a9..c6461572 100644 --- a/packages/experiments-playground/src/router/index.ts +++ b/packages/experiments-playground/src/router/index.ts @@ -1,17 +1,18 @@ -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 extends MatcherPatternQuery ? P : never @@ -134,6 +135,31 @@ const r_nested_a = normalizeRouteRecord({ 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\/(?[^/]+)$/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([ @@ -142,6 +168,7 @@ export const router = experimental_createRouter({ r_nested, r_nested_a, r_profiles_list, + r_profiles_detail, ]), }) diff --git a/packages/router/src/experimental/index.ts b/packages/router/src/experimental/index.ts index 17b9db0a..de1a58e8 100644 --- a/packages/router/src/experimental/index.ts +++ b/packages/router/src/experimental/index.ts @@ -22,6 +22,7 @@ export { MatcherPatternPathDynamic, MatcherPatternPathStatic, MatcherPatternPathStar, + MatcherPatternPathCustomParams, } from './route-resolver/matchers/matcher-pattern' export type { MatcherPattern, diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts index df6bf36d..0cc13cc5 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { MatcherPatternPathStatic, MatcherPatternPathStar, - MatcherPatternPathCustom, + MatcherPatternPathCustomParams, } from './matcher-pattern' import { pathEncoded } from '../resolver-abstract' import { invalid } from './errors' @@ -106,7 +106,7 @@ describe('MatcherPatternPathStar', () => { describe('MatcherPatternPathCustom', () => { it('single param', () => { - const pattern = new MatcherPatternPathCustom( + const pattern = new MatcherPatternPathCustomParams( /^\/teams\/([^/]+?)\/b$/i, { // all defaults @@ -133,7 +133,7 @@ describe('MatcherPatternPathCustom', () => { }) it('decodes single param', () => { - const pattern = new MatcherPatternPathCustom( + const pattern = new MatcherPatternPathCustomParams( /^\/teams\/([^/]+?)$/i, { teamId: {}, @@ -150,7 +150,7 @@ describe('MatcherPatternPathCustom', () => { }) it('optional param', () => { - const pattern = new MatcherPatternPathCustom( + const pattern = new MatcherPatternPathCustomParams( /^\/teams(?:\/([^/]+?))?\/b$/i, { teamId: { optional: true }, @@ -172,7 +172,7 @@ describe('MatcherPatternPathCustom', () => { }) it('repeatable param', () => { - const pattern = new MatcherPatternPathCustom( + const pattern = new MatcherPatternPathCustomParams( /^\/teams\/(.+?)\/b$/i, { teamId: { repeat: true }, @@ -196,7 +196,7 @@ describe('MatcherPatternPathCustom', () => { }) it('repeatable optional param', () => { - const pattern = new MatcherPatternPathCustom( + const pattern = new MatcherPatternPathCustomParams( /^\/teams(?:\/(.+?))?\/b$/i, { teamId: { repeat: true, optional: true }, diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts index 4f92a333..465f75fe 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts @@ -193,17 +193,8 @@ export type ParamsFromParsers

> = { : 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 @@ -211,12 +202,40 @@ interface MatcherPatternPathDynamicParam< parser?: Param_GetSet } -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 + +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 + +export const PARAM_NUMBER_REPEATABLE = { + get: (value: string[]) => value.map(PARAM_NUMBER.get), + set: (value: number[]) => value.map(PARAM_NUMBER.set), +} satisfies Param_GetSet + +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, + readonly params: Record< + string, + // @ts-expect-error: adapt with generic class + MatcherPatternPathCustomParamOptions + >, 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 @@ -236,7 +255,7 @@ export class MatcherPatternPathCustom implements MatcherPatternPath { 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.` @@ -251,9 +270,7 @@ export class MatcherPatternPathCustom implements MatcherPatternPath { ) : 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) { -- 2.47.3