Skip to content

Commit

Permalink
fix(useRouteHash,useRouteQuery,useRouteParams): re-evaluates the valu…
Browse files Browse the repository at this point in the history
…e immediately (#3002)
  • Loading branch information
anteriovieira committed May 9, 2023
1 parent 6b6701c commit d525244
Show file tree
Hide file tree
Showing 7 changed files with 386 additions and 40 deletions.
6 changes: 4 additions & 2 deletions packages/router/_types.ts
@@ -1,7 +1,9 @@
import type { MaybeRef } from '@vueuse/shared'
import type { useRoute, useRouter } from 'vue-router'
import type { RouteParamValueRaw, useRoute, useRouter } from 'vue-router'

export type RouterQueryValue = null | undefined | string | string[]
export type RouteQueryValueRaw = RouteParamValueRaw | string[]

export type RouteHashValueRaw = string | null | undefined

export interface ReactiveRouteOptions {
/**
Expand Down
65 changes: 65 additions & 0 deletions packages/router/useRouteHash/index.test.ts
@@ -0,0 +1,65 @@
import { nextTick } from 'vue-demi'
import { describe, expect, it } from 'vitest'
import { useRouteHash } from '.'

describe('useRouteHash', () => {
const getRoute = (hash?: any) => ({
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()
})
})
30 changes: 22 additions & 8 deletions packages/router/useRouteHash/index.ts
@@ -1,24 +1,38 @@
import { computed, nextTick } from 'vue-demi'
import { customRef, nextTick } from 'vue-demi'
import { useRoute, useRouter } from 'vue-router'
import { toValue } from '@vueuse/shared'
import type { ReactiveRouteOptions } from '../_types'
import { toValue, tryOnScopeDispose } from '@vueuse/shared'
import type { ReactiveRouteOptions, RouteHashValueRaw } from '../_types'

let _hash: RouteHashValueRaw

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

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

return customRef<RouteHashValueRaw>((track, trigger) => ({
get() {
return route.hash ?? defaultValue
track()

return _hash || defaultValue
},
set(v) {
_hash = v === null ? undefined : v

trigger()

nextTick(() => {
router[toValue(mode)]({ ...route, hash: v })
router[toValue(mode)]({ ...route, hash: _hash as string })
})
},
})
}))
}
127 changes: 124 additions & 3 deletions packages/router/useRouteParams/index.test.ts
@@ -1,25 +1,146 @@
import { effectScope, nextTick, ref } from 'vue-demi'
import { describe, expect, it } from 'vitest'
import type { Ref } from 'vue-demi'
import { useRouteParams } from '.'

describe('useRouteQuery', () => {
describe('useRouteParams', () => {
const getRoute = (params: Record<string, any> = {}) => ({
params,
query: {},
fullPath: '',
hash: '',
matched: [],
meta: {},
name: '',
params: { id: '1', ...params },
path: '',
redirectedFrom: undefined,
})

it('should return transformed value', () => {
it('should export', () => {
expect(useRouteParams).toBeDefined()
})

it('should return current value', () => {
const router = {} as any
const route = getRoute({
id: '1',
})

const id = useRouteParams('id', null, { route, router })

expect(id.value).toBe('1')
})

it('should return transformed value', () => {
const route = getRoute()
const router = {} as any

const id = useRouteParams('id', '1', { transform: Number, route, router })

expect(id.value).toBe(1)
})

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

const slug: Ref<any> = useRouteParams('slug', 'foo', { route, router })
const id: Ref<any> = useRouteParams('id', '123', { route, router })
const page: Ref<any> = useRouteParams('page', null, { route, router })

slug.value = 'bar'
id.value = '456'
page.value = '2'

expect(slug.value).toBe('bar')
expect(id.value).toBe('456')
expect(page.value).toBe('2')
})

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

const code: Ref<any> = useRouteParams('code', null, { route, router })
const page: Ref<any> = useRouteParams('page', null, { route, router })
const lang: Ref<any> = useRouteParams('lang', null, { route, router })

code.value = 'bar'
page.value = '1'
lang.value = 'en'

await nextTick()

expect(code.value).toBe('bar')
expect(route.params.code).toBe('bar')
expect(page.value).toBe('1')
expect(route.params.page).toBe('1')
expect(lang.value).toBe('en')
expect(route.params.lang).toBe('en')
})

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

const page: Ref<any> = useRouteParams('page', 10, { route, router })
const lang: Ref<any> = useRouteParams('lang', 'pt-BR', { route, router })

expect(page.value).toBe(10)
expect(lang.value).toBe('pt-BR')
})

it('should reset state on scope dispose', async () => {
let route = getRoute()
const router = { replace: (r: any) => route = r } as any
const scopeA = effectScope()
const scopeB = effectScope()

let page: Ref<any> = ref(null)
let lang: Ref<any> = ref(null)
let code: Ref<any> = ref(null)
let slug: Ref<any> = ref(null)

await scopeA.run(async () => {
page = useRouteParams('page', null, { route, router })
lang = useRouteParams('lang', null, { route, router })

page.value = 2
lang.value = 'pt-BR'

await nextTick()
})

expect(page.value).toBe(2)
expect(lang.value).toBe('pt-BR')
expect(route.params.page).toBe(2)
expect(route.params.lang).toBe('pt-BR')

await scopeB.run(async () => {
code = useRouteParams('code', null, { route, router })
slug = useRouteParams('slug', null, { route, router })

code.value = 'xyz'
slug.value = 'vueuse'

await nextTick()
})

expect(code.value).toBe('xyz')
expect(slug.value).toBe('vueuse')
expect(route.params.code).toBe('xyz')
expect(route.params.slug).toBe('vueuse')

scopeB.stop()

expect(page.value).toBe(2)
expect(lang.value).toBe('pt-BR')
expect(code.value).toBeNull()
expect(slug.value).toBeNull()

scopeA.stop()

expect(page.value).toBeNull()
expect(lang.value).toBeNull()
})
})
38 changes: 29 additions & 9 deletions packages/router/useRouteParams/index.ts
@@ -1,15 +1,18 @@
import type { Ref } from 'vue-demi'
import { computed, nextTick } from 'vue-demi'
import { customRef, nextTick } from 'vue-demi'
import type { RouteParamValueRaw } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { toValue } from '@vueuse/shared'
import type { ReactiveRouteOptionsWithTransform, RouterQueryValue } from '../_types'
import { toValue, tryOnScopeDispose } from '@vueuse/shared'
import type { ReactiveRouteOptionsWithTransform } from '../_types'

const _cache = new WeakMap()

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

export function useRouteParams<
T extends RouterQueryValue = RouterQueryValue,
T extends RouteParamValueRaw = RouteParamValueRaw,
K = T,
>(
name: string,
Expand All @@ -18,7 +21,7 @@ export function useRouteParams<
): Ref<K>

export function useRouteParams<
T extends RouterQueryValue = RouterQueryValue,
T extends RouteParamValueRaw = RouteParamValueRaw,
K = T,
>(
name: string,
Expand All @@ -32,15 +35,32 @@ export function useRouteParams<
transform = value => value as any as K,
} = options

return computed<any>({
if (!_cache.has(route))
_cache.set(route, new Map())

const _params: Map<string, any> = _cache.get(route)

tryOnScopeDispose(() => {
_params.delete(name)
})

_params.set(name, route.params[name])

return customRef<any>((track, trigger) => ({
get() {
const data = route.params[name] ?? defaultValue
track()

const data = _params.get(name) ?? defaultValue
return transform(data as T)
},
set(v) {
_params.set(name, (v === defaultValue || v === null) ? undefined : v)

trigger()

nextTick(() => {
router[toValue(mode)]({ ...route, params: { ...route.params, [name]: v } })
router[toValue(mode)]({ ...route, params: { ...route.params, ...Object.fromEntries(_params.entries()) } })
})
},
})
}))
}

0 comments on commit d525244

Please sign in to comment.