Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend query support to match URLSeachParams #310

Merged
merged 2 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api/components/RouterLink.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RouterLink component renders anchor tag (`<a>`) for routing both within the SPA
| Parameter | Type |
| :---- | :---- |
| to | [`Url`](/api/types/Url) \| `(resolve: RouterResolve) => Url` |
| query | `Record<string, string>` \| `undefined` |
| query | `ConstructorParameters<typeof URLSearchParams>[0]` \| `undefined` |
pleek91 marked this conversation as resolved.
Show resolved Hide resolved
| hash | `string` \| `undefined` |
| replace | `boolean` \| `undefined` |
| prefetch | `boolean` \| `PrefetchConfigOptions` \| `undefined` |
Expand Down
2 changes: 1 addition & 1 deletion docs/api/types/RouterPush.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Push will update the URL for the browser and also add the URL into the history s

```ts
{
query?: Record<string, string>,
query?: ConstructorParameters<typeof URLSearchParams>[0],
replace?: boolean,
hash?: string,
}
Expand Down
2 changes: 1 addition & 1 deletion docs/api/types/RouterReplace.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Replace has the same effect as [`RouterPush`](/api/types/RouterPush) but without

```ts
{
query?: Record<string, string>,
query?: ConstructorParameters<typeof URLSearchParams>[0],
hash?: string,
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/api/types/RouterResolve.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ If source is `Url`, expected type for params is `never`. Else when source is `TS

```ts
{
query?: Record<string, string>,
query?: ConstructorParameters<typeof URLSearchParams>[0]****,
hash?: string,
}
```
Expand Down
10 changes: 5 additions & 5 deletions src/services/createRouter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,28 +306,28 @@ test('query.delete updates the route', async () => {
expect(route.query.toString()).toBe('fiz=buz')
})

test.fails('query.values is reactive', async () => {
test('query.values is reactive', async () => {
const root = createRoute({
name: 'root',
component,
path: '/',
})

const { route, start } = createRouter([root], {
initialUrl: '/?foo=bar&fiz=buz',
initialUrl: '/?foo=foo1&bar=bar1',
})

await start()

const values = computed(() => Array.from(route.query.values()))

expect(values.value).toMatchObject(['bar', 'buz'])
expect(values.value).toMatchObject(['foo1', 'bar1'])

route.query.append('foo', 'bar2')
route.query.append('foo', 'foo2')

await flushPromises()

expect(values.value).toMatchObject(['bar', 'buz', 'bar2'])
expect(values.value).toMatchObject(['foo1', 'bar1', 'foo2'])
})

test('given an array of Routes, combines into single routes collection', () => {
Expand Down
7 changes: 4 additions & 3 deletions src/services/createRouterResolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { RoutesName } from '@/types/routesMap'
import { RouteParamsByKey } from '@/types/routeWithParams'
import { isUrl, Url } from '@/types/url'
import { AllPropertiesAreOptional } from '@/types/utilities'
import { createUrl } from './urlCreator'
import { parseUrl } from './urlParser'
import { createUrl } from '@/services/urlCreator'
import { parseUrl } from '@/services/urlParser'
import { QueryRecord } from '@/types/query'

export type RouterResolveOptions = {
query?: Record<string, string>,
query?: QueryRecord,
hash?: string,
}

Expand Down
6 changes: 3 additions & 3 deletions src/services/createRouterRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,21 @@ export function createRouterRoute<TRoute extends ResolvedRoute>(route: TRoute, p
const query = new URLSearchParams(route.query.toString())
query.set(...parameters)

update({}, { query: Object.fromEntries(query.entries()) })
update({}, { query })
}

const queryAppend: URLSearchParams['append'] = (...parameters) => {
const query = new URLSearchParams(route.query.toString())
query.append(...parameters)

update({}, { query: Object.fromEntries(query.entries()) })
update({}, { query })
}

const queryDelete: URLSearchParams['delete'] = (...parameters) => {
const query = new URLSearchParams(route.query.toString())
query.delete(...parameters)

update({}, { query: Object.fromEntries(query.entries()) })
update({}, { query })
}

const { id, matched, matches, name, query, params, state, hash } = toRefs(route)
Expand Down
2 changes: 1 addition & 1 deletion src/services/routeMatchScore.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { parseUrl } from '@/services/urlParser'
import { getParamValueFromUrl } from '@/services/paramsFinder'
import { Route } from '@/types/route'
import { routeHashMatches } from './routeMatchRules'
import { routeHashMatches } from '@/services/routeMatchRules'

type RouteSortMethod = (aRoute: Route, bRoute: Route) => number

Expand Down
34 changes: 18 additions & 16 deletions src/services/urlAssembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ import { getParamName, isOptionalParamSyntax } from '@/services/routeRegex'
import { Host } from '@/types/host'
import { paramEnd, paramStart } from '@/types/params'
import { Path } from '@/types/path'
import { Query } from '@/types/query'
import { Query, QueryRecord } from '@/types/query'
import { Route } from '@/types/route'
import { Url } from '@/types/url'
import { createUrl } from './urlCreator'
import { parseUrl } from './urlParser'
import { createUrl } from '@/services/urlCreator'
import { parseUrl } from '@/services/urlParser'
import { combineUrlSearchParams } from '@/utilities/urlSearchParams'

export type QueryRecord = Record<string, string>
type AssembleUrlOptions = {
params?: Record<string, unknown>,
query?: Record<string, string>,
query?: QueryRecord,
hash?: string,
}

export function assembleUrl(route: Route, options: AssembleUrlOptions = {}): Url {
const { params: paramValues = {}, query: queryValues } = options
const queryWithParamsSet = assembleQueryParamValues(route.query, paramValues)
const searchParams = new URLSearchParams({ ...queryWithParamsSet, ...queryValues })
const routeQuery = assembleQueryParamValues(route.query, paramValues)
const searchParams = combineUrlSearchParams(routeQuery, queryValues)
const pathname = assemblePathParamValues(route.path, paramValues)
const hash = createHash(route.hash.value ?? options.hash).value

Expand Down Expand Up @@ -57,29 +57,31 @@ function assemblePathParamValues(path: Path, paramValues: Record<string, unknown
}, path.value)
}

function assembleQueryParamValues(query: Query, paramValues: Record<string, unknown>): QueryRecord {
function assembleQueryParamValues(query: Query, paramValues: Record<string, unknown>): URLSearchParams {
const search = new URLSearchParams(query.value)

if (!query.value) {
return {}
return search
}

const search = new URLSearchParams(query.value)

return Array.from(search.entries()).reduce<QueryRecord>((url, [key, value]) => {
for (const [key, value] of search.entries()) {
const paramName = getParamName(value)
const isNotParam = !paramName

if (isNotParam) {
return { ...url, [key]: value }
continue
}

const paramValue = setParamValue(paramValues[paramName], query.params[paramName], isOptionalParamSyntax(value))
const valueNotProvidedAndNoDefaultUsed = paramValues[paramName] === undefined && paramValue === ''
const shouldLeaveEmptyValueOut = isOptionalParamSyntax(value) && valueNotProvidedAndNoDefaultUsed

if (shouldLeaveEmptyValueOut) {
return url
search.delete(key, value)
} else {
search.set(key, paramValue)
}
}

return { ...url, [key]: paramValue }
}, {})
return search
}
2 changes: 2 additions & 0 deletions src/types/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Param } from '@/types/paramTypes'
import { Identity } from '@/types/utilities'
import { isRecord } from '@/utilities/guards'

export type QueryRecord = ConstructorParameters<typeof URLSearchParams>[0]
pleek91 marked this conversation as resolved.
Show resolved Hide resolved

type ExtractQueryParamsFromQueryString<
TQuery extends string,
TParams extends Record<string, Param | undefined> = Record<never, never>
Expand Down
3 changes: 2 additions & 1 deletion src/types/routerPush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { RouteParamsByKey } from '@/types/routeWithParams'
import { RouteStateByName } from '@/types/state'
import { Url } from '@/types/url'
import { AllPropertiesAreOptional } from '@/types/utilities'
import { QueryRecord } from '@/types/query'

export type RouterPushOptions<
TState = unknown
> = {
query?: Record<string, string>,
query?: QueryRecord,
hash?: string,
replace?: boolean,
state?: Partial<TState>,
Expand Down
3 changes: 2 additions & 1 deletion src/types/routerReplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { RouteParamsByKey } from '@/types/routeWithParams'
import { RouteStateByName } from '@/types/state'
import { Url } from '@/types/url'
import { AllPropertiesAreOptional } from '@/types/utilities'
import { QueryRecord } from '@/types/query'

export type RouterReplaceOptions<
TState = unknown
> = {
query?: Record<string, string>,
query?: QueryRecord,
hash?: string,
state?: Partial<TState>,
}
Expand Down
26 changes: 26 additions & 0 deletions src/utilities/urlSearchParams.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect, test } from 'vitest'
import { combineUrlSearchParams } from './urlSearchParams'

test.each([
'foo=bar',
{ foo: 'bar' },
[['foo', 'bar']],
])('given different constructor types, normalizes into URLSearchParams', (params) => {
const response = combineUrlSearchParams(params, undefined)

expect(Array.from(response.entries())).toMatchObject([
['foo', 'bar'],
])
})

test('given duplicate keys, appends new entry', () => {
const aParams = new URLSearchParams({ foo: 'foo' })
const bParams = new URLSearchParams({ foo: 'bar' })

const response = combineUrlSearchParams(aParams, bParams)

expect(Array.from(response.entries())).toMatchObject([
['foo', 'foo'],
['foo', 'bar'],
])
})
12 changes: 12 additions & 0 deletions src/utilities/urlSearchParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { QueryRecord } from '@/types/query'

export function combineUrlSearchParams(aParams: URLSearchParams | QueryRecord, bParams: URLSearchParams | QueryRecord): URLSearchParams {
const combinedParams = new URLSearchParams(aParams)
const paramsToAdd = new URLSearchParams(bParams)

for (const [key, value] of paramsToAdd.entries()) {
combinedParams.append(key, value)
}

return combinedParams
}
Loading