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 all commits
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
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export default defineConfig({
{ text: 'RouterReplace', link: '/api/types/RouterReplace' },
{ text: 'RouterResolve', link: '/api/types/RouterResolve' },
{ text: 'Routes', link: '/api/types/Routes' },
{ text: 'QuerySource', link: '/api/types/QuerySource' },
{ text: 'Url', link: '/api/types/Url' },
],
},
Expand Down
4 changes: 3 additions & 1 deletion docs/api/components/RouterLink.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ 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 | `QuerySource` \| `undefined` |
| hash | `string` \| `undefined` |
| replace | `boolean` \| `undefined` |
| prefetch | `boolean` \| `PrefetchConfigOptions` \| `undefined` |

[QuerySource](/api/types/QuerySource)

[RouterResolve](/api/types/RouterResolve)

## Slots
Expand Down
7 changes: 7 additions & 0 deletions docs/api/types/QuerySource.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# QuerySource

QuerySource is our name for the constructor type passed to the built in [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams).

```ts
export type QuerySource = ConstructorParameters<typeof URLSearchParams>[0]
```
4 changes: 3 additions & 1 deletion docs/api/types/RouterPush.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ Push will update the URL for the browser and also add the URL into the history s

```ts
{
query?: Record<string, string>,
query?: QuerySource,
replace?: boolean,
hash?: string,
}
```

[QuerySource](/api/types/QuerySource)

## Returns

`Promise<void>`
4 changes: 3 additions & 1 deletion docs/api/types/RouterReplace.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ Replace has the same effect as [`RouterPush`](/api/types/RouterPush) but without

```ts
{
query?: Record<string, string>,
query?: QuerySource,
hash?: string,
}
```

[QuerySource](/api/types/QuerySource)

## Returns

`Promise<void>`
4 changes: 3 additions & 1 deletion docs/api/types/RouterResolve.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ If source is `Url`, expected type for params is `never`. Else when source is `TS

```ts
{
query?: Record<string, string>,
query?: QuerySource,
hash?: string,
}
```

[QuerySource](/api/types/QuerySource)

## Returns

`Url`
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 { QuerySource } from '@/types/query'

export type RouterResolveOptions = {
query?: Record<string, string>,
query?: QuerySource,
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, QuerySource } 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?: QuerySource,
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 QuerySource = ConstructorParameters<typeof URLSearchParams>[0]

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 { QuerySource } from '@/types/query'

export type RouterPushOptions<
TState = unknown
> = {
query?: Record<string, string>,
query?: QuerySource,
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 { QuerySource } from '@/types/query'

export type RouterReplaceOptions<
TState = unknown
> = {
query?: Record<string, string>,
query?: QuerySource,
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 { QuerySource } from '@/types/query'

export function combineUrlSearchParams(aParams: URLSearchParams | QuerySource, bParams: URLSearchParams | QuerySource): 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