Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow to use push instead of replace #60

Merged
merged 1 commit into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>,
optionsParam?: Options,
): Ref<T> {
const route = useRoute();
const router = useRouter();

const [transformer, options] = extractTransformerAndOptions(transformerOrOptions, optionsParam);
const navigationMode = options?.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