Skip to content

Commit

Permalink
feat: allow to use push instead of replace
Browse files Browse the repository at this point in the history
  • Loading branch information
Djaler committed Jul 11, 2023
1 parent 88d78f3 commit ebe97a6
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 11 deletions.
11 changes: 11 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ A several transformers provided by the library out of the box:
foo.value = Foo.BAZ; // Results in 'foo=BAZ' in the query
```

### Navigation mode

By default, all changes are applied using `router.replace` to not create additional entries in history.
But you may configure this behaviour using `mode` option.
```ts
const foo = useRouteQuery('foo', '', { mode: 'push' });
const bar = useRouteQuery('bar', '', transformer, { mode: 'push' });
```

### Update awaiting

Query update is asynchronous by its nature, but sometimes you may want to wait for query to be updated before doing something. For example, you may want to fetch some data from the server based on the query. To do that, you can use `waitForQueryUpdate`:

```ts
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/queue-query-update.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AsyncQueue, createAsyncQueue } from 'async-queue-chain';
import type { Router } from 'vue-router';

import { RouteQuery } from './types';
import { NavigationMode, RouteQuery } from './types';
import { removeEmptyValues } from './utils';

let queryReplaceQueue: AsyncQueue<RouteQuery> | undefined;
Expand All @@ -11,6 +11,7 @@ export function queueQueryUpdate(
currentQuery: RouteQuery,
key: string,
newValue: string | null | undefined,
mode: NavigationMode,
) {
if (!queryReplaceQueue) {
queryReplaceQueue = createAsyncQueue();
Expand All @@ -22,7 +23,7 @@ export function queueQueryUpdate(
[key]: newValue,
});

return updateQuery(router, previousQuery, newQuery);
return updateQuery(router, previousQuery, newQuery, mode);
});

void queryReplaceQueue.run(currentQuery);
Expand All @@ -34,9 +35,9 @@ export async function waitForQueryUpdate() {
}
}

async function updateQuery(router: Router, previousQuery: RouteQuery, query: RouteQuery) {
async function updateQuery(router: Router, previousQuery: RouteQuery, query: RouteQuery, mode: NavigationMode) {
try {
await router.replace({
await router[mode]({
query,
});
return query;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export interface RouteQuery {
[key: string]: string | Array<string | null> | null | undefined;
}

export type NavigationMode = 'push' | 'replace';
48 changes: 42 additions & 6 deletions packages/core/src/useRouteQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,37 @@ import { computed, reactive, Ref, watch } from 'vue-demi';
import { useRoute, useRouter } from './helpers';
import { queueQueryUpdate } from './queue-query-update';
import { RouteQueryTransformer } from './transformers';
import { RouteQuery } from './types';
import { NavigationMode, RouteQuery } from './types';
import { isObject } from './utils';

export function useRouteQuery(key: string, defaultValue: string): Ref<string>;
export function useRouteQuery(key: string, defaultValue: string | null): Ref<string | null>;
export function useRouteQuery<T>(key: string, defaultValue: T, transformer: RouteQueryTransformer<T>): Ref<T>;
export function useRouteQuery<T>(key: string, defaultValue: T, transformer?: RouteQueryTransformer<T>): Ref<T> {
interface Options {
mode?: NavigationMode;
}

type TransformerOrOptions<T> = RouteQueryTransformer<T> | Options;

export function useRouteQuery(key: string, defaultValue: string, options?: Options): Ref<string>;
export function useRouteQuery(key: string, defaultValue: string | null, options?: Options): Ref<string | null>;
export function useRouteQuery<T>(
key: string,
defaultValue: T,
transformer: RouteQueryTransformer<T>,
options?: Options,
): Ref<T>;
export function useRouteQuery<T>(
key: string,
defaultValue: T,
transformerOrOptions?: TransformerOrOptions<T>,
options?: Options,
): Ref<T> {
const route = useRoute();
const router = useRouter();

const [transformer, finalOptions] = extractTransformerAndOptions(transformerOrOptions, options);
const navigationMode = finalOptions?.mode ?? 'replace';

function updateQueryParam(newValue: string | null | undefined) {
queueQueryUpdate(router, route.value.query, key, newValue);
queueQueryUpdate(router, route.value.query, key, newValue, navigationMode);
}

function get(): T {
Expand Down Expand Up @@ -82,3 +101,20 @@ function getQueryValue(query: RouteQuery, key: string): string | null | undefine
}
return value;
}

function extractTransformerAndOptions<T>(
transformerOrOptions?: TransformerOrOptions<T>,
options?: Options,
): [RouteQueryTransformer<T> | undefined, Options | undefined] {
return isTransformer(transformerOrOptions)
? [transformerOrOptions, options]
: [undefined, transformerOrOptions ?? options];
}

function isTransformer<T>(
transformerOrOptions?: TransformerOrOptions<T>,
): transformerOrOptions is RouteQueryTransformer<T> {
return !!transformerOrOptions
&& 'fromQuery' in transformerOrOptions
&& 'toQuery' in transformerOrOptions;
}
44 changes: 43 additions & 1 deletion packages/tests-base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ interface Params {
replaceQuery: (query: RouteQuery) => Promise<void>;
mountComposition: <R>(callback: () => R) => MountResult<R>;
getCurrentQuery: () => RouteQuery;
waitForRouteChange: () => Promise<void>;
back: () => void;
it: TestAPI;
}

export function testUseRouteQuery(
{ replaceQuery, mountComposition, getCurrentQuery, it }: Params,
{
replaceQuery, mountComposition, getCurrentQuery, it, back, waitForRouteChange,
}: Params,
) {
it('should return string param if present', async () => {
await replaceQuery({
Expand Down Expand Up @@ -79,6 +83,21 @@ export function testUseRouteQuery(
expect(getCurrentQuery().foo).toBe('bar');
});

it('should update query using push when value set', async () => {
const { result } = mountComposition(() => useRouteQuery('foo', null, {
mode: 'push',
}));

result.value = 'bar';
result.value = 'baz';
await flushPromises();

expect(getCurrentQuery().foo).toBe('baz');
back();
await waitForRouteChange();
expect(getCurrentQuery().foo).toBe('bar');
});

it('should update query when value set with transformer', async () => {
const transformer: RouteQueryTransformer<string> = {
fromQuery(value) {
Expand All @@ -96,6 +115,29 @@ export function testUseRouteQuery(
expect(getCurrentQuery().foo).toBe('bar');
});

it('should update query using push when value set with transformer', async () => {
const transformer: RouteQueryTransformer<string> = {
fromQuery(value) {
return value.toUpperCase();
},
toQuery(value) {
return value?.toLowerCase();
},
};
const { result } = mountComposition(() => useRouteQuery('foo', null, transformer, {
mode: 'push',
}));

result.value = 'BAR';
result.value = 'BAZ';
await flushPromises();

expect(getCurrentQuery().foo).toBe('baz');
back();
await waitForRouteChange();
expect(getCurrentQuery().foo).toBe('bar');
});

it('should update query when value cleared', async () => {
await replaceQuery({
foo: 'bar',
Expand Down
7 changes: 7 additions & 0 deletions packages/tests-vue2.7/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ testUseRouteQuery({
await router.replace({ query });
},
getCurrentQuery: () => router.currentRoute.query,
back: () => router.back(),
waitForRouteChange: () => new Promise((resolve) => {
const unwatch = router.afterEach(() => {
resolve();
unwatch();
});
}),
mountComposition: callback => mountComposition(callback, {
router,
}),
Expand Down
7 changes: 7 additions & 0 deletions packages/tests-vue2/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ testUseRouteQuery({
await router.replace({ query });
},
getCurrentQuery: () => router.currentRoute.query,
back: () => router.back(),
waitForRouteChange: () => new Promise((resolve) => {
const unwatch = router.afterEach(() => {
resolve();
unwatch();
});
}),
mountComposition: callback => mountComposition(callback, {
router,
}),
Expand Down
7 changes: 7 additions & 0 deletions packages/tests-vue3/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ testUseRouteQuery({
await router.replace({ query });
},
getCurrentQuery: () => router.currentRoute.value.query,
back: () => router.back(),
waitForRouteChange: () => new Promise((resolve) => {
const unwatch = router.afterEach(() => {
resolve();
unwatch();
});
}),
mountComposition: callback => mountComposition(callback, {
global: {
plugins: [router],
Expand Down

0 comments on commit ebe97a6

Please sign in to comment.