]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: custom param
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 4 Aug 2025 12:23:00 +0000 (14:23 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 4 Aug 2025 12:23:00 +0000 (14:23 +0200)
packages/experiments-playground/src/App.vue
packages/experiments-playground/src/pages/profiles/[userId].vue [new file with mode: 0644]
packages/experiments-playground/src/router/index.ts
packages/router/src/experimental/index.ts
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts

index cbd335ec49b1608546f73529ebcf2f5c15d8033a..41779df696f89d4f003e3db1914418a17f0ecbe4 100644 (file)
@@ -59,7 +59,13 @@ const queryPage = computed({
     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>
diff --git a/packages/experiments-playground/src/pages/profiles/[userId].vue b/packages/experiments-playground/src/pages/profiles/[userId].vue
new file mode 100644 (file)
index 0000000..f02e2e6
--- /dev/null
@@ -0,0 +1,13 @@
+<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>
index b32576a92aebef5f3a190fb157cb92a495c11882..c646157268fcdb8f83c37400f80d069f7ac33dba 100644 (file)
@@ -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> =
 //   T extends MatcherPatternQuery<infer P> ? 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\/(?<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>([
@@ -142,6 +168,7 @@ export const router = experimental_createRouter({
     r_nested,
     r_nested_a,
     r_profiles_list,
+    r_profiles_detail,
   ]),
 })
 
index 17b9db0ab9852a7fbefc1728e748d54095534b94..de1a58e8d9ddfdd4be5138c3096d3796228730e1 100644 (file)
@@ -22,6 +22,7 @@ export {
   MatcherPatternPathDynamic,
   MatcherPatternPathStatic,
   MatcherPatternPathStar,
+  MatcherPatternPathCustomParams,
 } from './route-resolver/matchers/matcher-pattern'
 export type {
   MatcherPattern,
index df6bf36db62c1463fe2e9a89b714f63d71ac4101..0cc13cc5c9cbd7e6de049201e98d2e09c0232bcf 100644 (file)
@@ -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 },
index 4f92a333a09833ff970d458314ae75650f3c5233..465f75fe5eb11215dffa69603520ee4c350baf6a 100644 (file)
@@ -193,17 +193,8 @@ export type ParamsFromParsers<P extends Record<string, ParamParser_Generic>> = {
     : 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<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
@@ -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) {