diff --git a/packages/router/useRouteHash/index.test.ts b/packages/router/useRouteHash/index.test.ts index f5ce4414543..5a8f5f1a532 100644 --- a/packages/router/useRouteHash/index.test.ts +++ b/packages/router/useRouteHash/index.test.ts @@ -1,9 +1,9 @@ -import { nextTick } from 'vue-demi' +import { nextTick, reactive, ref } from 'vue-demi' import { describe, expect, it } from 'vitest' import { useRouteHash } from '.' describe('useRouteHash', () => { - const getRoute = (hash?: any) => ({ + const getRoute = (hash?: any) => reactive({ query: {}, fullPath: '', hash, @@ -62,4 +62,30 @@ describe('useRouteHash', () => { 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') + }) }) diff --git a/packages/router/useRouteHash/index.ts b/packages/router/useRouteHash/index.ts index 86f1cfa5dee..ea8eacd586b 100644 --- a/packages/router/useRouteHash/index.ts +++ b/packages/router/useRouteHash/index.ts @@ -1,12 +1,13 @@ -import { customRef, nextTick } from 'vue-demi' +import { customRef, nextTick, watch } from 'vue-demi' import { useRoute, useRouter } from 'vue-router' import { toValue, tryOnScopeDispose } from '@vueuse/shared' +import type { MaybeRefOrGetter } from '@vueuse/shared' import type { ReactiveRouteOptions, RouteHashValueRaw } from '../_types' let _hash: RouteHashValueRaw export function useRouteHash( - defaultValue?: RouteHashValueRaw, + defaultValue?: MaybeRefOrGetter, { mode = 'replace', route = useRoute(), @@ -19,20 +20,42 @@ export function useRouteHash( _hash = undefined }) - return customRef((track, trigger) => ({ - get() { - track() + let _trigger: () => void - return _hash || defaultValue - }, - set(v) { - _hash = v === null ? undefined : v + const proxy = customRef((track, trigger) => { + _trigger = trigger + + return { + get() { + track() + + return _hash || toValue(defaultValue) + }, + set(v) { + if (v === _hash) + return + + _hash = v === null ? undefined : v - trigger() + trigger() - nextTick(() => { - router[toValue(mode)]({ ...route, hash: _hash as string }) - }) + 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 } diff --git a/packages/router/useRouteParams/index.test.ts b/packages/router/useRouteParams/index.test.ts index 6c1d909528d..273d431f2f9 100644 --- a/packages/router/useRouteParams/index.test.ts +++ b/packages/router/useRouteParams/index.test.ts @@ -1,10 +1,10 @@ -import { effectScope, nextTick, ref } from 'vue-demi' -import { describe, expect, it } from 'vitest' +import { effectScope, nextTick, reactive, ref, watch } from 'vue-demi' +import { describe, expect, it, vi } from 'vitest' import type { Ref } from 'vue-demi' import { useRouteParams } from '.' describe('useRouteParams', () => { - const getRoute = (params: Record = {}) => ({ + const getRoute = (params: Record = {}) => reactive({ params, query: {}, fullPath: '', @@ -143,4 +143,73 @@ describe('useRouteParams', () => { expect(page.value).toBeNull() expect(lang.value).toBeNull() }) + + it('should change the value when the route changes', () => { + let route = getRoute() + const router = { replace: (r: any) => route = r } as any + + const lang: Ref = useRouteParams('lang', null, { route, router }) + + expect(lang.value).toBeNull() + + route.params = { lang: 'en' } + + expect(lang.value).toBe('en') + }) + + it('should avoid trigger effects when the value doesn\'t change', async () => { + let route = getRoute() + const router = { replace: (r: any) => route = r } as any + const onUpdate = vi.fn() + + const page = useRouteParams('page', 1, { transform: Number, route, router }) + + watch(page, onUpdate) + + page.value = 1 + + await nextTick() + + expect(page.value).toBe(1) + expect(route.params.page).toBe(1) + expect(onUpdate).not.toHaveBeenCalled() + }) + + it('should keep current query and hash', async () => { + let route = getRoute() + const router = { replace: (r: any) => route = r } as any + + route.query = { foo: 'bar' } + route.hash = '#hash' + + const id: Ref = useRouteParams('id', null, { route, router }) + + id.value = '2' + + await nextTick() + + expect(id.value).toBe('2') + expect(route.hash).toBe('#hash') + expect(route.query).toEqual({ foo: 'bar' }) + }) + + it('should allow ref or getter as default value', () => { + let route = getRoute() + const router = { replace: (r: any) => route = r } as any + + const defaultPage = ref(1) + const defaultLang = () => 'pt-BR' + + const page: Ref = useRouteParams('page', defaultPage, { route, router }) + const lang: Ref = useRouteParams('lang', defaultLang, { route, router }) + + expect(page.value).toBe(1) + expect(lang.value).toBe('pt-BR') + + page.value = 2 + lang.value = 'en-US' + + expect(page.value).toBe(2) + expect(lang.value).toBe('en-US') + }) }) diff --git a/packages/router/useRouteParams/index.ts b/packages/router/useRouteParams/index.ts index df59b435616..e17f38f1d3d 100644 --- a/packages/router/useRouteParams/index.ts +++ b/packages/router/useRouteParams/index.ts @@ -1,8 +1,9 @@ -import type { Ref } from 'vue-demi' -import { customRef, nextTick } from 'vue-demi' -import type { RouteParamValueRaw } from 'vue-router' +import { customRef, nextTick, watch } from 'vue-demi' import { useRoute, useRouter } from 'vue-router' import { toValue, tryOnScopeDispose } from '@vueuse/shared' +import type { Ref } from 'vue-demi' +import type { MaybeRefOrGetter } from '@vueuse/shared' +import type { LocationAsRelativeRaw, RouteParamValueRaw } from 'vue-router' import type { ReactiveRouteOptionsWithTransform } from '../_types' const _cache = new WeakMap() @@ -16,7 +17,7 @@ export function useRouteParams< K = T, >( name: string, - defaultValue?: T, + defaultValue?: MaybeRefOrGetter, options?: ReactiveRouteOptionsWithTransform ): Ref @@ -25,7 +26,7 @@ export function useRouteParams< K = T, >( name: string, - defaultValue?: T, + defaultValue?: MaybeRefOrGetter, options: ReactiveRouteOptionsWithTransform = {}, ): Ref { const { @@ -46,21 +47,51 @@ export function useRouteParams< _params.set(name, route.params[name]) - return customRef((track, trigger) => ({ - get() { - track() + let _trigger: () => void - const data = _params.get(name) ?? defaultValue - return transform(data as T) - }, - set(v) { - _params.set(name, (v === defaultValue || v === null) ? undefined : v) + const proxy = customRef((track, trigger) => { + _trigger = trigger + + return { + get() { + track() + + const data = _params.get(name) + + return transform(data !== undefined ? data : toValue(defaultValue)) + }, + set(v) { + if (_params.get(name) === v) + return - trigger() + _params.set(name, v) - nextTick(() => { - router[toValue(mode)]({ ...route, params: { ...route.params, ...Object.fromEntries(_params.entries()) } }) - }) + trigger() + + nextTick(() => { + const { params, query, hash } = route + router[toValue(mode)]({ + params: { + ...params, + ...Object.fromEntries(_params.entries()), + }, + query, + hash, + } as LocationAsRelativeRaw) + }) + }, + } + }) + + watch( + () => route.params[name], + (v) => { + _params.set(name, v) + + _trigger() }, - })) + { flush: 'sync' }, + ) + + return proxy as Ref } diff --git a/packages/router/useRouteQuery/index.test.ts b/packages/router/useRouteQuery/index.test.ts index 429631311d0..c18edda23ad 100644 --- a/packages/router/useRouteQuery/index.test.ts +++ b/packages/router/useRouteQuery/index.test.ts @@ -1,10 +1,10 @@ -import { effectScope, nextTick, ref } from 'vue-demi' -import { describe, expect, it } from 'vitest' +import { effectScope, nextTick, reactive, ref, watch } from 'vue-demi' +import { describe, expect, it, vi } from 'vitest' import type { Ref } from 'vue-demi' import { useRouteQuery } from '.' describe('useRouteQuery', () => { - const getRoute = (query: Record = {}) => ({ + const getRoute = (query: Record = {}) => reactive({ query, fullPath: '', hash: '', @@ -47,6 +47,8 @@ describe('useRouteQuery', () => { const code: Ref = useRouteQuery('code', 'foo', { route, router }) const search: Ref = useRouteQuery('search', null, { route, router }) + expect(code.value).toBe('foo') + code.value = 'bar' expect(code.value).toBe('bar') @@ -139,4 +141,96 @@ describe('useRouteQuery', () => { expect(page.value).toBeNull() expect(lang.value).toBeNull() }) + + it('should change the value when the route changes', () => { + let route = getRoute() + const router = { replace: (r: any) => route = r } as any + + const page: Ref = useRouteQuery('page', null, { route, router }) + + expect(page.value).toBeNull() + + route.query = { page: '2' } + + expect(page.value).toBe('2') + }) + + it ('should differentiate null and undefined', () => { + let route = getRoute({ + page: 1, + }) + const router = { replace: (r: any) => route = r } as any + + const lang: Ref = useRouteQuery('lang', undefined, { route, router }) + + expect(lang.value).toBeUndefined() + + route.query = { ...route.query, lang: null } + + expect(lang.value).toBeNull() + + const code: Ref = useRouteQuery('code', null, { route, router }) + + expect(code.value).toBeNull() + + const page: Ref = useRouteQuery('page', null, { route, router }) + + expect(page.value).toBe(1) + }) + + it('should avoid trigger effects when the value doesn\'t change', async () => { + let route = getRoute() + const router = { replace: (r: any) => route = r } as any + const onUpdate = vi.fn() + + const page = useRouteQuery('page', 1, { transform: Number, route, router }) + + watch(page, onUpdate) + + page.value = 1 + + await nextTick() + + expect(page.value).toBe(1) + expect(route.query.page).toBe(1) + expect(onUpdate).not.toHaveBeenCalled() + }) + + it('should keep current query and hash', async () => { + let route = getRoute() + const router = { replace: (r: any) => route = r } as any + + route.params = { foo: 'bar' } + route.hash = '#hash' + + const id: Ref = useRouteQuery('id', null, { route, router }) + + id.value = '2' + + await nextTick() + + expect(id.value).toBe('2') + expect(route.hash).toBe('#hash') + expect(route.params).toEqual({ foo: 'bar' }) + }) + + it('should allow ref or getter as default value', () => { + let route = getRoute() + const router = { replace: (r: any) => route = r } as any + + const defaultPage = ref(1) + const defaultLang = () => 'pt-BR' + + const page: Ref = useRouteQuery('page', defaultPage, { route, router }) + const lang: Ref = useRouteQuery('lang', defaultLang, { route, router }) + + expect(page.value).toBe(1) + expect(lang.value).toBe('pt-BR') + + page.value = 2 + lang.value = 'en-US' + + expect(page.value).toBe(2) + expect(lang.value).toBe('en-US') + }) }) diff --git a/packages/router/useRouteQuery/index.ts b/packages/router/useRouteQuery/index.ts index 3fe3a3b35f4..7f20a94705c 100644 --- a/packages/router/useRouteQuery/index.ts +++ b/packages/router/useRouteQuery/index.ts @@ -1,7 +1,8 @@ -import type { Ref } from 'vue-demi' -import { customRef, nextTick } from 'vue-demi' +import { customRef, nextTick, watch } from 'vue-demi' import { toValue, tryOnScopeDispose } from '@vueuse/shared' import { useRoute, useRouter } from 'vue-router' +import type { Ref } from 'vue-demi' +import type { MaybeRefOrGetter } from '@vueuse/shared' import type { ReactiveRouteOptionsWithTransform, RouteQueryValueRaw } from '../_types' const _cache = new WeakMap() @@ -15,7 +16,7 @@ export function useRouteQuery< K = T, >( name: string, - defaultValue?: T, + defaultValue?: MaybeRefOrGetter, options?: ReactiveRouteOptionsWithTransform ): Ref @@ -24,7 +25,7 @@ export function useRouteQuery< K = T, >( name: string, - defaultValue?: T, + defaultValue?: MaybeRefOrGetter, options: ReactiveRouteOptionsWithTransform = {}, ): Ref { const { @@ -45,24 +46,49 @@ export function useRouteQuery< _query.set(name, route.query[name]) - return customRef((track, trigger) => ({ - get() { - track() + let _trigger: () => void - const data = _query.get(name) ?? defaultValue - return transform(data as T) - }, - set(v) { - _query.set(name, (v === defaultValue || v === null) ? undefined : v) + const proxy = customRef((track, trigger) => { + _trigger = trigger + + return { + get() { + track() + + const data = _query.get(name) + + return transform(data !== undefined ? data : toValue(defaultValue)) + }, + set(v) { + if (_query.get(name) === v) + return - trigger() + _query.set(name, v) - nextTick(() => { - router[toValue(mode)]({ - ...route, - query: { ...route.query, ...Object.fromEntries(_query.entries()) }, + trigger() + + nextTick(() => { + const { params, query, hash } = route + + router[toValue(mode)]({ + params, + query: { ...query, ...Object.fromEntries(_query.entries()) }, + hash, + }) }) - }) + }, + } + }) + + watch( + () => route.query[name], + (v) => { + _query.set(name, v) + + _trigger() }, - })) + { flush: 'sync' }, + ) + + return proxy as any as Ref }