]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: MatcherPatternQueryParam
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 20 Aug 2025 09:15:06 +0000 (11:15 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 20 Aug 2025 09:15:06 +0000 (11:15 +0200)
packages/experiments-playground/src/router/index.ts
packages/router/src/experimental/index.ts
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts [new file with mode: 0644]
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts
packages/router/src/experimental/route-resolver/matchers/param-parsers/numbers.ts
packages/router/src/experimental/route-resolver/matchers/test-utils.ts
packages/router/src/experimental/route-resolver/resolver-fixed.spec.ts
packages/router/src/experimental/route-resolver/resolver-fixed.ts

index 066e22c1332931d3048af1564d16fef521d43d7b..5cb1a9e39f2b6e517544a32f0f4091c938bc4bfd 100644 (file)
@@ -3,9 +3,10 @@ import {
   experimental_createRouter,
   createFixedResolver,
   MatcherPatternPathStatic,
-  MatcherPatternPathCustomParams,
+  MatcherPatternPathDynamic,
   normalizeRouteRecord,
   PARAM_PARSER_INT,
+  MatcherPatternQueryParam,
 } from 'vue-router/experimental'
 import type {
   EXPERIMENTAL_RouteRecordNormalized_Matchable,
@@ -49,12 +50,29 @@ const r_group = normalizeRouteRecord({
   meta: {
     fromGroup: 'r_group',
   },
+
+  query: [
+    new MatcherPatternQueryParam(
+      'group',
+      'isGroup',
+      'value',
+      PARAM_PARSER_INT,
+      undefined
+    ),
+  ],
 })
 
 const r_home = normalizeRouteRecord({
   name: 'home',
   path: new MatcherPatternPathStatic('/'),
-  query: [PAGE_QUERY_PATTERN_MATCHER, QUERY_PATTERN_MATCHER],
+  query: [
+    new MatcherPatternQueryParam('pageArray', 'p', 'array', PARAM_PARSER_INT, [
+      1,
+    ]),
+    new MatcherPatternQueryParam('pageBoth', 'p', 'both', PARAM_PARSER_INT, 1),
+    new MatcherPatternQueryParam('page', 'p', 'value', PARAM_PARSER_INT, 1),
+    QUERY_PATTERN_MATCHER,
+  ],
   parent: r_group,
   components: { default: PageHome },
 })
@@ -101,7 +119,7 @@ const r_profiles_detail = normalizeRouteRecord({
   name: 'profiles-detail',
   components: { default: () => import('../pages/profiles/[userId].vue') },
   parent: r_profiles_layout,
-  path: new MatcherPatternPathCustomParams(
+  path: new MatcherPatternPathDynamic(
     /^\/profiles\/([^/]+)$/i,
     {
       // this version handles all kind of params but in practice,
index e16fe505a96263c22fce5ea376311d41ff7ee6cf..23fb09811d316a75a8f40ef46925ad59573342ab 100644 (file)
@@ -24,13 +24,17 @@ export type {
   MatcherPattern,
   MatcherPatternHash,
   MatcherPatternPath,
-  MatcherPatternQuery,
   MatcherParamsFormatted,
   MatcherQueryParams,
   MatcherQueryParamsValue,
   MatcherPatternPathDynamic_ParamOptions,
 } from './route-resolver/matchers/matcher-pattern'
 
+export {
+  type MatcherPatternQuery,
+  MatcherPatternQueryParam,
+} from './route-resolver/matchers/matcher-pattern-query'
+
 export {
   PARAM_PARSER_INT,
   type ParamParser,
diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts
new file mode 100644 (file)
index 0000000..9078b15
--- /dev/null
@@ -0,0 +1,102 @@
+import { toValue } from 'vue'
+import {
+  EmptyParams,
+  MatcherParamsFormatted,
+  MatcherPattern,
+  MatcherQueryParams,
+} from './matcher-pattern'
+import { ParamParser } from './param-parsers'
+
+/**
+ * Handles the `query` part of a URL. It can transform a query object into an
+ * object of params and vice versa.
+ */
+
+export interface MatcherPatternQuery<
+  TParams extends MatcherParamsFormatted = MatcherParamsFormatted,
+> extends MatcherPattern<MatcherQueryParams, TParams> {}
+
+export class MatcherPatternQueryParam<T, ParamName extends string>
+  implements MatcherPatternQuery<Record<ParamName, T>>
+{
+  constructor(
+    private paramName: ParamName,
+    private queryKey: string,
+    private format: 'value' | 'array' | 'both',
+    private parser: ParamParser<T>,
+    // TODO: optional values
+    // private format: 'value' | 'array' | 'both' = 'both',
+    // private parser: ParamParser<T> = PATH_PARAM_DEFAULT_PARSER,
+    private defaultValue?: (() => T) | T
+  ) {}
+
+  match(query: MatcherQueryParams): Record<ParamName, T> {
+    const queryValue = query[this.queryKey]
+
+    let valueBeforeParse =
+      this.format === 'value'
+        ? Array.isArray(queryValue)
+          ? queryValue[0]
+          : queryValue
+        : this.format === 'array'
+          ? Array.isArray(queryValue)
+            ? queryValue
+            : [queryValue]
+          : queryValue
+
+    let value: T | undefined
+
+    // if we have an array, we need to try catch each value
+    if (Array.isArray(valueBeforeParse)) {
+      // @ts-expect-error: T is not connected to valueBeforeParse
+      value = []
+      for (const v of valueBeforeParse) {
+        if (v != null) {
+          try {
+            ;(value as unknown[]).push(
+              // for ts errors
+              this.parser.get!(v)
+            )
+          } catch (error) {
+            // we skip the invalid value unless there is no defaultValue
+            if (this.defaultValue == null) {
+              throw error
+            }
+          }
+        }
+      }
+
+      // if we have no values, we want to fall back to the default value
+      if ((value as unknown[]).length === 0) {
+        value = undefined
+      }
+    } else {
+      try {
+        // FIXME: fallback to default getter
+        value = this.parser.get!(valueBeforeParse)
+      } catch (error) {
+        if (this.defaultValue == null) {
+          throw error
+        }
+      }
+    }
+
+    return {
+      [this.paramName]: value ?? toValue(this.defaultValue),
+      // This is a TS limitation
+    } as Record<ParamName, T>
+  }
+
+  build(params: Record<ParamName, T>): MatcherQueryParams {
+    const paramValue = params[this.paramName]
+
+    if (paramValue == null) {
+      return {} as EmptyParams
+    }
+
+    return {
+      // FIXME: default setter
+      [this.queryKey]: this.parser.set!(paramValue),
+    }
+  }
+}
index 128e39611d1083622a3e3234576f48e24f72f8a6..c797711aad5a9bdb2c724783134814bdb4c6ceef 100644 (file)
@@ -239,14 +239,6 @@ export class MatcherPatternPathDynamic<
   }
 }
 
-/**
- * Handles the `query` part of a URL. It can transform a query object into an
- * object of params and vice versa.
- */
-export interface MatcherPatternQuery<
-  TParams extends MatcherParamsFormatted = MatcherParamsFormatted,
-> extends MatcherPattern<MatcherQueryParams, TParams> {}
-
 /**
  * Handles the `hash` part of a URL. It can transform a hash string into an
  * object of params and vice versa.
index aba6bf5f0e7de265173328cd7cdb80349ac68977..89d4fcb445f3b4a19c273ac458b406bcc1f072ec 100644 (file)
@@ -21,10 +21,10 @@ export const PATH_PARAM_DEFAULT_SET = (
 ) => (value && Array.isArray(value) ? value.map(String) : String(value)) // TODO: `(value an null | undefined)` for types
 
 export const PATH_PARAM_SINGLE_DEFAULT: ParamParser<string, string> = {}
-export const PATH_PARAM_DEFAULT_PARSER: ParamParser = {
+export const PATH_PARAM_DEFAULT_PARSER = {
   get: PATH_PARAM_DEFAULT_GET,
   set: PATH_PARAM_DEFAULT_SET,
-}
+} satisfies ParamParser
 
 export type { ParamParser }
 
index 4ee079be13a628263ac93d8c6be6723b89de4f20..3fe9fa1fe92469006ebd652beef39eb21e025d0e 100644 (file)
@@ -4,7 +4,7 @@ import { ParamParser } from './types'
 export const PARAM_INTEGER_SINGLE = {
   get: (value: string) => {
     const num = Number(value)
-    if (Number.isInteger(num)) {
+    if (value && Number.isInteger(num)) {
       return num
     }
     throw miss()
index da0bf08e1ddfd50429fcd6b617a2f6e658fd1763..89cdc0b60ed39deb96dcf893f29a152493c2e036 100644 (file)
@@ -1,9 +1,6 @@
 import { EmptyParams } from './matcher-pattern'
-import {
-  MatcherPatternPath,
-  MatcherPatternQuery,
-  MatcherPatternHash,
-} from './matcher-pattern'
+import { MatcherPatternPath, MatcherPatternHash } from './matcher-pattern'
+import { MatcherPatternQuery } from './matcher-pattern-query'
 import { miss } from './errors'
 
 export const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{
index 187d6be94516a630b731b81447e9739c82bb9761..7d8249de48b5ab67c661437d31e7b6bf3cda348c 100644 (file)
@@ -6,10 +6,8 @@ import {
   MatcherPatternHash,
   MatcherQueryParams,
 } from './matchers/matcher-pattern'
-import {
-  MatcherPatternQuery,
-  MatcherPatternPathStatic,
-} from './matchers/matcher-pattern'
+import { MatcherPatternPathStatic } from './matchers/matcher-pattern'
+import { MatcherPatternQuery } from './matchers/matcher-pattern-query'
 import {
   EMPTY_PATH_PATTERN_MATCHER,
   USER_ID_PATH_PATTERN_MATCHER,
index aec91905d3f1c06aac161bf21f56e46f1c4801af..907844d3f575975abbfd714d9dc02b30c3c71550 100644 (file)
@@ -19,9 +19,9 @@ import {
 import { MatcherQueryParams } from './matchers/matcher-pattern'
 import type {
   MatcherPatternPath,
-  MatcherPatternQuery,
   MatcherPatternHash,
 } from './matchers/matcher-pattern'
+import type { MatcherPatternQuery } from './matchers/matcher-pattern-query'
 import { warn } from '../../warning'
 
 export interface EXPERIMENTAL_ResolverRecord_Base {