Skip to content
This repository has been archived by the owner on Aug 25, 2024. It is now read-only.

Commit

Permalink
feat: add router composables
Browse files Browse the repository at this point in the history
  • Loading branch information
elonehoo committed Dec 25, 2023
1 parent 0d02460 commit 580d6bf
Show file tree
Hide file tree
Showing 7 changed files with 899 additions and 0 deletions.
61 changes: 61 additions & 0 deletions composables/useRouteHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { customRef, nextTick, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toValue, tryOnScopeDispose } from '@vueuse/shared'
import type { MaybeRefOrGetter } from '@vueuse/shared'
import type { ReactiveRouteOptions, RouteHashValueRaw } from '~/types/router'

let _hash: RouteHashValueRaw

export function useRouteHash(
defaultValue?: MaybeRefOrGetter<RouteHashValueRaw>,
{
mode = 'replace',
route = useRoute(),
router = useRouter(),
}: ReactiveRouteOptions = {},
) {
_hash = route.hash

tryOnScopeDispose(() => {
_hash = undefined
})

let _trigger: () => void

const proxy = customRef<RouteHashValueRaw>((track, trigger) => {
_trigger = trigger

return {
get() {
track()

return _hash || toValue(defaultValue)
},
set(v) {
if (v === _hash)
return

_hash = v === null ? undefined : v

trigger()

nextTick(() => {
const { params, query } = route

router[toValue(mode)]({ params, query, hash: _hash as string })
})
},
}
})

watch(
() => route.hash,
() => {
_hash = route.hash
_trigger()
},
{ flush: 'sync' },
)

return proxy
}
103 changes: 103 additions & 0 deletions composables/useRouteParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { customRef, nextTick, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toValue, tryOnScopeDispose } from '@vueuse/shared'
import type { Ref } from 'vue'
import type { MaybeRefOrGetter } from '@vueuse/shared'
import type { LocationAsRelativeRaw, RouteParamValueRaw, Router } from 'vue-router'
import type { ReactiveRouteOptionsWithTransform } from '~/types/router'

const _queue = new WeakMap<Router, Map<string, any>>()

export function useRouteParams(
name: string
): Ref<null | string | string[]>

export function useRouteParams<
T extends RouteParamValueRaw = RouteParamValueRaw,
K = T,
>(
name: string,
defaultValue?: MaybeRefOrGetter<T>,
options?: ReactiveRouteOptionsWithTransform<T, K>
): Ref<K>

export function useRouteParams<
T extends RouteParamValueRaw = RouteParamValueRaw,
K = T,
>(
name: string,
defaultValue?: MaybeRefOrGetter<T>,
options: ReactiveRouteOptionsWithTransform<T, K> = {},
): Ref<K> {
const {
mode = 'replace',
route = useRoute(),
router = useRouter(),
transform = value => value as any as K,
} = options

if (!_queue.has(router))
_queue.set(router, new Map())

const _paramsQueue = _queue.get(router)!

let param = route.params[name] as any

tryOnScopeDispose(() => {
param = undefined
})

let _trigger: () => void

const proxy = customRef<any>((track, trigger) => {
_trigger = trigger

return {
get() {
track()

return transform(param !== undefined ? param : toValue(defaultValue))
},
set(v) {
if (param === v)
return

param = v
_paramsQueue.set(name, v)

trigger()

nextTick(() => {
if (_paramsQueue.size === 0)
return

const newParams = Object.fromEntries(_paramsQueue.entries())
_paramsQueue.clear()

const { params, query, hash } = route

router[toValue(mode)]({
params: {
...params,
...newParams,
},
query,
hash,
} as LocationAsRelativeRaw)
})
},
}
})

watch(
() => route.params[name],
(v) => {
param = v

_trigger()
},
{ flush: 'sync' },
)

return proxy as Ref<K>
}
100 changes: 100 additions & 0 deletions composables/useRouteQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { customRef, nextTick, watch } from 'vue'
import { toValue, tryOnScopeDispose } from '@vueuse/shared'
import { useRoute, useRouter } from 'vue-router'
import type { Router } from 'vue-router'
import type { Ref } from 'vue'
import type { MaybeRefOrGetter } from '@vueuse/shared'
import type { ReactiveRouteOptionsWithTransform, RouteQueryValueRaw } from '~/types/router'

const _queue = new WeakMap<Router, Map<string, any>>()

export function useRouteQuery(
name: string
): Ref<null | string | string[]>

export function useRouteQuery<
T extends RouteQueryValueRaw = RouteQueryValueRaw,
K = T,
>(
name: string,
defaultValue?: MaybeRefOrGetter<T>,
options?: ReactiveRouteOptionsWithTransform<T, K>
): Ref<K>

export function useRouteQuery<
T extends RouteQueryValueRaw = RouteQueryValueRaw,
K = T,
>(
name: string,
defaultValue?: MaybeRefOrGetter<T>,
options: ReactiveRouteOptionsWithTransform<T, K> = {},
): Ref<K> {
const {
mode = 'replace',
route = useRoute(),
router = useRouter(),
transform = value => value as any as K,
} = options

if (!_queue.has(router))
_queue.set(router, new Map())

const _queriesQueue = _queue.get(router)!

let query = route.query[name] as any

tryOnScopeDispose(() => {
query = undefined
})

let _trigger: () => void

const proxy = customRef<any>((track, trigger) => {
_trigger = trigger

return {
get() {
track()

return transform(query !== undefined ? query : toValue(defaultValue))
},
set(v) {
if (query === v)
return

query = v
_queriesQueue.set(name, v)

trigger()

nextTick(() => {
if (_queriesQueue.size === 0)
return

const newQueries = Object.fromEntries(_queriesQueue.entries())
_queriesQueue.clear()

const { params, query, hash } = route

router[toValue(mode)]({
params,
query: { ...query, ...newQueries },
hash,
})
})
},
}
})

watch(
() => route.query[name],
(v) => {
query = v

_trigger()
},
{ flush: 'sync' },
)

return proxy as any as Ref<K>
}
32 changes: 32 additions & 0 deletions types/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { MaybeRef } from '@vueuse/shared'
import type { RouteParamValueRaw, useRoute, useRouter } from 'vue-router'

export type RouteQueryValueRaw = RouteParamValueRaw | string[]

export type RouteHashValueRaw = string | null | undefined

export interface ReactiveRouteOptions {
/**
* Mode to update the router query, ref is also acceptable
*
* @default 'replace'
*/
mode?: MaybeRef<'replace' | 'push'>

/**
* Route instance, use `useRoute()` if not given
*/
route?: ReturnType<typeof useRoute>

/**
* Router instance, use `useRouter()` if not given
*/
router?: ReturnType<typeof useRouter>
}

export interface ReactiveRouteOptionsWithTransform<V, R> extends ReactiveRouteOptions {
/**
* Function to transform data before return
*/
transform?: (val: V) => R
}
91 changes: 91 additions & 0 deletions vitest/composables/useRouteHash.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { nextTick, reactive, ref } from 'vue'
import { describe, expect, it } from 'vitest'
import { useRouteHash } from '~/composables/useRouteHash'

describe('useRouteHash', () => {
const getRoute = (hash?: any) => reactive({
query: {},
fullPath: '',
hash,
matched: [],
meta: {},
name: '',
params: {},
path: '',
redirectedFrom: undefined,
})

it('should export', () => {
expect(useRouteHash).toBeDefined()
})

it('should return current value', () => {
let route = getRoute('header')
const router = { replace: (r: any) => route = r } as any

const hash = useRouteHash(null, { route, router })

expect(hash.value).toBe(route.hash)
})

it('should re-evaluate the value immediately', () => {
let route = getRoute('header')
const router = { replace: (r: any) => route = r } as any

const hash = useRouteHash(null, { route, router })

hash.value = 'footer'

expect(hash.value).toBe('footer')
})

it('should update the route', async () => {
let route = getRoute('foo')
const router = { replace: (r: any) => route = r } as any

const hash = useRouteHash(null, { route, router })

hash.value = 'footer'

await nextTick()

expect(hash.value).toBe('footer')
expect(route.hash).toBe('footer')
})

it('should return default value', () => {
let route = getRoute()
const router = { replace: (r: any) => route = r } as any

const hash = useRouteHash('baz', { route, router })

expect(hash.value).toBe('baz')
expect(route.hash).toBeUndefined()
})

it('should change the value when the route changes', () => {
let route = getRoute()
const router = { replace: (r: any) => route = r } as any

const hash = useRouteHash('baz', { route, router })

route.hash = 'foo'

expect(hash.value).toBe('foo')
})

it('should allow ref or getter as default value', () => {
let route = getRoute()
const router = { replace: (r: any) => route = r } as any

const defaultTarget = ref('foo')

const target = useRouteHash(defaultTarget, { route, router })

expect(target.value).toBe('foo')

target.value = 'bar'

expect(target.value).toBe('bar')
})
})
Loading

0 comments on commit 580d6bf

Please sign in to comment.