Skip to content

Commit

Permalink
Merge pull request #310 from kitbagjs/expand-supported-query-types
Browse files Browse the repository at this point in the history
Extend query support to match URLSeachParams
  • Loading branch information
pleek91 authored Nov 17, 2024
2 parents 9489179 + 9058037 commit da092f8
Show file tree
Hide file tree
Showing 16 changed files with 95 additions and 34 deletions.
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
}

0 comments on commit da092f8

Please sign in to comment.