]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: allow multiple query matchers per route
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 31 Jul 2025 10:01:59 +0000 (12:01 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 31 Jul 2025 10:01:59 +0000 (12:01 +0200)
packages/experiments-playground/src/App.vue
packages/experiments-playground/src/router/index.ts
packages/router/src/experimental/route-resolver/resolver-static.spec.ts
packages/router/src/experimental/route-resolver/resolver-static.ts

index 99a0b093d7b3e18c5e19eb2dc89f1551319b84d7..cbd335ec49b1608546f73529ebcf2f5c15d8033a 100644 (file)
@@ -1,10 +1,25 @@
 <script setup lang="ts">
-import { ref } from 'vue'
+import { computed, ref } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 
 const route = useRoute()
 const router = useRouter()
 const url = ref('')
+
+const queryPage = computed({
+  get: () =>
+    typeof route.params.page === 'number'
+      ? (route.params.page as number)
+      : null,
+  set: (value: number) => {
+    // TODO: relative location
+    router.push({
+      ...route,
+      // @ts-expect-error: FIXME: wtf
+      params: { ...route.params, page: value },
+    })
+  },
+})
 </script>
 
 <template>
@@ -20,7 +35,7 @@ const url = ref('')
       |
       <RouterLink to="/profiles">Profiles list</RouterLink>
     </nav>
-    <form @submit.prevent="router.push(url, route)">
+    <form @submit.prevent="router.push(url)">
       <label for="path">Path:</label>
       <input
         id="path"
@@ -43,6 +58,10 @@ const url = ref('')
     <br />
     params: <code>{{ route.params }}</code>
     <br />
+    <template v-if="queryPage != null">
+      page: <input type="number" v-model.number="queryPage" />
+      <br />
+    </template>
     meta: <code>{{ route.meta }}</code>
   </p>
 
index f1db882463b1b25881efa863364e8b19c03b3215..0ecadb8552385a5d0e207210c5408b11725621bf 100644 (file)
@@ -11,6 +11,7 @@ import type {
   MatcherPatternQuery,
 } 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
@@ -34,14 +35,16 @@ const PAGE_QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ page: number }> = {
   build: params => ({ page: String(params.page) }),
 }
 
-const QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ q: string }> = {
+const QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ q?: string }> = {
   match: query => {
     return {
       q: typeof query.q === 'string' ? query.q : '',
     }
   },
-  build: params => {
-    return { q: params.q || '' }
+  // NOTE: we need either to cast or to add an explicit return type annotation
+  // because of the special meaning of {} in TypeScript.
+  build: (params): { q?: string } => {
+    return params.q ? { q: params.q } : ({} as EmptyParams)
   },
 }
 
@@ -72,22 +75,6 @@ const QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ q: string }> = {
 //   QUERY_PATTERN_MATCHER
 // )
 
-const QUERY_MATCHER_COMBINED: MatcherPatternQuery<{
-  page: number
-  q: string
-}> = {
-  match: query => {
-    return {
-      ...PAGE_QUERY_PATTERN_MATCHER.match(query),
-      ...QUERY_PATTERN_MATCHER.match(query),
-    }
-  },
-  build: params => ({
-    ...PAGE_QUERY_PATTERN_MATCHER.build(params),
-    ...QUERY_PATTERN_MATCHER.build(params),
-  }),
-}
-
 const ANY_HASH_PATTERN_MATCHER: MatcherPatternHash<// hash could be named anything, in this case it creates a param named hash
 { hash: string | null }> = {
   match: hash => ({ hash: hash ? hash.slice(1) : null }),
@@ -104,7 +91,7 @@ const r_group = normalizeRouteRecord({
 const r_home = normalizeRouteRecord({
   name: 'home',
   path: new MatcherPatternPathStatic('/'),
-  query: QUERY_MATCHER_COMBINED,
+  query: [PAGE_QUERY_PATTERN_MATCHER, QUERY_PATTERN_MATCHER],
   parent: r_group,
   components: { default: PageHome },
 })
@@ -124,7 +111,7 @@ const r_profiles_layout = normalizeRouteRecord({
   meta: {
     layout: 'profile',
   },
-  query: PAGE_QUERY_PATTERN_MATCHER,
+  query: [PAGE_QUERY_PATTERN_MATCHER],
 })
 
 const r_profiles_list = normalizeRouteRecord({
index 2ca07dfec94b0841414d06fca545dc837c51721f..5f7db6b310c1d4abe77b18bab11ed5951cdd71b4 100644 (file)
@@ -117,7 +117,7 @@ describe('StaticResolver', () => {
           {
             name: 'any-path',
             path: ANY_PATH_PATTERN_MATCHER,
-            query: PAGE_QUERY_PATTERN_MATCHER_LOCAL,
+            query: [PAGE_QUERY_PATTERN_MATCHER_LOCAL],
           },
         ])
 
@@ -156,7 +156,7 @@ describe('StaticResolver', () => {
           {
             name: 'user-detail',
             path: USER_ID_PATH_PATTERN_MATCHER,
-            query: PAGE_QUERY_PATTERN_MATCHER_LOCAL,
+            query: [PAGE_QUERY_PATTERN_MATCHER_LOCAL],
             hash: ANY_HASH_PATTERN_MATCHER,
           },
         ])
@@ -317,14 +317,16 @@ describe('StaticResolver', () => {
             {
               name: 'query',
               path: EMPTY_PATH_PATTERN_MATCHER,
-              query: {
-                match(q) {
-                  return { q }
-                },
-                build({ q }) {
-                  return { ...q }
-                },
-              } satisfies MatcherPatternQuery<{ q: MatcherQueryParams }>,
+              query: [
+                {
+                  match(q) {
+                    return { q }
+                  },
+                  build({ q }) {
+                    return { ...q }
+                  },
+                } satisfies MatcherPatternQuery<{ q: MatcherQueryParams }>,
+              ],
             },
           ])
           expect(resolver.resolve('/?%23%2F%3F=%23%2F%3F')).toMatchObject({
index acc106974f8b3c63caeb1ceec1119435401381fd..4b7068be9b93cc8473e6777685fa85a11ca7d1a4 100644 (file)
@@ -41,7 +41,7 @@ export interface EXPERIMENTAL_ResolverRecord_Base {
   /**
    * {@link MatcherPattern} for the query section of the URI.
    */
-  query?: MatcherPatternQuery
+  query?: MatcherPatternQuery[]
 
   /**
    * {@link MatcherPattern} for the hash section of the URI.
@@ -63,7 +63,9 @@ export interface EXPERIMENTAL_ResolverRecord_Group
   extends EXPERIMENTAL_ResolverRecord_Base {
   name?: undefined
   path?: undefined
-  query?: undefined
+  // Query is the only kind of matcher that is non-exclusive
+  // all matched records get their queries merged
+  // query?: undefined
   hash?: undefined
 }
 
@@ -190,7 +192,9 @@ export function createStaticResolver<
           ...currentLocation?.query,
           ...normalizeQuery(to.query),
         },
-        ...matched.map(record => record.query?.build(params))
+        ...matched.flatMap(record =>
+          record.query?.map(query => query.build(params))
+        )
       )
 
       return {
@@ -233,7 +237,9 @@ export function createStaticResolver<
           matched = buildMatched(record)
           const queryParams: MatcherQueryParams = Object.assign(
             {},
-            ...matched.map(record => record.query?.match(url.query))
+            ...matched.flatMap(record =>
+              record.query?.map(query => query.match(url.query))
+            )
           )
           // TODO: test performance
           // for (const record of matched) {