diff --git a/package.json b/package.json index e2ac2ef..b32022e 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "eslint-plugin-prettier": "^3.3.1", "jest": "^26.6.3", "next": "^11.0.1", - "next-router-mock": "^0.1.4", + "next-router-mock": "^0.6.1", "prettier": "^2.2.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/readme.md b/readme.md index c045b28..9024ecb 100644 --- a/readme.md +++ b/readme.md @@ -4,13 +4,13 @@ React Pagination [![npm package](https://badge.fury.io/js/%40aboutbits%2Freact-pagination.svg)](https://badge.fury.io/js/%40aboutbits%2Freact-pagination) [![license](https://img.shields.io/github/license/aboutbits/react-pagination)](https://github.com/aboutbits/react-pagination/blob/main/license.md) -This package includes pagination hooks for React. The hooks support saving the search and pagination values in local +This package includes pagination hooks for React. The hooks support saving the query and pagination values in local state or in the browser URL. ## Table of content - [Usage](#usage) - - [useSearchAndPagination](#usesearchandpagination) + - [useQueryAndPagination](#usequeryandpagination) - [Supported Implementations](#supported-implementations) - [In Memory Pagination](#in-memory-pagination) - [React-Router based pagination](#react-router-based-pagination) @@ -26,7 +26,7 @@ First, you have to install the package: npm install @aboutbits/react-pagination ``` -Second, you can make use of the `useSearchAndPagination` hook. This package implements 3 versions of this hook: +Second, you can make use of the `useQueryAndPagination` hook. This package implements 3 versions of this hook: - [In Memory](#in-memory-pagination): Use this hook where you don't want to modify browser history. e.g. Dialogs - [React Router](#react-router-based-pagination): Use this hook if you want to keep track of the state in the URL and @@ -34,9 +34,9 @@ Second, you can make use of the `useSearchAndPagination` hook. This package impl - [NextJS Router](#nextjs-router-based-pagination): Use this hook if you want to keep track of the state in the URL and your project is using NextJS. -### useSearchAndPagination +### useQueryAndPagination -This hook supports the combination of a search value and pagination and manages the state of the search value, and the +This hook supports the combination of query parameters and pagination and manages the state of the query parameter values and the pagination values. #### The hook supports following configuration parameter object: @@ -45,31 +45,32 @@ pagination values. |---|---|---|---| |indexType|IndexType|IndexType.ZERO_BASED|It defines whether the pagination is zero or one based.| |pageSize|number|15|Page size of the pagination.| +|defaultQueryParameters/Record|{}|It defines the default value for each query parameter. This is used to remove a query parameter from the URL and also to clear the query. #### The hook returns the following object: |value|type|description| |---|---|---| -|search|string|value of your search parameter| +|queryParameters|object|values of your query parameters| |page|number|value of the current page| |size|number|max elements in a single page| -|actions|object|object with 3 functions: search, setPage, clear| +|actions|object|object with 3 functions: updateQuery, setPage, clear| #### Example usage with NextJS ```tsx -import { useSearchAndPagination } from '@aboutbits/react-pagination/dist/nextRouterPagination' +import { useQueryAndPagination } from '@aboutbits/react-pagination/dist/nextRouterPagination' const users = [ 'Alex', 'Simon', 'Natan', 'Nadia', 'Moritz', 'Marie' ] function UserList() { - const { page, size, search, actions } = useSearchAndPagination({pageSize: 2}) + const { page, size, queryParameters, actions } = useQueryAndPagination({pageSize: 2}) return (
- actions.search(value)}/> + actions.updateQuery({search: value})}/> @@ -99,7 +100,7 @@ This package includes 3 different implementations of the above hook. Use this pagination hook if you want to keep track of the pagination in memory. This is very handy for dialogs. ```tsx -import { useSearchAndPagination } from '@aboutbits/react-pagination/dist/inMemoryPagination' +import { useQueryAndPagination } from '@aboutbits/react-pagination/dist/inMemoryPagination' ``` ### React-Router based pagination @@ -107,7 +108,7 @@ import { useSearchAndPagination } from '@aboutbits/react-pagination/dist/inMemor These are specific hooks for applications that use [React Router](https://reactrouter.com/) for routing. ```tsx -import { useSearchAndPagination } from '@aboutbits/react-pagination/dist/reactRouterPagination' +import { useQueryAndPagination } from '@aboutbits/react-pagination/dist/reactRouterPagination' ``` ### NextJS Router based pagination @@ -116,7 +117,7 @@ These are specific hooks for applications that use [NextJS Router](https://nextj for routing. ```tsx -import { useSearchAndPagination } from '@aboutbits/react-pagination/dist/nextRouterPagination' +import { useQueryAndPagination } from '@aboutbits/react-pagination/dist/nextRouterPagination' ``` ## Build & Publish diff --git a/src/__test__/inMemoryPagination.test.ts b/src/__test__/inMemoryPagination.test.ts index 6c07d08..992dba2 100644 --- a/src/__test__/inMemoryPagination.test.ts +++ b/src/__test__/inMemoryPagination.test.ts @@ -1,17 +1,16 @@ import { act, renderHook } from '@testing-library/react-hooks' -import { useSearchAndPagination } from '../inMemoryPagination' +import { useQueryAndPagination } from '../inMemoryPagination' import { IndexType } from '../types' test('should initialize pagination', () => { - const { result } = renderHook(() => useSearchAndPagination()) + const { result } = renderHook(() => useQueryAndPagination()) expect(result.current.page).toBe(0) - expect(result.current.search).toBe('') expect(result.current.size).toBe(15) }) test('should change page', () => { - const { result } = renderHook(() => useSearchAndPagination()) + const { result } = renderHook(() => useQueryAndPagination()) act(() => { result.current.actions.setPage(2) @@ -21,17 +20,21 @@ test('should change page', () => { }) test('should change search', () => { - const { result } = renderHook(() => useSearchAndPagination()) + const { result } = renderHook(() => + useQueryAndPagination({ defaultQueryParameters: { search: '' } }) + ) act(() => { - result.current.actions.search('Max') + result.current.actions.updateQuery({ search: 'Max' }) }) - expect(result.current.search).toBe('Max') + expect(result.current.queryParameters.search).toBe('Max') }) test('on search change -> page should be reset', () => { - const { result } = renderHook(() => useSearchAndPagination()) + const { result } = renderHook(() => + useQueryAndPagination({ defaultQueryParameters: { search: '' } }) + ) act(() => { result.current.actions.setPage(2) @@ -40,40 +43,73 @@ test('on search change -> page should be reset', () => { expect(result.current.page).toBe(2) act(() => { - result.current.actions.search('Max') + result.current.actions.updateQuery({ search: 'Max' }) }) - expect(result.current.search).toBe('Max') + expect(result.current.queryParameters.search).toBe('Max') expect(result.current.page).toBe(0) }) test('clear pagination should reset search and page', () => { - const { result } = renderHook(() => useSearchAndPagination()) + const { result } = renderHook(() => + useQueryAndPagination({ defaultQueryParameters: { search: '' } }) + ) act(() => { - result.current.actions.search('Max') + result.current.actions.updateQuery({ search: 'Max' }) }) act(() => { result.current.actions.setPage(2) }) - expect(result.current.search).toBe('Max') + expect(result.current.queryParameters.search).toBe('Max') expect(result.current.page).toBe(2) act(() => { result.current.actions.clear() }) - expect(result.current.search).toBe('') + expect(result.current.queryParameters.search).toBe('') expect(result.current.page).toBe(0) }) test('change default parameters', () => { const { result } = renderHook(() => - useSearchAndPagination({ indexType: IndexType.ONE_BASED, pageSize: 10 }) + useQueryAndPagination({ indexType: IndexType.ONE_BASED, pageSize: 10 }) ) expect(result.current.page).toBe(1) expect(result.current.size).toBe(10) }) + +test('query multiple different properties, should keep them all', () => { + const { result } = renderHook(() => + useQueryAndPagination({ + defaultQueryParameters: { search: '', department: '' }, + }) + ) + + act(() => { + result.current.actions.updateQuery({ search: 'Max' }) + result.current.actions.updateQuery({ department: 'IT' }) + }) + + expect(result.current.queryParameters.search).toBe('Max') + expect(result.current.queryParameters.department).toBe('IT') +}) + +test('query a property that is not configured, should do nothing', () => { + const { result } = renderHook(() => + useQueryAndPagination({ + defaultQueryParameters: { search: '' }, + }) + ) + + act(() => { + result.current.actions.updateQuery({ department: 'IT' }) + }) + + expect(result.current.queryParameters.search).toBe('') + expect(result.current.queryParameters.department).toBeUndefined() +}) diff --git a/src/__test__/nextRouterPagination.test.tsx b/src/__test__/nextRouterPagination.test.tsx index ff82cd9..b9a9624 100644 --- a/src/__test__/nextRouterPagination.test.tsx +++ b/src/__test__/nextRouterPagination.test.tsx @@ -3,21 +3,24 @@ import React from 'react' import { act, renderHook } from '@testing-library/react-hooks' import router from 'next/router' -import { useSearchAndPagination } from '../nextRouterPagination' +import { useQueryAndPagination } from '../nextRouterPagination' import { IndexType } from '../types' jest.mock('next/router', () => require('next-router-mock')) +beforeEach(() => { + router.query = {} +}) + test('should initialize pagination', () => { - const { result } = renderHook(() => useSearchAndPagination()) + const { result } = renderHook(() => useQueryAndPagination()) expect(result.current.page).toBe(0) - expect(result.current.search).toBe('') expect(result.current.size).toBe(15) }) test('should change page', () => { - const { result } = renderHook(() => useSearchAndPagination()) + const { result } = renderHook(() => useQueryAndPagination()) act(() => { result.current.actions.setPage(2) @@ -28,18 +31,57 @@ test('should change page', () => { }) test('should change search', () => { - const { result } = renderHook(() => useSearchAndPagination()) + const { result } = renderHook(() => + useQueryAndPagination({ defaultQueryParameters: { search: '' } }) + ) + + act(() => { + result.current.actions.updateQuery({ search: 'Max' }) + }) + + expect(result.current.queryParameters.search).toBe('Max') + expect(router.query.search).toBe('Max') +}) + +test('clear pagination should reset search and page', () => { + const { result } = renderHook(() => + useQueryAndPagination({ defaultQueryParameters: { search: '' } }) + ) + + act(() => { + result.current.actions.updateQuery({ search: 'Max' }) + }) act(() => { - result.current.actions.search('Max') + result.current.actions.setPage(2) }) - expect(result.current.search).toBe('Max') expect(router.query.search).toBe('Max') + expect(router.query.page).toBe('2') + + act(() => { + result.current.actions.clear() + }) + + expect(result.current.queryParameters.search).toBe('') + expect(result.current.page).toBe(0) + expect(router.query.search).toBeUndefined() + expect(router.query.page).toBeUndefined() +}) + +test('change default parameters', () => { + const { result } = renderHook(() => + useQueryAndPagination({ indexType: IndexType.ONE_BASED, pageSize: 10 }) + ) + + expect(result.current.page).toBe(1) + expect(result.current.size).toBe(10) }) test('on search change -> page should be reset', () => { - const { result } = renderHook(() => useSearchAndPagination()) + const { result } = renderHook(() => + useQueryAndPagination({ defaultQueryParameters: { search: '' } }) + ) act(() => { result.current.actions.setPage(2) @@ -49,44 +91,80 @@ test('on search change -> page should be reset', () => { expect(router.query.page).toBe('2') act(() => { - result.current.actions.search('Max') + result.current.actions.updateQuery({ search: 'Max' }) }) - expect(result.current.search).toBe('Max') + expect(result.current.queryParameters.search).toBe('Max') expect(result.current.page).toBe(0) expect(router.query.search).toBe('Max') expect(router.query.page).toBeUndefined() }) -test('clear pagination should reset search and page', () => { - const { result } = renderHook(() => useSearchAndPagination()) +test('query multiple different properties, should keep them all', () => { + const { result } = renderHook(() => + useQueryAndPagination({ + defaultQueryParameters: { search: '', department: '' }, + }) + ) act(() => { - result.current.actions.search('Max') + result.current.actions.updateQuery({ search: 'Max' }) }) act(() => { - result.current.actions.setPage(2) + result.current.actions.updateQuery({ department: 'IT' }) }) - expect(router.query.search).toBe('Max') - expect(router.query.page).toBe('2') + expect(result.current.queryParameters.search).toBe('Max') + expect(result.current.queryParameters.department).toBe('IT') +}) + +test('query a property that is not configured, should do nothing', () => { + const { result } = renderHook(() => + useQueryAndPagination({ + defaultQueryParameters: { search: '' }, + }) + ) act(() => { - result.current.actions.clear() + result.current.actions.updateQuery({ department: 'IT' }) }) - expect(result.current.search).toBe('') - expect(result.current.page).toBe(0) - expect(router.query.search).toBeUndefined() - expect(router.query.page).toBeUndefined() + expect(result.current.queryParameters.search).toBe('') + expect(result.current.queryParameters.department).toBeUndefined() }) -test('change default parameters', () => { +test('properties in the URL, that are not part of the configuration should be left untouched', () => { + router.query = { greeting: 'hello' } + const { result } = renderHook(() => - useSearchAndPagination({ indexType: IndexType.ONE_BASED, pageSize: 10 }) + useQueryAndPagination({ + defaultQueryParameters: { search: '' }, + }) ) - expect(result.current.page).toBe(1) - expect(result.current.size).toBe(10) + act(() => { + result.current.actions.updateQuery({ search: 'Max' }) + }) + + expect(result.current.queryParameters.search).toBe('Max') + expect(result.current.queryParameters.greeting).toBeUndefined() + expect(router.query.greeting).toBe('hello') +}) + +test('query property with default value, should remove it from url', () => { + router.query = { search: 'Max' } + + const { result } = renderHook(() => + useQueryAndPagination({ + defaultQueryParameters: { search: '' }, + }) + ) + + act(() => { + result.current.actions.updateQuery({ search: '' }) + }) + + expect(result.current.queryParameters.search).toBe('') + expect(router.query.search).toBeUndefined() }) diff --git a/src/__test__/reactRouterPagination.test.tsx b/src/__test__/reactRouterPagination.test.tsx index 9be8fff..c296785 100644 --- a/src/__test__/reactRouterPagination.test.tsx +++ b/src/__test__/reactRouterPagination.test.tsx @@ -1,21 +1,23 @@ import React from 'react' import { act, renderHook } from '@testing-library/react-hooks' import { BrowserRouter as Router } from 'react-router-dom' -import { useSearchAndPagination } from '../reactRouterPagination' +import { useQueryAndPagination } from '../reactRouterPagination' import { IndexType } from '../types' - const wrapper: React.FC = ({ children }) => {children} +beforeEach(() => { + window.history.pushState({}, '', '/') +}) + test('should initialize pagination', () => { - const { result } = renderHook(() => useSearchAndPagination(), { wrapper }) + const { result } = renderHook(() => useQueryAndPagination(), { wrapper }) expect(result.current.page).toBe(0) - expect(result.current.search).toBe('') expect(result.current.size).toBe(15) }) test('should change page', () => { - const { result } = renderHook(() => useSearchAndPagination(), { wrapper }) + const { result } = renderHook(() => useQueryAndPagination(), { wrapper }) act(() => { result.current.actions.setPage(2) @@ -26,64 +28,126 @@ test('should change page', () => { }) test('should change search', () => { - const { result } = renderHook(() => useSearchAndPagination(), { wrapper }) + const { result } = renderHook( + () => useQueryAndPagination({ defaultQueryParameters: { search: '' } }), + { wrapper } + ) act(() => { - result.current.actions.search('Max') + result.current.actions.updateQuery({ search: 'Max' }) }) - expect(result.current.search).toBe('Max') + expect(result.current.queryParameters.search).toBe('Max') expect(window.location.search).toBe('?search=Max') }) -test('on search change -> page should be reset', () => { - const { result } = renderHook(() => useSearchAndPagination(), { wrapper }) +test('clear pagination should reset search and page', () => { + const { result } = renderHook( + () => useQueryAndPagination({ defaultQueryParameters: { search: '' } }), + { wrapper } + ) + + act(() => { + result.current.actions.updateQuery({ search: 'Max' }) + }) act(() => { result.current.actions.setPage(2) }) + expect(result.current.queryParameters.search).toBe('Max') expect(result.current.page).toBe(2) act(() => { - result.current.actions.search('Max') + result.current.actions.clear() }) - expect(result.current.search).toBe('Max') + expect(result.current.queryParameters.search).toBe('') expect(result.current.page).toBe(0) - expect(window.location.search).toBe('?search=Max') + expect(window.location.search).toBe('') }) -test('clear pagination should reset search and page', () => { - const { result } = renderHook(() => useSearchAndPagination(), { wrapper }) +test('change default parameters', () => { + const { result } = renderHook( + () => + useQueryAndPagination({ indexType: IndexType.ONE_BASED, pageSize: 10 }), + { wrapper } + ) + + expect(result.current.page).toBe(1) + expect(result.current.size).toBe(10) +}) + +test('query multiple different properties, should keep them all', () => { + const { result } = renderHook( + () => + useQueryAndPagination({ + defaultQueryParameters: { search: '', department: '' }, + }), + { wrapper } + ) act(() => { - result.current.actions.search('Max') + result.current.actions.updateQuery({ search: 'Max' }) + result.current.actions.updateQuery({ department: 'IT' }) }) + expect(result.current.queryParameters.search).toBe('Max') + expect(result.current.queryParameters.department).toBe('IT') +}) + +test('query a property that is not configured, should do nothing', () => { + const { result } = renderHook( + () => + useQueryAndPagination({ + defaultQueryParameters: { search: '' }, + }), + { wrapper } + ) + act(() => { - result.current.actions.setPage(2) + result.current.actions.updateQuery({ department: 'IT' }) }) - expect(result.current.search).toBe('Max') - expect(result.current.page).toBe(2) + expect(result.current.queryParameters.search).toBe('') + expect(result.current.queryParameters.department).toBeUndefined() +}) + +test('properties in the URL, that are not part of the configuration should be left untouched', () => { + window.history.pushState({}, '', '/?greeting=hello') + + const { result } = renderHook( + () => + useQueryAndPagination({ + defaultQueryParameters: { search: '' }, + }), + { wrapper } + ) act(() => { - result.current.actions.clear() + result.current.actions.updateQuery({ search: 'Max' }) }) - expect(result.current.search).toBe('') - expect(result.current.page).toBe(0) - expect(window.location.search).toBe('') + expect(result.current.queryParameters.search).toBe('Max') + expect(result.current.queryParameters.greeting).toBeUndefined() + expect(window.location.search).toBe('?greeting=hello&search=Max') }) -test('change default parameters', () => { +test('query property with default value, should remove it from url', () => { + window.history.pushState({}, '', '/?search=Anton') + const { result } = renderHook( () => - useSearchAndPagination({ indexType: IndexType.ONE_BASED, pageSize: 10 }), + useQueryAndPagination({ + defaultQueryParameters: { search: '' }, + }), { wrapper } ) - expect(result.current.page).toBe(1) - expect(result.current.size).toBe(10) + act(() => { + result.current.actions.updateQuery({ search: '' }) + }) + + expect(result.current.queryParameters.search).toBe('') + expect(window.location.search).toBe('') }) diff --git a/src/inMemoryPagination.tsx b/src/inMemoryPagination.tsx index 5a9f1df..9c26f77 100644 --- a/src/inMemoryPagination.tsx +++ b/src/inMemoryPagination.tsx @@ -1,28 +1,42 @@ import { useCallback, useMemo, useState } from 'react' -import { IndexType, IUseSearchAndPagination } from './types' +import { IndexType, IUseQueryAndPagination, QueryParameters } from './types' -export const useSearchAndPagination: IUseSearchAndPagination = function ( - config -) { +export const useQueryAndPagination: IUseQueryAndPagination = function (config) { const { indexType = IndexType.ZERO_BASED, pageSize = 15 } = config || {} const firstPage = indexType === IndexType.ZERO_BASED ? 0 : 1 const initialState = useMemo( () => ({ page: firstPage, size: pageSize, - searchQuery: '', + queryParameters: config?.defaultQueryParameters || {}, }), [] ) const [state, setState] = useState(initialState) - const search = useCallback((query: string) => { + const updateQuery = useCallback((queryParameters: QueryParameters) => { setState((currentState) => { + const updatedQueryParameters = { + ...currentState.queryParameters, + ...queryParameters, + } + + for (const parameter in queryParameters) { + if ( + !!config?.defaultQueryParameters && + (config.defaultQueryParameters[parameter] === undefined || + config.defaultQueryParameters[parameter] === + queryParameters[parameter]) + ) { + delete updatedQueryParameters[parameter] + } + } + return { ...currentState, page: 0, - searchQuery: query, + queryParameters: updatedQueryParameters, } }) }, []) @@ -43,9 +57,9 @@ export const useSearchAndPagination: IUseSearchAndPagination = function ( }, []) return { - search: state.searchQuery, + queryParameters: state.queryParameters, actions: { - search, + updateQuery, clear, setPage, }, diff --git a/src/nextRouterPagination.tsx b/src/nextRouterPagination.tsx index 67e8a71..744826a 100644 --- a/src/nextRouterPagination.tsx +++ b/src/nextRouterPagination.tsx @@ -1,7 +1,8 @@ +import { ParsedUrlQuery } from 'querystring' import { useRouter } from 'next/router' import { useCallback } from 'react' -import { IndexType, IUseSearchAndPagination } from './types' +import { IndexType, IUseQueryAndPagination, QueryParameters } from './types' import { convert } from './utils' function getSingleParameterValue( @@ -10,9 +11,29 @@ function getSingleParameterValue( return Array.isArray(parameter) ? parameter[0] : parameter } -export const useSearchAndPagination: IUseSearchAndPagination = function ( - config +function extractCurrentQueryParameters( + query: ParsedUrlQuery, + defaultQueryParameters?: QueryParameters ) { + if (!defaultQueryParameters) { + return {} + } + + const result: QueryParameters = { ...defaultQueryParameters } + + for (const parameter in defaultQueryParameters) { + if ( + query[parameter] && + getSingleParameterValue(query[parameter]) !== undefined + ) { + result[parameter] = getSingleParameterValue(query[parameter]) as string + } + } + + return result +} + +export const useQueryAndPagination: IUseQueryAndPagination = function (config) { const { indexType = IndexType.ZERO_BASED, pageSize = 15 } = config || {} const router = useRouter() @@ -30,11 +51,22 @@ export const useSearchAndPagination: IUseSearchAndPagination = function ( [router] ) - const search = useCallback( - (query: string) => { - const params: { page?: string; size?: string; search?: string } = { + const updateQuery = useCallback( + (queryParameters: QueryParameters) => { + const params = { ...router.query, - search: query === '' ? undefined : query, + } + + for (const parameter in queryParameters) { + if ( + config?.defaultQueryParameters && + config.defaultQueryParameters[parameter] === + queryParameters[parameter] + ) { + delete params[parameter] + } else { + params[parameter] = queryParameters[parameter].toString() + } } delete params['page'] @@ -52,24 +84,30 @@ export const useSearchAndPagination: IUseSearchAndPagination = function ( ...router.query, } - delete params['search'] delete params['page'] delete params['size'] + for (const parameter in config?.defaultQueryParameters) { + delete params[parameter] + } + router.push({ query: params, }) }, [router]) return { - search: getSingleParameterValue(router.query.search) || '', + queryParameters: extractCurrentQueryParameters( + router.query, + config?.defaultQueryParameters + ), page: convert( getSingleParameterValue(router.query.page) || null, indexType === IndexType.ZERO_BASED ? 0 : 1 ), size: convert(getSingleParameterValue(router.query.size) || null, pageSize), actions: { - search, + updateQuery, clear, setPage, }, diff --git a/src/reactRouterPagination.tsx b/src/reactRouterPagination.tsx index 4db2572..6b43520 100644 --- a/src/reactRouterPagination.tsx +++ b/src/reactRouterPagination.tsx @@ -1,12 +1,29 @@ import { useCallback, useMemo } from 'react' import { useHistory, useLocation, useRouteMatch } from 'react-router-dom' -import { IndexType, IUseSearchAndPagination } from './types' +import { IndexType, IUseQueryAndPagination, QueryParameters } from './types' import { convert } from './utils' -export const useSearchAndPagination: IUseSearchAndPagination = function ( - config +function extractCurrentQueryParameters( + query: URLSearchParams, + defaultQueryParameters?: QueryParameters ) { + if (!defaultQueryParameters) { + return {} + } + + const result: QueryParameters = { ...defaultQueryParameters } + + for (const parameter in defaultQueryParameters) { + if (query.get(parameter)) { + result[parameter] = query.get(parameter) as string + } + } + + return result +} + +export const useQueryAndPagination: IUseQueryAndPagination = function (config) { const { indexType = IndexType.ZERO_BASED, pageSize = 15 } = config || {} const routerHistory = useHistory() const { url: routerUrl } = useRouteMatch() @@ -14,13 +31,20 @@ export const useSearchAndPagination: IUseSearchAndPagination = function ( const params = useMemo(() => new URLSearchParams(routeQuery), [routeQuery]) - const search = useCallback( - (query: string) => { - if (query === '') { - params.delete('search') - } else { - params.set('search', query) + const updateQuery = useCallback( + (queryParameters: QueryParameters) => { + for (const parameter in queryParameters) { + if ( + config?.defaultQueryParameters && + config.defaultQueryParameters[parameter] === + queryParameters[parameter] + ) { + params.delete(parameter) + } else { + params.set(parameter, queryParameters[parameter].toString()) + } } + params.delete('page') params.delete('size') @@ -33,7 +57,10 @@ export const useSearchAndPagination: IUseSearchAndPagination = function ( ) const clear = useCallback(() => { - params.delete('search') + for (const parameter in config?.defaultQueryParameters) { + params.delete(parameter) + } + params.delete('page') params.delete('size') @@ -55,14 +82,17 @@ export const useSearchAndPagination: IUseSearchAndPagination = function ( ) return { - search: params.get('search') || '', + queryParameters: extractCurrentQueryParameters( + params, + config?.defaultQueryParameters + ), page: convert( params.get('page'), indexType === IndexType.ZERO_BASED ? 0 : 1 ), size: convert(params.get('size'), pageSize), actions: { - search, + updateQuery, clear, setPage, }, diff --git a/src/types.ts b/src/types.ts index 4a0efcf..109ec7d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,24 +3,27 @@ export enum IndexType { ONE_BASED = 1, } +export type QueryParameters = Record + export type Config = { indexType?: IndexType pageSize?: number + defaultQueryParameters?: QueryParameters } export type Actions = { - search: (query: string) => void + updateQuery: (query: QueryParameters) => void clear: () => void setPage: (page: number) => void } -export type UseSearchAndPagination = { - search: string +export type UseQueryAndPagination = { + queryParameters: QueryParameters page: number size: number actions: Actions } -export interface IUseSearchAndPagination { - (config?: Config): UseSearchAndPagination +export interface IUseQueryAndPagination { + (config?: Config): UseQueryAndPagination } diff --git a/tsconfig.esnext.json b/tsconfig.esnext.json index b1898a6..074e202 100644 --- a/tsconfig.esnext.json +++ b/tsconfig.esnext.json @@ -9,7 +9,8 @@ "outDir": "./dist/esm", "declaration": true, "strict": true, + "skipLibCheck": true }, "include": ["src"], "exclude": ["node_modules", "**/__test__/*", "**/__tests__/*"] -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index 4eba278..fc22be6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "outDir": "./dist", "declaration": true, "strict": true, - "sourceMap": true + "sourceMap": true, + "skipLibCheck": true }, "include": ["src"], "exclude": ["node_modules", "**/__test__/*", "**/__tests__/*"]