From 8bcf9de8c7550d70cd7e8b0ec01a815c58e63f4c Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 20 Aug 2025 11:15:06 +0200 Subject: [PATCH] feat: MatcherPatternQueryParam --- .../src/router/index.ts | 24 ++++- packages/router/src/experimental/index.ts | 6 +- .../matchers/matcher-pattern-query.ts | 102 ++++++++++++++++++ .../matchers/matcher-pattern.ts | 8 -- .../matchers/param-parsers/index.ts | 4 +- .../matchers/param-parsers/numbers.ts | 2 +- .../route-resolver/matchers/test-utils.ts | 7 +- .../route-resolver/resolver-fixed.spec.ts | 6 +- .../route-resolver/resolver-fixed.ts | 2 +- 9 files changed, 136 insertions(+), 25 deletions(-) create mode 100644 packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts diff --git a/packages/experiments-playground/src/router/index.ts b/packages/experiments-playground/src/router/index.ts index 066e22c1..5cb1a9e3 100644 --- a/packages/experiments-playground/src/router/index.ts +++ b/packages/experiments-playground/src/router/index.ts @@ -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, diff --git a/packages/router/src/experimental/index.ts b/packages/router/src/experimental/index.ts index e16fe505..23fb0981 100644 --- a/packages/router/src/experimental/index.ts +++ b/packages/router/src/experimental/index.ts @@ -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 index 00000000..9078b155 --- /dev/null +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts @@ -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 {} + +export class MatcherPatternQueryParam + implements MatcherPatternQuery> +{ + constructor( + private paramName: ParamName, + private queryKey: string, + private format: 'value' | 'array' | 'both', + private parser: ParamParser, + // TODO: optional values + // private format: 'value' | 'array' | 'both' = 'both', + // private parser: ParamParser = PATH_PARAM_DEFAULT_PARSER, + private defaultValue?: (() => T) | T + ) {} + + match(query: MatcherQueryParams): Record { + 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 + } + + build(params: Record): MatcherQueryParams { + const paramValue = params[this.paramName] + + if (paramValue == null) { + return {} as EmptyParams + } + + return { + // FIXME: default setter + [this.queryKey]: this.parser.set!(paramValue), + } + } +} 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 128e3961..c797711a 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts @@ -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 {} - /** * Handles the `hash` part of a URL. It can transform a hash string into an * object of params and vice versa. diff --git a/packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts b/packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts index aba6bf5f..89d4fcb4 100644 --- a/packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts +++ b/packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts @@ -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 = {} -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 } diff --git a/packages/router/src/experimental/route-resolver/matchers/param-parsers/numbers.ts b/packages/router/src/experimental/route-resolver/matchers/param-parsers/numbers.ts index 4ee079be..3fe9fa1f 100644 --- a/packages/router/src/experimental/route-resolver/matchers/param-parsers/numbers.ts +++ b/packages/router/src/experimental/route-resolver/matchers/param-parsers/numbers.ts @@ -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() diff --git a/packages/router/src/experimental/route-resolver/matchers/test-utils.ts b/packages/router/src/experimental/route-resolver/matchers/test-utils.ts index da0bf08e..89cdc0b6 100644 --- a/packages/router/src/experimental/route-resolver/matchers/test-utils.ts +++ b/packages/router/src/experimental/route-resolver/matchers/test-utils.ts @@ -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<{ diff --git a/packages/router/src/experimental/route-resolver/resolver-fixed.spec.ts b/packages/router/src/experimental/route-resolver/resolver-fixed.spec.ts index 187d6be9..7d8249de 100644 --- a/packages/router/src/experimental/route-resolver/resolver-fixed.spec.ts +++ b/packages/router/src/experimental/route-resolver/resolver-fixed.spec.ts @@ -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, diff --git a/packages/router/src/experimental/route-resolver/resolver-fixed.ts b/packages/router/src/experimental/route-resolver/resolver-fixed.ts index aec91905..907844d3 100644 --- a/packages/router/src/experimental/route-resolver/resolver-fixed.ts +++ b/packages/router/src/experimental/route-resolver/resolver-fixed.ts @@ -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 { -- 2.47.3