Skip to content

Commit 8b8cebf

Browse files
committed
feat: custom param
1 parent 5fa416a commit 8b8cebf

File tree

6 files changed

+90
-26
lines changed

6 files changed

+90
-26
lines changed

packages/experiments-playground/src/App.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,13 @@ const queryPage = computed({
5959
params: <code>{{ route.params }}</code>
6060
<br />
6161
<template v-if="queryPage != null">
62-
page: <input type="number" v-model.number="queryPage" />
62+
page:
63+
<input
64+
type="number"
65+
v-model.number="queryPage"
66+
autocomplete="off"
67+
data-1p-ignore
68+
/>
6369
<br />
6470
</template>
6571
meta: <code>{{ route.meta }}</code>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script setup lang="ts">
2+
import { useRoute } from 'vue-router'
3+
4+
const route = useRoute()
5+
</script>
6+
7+
<template>
8+
<main>
9+
<h2>Details</h2>
10+
<p>{{ route.fullPath }}</p>
11+
<pre>{{ route.params }}</pre>
12+
</main>
13+
</template>

packages/experiments-playground/src/router/index.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import { createWebHistory } from 'vue-router'
1+
import { createWebHistory, type RouteParamValue } from 'vue-router'
22
import {
33
experimental_createRouter,
44
createStaticResolver,
55
MatcherPatternPathStatic,
6+
MatcherPatternPathCustomParams,
67
normalizeRouteRecord,
78
} from 'vue-router/experimental'
89
import type {
910
EXPERIMENTAL_RouteRecordNormalized_Matchable,
1011
MatcherPatternHash,
1112
MatcherPatternQuery,
13+
EmptyParams,
1214
} from 'vue-router/experimental'
1315
import PageHome from '../pages/(home).vue'
14-
import type { EmptyParams } from 'vue-router/experimental'
1516

1617
// type ExtractMatcherQueryParams<T> =
1718
// T extends MatcherPatternQuery<infer P> ? P : never
@@ -134,6 +135,31 @@ const r_nested_a = normalizeRouteRecord({
134135
path: new MatcherPatternPathStatic('/nested/a'),
135136
})
136137

138+
const r_profiles_detail = normalizeRouteRecord({
139+
name: 'profiles-detail',
140+
components: { default: () => import('../pages/profiles/[userId].vue') },
141+
parent: r_profiles_layout,
142+
path: new MatcherPatternPathCustomParams(
143+
/^\/profiles\/(?<userId>[^/]+)$/i,
144+
{
145+
userId: {
146+
parser: {
147+
// @ts-expect-error: FIXME: should would with generic class
148+
get: (value: string): number => Number(value),
149+
// @ts-expect-error: FIXME: should would with generic class
150+
set: (value: number): string => String(value),
151+
},
152+
},
153+
},
154+
({ userId }) => {
155+
if (typeof userId !== 'number') {
156+
throw new Error('userId must be a number')
157+
}
158+
return `/profiles/${userId}`
159+
}
160+
),
161+
})
162+
137163
export const router = experimental_createRouter({
138164
history: createWebHistory(),
139165
resolver: createStaticResolver<EXPERIMENTAL_RouteRecordNormalized_Matchable>([
@@ -142,6 +168,7 @@ export const router = experimental_createRouter({
142168
r_nested,
143169
r_nested_a,
144170
r_profiles_list,
171+
r_profiles_detail,
145172
]),
146173
})
147174

packages/router/src/experimental/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export {
2222
MatcherPatternPathDynamic,
2323
MatcherPatternPathStatic,
2424
MatcherPatternPathStar,
25+
MatcherPatternPathCustomParams,
2526
} from './route-resolver/matchers/matcher-pattern'
2627
export type {
2728
MatcherPattern,

packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
22
import {
33
MatcherPatternPathStatic,
44
MatcherPatternPathStar,
5-
MatcherPatternPathCustom,
5+
MatcherPatternPathCustomParams,
66
} from './matcher-pattern'
77
import { pathEncoded } from '../resolver-abstract'
88
import { invalid } from './errors'
@@ -106,7 +106,7 @@ describe('MatcherPatternPathStar', () => {
106106

107107
describe('MatcherPatternPathCustom', () => {
108108
it('single param', () => {
109-
const pattern = new MatcherPatternPathCustom(
109+
const pattern = new MatcherPatternPathCustomParams(
110110
/^\/teams\/([^/]+?)\/b$/i,
111111
{
112112
// all defaults
@@ -133,7 +133,7 @@ describe('MatcherPatternPathCustom', () => {
133133
})
134134

135135
it('decodes single param', () => {
136-
const pattern = new MatcherPatternPathCustom(
136+
const pattern = new MatcherPatternPathCustomParams(
137137
/^\/teams\/([^/]+?)$/i,
138138
{
139139
teamId: {},
@@ -150,7 +150,7 @@ describe('MatcherPatternPathCustom', () => {
150150
})
151151

152152
it('optional param', () => {
153-
const pattern = new MatcherPatternPathCustom(
153+
const pattern = new MatcherPatternPathCustomParams(
154154
/^\/teams(?:\/([^/]+?))?\/b$/i,
155155
{
156156
teamId: { optional: true },
@@ -172,7 +172,7 @@ describe('MatcherPatternPathCustom', () => {
172172
})
173173

174174
it('repeatable param', () => {
175-
const pattern = new MatcherPatternPathCustom(
175+
const pattern = new MatcherPatternPathCustomParams(
176176
/^\/teams\/(.+?)\/b$/i,
177177
{
178178
teamId: { repeat: true },
@@ -196,7 +196,7 @@ describe('MatcherPatternPathCustom', () => {
196196
})
197197

198198
it('repeatable optional param', () => {
199-
const pattern = new MatcherPatternPathCustom(
199+
const pattern = new MatcherPatternPathCustomParams(
200200
/^\/teams(?:\/(.+?))?\/b$/i,
201201
{
202202
teamId: { repeat: true, optional: true },

packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -193,30 +193,49 @@ export type ParamsFromParsers<P extends Record<string, ParamParser_Generic>> = {
193193
: never
194194
}
195195

196-
/**
197-
* TODO: it should accept a dict of param parsers for each param and if they are repeatable and optional
198-
* The object order matters, they get matched in that order
199-
*/
200-
201-
interface MatcherPatternPathDynamicParam<
202-
TIn extends string | string[] | null | undefined =
203-
| string
204-
| string[]
205-
| null
206-
| undefined,
196+
interface MatcherPatternPathCustomParamOptions<
197+
TIn extends string | string[] | null = string | string[] | null,
207198
TOut = string | string[] | null,
208199
> {
209200
repeat?: boolean
210201
optional?: boolean
211202
parser?: Param_GetSet<TIn, TOut>
212203
}
213204

214-
export class MatcherPatternPathCustom implements MatcherPatternPath {
205+
export const PARAM_NUMBER = {
206+
get: (value: string) => {
207+
const num = Number(value)
208+
if (Number.isFinite(num)) {
209+
return num
210+
}
211+
throw miss()
212+
},
213+
set: (value: number) => String(value),
214+
} satisfies Param_GetSet<string, number>
215+
216+
export const PARAM_NUMBER_OPTIONAL = {
217+
get: (value: string | null) =>
218+
value == null ? null : PARAM_NUMBER.get(value),
219+
set: (value: number | null) =>
220+
value != null ? PARAM_NUMBER.set(value) : null,
221+
} satisfies Param_GetSet<string | null, number | null>
222+
223+
export const PARAM_NUMBER_REPEATABLE = {
224+
get: (value: string[]) => value.map(PARAM_NUMBER.get),
225+
set: (value: number[]) => value.map(PARAM_NUMBER.set),
226+
} satisfies Param_GetSet<string[], number[]>
227+
228+
export class MatcherPatternPathCustomParams implements MatcherPatternPath {
215229
// private paramsKeys: string[]
216230

217231
constructor(
232+
// TODO: make this work with named groups and simplify `params` to be an array of the repeat flag
218233
readonly re: RegExp,
219-
readonly params: Record<string, MatcherPatternPathDynamicParam>,
234+
readonly params: Record<
235+
string,
236+
// @ts-expect-error: adapt with generic class
237+
MatcherPatternPathCustomParamOptions<unknown, unknown>
238+
>,
220239
readonly build: (params: MatcherParamsFormatted) => string
221240
// A better version could be using all the parts to join them
222241
// .e.g ['users', 0, 'profile', 1] -> /users/123/profile/456
@@ -236,7 +255,7 @@ export class MatcherPatternPathCustom implements MatcherPatternPath {
236255
for (const paramName in this.params) {
237256
const currentParam = this.params[paramName]
238257
// an optional group in the regexp will return undefined
239-
const currentMatch = match[i++] as string | undefined
258+
const currentMatch = (match[i++] as string | undefined) ?? null
240259
if (__DEV__ && !currentParam.optional && !currentMatch) {
241260
warn(
242261
`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 {
251270
)
252271
: decode(currentMatch)
253272

254-
console.log(paramName, currentParam, value)
255-
256-
params[paramName] = (currentParam.parser?.get || (v => v ?? null))(value)
273+
params[paramName] = (currentParam.parser?.get || (v => v))(value)
257274
}
258275

259276
if (__DEV__ && i !== match.length) {

0 commit comments

Comments
 (0)