From b73d1826c2dc96e5d277279d6cfd8dc92e152838 Mon Sep 17 00:00:00 2001 From: devgioele Date: Sun, 16 Jul 2023 16:38:19 +0200 Subject: [PATCH 01/26] rewrite engine --- src/__test__/inMemoryPagination.test.ts | 129 -------------- src/__test__/nextRouterPagination.test.tsx | 185 -------------------- src/__test__/reactRouterPagination.test.tsx | 177 ------------------- src/inMemoryPagination.tsx | 75 -------- src/nextRouterPagination.tsx | 113 ------------ src/reactRouterPagination.tsx | 101 ----------- src/types.ts | 29 --- src/useInMemoryRouterQuery.ts | 14 ++ src/useNextRouterQuery.ts | 30 ++++ src/useQuery.ts | 58 ++++++ src/useReactRouterQuery.ts | 44 +++++ src/utils.ts | 11 -- 12 files changed, 146 insertions(+), 820 deletions(-) delete mode 100644 src/__test__/inMemoryPagination.test.ts delete mode 100644 src/__test__/nextRouterPagination.test.tsx delete mode 100644 src/__test__/reactRouterPagination.test.tsx delete mode 100644 src/inMemoryPagination.tsx delete mode 100644 src/nextRouterPagination.tsx delete mode 100644 src/reactRouterPagination.tsx delete mode 100644 src/types.ts create mode 100644 src/useInMemoryRouterQuery.ts create mode 100644 src/useNextRouterQuery.ts create mode 100644 src/useQuery.ts create mode 100644 src/useReactRouterQuery.ts delete mode 100644 src/utils.ts diff --git a/src/__test__/inMemoryPagination.test.ts b/src/__test__/inMemoryPagination.test.ts deleted file mode 100644 index 1c9955e..0000000 --- a/src/__test__/inMemoryPagination.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { act, renderHook } from '@testing-library/react' -import { useQueryAndPagination } from '../inMemoryPagination' -import { IndexType } from '../types' - -test('should initialize pagination', () => { - const { result } = renderHook(() => useQueryAndPagination()) - - expect(result.current.page).toBe(0) - expect(result.current.size).toBe(15) -}) - -test('should change page', () => { - const { result } = renderHook(() => useQueryAndPagination()) - - act(() => { - result.current.actions.setPage(2) - }) - - expect(result.current.page).toBe(2) -}) - -test('should change search', () => { - const { result } = renderHook(() => - useQueryAndPagination({ defaultQueryParameters: { search: '' } }) - ) - - act(() => { - result.current.actions.updateQuery({ search: 'Max' }) - }) - - expect(result.current.queryParameters.search).toBe('Max') -}) - -test('on search change -> page should be reset', () => { - const { result } = renderHook(() => - useQueryAndPagination({ defaultQueryParameters: { search: '' } }) - ) - - act(() => { - result.current.actions.setPage(2) - }) - - expect(result.current.page).toBe(2) - - act(() => { - result.current.actions.updateQuery({ search: '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(() => - useQueryAndPagination({ defaultQueryParameters: { search: '' } }) - ) - - 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.clear() - }) - - expect(result.current.queryParameters.search).toBe('') - expect(result.current.page).toBe(0) -}) - -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('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() -}) - -test('query property with empty value and different default value', () => { - const { result } = renderHook(() => - useQueryAndPagination({ - defaultQueryParameters: { search: 'Default search' }, - }) - ) - - act(() => { - result.current.actions.updateQuery({ search: '' }) - }) - - expect(result.current.queryParameters.search).toBe('') -}) diff --git a/src/__test__/nextRouterPagination.test.tsx b/src/__test__/nextRouterPagination.test.tsx deleted file mode 100644 index 696e581..0000000 --- a/src/__test__/nextRouterPagination.test.tsx +++ /dev/null @@ -1,185 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import React from 'react' -import { act, renderHook } from '@testing-library/react' -import router from 'next/router' - -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(() => useQueryAndPagination()) - - expect(result.current.page).toBe(0) - expect(result.current.size).toBe(15) -}) - -test('should change page', () => { - const { result } = renderHook(() => useQueryAndPagination()) - - act(() => { - result.current.actions.setPage(2) - }) - - expect(result.current.page).toBe(2) - expect(router.query.page).toBe('2') -}) - -test('should change search', () => { - 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.setPage(2) - }) - - 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(() => - useQueryAndPagination({ defaultQueryParameters: { search: '' } }) - ) - - act(() => { - result.current.actions.setPage(2) - }) - - expect(result.current.page).toBe(2) - expect(router.query.page).toBe('2') - - act(() => { - result.current.actions.updateQuery({ search: '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('query multiple different properties, should keep them all', () => { - const { result } = renderHook(() => - useQueryAndPagination({ - defaultQueryParameters: { search: '', department: '' }, - }) - ) - - act(() => { - result.current.actions.updateQuery({ search: 'Max' }) - }) - - act(() => { - 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() -}) - -test('properties in the URL, that are not part of the configuration should be left untouched', () => { - router.query = { greeting: 'hello' } - - const { result } = renderHook(() => - useQueryAndPagination({ - defaultQueryParameters: { search: '' }, - }) - ) - - 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() -}) - -test('query property with empty value and different default value', () => { - const { result } = renderHook(() => - useQueryAndPagination({ - defaultQueryParameters: { search: 'Default search' }, - }) - ) - - act(() => { - result.current.actions.updateQuery({ search: '' }) - }) - - expect(result.current.queryParameters.search).toBe('') - expect(router.query.search).toBe('') -}) diff --git a/src/__test__/reactRouterPagination.test.tsx b/src/__test__/reactRouterPagination.test.tsx deleted file mode 100644 index 19d4cce..0000000 --- a/src/__test__/reactRouterPagination.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import React, { ReactNode } from 'react' -import { act, renderHook } from '@testing-library/react' -import { BrowserRouter as Router } from 'react-router-dom' -import { useQueryAndPagination } from '../reactRouterPagination' -import { IndexType } from '../types' - -function Wrapper({ children }: { children?: ReactNode }) { - return {children} -} - -beforeEach(() => { - window.history.pushState({}, '', '/') -}) - -test('should initialize pagination', () => { - const { result } = renderHook(() => useQueryAndPagination(), { - wrapper: Wrapper, - }) - - expect(result.current.page).toBe(0) - expect(result.current.size).toBe(15) -}) - -test('should change page', () => { - const { result } = renderHook(() => useQueryAndPagination(), { - wrapper: Wrapper, - }) - - act(() => { - result.current.actions.setPage(2) - }) - - expect(result.current.page).toBe(2) - expect(window.location.search).toBe('?page=2') -}) - -test('should change search', () => { - const { result } = renderHook( - () => useQueryAndPagination({ defaultQueryParameters: { search: '' } }), - { wrapper: Wrapper } - ) - - act(() => { - result.current.actions.updateQuery({ search: 'Max' }) - }) - - expect(result.current.queryParameters.search).toBe('Max') - expect(window.location.search).toBe('?search=Max') -}) - -test('clear pagination should reset search and page', () => { - const { result } = renderHook( - () => useQueryAndPagination({ defaultQueryParameters: { search: '' } }), - { wrapper: 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.clear() - }) - - expect(result.current.queryParameters.search).toBe('') - expect(result.current.page).toBe(0) - expect(window.location.search).toBe('') -}) - -test('change default parameters', () => { - const { result } = renderHook( - () => - useQueryAndPagination({ indexType: IndexType.ONE_BASED, pageSize: 10 }), - { wrapper: 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: Wrapper } - ) - - 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: '' }, - }), - { wrapper: Wrapper } - ) - - act(() => { - result.current.actions.updateQuery({ department: 'IT' }) - }) - - 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: Wrapper } - ) - - act(() => { - result.current.actions.updateQuery({ search: 'Max' }) - }) - - expect(result.current.queryParameters.search).toBe('Max') - expect(result.current.queryParameters.greeting).toBeUndefined() - expect(window.location.search).toBe('?greeting=hello&search=Max') -}) - -test('query property with default value, should remove it from url', () => { - window.history.pushState({}, '', '/?search=Anton') - - const { result } = renderHook( - () => - useQueryAndPagination({ - defaultQueryParameters: { search: '' }, - }), - { wrapper: Wrapper } - ) - - act(() => { - result.current.actions.updateQuery({ search: '' }) - }) - - expect(result.current.queryParameters.search).toBe('') - expect(window.location.search).toBe('') -}) - -test('query property with empty value and different default value', () => { - const { result } = renderHook( - () => - useQueryAndPagination({ - defaultQueryParameters: { search: 'Default search' }, - }), - { wrapper: Wrapper } - ) - - act(() => { - result.current.actions.updateQuery({ search: '' }) - }) - - expect(result.current.queryParameters.search).toBe('') - expect(window.location.search).toBe('?search=') -}) diff --git a/src/inMemoryPagination.tsx b/src/inMemoryPagination.tsx deleted file mode 100644 index 003d84b..0000000 --- a/src/inMemoryPagination.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useCallback, useMemo, useState } from 'react' -import { IndexType, IUseQueryAndPagination, QueryParameters } from './types' - -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, - queryParameters: config?.defaultQueryParameters || {}, - }), - [firstPage, pageSize, config?.defaultQueryParameters] - ) - - const [state, setState] = useState(initialState) - - 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, - queryParameters: updatedQueryParameters, - } - }) - }, - [config?.defaultQueryParameters] - ) - - const clear = useCallback(() => { - setState(() => { - return initialState - }) - }, [initialState]) - - const setPage = useCallback((page: number) => { - setState((currentState) => { - return { - ...currentState, - page, - } - }) - }, []) - - return { - queryParameters: { - ...(config?.defaultQueryParameters ? config.defaultQueryParameters : {}), - ...state.queryParameters, - }, - actions: { - updateQuery, - clear, - setPage, - }, - page: state.page, - size: state.size, - } -} diff --git a/src/nextRouterPagination.tsx b/src/nextRouterPagination.tsx deleted file mode 100644 index 721e7dc..0000000 --- a/src/nextRouterPagination.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { ParsedUrlQuery } from 'querystring' -import { useRouter } from 'next/router' -import { useCallback } from 'react' -import { IndexType, IUseQueryAndPagination, QueryParameters } from './types' -import { convert } from './utils' - -function getSingleParameterValue( - parameter: string | string[] | undefined -): string | undefined { - return Array.isArray(parameter) ? parameter[0] : parameter -} - -function extractCurrentQueryParameters( - query: ParsedUrlQuery, - defaultQueryParameters?: QueryParameters -) { - if (!defaultQueryParameters) { - return {} - } - - const result: QueryParameters = { ...defaultQueryParameters } - - for (const parameter in defaultQueryParameters) { - const queryParameter = getSingleParameterValue(query[parameter]) - - if (typeof queryParameter !== 'undefined') { - result[parameter] = queryParameter - } - } - - return result -} - -export const useQueryAndPagination: IUseQueryAndPagination = function (config) { - const { indexType = IndexType.ZERO_BASED, pageSize = 15 } = config || {} - const router = useRouter() - - const setPage = useCallback( - (page: number) => { - const params = { - ...router.query, - page: page.toString(), - } - - router.push({ - query: params, - }) - }, - [router] - ) - - const updateQuery = useCallback( - (queryParameters: QueryParameters) => { - const params = { - ...router.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'] - delete params['size'] - - router.push({ - query: params, - }) - }, - [router, config?.defaultQueryParameters] - ) - - const clear = useCallback(() => { - const params = { - ...router.query, - } - - delete params['page'] - delete params['size'] - - for (const parameter in config?.defaultQueryParameters) { - delete params[parameter] - } - - router.push({ - query: params, - }) - }, [router, config?.defaultQueryParameters]) - - return { - 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: { - updateQuery, - clear, - setPage, - }, - } -} diff --git a/src/reactRouterPagination.tsx b/src/reactRouterPagination.tsx deleted file mode 100644 index 7e78ba0..0000000 --- a/src/reactRouterPagination.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useCallback, useMemo } from 'react' -import { useNavigate, useLocation } from 'react-router-dom' - -import { IndexType, IUseQueryAndPagination, QueryParameters } from './types' -import { convert } from './utils' - -function extractCurrentQueryParameters( - query: URLSearchParams, - defaultQueryParameters?: QueryParameters -) { - if (!defaultQueryParameters) { - return {} - } - - const result: QueryParameters = { ...defaultQueryParameters } - - for (const parameter in defaultQueryParameters) { - const queryParameter = query.get(parameter) - - if (queryParameter !== null) { - result[parameter] = queryParameter - } - } - - return result -} - -export const useQueryAndPagination: IUseQueryAndPagination = function (config) { - const { indexType = IndexType.ZERO_BASED, pageSize = 15 } = config || {} - const navigate = useNavigate() - const { pathname: routerUrl, search: routeQuery } = useLocation() - - const params = useMemo(() => new URLSearchParams(routeQuery), [routeQuery]) - - 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] as string) - } - } - - params.delete('page') - params.delete('size') - - navigate({ - pathname: routerUrl, - search: params.toString(), - }) - }, - [navigate, params, config?.defaultQueryParameters, routerUrl] - ) - - const clear = useCallback(() => { - for (const parameter in config?.defaultQueryParameters) { - params.delete(parameter) - } - - params.delete('page') - params.delete('size') - - navigate({ - pathname: routerUrl, - search: params.toString(), - }) - }, [navigate, params, config?.defaultQueryParameters, routerUrl]) - - const setPage = useCallback( - (page: number) => { - params.set('page', page.toString()) - navigate({ - pathname: routerUrl, - search: params.toString(), - }) - }, - [navigate, params, routerUrl] - ) - - return { - queryParameters: extractCurrentQueryParameters( - params, - config?.defaultQueryParameters - ), - page: convert( - params.get('page'), - indexType === IndexType.ZERO_BASED ? 0 : 1 - ), - size: convert(params.get('size'), pageSize), - actions: { - updateQuery, - clear, - setPage, - }, - } -} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 109ec7d..0000000 --- a/src/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -export enum IndexType { - ZERO_BASED = 0, - ONE_BASED = 1, -} - -export type QueryParameters = Record - -export type Config = { - indexType?: IndexType - pageSize?: number - defaultQueryParameters?: QueryParameters -} - -export type Actions = { - updateQuery: (query: QueryParameters) => void - clear: () => void - setPage: (page: number) => void -} - -export type UseQueryAndPagination = { - queryParameters: QueryParameters - page: number - size: number - actions: Actions -} - -export interface IUseQueryAndPagination { - (config?: Config): UseQueryAndPagination -} diff --git a/src/useInMemoryRouterQuery.ts b/src/useInMemoryRouterQuery.ts new file mode 100644 index 0000000..f7b2e31 --- /dev/null +++ b/src/useInMemoryRouterQuery.ts @@ -0,0 +1,14 @@ +import { useState } from 'react' +import { Query, ParseQuery, useQuery } from './useQuery' + +export const useInMemoryRouterQuery = ( + defaultQuery: T, + parse: ParseQuery +) => { + const [query, setQuery] = useState(defaultQuery) + + return useQuery(defaultQuery, parse, { + getQuery: () => query, + setQuery: (query) => setQuery((prevQuery) => ({ ...prevQuery, ...query })), + }) +} diff --git a/src/useNextRouterQuery.ts b/src/useNextRouterQuery.ts new file mode 100644 index 0000000..b176be6 --- /dev/null +++ b/src/useNextRouterQuery.ts @@ -0,0 +1,30 @@ +import { useRouter } from 'next/router' +import { Query, ParseQuery, useQuery } from './useQuery' + +export const useNextRouterQuery = ( + defaultQuery: T, + parse: ParseQuery +) => { + const nextRouter = useRouter() + + return useQuery(defaultQuery, parse, { + getQuery: () => { + const query: Query = {} + for (const [key, value] of Object.entries(nextRouter.query)) { + if (value !== undefined) { + query[key] = value + } + } + return { ...defaultQuery, ...query } + }, + setQuery: (query) => { + const newQuery = { ...nextRouter.query, ...query } + for (const [key, value] of Object.entries(query)) { + if (value === defaultQuery[key]) { + delete newQuery[key] + } + } + nextRouter.push({ query: newQuery }) + }, + }) +} diff --git a/src/useQuery.ts b/src/useQuery.ts new file mode 100644 index 0000000..d6d1598 --- /dev/null +++ b/src/useQuery.ts @@ -0,0 +1,58 @@ +import { useMemo } from 'react' + +export type Query = Record + +/** + * @throws On failed parsing. + */ +export type ParseQuery = (query: Query) => T + +export type Router = { + getQuery: () => Query + /** + * Updates the query by merging the given query with the current query. + * If the query is bound to the URL, the user is navigated to the new URL. + */ + setQuery: (query: Query) => void +} + +export const useQuery = ( + defaultQuery: T, + parse: ParseQuery, + router: Router +) => { + const resetQuery = () => router.setQuery(defaultQuery) + + const parsedQuery = useMemo(() => { + const query = router.getQuery() + try { + return parse(query) + } catch (e) { + return undefined + } + }, [parse, router]) + + return { + query: parsedQuery, + setQuery: (query: T) => router.setQuery(query), + resetQuery, + } +} + +// type PaginationQuery = { +// page: number +// size: number +// } + +// export const usePagination = ( +// defaultQuery: PaginationQuery, +// router: Router +// ) => { +// const { setQuery, query, resetQuery } = useQuery(defaultQuery, router) + +// return { +// setPagination: setQuery, +// resetPagination: resetQuery, +// ...query, +// } +// } diff --git a/src/useReactRouterQuery.ts b/src/useReactRouterQuery.ts new file mode 100644 index 0000000..b5c28cf --- /dev/null +++ b/src/useReactRouterQuery.ts @@ -0,0 +1,44 @@ +import { useLocation, useNavigate } from 'react-router-dom' +import { Query, ParseQuery, useQuery } from './useQuery' + +export const useReactRouterQuery = ( + defaultQuery: T, + parse: ParseQuery +) => { + const navigate = useNavigate() + const { pathname, search } = useLocation() + + return useQuery(defaultQuery, parse, { + getQuery: () => { + const query: Query = {} + for (const [key, value] of new URLSearchParams(search).entries()) { + const decodedValues = value.split(',').map((v) => decodeURIComponent(v)) + query[key] = + decodedValues.length === 1 + ? (decodedValues[0] as string) + : decodedValues + } + return { ...defaultQuery, ...query } + }, + setQuery: (query) => { + const urlSearchParams = new URLSearchParams(search) + for (const [key, value] of Object.entries(query)) { + if (value === defaultQuery[key]) { + urlSearchParams.delete(key) + } else { + let values: string[] + if (Array.isArray(value)) { + values = value + } else { + values = [value] + } + const encodedValues = values + .map((v) => encodeURIComponent(v)) + .join(',') + urlSearchParams.set(key, encodedValues) + } + } + navigate({ pathname, search: urlSearchParams.toString() }) + }, + }) +} diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 51f41ad..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function convert(parameter: string | null, fallback: number): number { - if (parameter !== null) { - const converted = parseInt(parameter) - - if (Number.isInteger(converted) && converted > 0) { - return converted - } - } - - return fallback -} From fc10df22edace478e899bd32589a86d8b5ad4126 Mon Sep 17 00:00:00 2001 From: devgioele Date: Mon, 17 Jul 2023 08:50:25 +0200 Subject: [PATCH 02/26] fix package entrypoints --- package.json | 3 +++ src/index.ts | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 src/index.ts diff --git a/package.json b/package.json index 47be365..6f95a86 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "@aboutbits/react-pagination", "version": "1.0.2", "description": "Pagination hooks for React", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/types/index.d.ts", "sideEffects": false, "engines": { "npm": "^8", diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b3c1d37 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from './useNextRouterQuery' +export * from './useReactRouterQuery' +export * from './useInMemoryRouterQuery' From d60509d533afa67fcec50a50bd4cb472756e8453 Mon Sep 17 00:00:00 2001 From: devgioele Date: Mon, 17 Jul 2023 21:09:08 +0200 Subject: [PATCH 03/26] run tests using vitest --- .eslintcache | 1 + .eslintrc.json | 16 ++++++++++++++-- package.json | 16 ++++++++-------- src/vitest.ts | 1 + vite.config.ts | 11 +++++++++++ 5 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 .eslintcache create mode 100644 src/vitest.ts create mode 100644 vite.config.ts diff --git a/.eslintcache b/.eslintcache new file mode 100644 index 0000000..b1477d5 --- /dev/null +++ b/.eslintcache @@ -0,0 +1 @@ +[{"/Users/the-anarcho-primitivist/code/react-pagination/jest-setup.ts":"1","/Users/the-anarcho-primitivist/code/react-pagination/src/index.ts":"2","/Users/the-anarcho-primitivist/code/react-pagination/src/useInMemoryRouterQuery.ts":"3","/Users/the-anarcho-primitivist/code/react-pagination/src/useNextRouterQuery.ts":"4","/Users/the-anarcho-primitivist/code/react-pagination/src/useQuery.ts":"5","/Users/the-anarcho-primitivist/code/react-pagination/src/useReactRouterQuery.ts":"6","/Users/the-anarcho-primitivist/code/react-pagination/src/vitest.ts":"7","/Users/the-anarcho-primitivist/code/react-pagination/vite.config.ts":"8"},{"size":35,"mtime":1687784551871,"results":"9","hashOfConfig":"10"},{"size":116,"mtime":1689576597280,"results":"11","hashOfConfig":"10"},{"size":411,"mtime":1689576530547,"results":"12","hashOfConfig":"10"},{"size":819,"mtime":1689576530547,"results":"13","hashOfConfig":"10"},{"size":1256,"mtime":1689576530547,"results":"14","hashOfConfig":"10"},{"size":1388,"mtime":1689576530547,"results":"15","hashOfConfig":"10"},{"size":35,"mtime":1689620493485,"results":"16","hashOfConfig":"10"},{"size":199,"mtime":1689620373796,"results":"17","hashOfConfig":"10"},{"filePath":"18","messages":"19","suppressedMessages":"20","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"8y9yfn",{"filePath":"21","messages":"22","suppressedMessages":"23","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"24","messages":"25","suppressedMessages":"26","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"27","messages":"28","suppressedMessages":"29","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"30","messages":"31","suppressedMessages":"32","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"33","messages":"34","suppressedMessages":"35","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"36","messages":"37","suppressedMessages":"38","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"39","messages":"40","suppressedMessages":"41","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/the-anarcho-primitivist/code/react-pagination/jest-setup.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/index.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/useInMemoryRouterQuery.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/useNextRouterQuery.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/useQuery.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/useReactRouterQuery.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/vitest.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/vite.config.ts",[],[]] \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index dff6579..d5bbb3a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,18 +4,30 @@ "browser": true, "node": true }, - "plugins": ["@typescript-eslint", "react", "react-hooks", "jest"], + "plugins": ["@typescript-eslint", "react", "react-hooks"], "extends": [ + "eslint:recommended", "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", "plugin:import/recommended", "plugin:import/typescript", - "plugin:jest/recommended", "plugin:prettier/recommended" ], "parserOptions": { "sourceType": "module", "ecmaVersion": 2018 }, + "settings": { + "react": { + "version": "detect" + }, + "import/resolver": { + "node": { + "paths": ["./"] + } + } + }, "rules": { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "error", diff --git a/package.json b/package.json index 6f95a86..6d22961 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,12 @@ "build:esm": "tsc -p tsconfig.esm.json", "build:cjs": "tsc -p tsconfig.cjs.json", "build:types": "tsc -p tsconfig.types.json", - "lint": "eslint --ext js,ts,tsx src", + "lint": "eslint --ext js,ts,tsx --cache .", "lint:fix": "npm run lint -- --fix", + "test": "vitest run --passWithNoTests", + "test:watch": "vitest watch --passWithNoTests", "typecheck": "tsc --noEmit", - "test": "jest --verbose --passWithNoTests", + "checks": "npm run test && npm run lint && npm run typecheck", "prepare": "npm run build", "prepublishOnly": "npm run test && npm run lint", "version": "npm run test && npm run lint && git add -A src", @@ -50,7 +52,6 @@ "devDependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", - "@types/jest": "^28.1.3", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/react-router-dom": "^5.3.3", @@ -59,12 +60,10 @@ "eslint": "^8.18.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "^26.5.3", "eslint-plugin-prettier": "^4.1.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", - "jest": "^28.1.1", - "jest-environment-jsdom": "^28.1.1", + "jsdom": "^22.1.0", "next": "^13.1.3", "next-router-mock": "^0.7.4", "prettier": "^2.7.1", @@ -72,8 +71,9 @@ "react-dom": "^18.0.0", "react-router-dom": "^6.7.0", "rimraf": "^5.0.1", - "ts-jest": "^28.0.5", - "typescript": "^5.1.6" + "typescript": "^5.1.6", + "vite": "^4.4.4", + "vitest": "^0.33.0" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0", diff --git a/src/vitest.ts b/src/vitest.ts new file mode 100644 index 0000000..c44951a --- /dev/null +++ b/src/vitest.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom' diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..b662c16 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +/// + +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/vitest.ts', + }, +}) From d7aaa7b7b0e69ac4b94316ced971991587cba82c Mon Sep 17 00:00:00 2001 From: devgioele Date: Tue, 18 Jul 2023 12:24:22 +0200 Subject: [PATCH 04/26] prototype 1 --- package.json | 3 +- src/__test__/inMemoryPagination.test.ts | 129 ++++++++++ src/__test__/nextRouterPagination.test.tsx | 183 ++++++++++++++ src/__test__/reactRouterPagination.test.tsx | 177 ++++++++++++++ src/__test__/useNextRouterQuery.test.tsx | 254 ++++++++++++++++++++ src/nextRouter.ts | 54 +++++ src/nextRouterPagination.tsx | 114 +++++++++ src/useNextRouterQuery.ts | 30 --- src/useQuery.ts | 134 ++++++++--- src/utils.ts | 5 + tsconfig.cjs.json | 10 +- tsconfig.esm.json | 10 +- tsconfig.json | 5 - tsconfig.types.json | 10 +- 14 files changed, 1041 insertions(+), 77 deletions(-) create mode 100644 src/__test__/inMemoryPagination.test.ts create mode 100644 src/__test__/nextRouterPagination.test.tsx create mode 100644 src/__test__/reactRouterPagination.test.tsx create mode 100644 src/__test__/useNextRouterQuery.test.tsx create mode 100644 src/nextRouter.ts create mode 100644 src/nextRouterPagination.tsx delete mode 100644 src/useNextRouterQuery.ts create mode 100644 src/utils.ts diff --git a/package.json b/package.json index 6d22961..517d202 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,8 @@ "rimraf": "^5.0.1", "typescript": "^5.1.6", "vite": "^4.4.4", - "vitest": "^0.33.0" + "vitest": "^0.33.0", + "zod": "^3.21.4" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0", diff --git a/src/__test__/inMemoryPagination.test.ts b/src/__test__/inMemoryPagination.test.ts new file mode 100644 index 0000000..1c9955e --- /dev/null +++ b/src/__test__/inMemoryPagination.test.ts @@ -0,0 +1,129 @@ +import { act, renderHook } from '@testing-library/react' +import { useQueryAndPagination } from '../inMemoryPagination' +import { IndexType } from '../types' + +test('should initialize pagination', () => { + const { result } = renderHook(() => useQueryAndPagination()) + + expect(result.current.page).toBe(0) + expect(result.current.size).toBe(15) +}) + +test('should change page', () => { + const { result } = renderHook(() => useQueryAndPagination()) + + act(() => { + result.current.actions.setPage(2) + }) + + expect(result.current.page).toBe(2) +}) + +test('should change search', () => { + const { result } = renderHook(() => + useQueryAndPagination({ defaultQueryParameters: { search: '' } }) + ) + + act(() => { + result.current.actions.updateQuery({ search: 'Max' }) + }) + + expect(result.current.queryParameters.search).toBe('Max') +}) + +test('on search change -> page should be reset', () => { + const { result } = renderHook(() => + useQueryAndPagination({ defaultQueryParameters: { search: '' } }) + ) + + act(() => { + result.current.actions.setPage(2) + }) + + expect(result.current.page).toBe(2) + + act(() => { + result.current.actions.updateQuery({ search: '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(() => + useQueryAndPagination({ defaultQueryParameters: { search: '' } }) + ) + + 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.clear() + }) + + expect(result.current.queryParameters.search).toBe('') + expect(result.current.page).toBe(0) +}) + +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('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() +}) + +test('query property with empty value and different default value', () => { + const { result } = renderHook(() => + useQueryAndPagination({ + defaultQueryParameters: { search: 'Default search' }, + }) + ) + + act(() => { + result.current.actions.updateQuery({ search: '' }) + }) + + expect(result.current.queryParameters.search).toBe('') +}) diff --git a/src/__test__/nextRouterPagination.test.tsx b/src/__test__/nextRouterPagination.test.tsx new file mode 100644 index 0000000..9cd18c3 --- /dev/null +++ b/src/__test__/nextRouterPagination.test.tsx @@ -0,0 +1,183 @@ +import { act, renderHook } from '@testing-library/react' +import router from 'next/router' + +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(() => useQueryAndPagination()) + + expect(result.current.page).toBe(0) + expect(result.current.size).toBe(15) +}) + +test('should change page', () => { + const { result } = renderHook(() => useQueryAndPagination()) + + act(() => { + result.current.actions.setPage(2) + }) + + expect(result.current.page).toBe(2) + expect(router.query.page).toBe('2') +}) + +test('should change search', () => { + 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.setPage(2) + }) + + 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(() => + useQueryAndPagination({ defaultQueryParameters: { search: '' } }) + ) + + act(() => { + result.current.actions.setPage(2) + }) + + expect(result.current.page).toBe(2) + expect(router.query.page).toBe('2') + + act(() => { + result.current.actions.updateQuery({ search: '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('query multiple different properties, should keep them all', () => { + const { result } = renderHook(() => + useQueryAndPagination({ + defaultQueryParameters: { search: '', department: '' }, + }) + ) + + act(() => { + result.current.actions.updateQuery({ search: 'Max' }) + }) + + act(() => { + 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() +}) + +test('properties in the URL, that are not part of the configuration should be left untouched', () => { + router.query = { greeting: 'hello' } + + const { result } = renderHook(() => + useQueryAndPagination({ + defaultQueryParameters: { search: '' }, + }) + ) + + 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() +}) + +test('query property with empty value and different default value', () => { + const { result } = renderHook(() => + useQueryAndPagination({ + defaultQueryParameters: { search: 'Default search' }, + }) + ) + + act(() => { + result.current.actions.updateQuery({ search: '' }) + }) + + expect(result.current.queryParameters.search).toBe('') + expect(router.query.search).toBe('') +}) diff --git a/src/__test__/reactRouterPagination.test.tsx b/src/__test__/reactRouterPagination.test.tsx new file mode 100644 index 0000000..19d4cce --- /dev/null +++ b/src/__test__/reactRouterPagination.test.tsx @@ -0,0 +1,177 @@ +import React, { ReactNode } from 'react' +import { act, renderHook } from '@testing-library/react' +import { BrowserRouter as Router } from 'react-router-dom' +import { useQueryAndPagination } from '../reactRouterPagination' +import { IndexType } from '../types' + +function Wrapper({ children }: { children?: ReactNode }) { + return {children} +} + +beforeEach(() => { + window.history.pushState({}, '', '/') +}) + +test('should initialize pagination', () => { + const { result } = renderHook(() => useQueryAndPagination(), { + wrapper: Wrapper, + }) + + expect(result.current.page).toBe(0) + expect(result.current.size).toBe(15) +}) + +test('should change page', () => { + const { result } = renderHook(() => useQueryAndPagination(), { + wrapper: Wrapper, + }) + + act(() => { + result.current.actions.setPage(2) + }) + + expect(result.current.page).toBe(2) + expect(window.location.search).toBe('?page=2') +}) + +test('should change search', () => { + const { result } = renderHook( + () => useQueryAndPagination({ defaultQueryParameters: { search: '' } }), + { wrapper: Wrapper } + ) + + act(() => { + result.current.actions.updateQuery({ search: 'Max' }) + }) + + expect(result.current.queryParameters.search).toBe('Max') + expect(window.location.search).toBe('?search=Max') +}) + +test('clear pagination should reset search and page', () => { + const { result } = renderHook( + () => useQueryAndPagination({ defaultQueryParameters: { search: '' } }), + { wrapper: 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.clear() + }) + + expect(result.current.queryParameters.search).toBe('') + expect(result.current.page).toBe(0) + expect(window.location.search).toBe('') +}) + +test('change default parameters', () => { + const { result } = renderHook( + () => + useQueryAndPagination({ indexType: IndexType.ONE_BASED, pageSize: 10 }), + { wrapper: 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: Wrapper } + ) + + 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: '' }, + }), + { wrapper: Wrapper } + ) + + act(() => { + result.current.actions.updateQuery({ department: 'IT' }) + }) + + 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: Wrapper } + ) + + act(() => { + result.current.actions.updateQuery({ search: 'Max' }) + }) + + expect(result.current.queryParameters.search).toBe('Max') + expect(result.current.queryParameters.greeting).toBeUndefined() + expect(window.location.search).toBe('?greeting=hello&search=Max') +}) + +test('query property with default value, should remove it from url', () => { + window.history.pushState({}, '', '/?search=Anton') + + const { result } = renderHook( + () => + useQueryAndPagination({ + defaultQueryParameters: { search: '' }, + }), + { wrapper: Wrapper } + ) + + act(() => { + result.current.actions.updateQuery({ search: '' }) + }) + + expect(result.current.queryParameters.search).toBe('') + expect(window.location.search).toBe('') +}) + +test('query property with empty value and different default value', () => { + const { result } = renderHook( + () => + useQueryAndPagination({ + defaultQueryParameters: { search: 'Default search' }, + }), + { wrapper: Wrapper } + ) + + act(() => { + result.current.actions.updateQuery({ search: '' }) + }) + + expect(result.current.queryParameters.search).toBe('') + expect(window.location.search).toBe('?search=') +}) diff --git a/src/__test__/useNextRouterQuery.test.tsx b/src/__test__/useNextRouterQuery.test.tsx new file mode 100644 index 0000000..ac20fe9 --- /dev/null +++ b/src/__test__/useNextRouterQuery.test.tsx @@ -0,0 +1,254 @@ +import { act, renderHook } from '@testing-library/react' +import router from 'next/router' +import { z } from 'zod' +import { useNextRouterPagination, useNextRouterQuery } from '../index' +import { NonNullableRecord } from '../utils' + +jest.mock('next/router', () => require('next-router-mock')) + +beforeEach(() => { + router.query = {} +}) + +const paginationSchema = z.object({ + page: z.string().pipe(z.coerce.number().optional()).catch(undefined), + size: z.string().pipe(z.coerce.number().optional()).catch(undefined), +}) + +const useConfiguredNextRouterPagination = ( + defaultQuery: NonNullableRecord> +) => { + return useNextRouterPagination(defaultQuery, (q) => paginationSchema.parse(q)) +} + +const searchSchema = z.object({ + search: z.string().optional().catch(undefined), +}) + +const useNextRouterQueryWithSearch = ( + defaultQuery: NonNullableRecord> +) => useNextRouterQuery(defaultQuery, (q) => searchSchema.parse(q)) + +test('should initialize pagination', () => { + const page = 0 + const size = 15 + const { result } = renderHook(() => + useConfiguredNextRouterPagination({ page, size }) + ) + + expect(result.current.page).toBe(page) + expect(result.current.size).toBe(size) +}) + +test('should change page', () => { + const page = 2 + + const { result } = renderHook(() => + useConfiguredNextRouterPagination({ page: 0, size: 15 }) + ) + + act(() => { + result.current.setPage(page) + }) + + expect(result.current.page).toBe(page) + expect(router.query.page).toBe(page.toString()) +}) + +test('should change search', () => { + const search = 'Max' + + const { result } = renderHook(() => + useNextRouterQueryWithSearch({ search: '' }) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + expect(result.current.query?.search).toBe(search) + expect(router.query.search).toBe(search) +}) + +test('resetting search should not reset pagination', () => { + const defaultSearch = '' + const search = 'Max' + const page = 2 + + const { result: resultSearch } = renderHook(() => + useNextRouterQueryWithSearch({ search: defaultSearch }) + ) + const { result: resultPagination } = renderHook(() => + useConfiguredNextRouterPagination({ page: 0, size: 15 }) + ) + + act(() => { + resultSearch.current.setQuery({ search }) + }) + + act(() => { + resultPagination.current.setPage(page) + }) + + expect(router.query.search).toBe(search) + expect(router.query.page).toBe(page.toString()) + + act(() => { + resultSearch.current.resetQuery() + }) + + expect(resultSearch.current.query.search).toBe(defaultSearch) + expect(resultPagination.current.page).toBe(page) + expect(router.query.search).toBeUndefined() + expect(router.query.page).toBeDefined() +}) + +test('resetting pagination should not reset search', () => { + const search = 'Max' + const defaultPage = 0 + const page = 2 + + const { result: resultSearch } = renderHook(() => + useNextRouterQueryWithSearch({ search: '' }) + ) + const { result: resultPagination } = renderHook(() => + useConfiguredNextRouterPagination({ page: defaultPage, size: 15 }) + ) + + act(() => { + resultSearch.current.setQuery({ search }) + }) + + act(() => { + resultPagination.current.setPage(page) + }) + + expect(router.query.search).toBe(search) + expect(router.query.page).toBe(page.toString()) + + act(() => { + resultPagination.current.resetPagination() + }) + + expect(resultSearch.current.query.search).toBe(search) + expect(resultPagination.current.page).toBe(defaultPage) + expect(router.query.search).toBeDefined() + expect(router.query.page).toBeUndefined() +}) + +test('change default parameters', () => { + const page = 1 + const size = 10 + const { result } = renderHook(() => + useNextRouterPagination({ page, size }, paginationParser) + ) + + expect(result.current.page).toBe(page) + expect(result.current.size).toBe(size) +}) + +test('on search change -> page should be reset', () => { + const { result } = renderHook(() => + useNextRouterQuery({ defaultQueryParameters: { search: '' } }) + ) + + act(() => { + result.current.actions.setPage(2) + }) + + expect(result.current.page).toBe(2) + expect(router.query.page).toBe('2') + + act(() => { + result.current.actions.updateQuery({ search: '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('query multiple different properties, should keep them all', () => { + const { result } = renderHook(() => + useNextRouterQuery({ + defaultQueryParameters: { search: '', department: '' }, + }) + ) + + act(() => { + result.current.actions.updateQuery({ search: 'Max' }) + }) + + act(() => { + 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(() => + useNextRouterQuery({ + defaultQueryParameters: { search: '' }, + }) + ) + + act(() => { + result.current.actions.updateQuery({ department: 'IT' }) + }) + + 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', () => { + router.query = { greeting: 'hello' } + + const { result } = renderHook(() => + useNextRouterQuery({ + defaultQueryParameters: { search: '' }, + }) + ) + + 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(() => + useNextRouterQuery({ + defaultQueryParameters: { search: '' }, + }) + ) + + act(() => { + result.current.actions.updateQuery({ search: '' }) + }) + + expect(result.current.queryParameters.search).toBe('') + expect(router.query.search).toBeUndefined() +}) + +test('query property with empty value and different default value', () => { + const { result } = renderHook(() => + useNextRouterQuery({ + defaultQueryParameters: { search: 'Default search' }, + }) + ) + + act(() => { + result.current.actions.updateQuery({ search: '' }) + }) + + expect(result.current.queryParameters.search).toBe('') + expect(router.query.search).toBe('') +}) diff --git a/src/nextRouter.ts b/src/nextRouter.ts new file mode 100644 index 0000000..8fb5478 --- /dev/null +++ b/src/nextRouter.ts @@ -0,0 +1,54 @@ +import { useRouter } from 'next/router' +import { + Query, + ParseQuery, + useAbstractQuery, + usePagination, + Router, + AbstractQuery, + PaginationQuery, +} from './useQuery' + +const useNextRouter = (defaultQuery: T): Router => { + const nextRouter = useRouter() + + return { + getQuery: () => { + const query: Query = {} + for (const [key, value] of Object.entries(nextRouter.query)) { + if (value !== undefined) { + query[key] = value + } + } + return { ...defaultQuery, ...query } + }, + setQuery: (query) => { + const newQuery = { ...nextRouter.query, ...query } + for (const [key, value] of Object.entries(query)) { + if (value === defaultQuery[key]) { + delete newQuery[key] + } + } + nextRouter.push({ query: newQuery }, undefined, { shallow: true }) + }, + clearQuery: () => { + nextRouter.push({ query: {} }, undefined, { shallow: true }) + }, + } +} + +export const useNextRouterQuery = ( + defaultQuery: T, + parse: ParseQuery +) => { + const router = useNextRouter(defaultQuery) + return useAbstractQuery(defaultQuery, router, parse) +} + +export const useNextRouterPagination = ( + defaultQuery: PaginationQuery, + parse: ParseQuery +) => { + const router = useNextRouter(defaultQuery) + return usePagination(defaultQuery, router, parse) +} diff --git a/src/nextRouterPagination.tsx b/src/nextRouterPagination.tsx new file mode 100644 index 0000000..0c36b77 --- /dev/null +++ b/src/nextRouterPagination.tsx @@ -0,0 +1,114 @@ +import { ParsedUrlQuery } from 'querystring' +import { useRouter } from 'next/router' +import { useCallback } from 'react' + +import { IndexType, IUseQueryAndPagination, QueryParameters } from './types' +import { convert } from './utils' + +function getSingleParameterValue( + parameter: string | string[] | undefined +): string | undefined { + return Array.isArray(parameter) ? parameter[0] : parameter +} + +function extractCurrentQueryParameters( + query: ParsedUrlQuery, + defaultQueryParameters?: QueryParameters +) { + if (!defaultQueryParameters) { + return {} + } + + const result: QueryParameters = { ...defaultQueryParameters } + + for (const parameter in defaultQueryParameters) { + const queryParameter = getSingleParameterValue(query[parameter]) + + if (typeof queryParameter !== 'undefined') { + result[parameter] = queryParameter + } + } + + return result +} + +export const useQueryAndPagination: IUseQueryAndPagination = function (config) { + const { indexType = IndexType.ZERO_BASED, pageSize = 15 } = config || {} + const router = useRouter() + + const setPage = useCallback( + (page: number) => { + const params = { + ...router.query, + page: page.toString(), + } + + router.push({ + query: params, + }) + }, + [router] + ) + + const updateQuery = useCallback( + (queryParameters: QueryParameters) => { + const params = { + ...router.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'] + delete params['size'] + + router.push({ + query: params, + }) + }, + [router] + ) + + const clear = useCallback(() => { + const params = { + ...router.query, + } + + delete params['page'] + delete params['size'] + + for (const parameter in config?.defaultQueryParameters) { + delete params[parameter] + } + + router.push({ + query: params, + }) + }, [router]) + + return { + 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: { + updateQuery, + clear, + setPage, + }, + } +} diff --git a/src/useNextRouterQuery.ts b/src/useNextRouterQuery.ts deleted file mode 100644 index b176be6..0000000 --- a/src/useNextRouterQuery.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useRouter } from 'next/router' -import { Query, ParseQuery, useQuery } from './useQuery' - -export const useNextRouterQuery = ( - defaultQuery: T, - parse: ParseQuery -) => { - const nextRouter = useRouter() - - return useQuery(defaultQuery, parse, { - getQuery: () => { - const query: Query = {} - for (const [key, value] of Object.entries(nextRouter.query)) { - if (value !== undefined) { - query[key] = value - } - } - return { ...defaultQuery, ...query } - }, - setQuery: (query) => { - const newQuery = { ...nextRouter.query, ...query } - for (const [key, value] of Object.entries(query)) { - if (value === defaultQuery[key]) { - delete newQuery[key] - } - } - nextRouter.push({ query: newQuery }) - }, - }) -} diff --git a/src/useQuery.ts b/src/useQuery.ts index d6d1598..a8521e6 100644 --- a/src/useQuery.ts +++ b/src/useQuery.ts @@ -1,58 +1,130 @@ -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' export type Query = Record +export type AbstractQueryValue = string | number | boolean + +export type AbstractQuery = Record< + string, + AbstractQueryValue | AbstractQueryValue[] +> + /** - * @throws On failed parsing. + * Parses the query. + * + * @returns The parameters that could be parsed. An empty record if no parameter could be parsed. + * @throws If the parsing failed. */ -export type ParseQuery = (query: Query) => T +export type ParseQuery = (query: Query) => Partial export type Router = { + /** + * @returns The current query. + */ getQuery: () => Query + /** * Updates the query by merging the given query with the current query. - * If the query is bound to the URL, the user is navigated to the new URL. + * If the query is bound to the URL, the router navigates to the new URL. + */ + setQuery: (query: Partial) => void + + /** + * Completely clears the query. */ - setQuery: (query: Query) => void + clearQuery: () => void +} + +export const useQuery = (defaultQuery: T, router: Router) => { + const resetQuery = useCallback( + () => router.setQuery(defaultQuery), + [router, defaultQuery] + ) + + const clearQuery = useCallback(() => router.clearQuery(), [router]) + + const query = useMemo(() => router.getQuery(), [router]) + + return { + query, + setQuery: (query: Partial) => router.setQuery(query), + resetQuery, + clearQuery, + } +} + +const abstractQueryToQuery = >( + abstractQuery: T +) => { + const query: Query = {} + for (const [key, value] of Object.entries(abstractQuery)) { + if (value !== undefined) { + query[key] = value.toString() + } + } + return query } -export const useQuery = ( +export const useAbstractQuery = ( defaultQuery: T, - parse: ParseQuery, - router: Router + router: Router, + parse: ParseQuery ) => { - const resetQuery = () => router.setQuery(defaultQuery) + const convertedDefaultQuery = useMemo( + () => abstractQueryToQuery(defaultQuery), + [defaultQuery] + ) + + const { query, setQuery, resetQuery, clearQuery } = useQuery( + convertedDefaultQuery, + router + ) const parsedQuery = useMemo(() => { - const query = router.getQuery() + let parsed: Partial try { - return parse(query) + parsed = parse(query) } catch (e) { - return undefined + parsed = {} + } + return { + ...defaultQuery, + ...parsed, } - }, [parse, router]) + }, [defaultQuery, parse, query]) + + const setAbstractQuery = useCallback( + (query: Partial) => setQuery(abstractQueryToQuery(query)), + [setQuery] + ) return { query: parsedQuery, - setQuery: (query: T) => router.setQuery(query), + setQuery: setAbstractQuery, resetQuery, + clearQuery, } } -// type PaginationQuery = { -// page: number -// size: number -// } - -// export const usePagination = ( -// defaultQuery: PaginationQuery, -// router: Router -// ) => { -// const { setQuery, query, resetQuery } = useQuery(defaultQuery, router) - -// return { -// setPagination: setQuery, -// resetPagination: resetQuery, -// ...query, -// } -// } +export type PaginationQuery = { page: number; size: number } + +export const usePagination = ( + defaultQuery: PaginationQuery, + router: Router, + parse: ParseQuery +) => { + const { query, setQuery, resetQuery, clearQuery } = useAbstractQuery( + defaultQuery, + router, + parse + ) + + return { + page: query.page, + size: query.size, + setSize: (size: PaginationQuery['size']) => setQuery({ size }), + setPage: (page: PaginationQuery['page']) => setQuery({ page }), + resetPagination: resetQuery, + clearQuery, + } +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..dff6056 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,5 @@ +export type NonNullableRecord< + T extends Record +> = { + [Key in keyof T]-?: NonNullable + } diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index 07f9e19..678c53c 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -8,6 +8,12 @@ "sourceMap": false, "removeComments": true }, - "exclude": ["node_modules", "**/__test__/*", "**/__tests__/*"], - "include": ["src"] + "exclude": [ + "src/vitest.ts", + "**/__test__/*", + "**/__tests__/*" + ], + "include": [ + "src" + ] } diff --git a/tsconfig.esm.json b/tsconfig.esm.json index d6a606e..999299b 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -8,6 +8,12 @@ "sourceMap": false, "removeComments": true }, - "exclude": ["node_modules", "**/__test__/*", "**/__tests__/*"], - "include": ["src"] + "exclude": [ + "src/vitest.ts", + "**/__test__/*", + "**/__tests__/*" + ], + "include": [ + "src" + ] } diff --git a/tsconfig.json b/tsconfig.json index 27e4d23..11c6705 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,10 +22,5 @@ }, "include": [ "src" - ], - "exclude": [ - "node_modules", - "**/__test__/*", - "**/__tests__/*" ] } diff --git a/tsconfig.types.json b/tsconfig.types.json index 5185a65..5825707 100644 --- a/tsconfig.types.json +++ b/tsconfig.types.json @@ -10,11 +10,9 @@ "exclude": [ "src/vitest.ts", "**/__test__/*", - "**/__tests__/*", - "**/*.stories.tsx", - "**/*.stories.ts", - "src/examples/*", - ".storybook/*" + "**/__tests__/*" ], - "include": ["src"] + "include": [ + "src" + ] } From 466064fe3ef2d1ff85180f9baf550361fdee6584 Mon Sep 17 00:00:00 2001 From: devgioele Date: Tue, 18 Jul 2023 12:25:14 +0200 Subject: [PATCH 05/26] prototype 2 --- ...outerQuery.test.tsx => nextRouter.test.ts} | 0 src/useQuery.ts | 105 +++++++++++------- src/utils.ts | 17 ++- 3 files changed, 79 insertions(+), 43 deletions(-) rename src/__test__/{useNextRouterQuery.test.tsx => nextRouter.test.ts} (100%) diff --git a/src/__test__/useNextRouterQuery.test.tsx b/src/__test__/nextRouter.test.ts similarity index 100% rename from src/__test__/useNextRouterQuery.test.tsx rename to src/__test__/nextRouter.test.ts diff --git a/src/useQuery.ts b/src/useQuery.ts index a8521e6..484771a 100644 --- a/src/useQuery.ts +++ b/src/useQuery.ts @@ -1,12 +1,13 @@ import { useCallback, useMemo } from 'react' +import { queryValueToIntOrUndefined } from './utils' export type Query = Record -export type AbstractQueryValue = string | number | boolean +export type AbstractQueryValueElement = string | number | boolean export type AbstractQuery = Record< string, - AbstractQueryValue | AbstractQueryValue[] + AbstractQueryValueElement | AbstractQueryValueElement[] > /** @@ -28,11 +29,6 @@ export type Router = { * If the query is bound to the URL, the router navigates to the new URL. */ setQuery: (query: Partial) => void - - /** - * Completely clears the query. - */ - clearQuery: () => void } export const useQuery = (defaultQuery: T, router: Router) => { @@ -41,49 +37,56 @@ export const useQuery = (defaultQuery: T, router: Router) => { [router, defaultQuery] ) - const clearQuery = useCallback(() => router.clearQuery(), [router]) - const query = useMemo(() => router.getQuery(), [router]) return { query, setQuery: (query: Partial) => router.setQuery(query), resetQuery, - clearQuery, } } -const abstractQueryToQuery = >( - abstractQuery: T -) => { - const query: Query = {} - for (const [key, value] of Object.entries(abstractQuery)) { - if (value !== undefined) { - query[key] = value.toString() +type AbstractQueryOptions = { + convertToQuery: >(abstractQuery: T) => Query +} + +const DEFAULT_ABSTRACT_QUERY_OPTIONS: AbstractQueryOptions = { + convertToQuery: (abstractQuery) => { + const query: Query = {} + for (const [key, value] of Object.entries(abstractQuery)) { + if (value !== undefined) { + query[key] = value.toString() + } } - } - return query + return query + }, } export const useAbstractQuery = ( defaultQuery: T, + parseQuery: ParseQuery, router: Router, - parse: ParseQuery + options?: Partial ) => { + const mergedOptions = useMemo( + () => ({ ...DEFAULT_ABSTRACT_QUERY_OPTIONS, ...options }), + [options] + ) + const convertedDefaultQuery = useMemo( - () => abstractQueryToQuery(defaultQuery), - [defaultQuery] + () => mergedOptions.convertToQuery(defaultQuery), + [mergedOptions, defaultQuery] ) - const { query, setQuery, resetQuery, clearQuery } = useQuery( + const { query, setQuery, resetQuery } = useQuery( convertedDefaultQuery, router ) - const parsedQuery = useMemo(() => { + const parsedQuery: T = useMemo(() => { let parsed: Partial try { - parsed = parse(query) + parsed = parseQuery(query) } catch (e) { parsed = {} } @@ -91,40 +94,58 @@ export const useAbstractQuery = ( ...defaultQuery, ...parsed, } - }, [defaultQuery, parse, query]) + }, [defaultQuery, parseQuery, query]) const setAbstractQuery = useCallback( - (query: Partial) => setQuery(abstractQueryToQuery(query)), - [setQuery] + (query: Partial) => setQuery(mergedOptions.convertToQuery(query)), + [setQuery, mergedOptions] ) return { query: parsedQuery, setQuery: setAbstractQuery, resetQuery, - clearQuery, } } export type PaginationQuery = { page: number; size: number } -export const usePagination = ( - defaultQuery: PaginationQuery, - router: Router, - parse: ParseQuery +const parsePagination = (query: Query): Partial => { + return { + page: queryValueToIntOrUndefined(query.page), + size: queryValueToIntOrUndefined(query.size), + } +} + +export const useQueryAndPagination = ( + defaultQuery: T, + parseQuery: ParseQuery, + defaultPagination: PaginationQuery, + router: Router ) => { - const { query, setQuery, resetQuery, clearQuery } = useAbstractQuery( + const { query, setQuery, resetQuery } = useAbstractQuery( defaultQuery, - router, - parse + parseQuery, + router ) + const { + query: pagination, + setQuery: setPagination, + resetQuery: resetPagination, + } = useAbstractQuery(defaultPagination, parsePagination, router) + return { - page: query.page, - size: query.size, - setSize: (size: PaginationQuery['size']) => setQuery({ size }), - setPage: (page: PaginationQuery['page']) => setQuery({ page }), - resetPagination: resetQuery, - clearQuery, + query, + setQuery, + resetQuery, + page: pagination.page, + size: pagination.size, + setPage: (page: PaginationQuery['page']) => setPagination({ page }), + setSize: (size: PaginationQuery['size']) => setPagination({ size }), + setPagination, + resetPagination, + resetQueryAndPagination: () => + setQuery({ ...defaultQuery, ...defaultPagination }), } } diff --git a/src/utils.ts b/src/utils.ts index dff6056..c13ca90 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,20 @@ +import { Query } from './useQuery' + export type NonNullableRecord< T extends Record > = { - [Key in keyof T]-?: NonNullable + [Key in keyof T]-?: NonNullable +} + +export const queryValueToIntOrUndefined = ( + value: Query[keyof Query] | undefined +): number | undefined => { + if (value === undefined || Array.isArray(value)) { + return undefined } + const parsed = parseInt(value) + if (isNaN(parsed)) { + return undefined + } + return parsed +} From 3d14c6dc4f5cd9debfa9a959a849e389391faad3 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 08:13:20 +0200 Subject: [PATCH 06/26] add routers and test next router --- .eslintcache | 1 - .gitignore | 3 + jest-setup.ts | 1 - jest.config.json | 9 - package.json | 22 +- src/__test__/inMemoryPagination.test.ts | 129 ---------- src/__test__/nextRouter.test.ts | 254 -------------------- src/__test__/nextRouterPagination.test.tsx | 183 -------------- src/__test__/reactRouterPagination.test.tsx | 177 -------------- src/engine/index.ts | 2 + src/engine/pagination.ts | 85 +++++++ src/{useQuery.ts => engine/query.ts} | 58 +---- src/index.ts | 5 +- src/nextRouter.ts | 54 ----- src/nextRouterPagination.tsx | 114 --------- src/routers/__test__/nextRouter.test.ts | 252 +++++++++++++++++++ src/routers/inMemory.ts | 59 +++++ src/routers/index.ts | 1 + src/routers/nextRouter.ts | 69 ++++++ src/routers/reactRouter.ts | 85 +++++++ src/useInMemoryRouterQuery.ts | 14 -- src/useReactRouterQuery.ts | 44 ---- src/utils.ts | 2 +- src/zod/index.ts | 1 + src/zod/routers/inMemory.ts | 38 +++ src/zod/routers/index.ts | 1 + src/zod/routers/nextRouter.ts | 38 +++ src/zod/routers/reactRouter.ts | 38 +++ src/zod/util.ts | 14 ++ 29 files changed, 714 insertions(+), 1039 deletions(-) delete mode 100644 .eslintcache delete mode 100644 jest-setup.ts delete mode 100644 jest.config.json delete mode 100644 src/__test__/inMemoryPagination.test.ts delete mode 100644 src/__test__/nextRouter.test.ts delete mode 100644 src/__test__/nextRouterPagination.test.tsx delete mode 100644 src/__test__/reactRouterPagination.test.tsx create mode 100644 src/engine/index.ts create mode 100644 src/engine/pagination.ts rename src/{useQuery.ts => engine/query.ts} (58%) delete mode 100644 src/nextRouter.ts delete mode 100644 src/nextRouterPagination.tsx create mode 100644 src/routers/__test__/nextRouter.test.ts create mode 100644 src/routers/inMemory.ts create mode 100644 src/routers/index.ts create mode 100644 src/routers/nextRouter.ts create mode 100644 src/routers/reactRouter.ts delete mode 100644 src/useInMemoryRouterQuery.ts delete mode 100644 src/useReactRouterQuery.ts create mode 100644 src/zod/index.ts create mode 100644 src/zod/routers/inMemory.ts create mode 100644 src/zod/routers/index.ts create mode 100644 src/zod/routers/nextRouter.ts create mode 100644 src/zod/routers/reactRouter.ts create mode 100644 src/zod/util.ts diff --git a/.eslintcache b/.eslintcache deleted file mode 100644 index b1477d5..0000000 --- a/.eslintcache +++ /dev/null @@ -1 +0,0 @@ -[{"/Users/the-anarcho-primitivist/code/react-pagination/jest-setup.ts":"1","/Users/the-anarcho-primitivist/code/react-pagination/src/index.ts":"2","/Users/the-anarcho-primitivist/code/react-pagination/src/useInMemoryRouterQuery.ts":"3","/Users/the-anarcho-primitivist/code/react-pagination/src/useNextRouterQuery.ts":"4","/Users/the-anarcho-primitivist/code/react-pagination/src/useQuery.ts":"5","/Users/the-anarcho-primitivist/code/react-pagination/src/useReactRouterQuery.ts":"6","/Users/the-anarcho-primitivist/code/react-pagination/src/vitest.ts":"7","/Users/the-anarcho-primitivist/code/react-pagination/vite.config.ts":"8"},{"size":35,"mtime":1687784551871,"results":"9","hashOfConfig":"10"},{"size":116,"mtime":1689576597280,"results":"11","hashOfConfig":"10"},{"size":411,"mtime":1689576530547,"results":"12","hashOfConfig":"10"},{"size":819,"mtime":1689576530547,"results":"13","hashOfConfig":"10"},{"size":1256,"mtime":1689576530547,"results":"14","hashOfConfig":"10"},{"size":1388,"mtime":1689576530547,"results":"15","hashOfConfig":"10"},{"size":35,"mtime":1689620493485,"results":"16","hashOfConfig":"10"},{"size":199,"mtime":1689620373796,"results":"17","hashOfConfig":"10"},{"filePath":"18","messages":"19","suppressedMessages":"20","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"8y9yfn",{"filePath":"21","messages":"22","suppressedMessages":"23","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"24","messages":"25","suppressedMessages":"26","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"27","messages":"28","suppressedMessages":"29","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"30","messages":"31","suppressedMessages":"32","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"33","messages":"34","suppressedMessages":"35","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"36","messages":"37","suppressedMessages":"38","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"39","messages":"40","suppressedMessages":"41","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/the-anarcho-primitivist/code/react-pagination/jest-setup.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/index.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/useInMemoryRouterQuery.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/useNextRouterQuery.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/useQuery.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/useReactRouterQuery.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/src/vitest.ts",[],[],"/Users/the-anarcho-primitivist/code/react-pagination/vite.config.ts",[],[]] \ No newline at end of file diff --git a/.gitignore b/.gitignore index 090fba5..1d33405 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ /package-lock.json /node_modules /dist + +# ESLint +.eslintcache diff --git a/jest-setup.ts b/jest-setup.ts deleted file mode 100644 index c44951a..0000000 --- a/jest-setup.ts +++ /dev/null @@ -1 +0,0 @@ -import '@testing-library/jest-dom' diff --git a/jest.config.json b/jest.config.json deleted file mode 100644 index 060d8d1..0000000 --- a/jest.config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "testEnvironment": "jsdom", - "transform": { - "^.+\\.(t|j)sx?$": "ts-jest" - }, - "testRegex": "(/__tests?__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", - "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], - "setupFilesAfterEnv": ["/jest-setup.ts"] -} diff --git a/package.json b/package.json index 517d202..df75ad0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,18 @@ "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "dist/types/index.d.ts", - "sideEffects": false, + "exports": { + ".": { + "types": "dist/types/index.d.ts", + "import": "dist/esm/index.js", + "require": "dist/cjs/index.js" + }, + "./zod": { + "types": "dist/zod/types/index.d.ts", + "import": "dist/zod/esm/index.js", + "require": "dist/zod/cjs/index.js" + } + }, "engines": { "npm": "^8", "node": "^16" @@ -73,13 +84,13 @@ "rimraf": "^5.0.1", "typescript": "^5.1.6", "vite": "^4.4.4", - "vitest": "^0.33.0", - "zod": "^3.21.4" + "vitest": "^0.33.0" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0", "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-router-dom": "^6.0.0" + "react-router-dom": "^6.0.0", + "zod": "^3.0.0" }, "peerDependenciesMeta": { "next": { @@ -87,6 +98,9 @@ }, "react-router-dom": { "optional": true + }, + "zod": { + "optional": true } } } diff --git a/src/__test__/inMemoryPagination.test.ts b/src/__test__/inMemoryPagination.test.ts deleted file mode 100644 index 1c9955e..0000000 --- a/src/__test__/inMemoryPagination.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { act, renderHook } from '@testing-library/react' -import { useQueryAndPagination } from '../inMemoryPagination' -import { IndexType } from '../types' - -test('should initialize pagination', () => { - const { result } = renderHook(() => useQueryAndPagination()) - - expect(result.current.page).toBe(0) - expect(result.current.size).toBe(15) -}) - -test('should change page', () => { - const { result } = renderHook(() => useQueryAndPagination()) - - act(() => { - result.current.actions.setPage(2) - }) - - expect(result.current.page).toBe(2) -}) - -test('should change search', () => { - const { result } = renderHook(() => - useQueryAndPagination({ defaultQueryParameters: { search: '' } }) - ) - - act(() => { - result.current.actions.updateQuery({ search: 'Max' }) - }) - - expect(result.current.queryParameters.search).toBe('Max') -}) - -test('on search change -> page should be reset', () => { - const { result } = renderHook(() => - useQueryAndPagination({ defaultQueryParameters: { search: '' } }) - ) - - act(() => { - result.current.actions.setPage(2) - }) - - expect(result.current.page).toBe(2) - - act(() => { - result.current.actions.updateQuery({ search: '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(() => - useQueryAndPagination({ defaultQueryParameters: { search: '' } }) - ) - - 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.clear() - }) - - expect(result.current.queryParameters.search).toBe('') - expect(result.current.page).toBe(0) -}) - -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('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() -}) - -test('query property with empty value and different default value', () => { - const { result } = renderHook(() => - useQueryAndPagination({ - defaultQueryParameters: { search: 'Default search' }, - }) - ) - - act(() => { - result.current.actions.updateQuery({ search: '' }) - }) - - expect(result.current.queryParameters.search).toBe('') -}) diff --git a/src/__test__/nextRouter.test.ts b/src/__test__/nextRouter.test.ts deleted file mode 100644 index ac20fe9..0000000 --- a/src/__test__/nextRouter.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { act, renderHook } from '@testing-library/react' -import router from 'next/router' -import { z } from 'zod' -import { useNextRouterPagination, useNextRouterQuery } from '../index' -import { NonNullableRecord } from '../utils' - -jest.mock('next/router', () => require('next-router-mock')) - -beforeEach(() => { - router.query = {} -}) - -const paginationSchema = z.object({ - page: z.string().pipe(z.coerce.number().optional()).catch(undefined), - size: z.string().pipe(z.coerce.number().optional()).catch(undefined), -}) - -const useConfiguredNextRouterPagination = ( - defaultQuery: NonNullableRecord> -) => { - return useNextRouterPagination(defaultQuery, (q) => paginationSchema.parse(q)) -} - -const searchSchema = z.object({ - search: z.string().optional().catch(undefined), -}) - -const useNextRouterQueryWithSearch = ( - defaultQuery: NonNullableRecord> -) => useNextRouterQuery(defaultQuery, (q) => searchSchema.parse(q)) - -test('should initialize pagination', () => { - const page = 0 - const size = 15 - const { result } = renderHook(() => - useConfiguredNextRouterPagination({ page, size }) - ) - - expect(result.current.page).toBe(page) - expect(result.current.size).toBe(size) -}) - -test('should change page', () => { - const page = 2 - - const { result } = renderHook(() => - useConfiguredNextRouterPagination({ page: 0, size: 15 }) - ) - - act(() => { - result.current.setPage(page) - }) - - expect(result.current.page).toBe(page) - expect(router.query.page).toBe(page.toString()) -}) - -test('should change search', () => { - const search = 'Max' - - const { result } = renderHook(() => - useNextRouterQueryWithSearch({ search: '' }) - ) - - act(() => { - result.current.setQuery({ search }) - }) - - expect(result.current.query?.search).toBe(search) - expect(router.query.search).toBe(search) -}) - -test('resetting search should not reset pagination', () => { - const defaultSearch = '' - const search = 'Max' - const page = 2 - - const { result: resultSearch } = renderHook(() => - useNextRouterQueryWithSearch({ search: defaultSearch }) - ) - const { result: resultPagination } = renderHook(() => - useConfiguredNextRouterPagination({ page: 0, size: 15 }) - ) - - act(() => { - resultSearch.current.setQuery({ search }) - }) - - act(() => { - resultPagination.current.setPage(page) - }) - - expect(router.query.search).toBe(search) - expect(router.query.page).toBe(page.toString()) - - act(() => { - resultSearch.current.resetQuery() - }) - - expect(resultSearch.current.query.search).toBe(defaultSearch) - expect(resultPagination.current.page).toBe(page) - expect(router.query.search).toBeUndefined() - expect(router.query.page).toBeDefined() -}) - -test('resetting pagination should not reset search', () => { - const search = 'Max' - const defaultPage = 0 - const page = 2 - - const { result: resultSearch } = renderHook(() => - useNextRouterQueryWithSearch({ search: '' }) - ) - const { result: resultPagination } = renderHook(() => - useConfiguredNextRouterPagination({ page: defaultPage, size: 15 }) - ) - - act(() => { - resultSearch.current.setQuery({ search }) - }) - - act(() => { - resultPagination.current.setPage(page) - }) - - expect(router.query.search).toBe(search) - expect(router.query.page).toBe(page.toString()) - - act(() => { - resultPagination.current.resetPagination() - }) - - expect(resultSearch.current.query.search).toBe(search) - expect(resultPagination.current.page).toBe(defaultPage) - expect(router.query.search).toBeDefined() - expect(router.query.page).toBeUndefined() -}) - -test('change default parameters', () => { - const page = 1 - const size = 10 - const { result } = renderHook(() => - useNextRouterPagination({ page, size }, paginationParser) - ) - - expect(result.current.page).toBe(page) - expect(result.current.size).toBe(size) -}) - -test('on search change -> page should be reset', () => { - const { result } = renderHook(() => - useNextRouterQuery({ defaultQueryParameters: { search: '' } }) - ) - - act(() => { - result.current.actions.setPage(2) - }) - - expect(result.current.page).toBe(2) - expect(router.query.page).toBe('2') - - act(() => { - result.current.actions.updateQuery({ search: '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('query multiple different properties, should keep them all', () => { - const { result } = renderHook(() => - useNextRouterQuery({ - defaultQueryParameters: { search: '', department: '' }, - }) - ) - - act(() => { - result.current.actions.updateQuery({ search: 'Max' }) - }) - - act(() => { - 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(() => - useNextRouterQuery({ - defaultQueryParameters: { search: '' }, - }) - ) - - act(() => { - result.current.actions.updateQuery({ department: 'IT' }) - }) - - 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', () => { - router.query = { greeting: 'hello' } - - const { result } = renderHook(() => - useNextRouterQuery({ - defaultQueryParameters: { search: '' }, - }) - ) - - 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(() => - useNextRouterQuery({ - defaultQueryParameters: { search: '' }, - }) - ) - - act(() => { - result.current.actions.updateQuery({ search: '' }) - }) - - expect(result.current.queryParameters.search).toBe('') - expect(router.query.search).toBeUndefined() -}) - -test('query property with empty value and different default value', () => { - const { result } = renderHook(() => - useNextRouterQuery({ - defaultQueryParameters: { search: 'Default search' }, - }) - ) - - act(() => { - result.current.actions.updateQuery({ search: '' }) - }) - - expect(result.current.queryParameters.search).toBe('') - expect(router.query.search).toBe('') -}) diff --git a/src/__test__/nextRouterPagination.test.tsx b/src/__test__/nextRouterPagination.test.tsx deleted file mode 100644 index 9cd18c3..0000000 --- a/src/__test__/nextRouterPagination.test.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { act, renderHook } from '@testing-library/react' -import router from 'next/router' - -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(() => useQueryAndPagination()) - - expect(result.current.page).toBe(0) - expect(result.current.size).toBe(15) -}) - -test('should change page', () => { - const { result } = renderHook(() => useQueryAndPagination()) - - act(() => { - result.current.actions.setPage(2) - }) - - expect(result.current.page).toBe(2) - expect(router.query.page).toBe('2') -}) - -test('should change search', () => { - 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.setPage(2) - }) - - 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(() => - useQueryAndPagination({ defaultQueryParameters: { search: '' } }) - ) - - act(() => { - result.current.actions.setPage(2) - }) - - expect(result.current.page).toBe(2) - expect(router.query.page).toBe('2') - - act(() => { - result.current.actions.updateQuery({ search: '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('query multiple different properties, should keep them all', () => { - const { result } = renderHook(() => - useQueryAndPagination({ - defaultQueryParameters: { search: '', department: '' }, - }) - ) - - act(() => { - result.current.actions.updateQuery({ search: 'Max' }) - }) - - act(() => { - 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() -}) - -test('properties in the URL, that are not part of the configuration should be left untouched', () => { - router.query = { greeting: 'hello' } - - const { result } = renderHook(() => - useQueryAndPagination({ - defaultQueryParameters: { search: '' }, - }) - ) - - 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() -}) - -test('query property with empty value and different default value', () => { - const { result } = renderHook(() => - useQueryAndPagination({ - defaultQueryParameters: { search: 'Default search' }, - }) - ) - - act(() => { - result.current.actions.updateQuery({ search: '' }) - }) - - expect(result.current.queryParameters.search).toBe('') - expect(router.query.search).toBe('') -}) diff --git a/src/__test__/reactRouterPagination.test.tsx b/src/__test__/reactRouterPagination.test.tsx deleted file mode 100644 index 19d4cce..0000000 --- a/src/__test__/reactRouterPagination.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import React, { ReactNode } from 'react' -import { act, renderHook } from '@testing-library/react' -import { BrowserRouter as Router } from 'react-router-dom' -import { useQueryAndPagination } from '../reactRouterPagination' -import { IndexType } from '../types' - -function Wrapper({ children }: { children?: ReactNode }) { - return {children} -} - -beforeEach(() => { - window.history.pushState({}, '', '/') -}) - -test('should initialize pagination', () => { - const { result } = renderHook(() => useQueryAndPagination(), { - wrapper: Wrapper, - }) - - expect(result.current.page).toBe(0) - expect(result.current.size).toBe(15) -}) - -test('should change page', () => { - const { result } = renderHook(() => useQueryAndPagination(), { - wrapper: Wrapper, - }) - - act(() => { - result.current.actions.setPage(2) - }) - - expect(result.current.page).toBe(2) - expect(window.location.search).toBe('?page=2') -}) - -test('should change search', () => { - const { result } = renderHook( - () => useQueryAndPagination({ defaultQueryParameters: { search: '' } }), - { wrapper: Wrapper } - ) - - act(() => { - result.current.actions.updateQuery({ search: 'Max' }) - }) - - expect(result.current.queryParameters.search).toBe('Max') - expect(window.location.search).toBe('?search=Max') -}) - -test('clear pagination should reset search and page', () => { - const { result } = renderHook( - () => useQueryAndPagination({ defaultQueryParameters: { search: '' } }), - { wrapper: 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.clear() - }) - - expect(result.current.queryParameters.search).toBe('') - expect(result.current.page).toBe(0) - expect(window.location.search).toBe('') -}) - -test('change default parameters', () => { - const { result } = renderHook( - () => - useQueryAndPagination({ indexType: IndexType.ONE_BASED, pageSize: 10 }), - { wrapper: 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: Wrapper } - ) - - 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: '' }, - }), - { wrapper: Wrapper } - ) - - act(() => { - result.current.actions.updateQuery({ department: 'IT' }) - }) - - 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: Wrapper } - ) - - act(() => { - result.current.actions.updateQuery({ search: 'Max' }) - }) - - expect(result.current.queryParameters.search).toBe('Max') - expect(result.current.queryParameters.greeting).toBeUndefined() - expect(window.location.search).toBe('?greeting=hello&search=Max') -}) - -test('query property with default value, should remove it from url', () => { - window.history.pushState({}, '', '/?search=Anton') - - const { result } = renderHook( - () => - useQueryAndPagination({ - defaultQueryParameters: { search: '' }, - }), - { wrapper: Wrapper } - ) - - act(() => { - result.current.actions.updateQuery({ search: '' }) - }) - - expect(result.current.queryParameters.search).toBe('') - expect(window.location.search).toBe('') -}) - -test('query property with empty value and different default value', () => { - const { result } = renderHook( - () => - useQueryAndPagination({ - defaultQueryParameters: { search: 'Default search' }, - }), - { wrapper: Wrapper } - ) - - act(() => { - result.current.actions.updateQuery({ search: '' }) - }) - - expect(result.current.queryParameters.search).toBe('') - expect(window.location.search).toBe('?search=') -}) diff --git a/src/engine/index.ts b/src/engine/index.ts new file mode 100644 index 0000000..19b3e14 --- /dev/null +++ b/src/engine/index.ts @@ -0,0 +1,2 @@ +export * from './query' +export * from './pagination' diff --git a/src/engine/pagination.ts b/src/engine/pagination.ts new file mode 100644 index 0000000..2ac8f87 --- /dev/null +++ b/src/engine/pagination.ts @@ -0,0 +1,85 @@ +import { queryValueToIntOrUndefined } from '../utils' +import { + Query, + AbstractQuery, + ParseQuery, + useAbstractQuery, + Router, +} from './query' + +export type PaginationQuery = { page: number; size: number } + +export type ChangeQueryOptions = { resetPage: boolean } + +const DEFAULT_PAGINATION: PaginationQuery = { + page: 0, + size: 15, +} + +const parsePagination = (query: Query): Partial => { + return { + page: queryValueToIntOrUndefined(query.page), + size: queryValueToIntOrUndefined(query.size), + } +} + +const DEFAULT_RESET_PAGE = true + +export const useAbstractQueryAndPagination = ( + defaultQuery: T, + parseQuery: ParseQuery, + router: Router, + defaultPagination?: PaginationQuery +) => { + const mergedDefaultPagination = { + ...DEFAULT_PAGINATION, + ...defaultPagination, + } + const mergedDefaultQueryAndPagination = { + ...defaultQuery, + ...mergedDefaultPagination, + } + + const { query, setQuery } = useAbstractQuery( + mergedDefaultQueryAndPagination, + parseQuery, + router + ) + + const { + query: pagination, + setQuery: setPagination, + resetQuery: resetPagination, + } = useAbstractQuery(mergedDefaultPagination, parsePagination, router) + + return { + query, + setQuery: (query: Partial, options?: Partial) => { + const resetPage = + options?.resetPage === undefined + ? DEFAULT_RESET_PAGE + : options.resetPage + setQuery({ + ...query, + page: resetPage ? mergedDefaultPagination.page : undefined, + }) + }, + resetQuery: (options?: Partial) => { + const resetPage = + options?.resetPage === undefined + ? DEFAULT_RESET_PAGE + : options.resetPage + setQuery({ + ...defaultQuery, + page: resetPage ? mergedDefaultPagination.page : undefined, + }) + }, + page: pagination.page, + size: pagination.size, + setPage: (page: PaginationQuery['page']) => setPagination({ page }), + setSize: (size: PaginationQuery['size']) => setPagination({ size }), + setPagination, + resetPagination, + resetQueryAndPagination: () => setQuery(mergedDefaultQueryAndPagination), + } +} diff --git a/src/useQuery.ts b/src/engine/query.ts similarity index 58% rename from src/useQuery.ts rename to src/engine/query.ts index 484771a..9494d63 100644 --- a/src/useQuery.ts +++ b/src/engine/query.ts @@ -1,5 +1,4 @@ import { useCallback, useMemo } from 'react' -import { queryValueToIntOrUndefined } from './utils' export type Query = Record @@ -22,32 +21,31 @@ export type Router = { /** * @returns The current query. */ - getQuery: () => Query + getQuery: (defaultQuery: Query) => Query /** * Updates the query by merging the given query with the current query. - * If the query is bound to the URL, the router navigates to the new URL. */ - setQuery: (query: Partial) => void + setQuery: (query: Partial, defaultQuery: Query) => void } export const useQuery = (defaultQuery: T, router: Router) => { const resetQuery = useCallback( - () => router.setQuery(defaultQuery), + () => router.setQuery(defaultQuery, defaultQuery), [router, defaultQuery] ) - const query = useMemo(() => router.getQuery(), [router]) + const query = router.getQuery(defaultQuery) return { query, - setQuery: (query: Partial) => router.setQuery(query), + setQuery: (query: Partial) => router.setQuery(query, defaultQuery), resetQuery, } } -type AbstractQueryOptions = { - convertToQuery: >(abstractQuery: T) => Query +export type AbstractQueryOptions = { + convertToQuery: (abstractQuery: Partial) => Query } const DEFAULT_ABSTRACT_QUERY_OPTIONS: AbstractQueryOptions = { @@ -107,45 +105,3 @@ export const useAbstractQuery = ( resetQuery, } } - -export type PaginationQuery = { page: number; size: number } - -const parsePagination = (query: Query): Partial => { - return { - page: queryValueToIntOrUndefined(query.page), - size: queryValueToIntOrUndefined(query.size), - } -} - -export const useQueryAndPagination = ( - defaultQuery: T, - parseQuery: ParseQuery, - defaultPagination: PaginationQuery, - router: Router -) => { - const { query, setQuery, resetQuery } = useAbstractQuery( - defaultQuery, - parseQuery, - router - ) - - const { - query: pagination, - setQuery: setPagination, - resetQuery: resetPagination, - } = useAbstractQuery(defaultPagination, parsePagination, router) - - return { - query, - setQuery, - resetQuery, - page: pagination.page, - size: pagination.size, - setPage: (page: PaginationQuery['page']) => setPagination({ page }), - setSize: (size: PaginationQuery['size']) => setPagination({ size }), - setPagination, - resetPagination, - resetQueryAndPagination: () => - setQuery({ ...defaultQuery, ...defaultPagination }), - } -} diff --git a/src/index.ts b/src/index.ts index b3c1d37..6199638 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ -export * from './useNextRouterQuery' -export * from './useReactRouterQuery' -export * from './useInMemoryRouterQuery' +export * from './engine' +export * from './routers' diff --git a/src/nextRouter.ts b/src/nextRouter.ts deleted file mode 100644 index 8fb5478..0000000 --- a/src/nextRouter.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useRouter } from 'next/router' -import { - Query, - ParseQuery, - useAbstractQuery, - usePagination, - Router, - AbstractQuery, - PaginationQuery, -} from './useQuery' - -const useNextRouter = (defaultQuery: T): Router => { - const nextRouter = useRouter() - - return { - getQuery: () => { - const query: Query = {} - for (const [key, value] of Object.entries(nextRouter.query)) { - if (value !== undefined) { - query[key] = value - } - } - return { ...defaultQuery, ...query } - }, - setQuery: (query) => { - const newQuery = { ...nextRouter.query, ...query } - for (const [key, value] of Object.entries(query)) { - if (value === defaultQuery[key]) { - delete newQuery[key] - } - } - nextRouter.push({ query: newQuery }, undefined, { shallow: true }) - }, - clearQuery: () => { - nextRouter.push({ query: {} }, undefined, { shallow: true }) - }, - } -} - -export const useNextRouterQuery = ( - defaultQuery: T, - parse: ParseQuery -) => { - const router = useNextRouter(defaultQuery) - return useAbstractQuery(defaultQuery, router, parse) -} - -export const useNextRouterPagination = ( - defaultQuery: PaginationQuery, - parse: ParseQuery -) => { - const router = useNextRouter(defaultQuery) - return usePagination(defaultQuery, router, parse) -} diff --git a/src/nextRouterPagination.tsx b/src/nextRouterPagination.tsx deleted file mode 100644 index 0c36b77..0000000 --- a/src/nextRouterPagination.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { ParsedUrlQuery } from 'querystring' -import { useRouter } from 'next/router' -import { useCallback } from 'react' - -import { IndexType, IUseQueryAndPagination, QueryParameters } from './types' -import { convert } from './utils' - -function getSingleParameterValue( - parameter: string | string[] | undefined -): string | undefined { - return Array.isArray(parameter) ? parameter[0] : parameter -} - -function extractCurrentQueryParameters( - query: ParsedUrlQuery, - defaultQueryParameters?: QueryParameters -) { - if (!defaultQueryParameters) { - return {} - } - - const result: QueryParameters = { ...defaultQueryParameters } - - for (const parameter in defaultQueryParameters) { - const queryParameter = getSingleParameterValue(query[parameter]) - - if (typeof queryParameter !== 'undefined') { - result[parameter] = queryParameter - } - } - - return result -} - -export const useQueryAndPagination: IUseQueryAndPagination = function (config) { - const { indexType = IndexType.ZERO_BASED, pageSize = 15 } = config || {} - const router = useRouter() - - const setPage = useCallback( - (page: number) => { - const params = { - ...router.query, - page: page.toString(), - } - - router.push({ - query: params, - }) - }, - [router] - ) - - const updateQuery = useCallback( - (queryParameters: QueryParameters) => { - const params = { - ...router.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'] - delete params['size'] - - router.push({ - query: params, - }) - }, - [router] - ) - - const clear = useCallback(() => { - const params = { - ...router.query, - } - - delete params['page'] - delete params['size'] - - for (const parameter in config?.defaultQueryParameters) { - delete params[parameter] - } - - router.push({ - query: params, - }) - }, [router]) - - return { - 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: { - updateQuery, - clear, - setPage, - }, - } -} diff --git a/src/routers/__test__/nextRouter.test.ts b/src/routers/__test__/nextRouter.test.ts new file mode 100644 index 0000000..08f6b78 --- /dev/null +++ b/src/routers/__test__/nextRouter.test.ts @@ -0,0 +1,252 @@ +import { act, renderHook } from '@testing-library/react' +import router from 'next/router' +import { z } from 'zod' +import { vi } from 'vitest' +import { NonNullableRecord } from '../../utils' +import { + useNextRouterQuery, + useNextRouterPagination, + useNextRouterQueryAndPagination, +} from '../../zod' + +vi.mock('next/router', () => require('next-router-mock')) + +describe('NextRouter', () => { + beforeEach(() => { + router.query = {} + }) + + const searchSchema = z.object({ + search: z.string().optional().catch(undefined), + }) + + const useNextRouterQueryWithSearch = ( + defaultQuery: NonNullableRecord> + ) => useNextRouterQuery(defaultQuery, searchSchema) + + test('should set default page and size', () => { + const page = 0 + const size = 15 + const { result } = renderHook(() => useNextRouterPagination({ page, size })) + + expect(result.current.page).toBe(page) + expect(result.current.size).toBe(size) + }) + + test('should change page', () => { + const page = 2 + + const { result } = renderHook(() => useNextRouterPagination()) + + act(() => { + result.current.setPage(page) + }) + + expect(result.current.page).toBe(page) + expect(router.query.page).toBe(page.toString()) + }) + + test('should change search', () => { + const search = 'Max' + + const { result } = renderHook(() => + useNextRouterQueryWithSearch({ search: '' }) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + expect(result.current.query?.search).toBe(search) + expect(router.query.search).toBe(search) + }) + + test('changing pagination should not reset the remaining query', () => { + const search = 'Max' + const page = 2 + + const { result } = renderHook(() => + useNextRouterQueryAndPagination({ search: '' }, searchSchema) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + act(() => { + result.current.setPage(page) + }) + + expect(router.query.search).toBe(search) + expect(router.query.page).toBe(page.toString()) + + act(() => { + result.current.resetPagination() + }) + + expect(result.current.query.search).toBe(search) + expect(router.query.search).toBe(search) + expect(result.current.page).toBe(0) + expect(router.query.page).toBeUndefined() + }) + + test('change default parameters', () => { + const page = 1 + const size = 10 + const { result } = renderHook(() => useNextRouterPagination({ page, size })) + + expect(result.current.page).toBe(page) + expect(result.current.size).toBe(size) + }) + + test('changing the remaining query should reset the page', () => { + const defaultSearch = '' + const search1 = 'Max' + const search2 = 'Peter' + const page = 2 + + const { result } = renderHook(() => + useNextRouterQueryAndPagination({ search: defaultSearch }, searchSchema) + ) + + act(() => { + result.current.setQuery({ search: search1 }) + }) + + act(() => { + result.current.setPage(page) + }) + + expect(result.current.query.search).toBe(search1) + expect(router.query.search).toBe(search1) + + expect(result.current.page).toBe(page) + expect(router.query.page).toBe(page.toString()) + + act(() => { + result.current.setQuery({ search: search2 }) + }) + + expect(result.current.query.search).toBe(search2) + expect(router.query.search).toBe(search2) + expect(result.current.page).toBe(0) + expect(router.query.page).toBeUndefined() + + act(() => { + result.current.resetQuery() + }) + + expect(result.current.query.search).toBe(defaultSearch) + expect(router.query.search).toBeUndefined() + expect(result.current.page).toBe(0) + expect(router.query.page).toBeUndefined() + }) + + test('changing the remaining query with resetPage set to false should not reset the page', () => { + const defaultSearch = '' + const search = 'Max' + const page = 2 + + const { result } = renderHook(() => + useNextRouterQueryAndPagination({ search: defaultSearch }, searchSchema) + ) + + act(() => { + result.current.setPage(page) + }) + + expect(result.current.page).toBe(page) + expect(router.query.page).toBe(page.toString()) + + act(() => { + result.current.setQuery({ search }, { resetPage: false }) + }) + + expect(result.current.query.search).toBe(search) + expect(router.query.search).toBe(search) + expect(result.current.page).toBe(page) + expect(router.query.page).toBe(page.toString()) + + act(() => { + result.current.resetQuery({ resetPage: false }) + }) + + expect(result.current.query.search).toBe(defaultSearch) + expect(router.query.search).toBeUndefined() + expect(result.current.page).toBe(page) + expect(router.query.page).toBe(page.toString()) + }) + + test('setting a query key should not overwrite other query keys', () => { + const search = 'Max' + const department = 'IT' + + const schema = z.object({ + search: z.string().optional().catch(undefined), + department: z.string().optional().catch(undefined), + }) + + const { result } = renderHook(() => + useNextRouterQuery({ search: '', department: '' }, schema) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + act(() => { + result.current.setQuery({ department }) + }) + + expect(result.current.query.search).toBe(search) + expect(result.current.query.department).toBe(department) + }) + + test('unspecified query keys should be left untouched', () => { + const greeting = 'hello' + router.query = { greeting } + + const { result } = renderHook(() => + useNextRouterQuery({ search: '' }, searchSchema) + ) + + act(() => { + result.current.setQuery({ search: 'Max' }) + }) + + expect(result.current.query.search).toBe('Max') + expect(router.query.greeting).toBe(greeting) + }) + + test('query keys with default value should not be stored in the url', () => { + const defaultSearch = '' + + router.query = { search: 'Max' } + + const { result } = renderHook(() => + useNextRouterQuery({ search: defaultSearch }, searchSchema) + ) + + act(() => { + result.current.resetQuery() + }) + + expect(result.current.query.search).toBe(defaultSearch) + expect(router.query.search).toBeUndefined() + }) + + test('default query value should not need to be an empty string', () => { + const search = '' + + const { result } = renderHook(() => + useNextRouterQuery({ search: 'Default search' }, searchSchema) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + expect(result.current.query.search).toBe(search) + expect(router.query.search).toBe(search) + }) +}) diff --git a/src/routers/inMemory.ts b/src/routers/inMemory.ts new file mode 100644 index 0000000..3baeb01 --- /dev/null +++ b/src/routers/inMemory.ts @@ -0,0 +1,59 @@ +import { useState } from 'react' +import { + AbstractQuery, + AbstractQueryOptions, + PaginationQuery, + ParseQuery, + Query, + Router, + useAbstractQuery, + useAbstractQueryAndPagination, +} from '../engine' + +const useInMemoryRouter = (): Router => { + const [inMemoryQuery, setInMemoryQuery] = useState({}) + + return { + getQuery: (defaultQuery) => { + return { ...defaultQuery, ...inMemoryQuery } + }, + setQuery: (query) => { + const sanitizedQuery: Query = {} + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) { + sanitizedQuery[key] = value + } + } + setInMemoryQuery({ ...inMemoryQuery, ...sanitizedQuery }) + }, + } +} + +export const useInMemoryQuery = ( + defaultQuery: T, + parseQuery: ParseQuery, + options?: Partial +) => { + const router = useInMemoryRouter() + return useAbstractQuery(defaultQuery, parseQuery, router, options) +} + +export const useInMemoryQueryAndPagination = ( + defaultQuery: T, + parseQuery: ParseQuery, + defaultPagination?: PaginationQuery +) => { + const router = useInMemoryRouter() + return useAbstractQueryAndPagination( + defaultQuery, + parseQuery, + router, + defaultPagination + ) +} + +export const useInMemoryPagination = (defaultPagination?: PaginationQuery) => { + const { page, size, setPage, setSize, setPagination, resetPagination } = + useInMemoryQueryAndPagination({}, () => ({}), defaultPagination) + return { page, size, setPage, setSize, setPagination, resetPagination } +} diff --git a/src/routers/index.ts b/src/routers/index.ts new file mode 100644 index 0000000..13bbd9f --- /dev/null +++ b/src/routers/index.ts @@ -0,0 +1 @@ +export * from './nextRouter' diff --git a/src/routers/nextRouter.ts b/src/routers/nextRouter.ts new file mode 100644 index 0000000..14f25be --- /dev/null +++ b/src/routers/nextRouter.ts @@ -0,0 +1,69 @@ +import { useRouter } from 'next/router' +import { + Query, + ParseQuery, + useAbstractQuery, + Router, + AbstractQuery, + AbstractQueryOptions, +} from '../engine/query' +import { + PaginationQuery, + useAbstractQueryAndPagination, +} from '../engine/pagination' + +const useNextRouter = (): Router => { + const nextRouter = useRouter() + + return { + getQuery: (defaultQuery) => { + const query: Query = {} + for (const [key, value] of Object.entries(nextRouter.query)) { + if (value !== undefined) { + query[key] = value + } + } + return { ...defaultQuery, ...query } + }, + setQuery: (query, defaultQuery) => { + const newQuery = { ...nextRouter.query, ...query } + for (const [key, value] of Object.entries(query)) { + if (value === defaultQuery[key]) { + delete newQuery[key] + } + } + nextRouter.push({ query: newQuery }, undefined, { shallow: true }) + }, + } +} + +export const useNextRouterQuery = ( + defaultQuery: T, + parseQuery: ParseQuery, + options?: Partial +) => { + const router = useNextRouter() + return useAbstractQuery(defaultQuery, parseQuery, router, options) +} + +export const useNextRouterQueryAndPagination = ( + defaultQuery: T, + parseQuery: ParseQuery, + defaultPagination?: PaginationQuery +) => { + const router = useNextRouter() + return useAbstractQueryAndPagination( + defaultQuery, + parseQuery, + router, + defaultPagination + ) +} + +export const useNextRouterPagination = ( + defaultPagination?: PaginationQuery +) => { + const { page, size, setPage, setSize, setPagination, resetPagination } = + useNextRouterQueryAndPagination({}, () => ({}), defaultPagination) + return { page, size, setPage, setSize, setPagination, resetPagination } +} diff --git a/src/routers/reactRouter.ts b/src/routers/reactRouter.ts new file mode 100644 index 0000000..b31f9b2 --- /dev/null +++ b/src/routers/reactRouter.ts @@ -0,0 +1,85 @@ +import { useLocation, useNavigate } from 'react-router-dom' +import { + Query, + AbstractQuery, + ParseQuery, + AbstractQueryOptions, + useAbstractQuery, + PaginationQuery, + useAbstractQueryAndPagination, + Router, +} from '../engine' + +const QUERY_ARRAY_SEPARATOR = ',' + +const useReactRouter = (): Router => { + const navigate = useNavigate() + const { pathname, search } = useLocation() + + return { + getQuery: (defaultQuery) => { + const query: Query = {} + for (const [key, value] of new URLSearchParams(search).entries()) { + const decodedValues = value + .split(QUERY_ARRAY_SEPARATOR) + .map((v) => decodeURIComponent(v)) + query[key] = + decodedValues.length === 1 + ? (decodedValues[0] as string) + : decodedValues + } + return { ...defaultQuery, ...query } + }, + setQuery: (query, defaultQuery) => { + const urlSearchParams = new URLSearchParams(search) + for (const [key, value] of Object.entries(query)) { + if (value === defaultQuery[key]) { + urlSearchParams.delete(key) + } else if (value !== undefined) { + let values: string[] + if (Array.isArray(value)) { + values = value + } else { + values = [value] + } + const encodedValues = values + .map((v) => encodeURIComponent(v)) + .join(QUERY_ARRAY_SEPARATOR) + urlSearchParams.set(key, encodedValues) + } + } + navigate({ pathname, search: urlSearchParams.toString() }) + }, + } +} + +export const useReactRouterQuery = ( + defaultQuery: T, + parseQuery: ParseQuery, + options?: Partial +) => { + const router = useReactRouter() + return useAbstractQuery(defaultQuery, parseQuery, router, options) +} + +export const useReactRouterQueryAndPagination = ( + defaultQuery: T, + parseQuery: ParseQuery, + defaultPagination?: PaginationQuery +) => { + const router = useReactRouter() + return useAbstractQueryAndPagination( + defaultQuery, + parseQuery, + router, + defaultPagination + ) +} + +export const useReactRouterPagination = ( + defaultPagination?: PaginationQuery +) => { + const { page, size, setPage, setSize, setPagination, resetPagination } = + useReactRouterQueryAndPagination({}, () => ({}), defaultPagination) + return { page, size, setPage, setSize, setPagination, resetPagination } +} diff --git a/src/useInMemoryRouterQuery.ts b/src/useInMemoryRouterQuery.ts deleted file mode 100644 index f7b2e31..0000000 --- a/src/useInMemoryRouterQuery.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useState } from 'react' -import { Query, ParseQuery, useQuery } from './useQuery' - -export const useInMemoryRouterQuery = ( - defaultQuery: T, - parse: ParseQuery -) => { - const [query, setQuery] = useState(defaultQuery) - - return useQuery(defaultQuery, parse, { - getQuery: () => query, - setQuery: (query) => setQuery((prevQuery) => ({ ...prevQuery, ...query })), - }) -} diff --git a/src/useReactRouterQuery.ts b/src/useReactRouterQuery.ts deleted file mode 100644 index b5c28cf..0000000 --- a/src/useReactRouterQuery.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useLocation, useNavigate } from 'react-router-dom' -import { Query, ParseQuery, useQuery } from './useQuery' - -export const useReactRouterQuery = ( - defaultQuery: T, - parse: ParseQuery -) => { - const navigate = useNavigate() - const { pathname, search } = useLocation() - - return useQuery(defaultQuery, parse, { - getQuery: () => { - const query: Query = {} - for (const [key, value] of new URLSearchParams(search).entries()) { - const decodedValues = value.split(',').map((v) => decodeURIComponent(v)) - query[key] = - decodedValues.length === 1 - ? (decodedValues[0] as string) - : decodedValues - } - return { ...defaultQuery, ...query } - }, - setQuery: (query) => { - const urlSearchParams = new URLSearchParams(search) - for (const [key, value] of Object.entries(query)) { - if (value === defaultQuery[key]) { - urlSearchParams.delete(key) - } else { - let values: string[] - if (Array.isArray(value)) { - values = value - } else { - values = [value] - } - const encodedValues = values - .map((v) => encodeURIComponent(v)) - .join(',') - urlSearchParams.set(key, encodedValues) - } - } - navigate({ pathname, search: urlSearchParams.toString() }) - }, - }) -} diff --git a/src/utils.ts b/src/utils.ts index c13ca90..c49ff18 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { Query } from './useQuery' +import { Query } from './engine/query' export type NonNullableRecord< T extends Record diff --git a/src/zod/index.ts b/src/zod/index.ts new file mode 100644 index 0000000..8a7a318 --- /dev/null +++ b/src/zod/index.ts @@ -0,0 +1 @@ +export * from './routers' diff --git a/src/zod/routers/inMemory.ts b/src/zod/routers/inMemory.ts new file mode 100644 index 0000000..25b410c --- /dev/null +++ b/src/zod/routers/inMemory.ts @@ -0,0 +1,38 @@ +import { ZodType, ZodTypeDef } from 'zod' +import { PaginationQuery, AbstractQuery } from '../../engine' +import { + useInMemoryQuery as useInMemoryQueryVanilla, + useInMemoryPagination as useInMemoryPaginationVanilla, + useInMemoryQueryAndPagination as useInMemoryQueryAndPaginationVanilla, +} from '../../routers/inMemory' +import { zodParser } from '../util' + +export const useInMemoryQuery = < + TQuery extends AbstractQuery, + // TODO: `TSchemaOutput` should not just extend `Partial`, but be exactly `Partial`, with not additional keys + TSchemaOutput extends Partial = Partial, + TSchemaDef extends ZodTypeDef = ZodTypeDef, + TSchemaInput = TSchemaOutput +>( + defaultQuery: TQuery, + schemaQuery: ZodType +) => useInMemoryQueryVanilla(defaultQuery, zodParser(schemaQuery)) + +export const useInMemoryQueryAndPagination = < + TQuery extends AbstractQuery, + TSchemaOutput extends Partial = Partial, + TSchemaDef extends ZodTypeDef = ZodTypeDef, + TSchemaInput = TSchemaOutput +>( + defaultQuery: TQuery, + schemaQuery: ZodType, + defaultPagination?: PaginationQuery +) => + useInMemoryQueryAndPaginationVanilla( + defaultQuery, + zodParser(schemaQuery), + defaultPagination + ) + +export const useInMemoryPagination = (defaultPagination?: PaginationQuery) => + useInMemoryPaginationVanilla(defaultPagination) diff --git a/src/zod/routers/index.ts b/src/zod/routers/index.ts new file mode 100644 index 0000000..13bbd9f --- /dev/null +++ b/src/zod/routers/index.ts @@ -0,0 +1 @@ +export * from './nextRouter' diff --git a/src/zod/routers/nextRouter.ts b/src/zod/routers/nextRouter.ts new file mode 100644 index 0000000..9421660 --- /dev/null +++ b/src/zod/routers/nextRouter.ts @@ -0,0 +1,38 @@ +import { ZodType, ZodTypeDef } from 'zod' +import { PaginationQuery, AbstractQuery } from '../../engine' +import { + useNextRouterQuery as useNextRouterQueryVanilla, + useNextRouterPagination as useNextRouterPaginationVanilla, + useNextRouterQueryAndPagination as useNextRouterQueryAndPaginationVanilla, +} from '../../routers/nextRouter' +import { zodParser } from '../util' + +export const useNextRouterQuery = < + TQuery extends AbstractQuery, + // TODO: `TSchemaOutput` should not just extend `Partial`, but be exactly `Partial`, with not additional keys + TSchemaOutput extends Partial = Partial, + TSchemaDef extends ZodTypeDef = ZodTypeDef, + TSchemaInput = TSchemaOutput +>( + defaultQuery: TQuery, + schemaQuery: ZodType +) => useNextRouterQueryVanilla(defaultQuery, zodParser(schemaQuery)) + +export const useNextRouterQueryAndPagination = < + TQuery extends AbstractQuery, + TSchemaOutput extends Partial = Partial, + TSchemaDef extends ZodTypeDef = ZodTypeDef, + TSchemaInput = TSchemaOutput +>( + defaultQuery: TQuery, + schemaQuery: ZodType, + defaultPagination?: PaginationQuery +) => + useNextRouterQueryAndPaginationVanilla( + defaultQuery, + zodParser(schemaQuery), + defaultPagination + ) + +export const useNextRouterPagination = (defaultPagination?: PaginationQuery) => + useNextRouterPaginationVanilla(defaultPagination) diff --git a/src/zod/routers/reactRouter.ts b/src/zod/routers/reactRouter.ts new file mode 100644 index 0000000..9b90387 --- /dev/null +++ b/src/zod/routers/reactRouter.ts @@ -0,0 +1,38 @@ +import { ZodType, ZodTypeDef } from 'zod' +import { PaginationQuery, AbstractQuery } from '../../engine' +import { + useReactRouterQuery as useReactRouterQueryVanilla, + useReactRouterPagination as useReactRouterPaginationVanilla, + useReactRouterQueryAndPagination as useReactRouterQueryAndPaginationVanilla, +} from '../../routers/reactRouter' +import { zodParser } from '../util' + +export const useReactRouterQuery = < + TQuery extends AbstractQuery, + // TODO: `TSchemaOutput` should not just extend `Partial`, but be exactly `Partial`, with not additional keys + TSchemaOutput extends Partial = Partial, + TSchemaDef extends ZodTypeDef = ZodTypeDef, + TSchemaInput = TSchemaOutput +>( + defaultQuery: TQuery, + schemaQuery: ZodType +) => useReactRouterQueryVanilla(defaultQuery, zodParser(schemaQuery)) + +export const useReactRouterQueryAndPagination = < + TQuery extends AbstractQuery, + TSchemaOutput extends Partial = Partial, + TSchemaDef extends ZodTypeDef = ZodTypeDef, + TSchemaInput = TSchemaOutput +>( + defaultQuery: TQuery, + schemaQuery: ZodType, + defaultPagination?: PaginationQuery +) => + useReactRouterQueryAndPaginationVanilla( + defaultQuery, + zodParser(schemaQuery), + defaultPagination + ) + +export const useReactRouterPagination = (defaultPagination?: PaginationQuery) => + useReactRouterPaginationVanilla(defaultPagination) diff --git a/src/zod/util.ts b/src/zod/util.ts new file mode 100644 index 0000000..fab1924 --- /dev/null +++ b/src/zod/util.ts @@ -0,0 +1,14 @@ +import { ZodType, ZodTypeDef } from 'zod' +import { AbstractQuery, ParseQuery } from '../engine/query' + +export const zodParser = + < + TQuery extends AbstractQuery, + Output extends Partial, + Def extends ZodTypeDef, + Input + >( + schema: ZodType + ): ParseQuery => + (q) => + schema.parse(q) From 874056f13d0b1fba190a53aa28c7c6e22a022242 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 08:15:57 +0200 Subject: [PATCH 07/26] use new checks script where relevant and improve publishing and versioning --- .githooks/pre-commit | 3 +-- .github/workflows/test.yml | 4 +--- package.json | 5 +++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 001b0a4..1f1532b 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -3,5 +3,4 @@ set -e set -o pipefail -npm run lint -npm run typecheck +npm run checks diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db94f16..c704bf3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,4 @@ jobs: node-version: '16.x' registry-url: 'https://registry.npmjs.org' - run: npm install - - run: npm run lint - - run: npm run typecheck - - run: npm run test + - run: npm run checks diff --git a/package.json b/package.json index df75ad0..b920145 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,10 @@ "test:watch": "vitest watch --passWithNoTests", "typecheck": "tsc --noEmit", "checks": "npm run test && npm run lint && npm run typecheck", + "prepublishOnly": "npm run checks", "prepare": "npm run build", - "prepublishOnly": "npm run test && npm run lint", - "version": "npm run test && npm run lint && git add -A src", + "preversion": "npm run checks", + "version": "git add -A src", "postversion": "git push && git push --tags" }, "repository": { From 8371b39d9236905f5f7ee3865e9aaba5dacd6675 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 09:20:09 +0200 Subject: [PATCH 08/26] fix exports --- src/routers/index.ts | 2 ++ src/zod/routers/index.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/routers/index.ts b/src/routers/index.ts index 13bbd9f..a58cc7f 100644 --- a/src/routers/index.ts +++ b/src/routers/index.ts @@ -1 +1,3 @@ export * from './nextRouter' +export * from './inMemory' +export * from './reactRouter' diff --git a/src/zod/routers/index.ts b/src/zod/routers/index.ts index 13bbd9f..a58cc7f 100644 --- a/src/zod/routers/index.ts +++ b/src/zod/routers/index.ts @@ -1 +1,3 @@ export * from './nextRouter' +export * from './inMemory' +export * from './reactRouter' From f2e5f4d84fff557de9fcd5c5a344c41cd0d6866b Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 09:20:36 +0200 Subject: [PATCH 09/26] add in-memory router tests --- src/routers/__test__/inMemory.test.ts | 193 ++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 src/routers/__test__/inMemory.test.ts diff --git a/src/routers/__test__/inMemory.test.ts b/src/routers/__test__/inMemory.test.ts new file mode 100644 index 0000000..27a6c18 --- /dev/null +++ b/src/routers/__test__/inMemory.test.ts @@ -0,0 +1,193 @@ +import { act, renderHook } from '@testing-library/react' +import { z } from 'zod' +import { NonNullableRecord } from '../../utils' +import { + useInMemoryQuery, + useInMemoryPagination, + useInMemoryQueryAndPagination, +} from '../../zod' + +describe('InMemory', () => { + const searchSchema = z.object({ + search: z.string().optional().catch(undefined), + }) + + const useInMemoryQueryWithSearch = ( + defaultQuery: NonNullableRecord> + ) => useInMemoryQuery(defaultQuery, searchSchema) + + test('should set default page and size', () => { + const page = 0 + const size = 15 + const { result } = renderHook(() => useInMemoryPagination({ page, size })) + + expect(result.current.page).toBe(page) + expect(result.current.size).toBe(size) + }) + + test('should change page', () => { + const page = 2 + + const { result } = renderHook(() => useInMemoryPagination()) + + act(() => { + result.current.setPage(page) + }) + + expect(result.current.page).toBe(page) + }) + + test('should change search', () => { + const search = 'Max' + + const { result } = renderHook(() => + useInMemoryQueryWithSearch({ search: '' }) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + expect(result.current.query?.search).toBe(search) + }) + + test('changing pagination should not reset the remaining query', () => { + const search = 'Max' + const page = 2 + + const { result } = renderHook(() => + useInMemoryQueryAndPagination({ search: '' }, searchSchema) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + act(() => { + result.current.setPage(page) + }) + + expect(result.current.query.search).toBe(search) + + act(() => { + result.current.resetPagination() + }) + + expect(result.current.query.search).toBe(search) + expect(result.current.page).toBe(0) + }) + + test('change default parameters', () => { + const page = 1 + const size = 10 + const { result } = renderHook(() => useInMemoryPagination({ page, size })) + + expect(result.current.page).toBe(page) + expect(result.current.size).toBe(size) + }) + + test('changing the remaining query should reset the page', () => { + const defaultSearch = '' + const search1 = 'Max' + const search2 = 'Peter' + const page = 2 + + const { result } = renderHook(() => + useInMemoryQueryAndPagination({ search: defaultSearch }, searchSchema) + ) + + act(() => { + result.current.setQuery({ search: search1 }) + }) + + act(() => { + result.current.setPage(page) + }) + + expect(result.current.query.search).toBe(search1) + expect(result.current.page).toBe(page) + + act(() => { + result.current.setQuery({ search: search2 }) + }) + + expect(result.current.query.search).toBe(search2) + expect(result.current.page).toBe(0) + + act(() => { + result.current.resetQuery() + }) + + expect(result.current.query.search).toBe(defaultSearch) + expect(result.current.page).toBe(0) + }) + + test('changing the remaining query with resetPage set to false should not reset the page', () => { + const defaultSearch = '' + const search = 'Max' + const page = 2 + + const { result } = renderHook(() => + useInMemoryQueryAndPagination({ search: defaultSearch }, searchSchema) + ) + + act(() => { + result.current.setPage(page) + }) + + expect(result.current.page).toBe(page) + + act(() => { + result.current.setQuery({ search }, { resetPage: false }) + }) + + expect(result.current.query.search).toBe(search) + expect(result.current.page).toBe(page) + + act(() => { + result.current.resetQuery({ resetPage: false }) + }) + + expect(result.current.query.search).toBe(defaultSearch) + expect(result.current.page).toBe(page) + }) + + test('setting a query key should not overwrite other query keys', () => { + const search = 'Max' + const department = 'IT' + + const schema = z.object({ + search: z.string().optional().catch(undefined), + department: z.string().optional().catch(undefined), + }) + + const { result } = renderHook(() => + useInMemoryQuery({ search: '', department: '' }, schema) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + act(() => { + result.current.setQuery({ department }) + }) + + expect(result.current.query.search).toBe(search) + expect(result.current.query.department).toBe(department) + }) + + test('default query value should not need to be an empty string', () => { + const search = '' + + const { result } = renderHook(() => + useInMemoryQuery({ search: 'Default search' }, searchSchema) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + expect(result.current.query.search).toBe(search) + }) +}) From bbf4d779902ec43dded366f8eee503224cfd2f93 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 09:39:18 +0200 Subject: [PATCH 10/26] add react-router tests --- src/routers/__test__/reactRouter.test.tsx | 257 ++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 src/routers/__test__/reactRouter.test.tsx diff --git a/src/routers/__test__/reactRouter.test.tsx b/src/routers/__test__/reactRouter.test.tsx new file mode 100644 index 0000000..bf4d904 --- /dev/null +++ b/src/routers/__test__/reactRouter.test.tsx @@ -0,0 +1,257 @@ +import { act, renderHook } from '@testing-library/react' +import { z } from 'zod' +import { BrowserRouter } from 'react-router-dom' +import { NonNullableRecord } from '../../utils' +import { + useReactRouterQuery, + useReactRouterPagination, + useReactRouterQueryAndPagination, +} from '../../zod' + +const renderHookWithContext = ( + render: (initialProps: Props) => Result +) => + renderHook(render, { + wrapper: ({ children }) => { + return {children} + }, + }) + +describe('ReactRouter', () => { + beforeEach(() => { + window.history.pushState({}, '', '/') + }) + + const searchSchema = z.object({ + search: z.string().optional().catch(undefined), + }) + + const useReactRouterQueryWithSearch = ( + defaultQuery: NonNullableRecord> + ) => useReactRouterQuery(defaultQuery, searchSchema) + + test('should set default page and size', () => { + const page = 0 + const size = 15 + const { result } = renderHookWithContext(() => + useReactRouterPagination({ page, size }) + ) + + expect(result.current.page).toBe(page) + expect(result.current.size).toBe(size) + }) + + test('should change page', () => { + const page = 2 + + const { result } = renderHookWithContext(() => useReactRouterPagination()) + + act(() => { + result.current.setPage(page) + }) + + expect(result.current.page).toBe(page) + expect(window.location.search).toBe(`?page=${page}`) + }) + + test('should change search', () => { + const search = 'Max' + + const { result } = renderHookWithContext(() => + useReactRouterQueryWithSearch({ search: '' }) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + expect(result.current.query?.search).toBe(search) + expect(window.location.search).toBe(`?search=${search}`) + }) + + test('changing pagination should not reset the remaining query', () => { + const search = 'Max' + const page = 2 + + const { result } = renderHookWithContext(() => + useReactRouterQueryAndPagination({ search: '' }, searchSchema) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + act(() => { + result.current.setPage(page) + }) + + expect(window.location.search).toBe(`?search=${search}&page=${page}`) + + act(() => { + result.current.resetPagination() + }) + + expect(result.current.query.search).toBe(search) + expect(result.current.page).toBe(0) + expect(window.location.search).toBe(`?search=${search}`) + }) + + test('change default parameters', () => { + const page = 1 + const size = 10 + const { result } = renderHookWithContext(() => + useReactRouterPagination({ page, size }) + ) + + expect(result.current.page).toBe(page) + expect(result.current.size).toBe(size) + }) + + test('changing the remaining query should reset the page', () => { + const defaultSearch = '' + const search1 = 'Max' + const search2 = 'Peter' + const page = 2 + + const { result } = renderHookWithContext(() => + useReactRouterQueryAndPagination({ search: defaultSearch }, searchSchema) + ) + + act(() => { + result.current.setQuery({ search: search1 }) + }) + + act(() => { + result.current.setPage(page) + }) + + expect(result.current.query.search).toBe(search1) + expect(result.current.page).toBe(page) + expect(window.location.search).toBe(`?search=${search1}&page=${page}`) + + act(() => { + result.current.setQuery({ search: search2 }) + }) + + expect(result.current.query.search).toBe(search2) + expect(result.current.page).toBe(0) + expect(window.location.search).toBe(`?search=${search2}`) + + act(() => { + result.current.resetQuery() + }) + + expect(result.current.query.search).toBe(defaultSearch) + expect(result.current.page).toBe(0) + expect(window.location.search).toBe('') + }) + + test('changing the remaining query with resetPage set to false should not reset the page', () => { + const defaultSearch = '' + const search = 'Max' + const page = 2 + + const { result } = renderHookWithContext(() => + useReactRouterQueryAndPagination({ search: defaultSearch }, searchSchema) + ) + + act(() => { + result.current.setPage(page) + }) + + expect(result.current.page).toBe(page) + expect(window.location.search).toBe(`?page=${page}`) + + act(() => { + result.current.setQuery({ search }, { resetPage: false }) + }) + + expect(result.current.query.search).toBe(search) + expect(result.current.page).toBe(page) + expect(window.location.search).toBe(`?page=${page}&search=${search}`) + + act(() => { + result.current.resetQuery({ resetPage: false }) + }) + + expect(result.current.query.search).toBe(defaultSearch) + expect(result.current.page).toBe(page) + expect(window.location.search).toBe(`?page=${page}`) + }) + + test('setting a query key should not overwrite other query keys', () => { + const search = 'Max' + const department = 'IT' + + const schema = z.object({ + search: z.string().optional().catch(undefined), + department: z.string().optional().catch(undefined), + }) + + const { result } = renderHookWithContext(() => + useReactRouterQuery({ search: '', department: '' }, schema) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + act(() => { + result.current.setQuery({ department }) + }) + + expect(result.current.query.search).toBe(search) + expect(result.current.query.department).toBe(department) + }) + + test('unspecified query keys should be left untouched', () => { + const greeting = 'hello' + const search = 'Max' + window.history.pushState({}, '', `/?greeting=${greeting}`) + + const { result } = renderHookWithContext(() => + useReactRouterQuery({ search: '' }, searchSchema) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + expect(result.current.query.search).toBe(search) + expect(window.location.search).toBe( + `?greeting=${greeting}&search=${search}` + ) + }) + + test('query keys with default value should not be stored in the url', () => { + const defaultSearch = '' + + window.history.pushState({}, '', `/?search=Max`) + + const { result } = renderHookWithContext(() => + useReactRouterQuery({ search: defaultSearch }, searchSchema) + ) + + act(() => { + result.current.resetQuery() + }) + + expect(result.current.query.search).toBe(defaultSearch) + expect(window.location.search).toBe('') + }) + + test('default query value should not need to be an empty string', () => { + const search = '' + + const { result } = renderHookWithContext(() => + useReactRouterQuery({ search: 'Default search' }, searchSchema) + ) + + act(() => { + result.current.setQuery({ search }) + }) + + expect(result.current.query.search).toBe(search) + expect(window.location.search).toBe(`?search=`) + }) +}) From 62e4b2c9351b9b7dbe7efcb20dae3b9b1c54718d Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 09:52:34 +0200 Subject: [PATCH 11/26] try using relative paths for entry points --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b920145..b38b495 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,14 @@ "types": "dist/types/index.d.ts", "exports": { ".": { - "types": "dist/types/index.d.ts", - "import": "dist/esm/index.js", - "require": "dist/cjs/index.js" + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" }, "./zod": { - "types": "dist/zod/types/index.d.ts", - "import": "dist/zod/esm/index.js", - "require": "dist/zod/cjs/index.js" + "types": "./dist/zod/types/index.d.ts", + "import": "./dist/zod/esm/index.js", + "require": "./dist/zod/cjs/index.js" } }, "engines": { From 9d7478cdebdd93876a3f08108cc49b1c4591cb0f Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 09:55:41 +0200 Subject: [PATCH 12/26] fix zod entry point --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b38b495..097afd1 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "require": "./dist/cjs/index.js" }, "./zod": { - "types": "./dist/zod/types/index.d.ts", - "import": "./dist/zod/esm/index.js", - "require": "./dist/zod/cjs/index.js" + "types": "./dist/types/zod/index.d.ts", + "import": "./dist/esm/zod/index.js", + "require": "./dist/cjs/zod/index.js" } }, "engines": { From c4d67cd12b344d117dbdaace86b9c896e1b6f807 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 10:04:03 +0200 Subject: [PATCH 13/26] add number example to next router tests --- src/routers/__test__/nextRouter.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/routers/__test__/nextRouter.test.ts b/src/routers/__test__/nextRouter.test.ts index 08f6b78..4823af2 100644 --- a/src/routers/__test__/nextRouter.test.ts +++ b/src/routers/__test__/nextRouter.test.ts @@ -180,14 +180,19 @@ describe('NextRouter', () => { test('setting a query key should not overwrite other query keys', () => { const search = 'Max' const department = 'IT' + const defaultAge = 42 const schema = z.object({ search: z.string().optional().catch(undefined), department: z.string().optional().catch(undefined), + age: z.string().pipe(z.coerce.number().optional()).catch(undefined), }) const { result } = renderHook(() => - useNextRouterQuery({ search: '', department: '' }, schema) + useNextRouterQuery( + { search: '', department: '', age: defaultAge }, + schema + ) ) act(() => { @@ -200,6 +205,7 @@ describe('NextRouter', () => { expect(result.current.query.search).toBe(search) expect(result.current.query.department).toBe(department) + expect(result.current.query.age).toBe(defaultAge) }) test('unspecified query keys should be left untouched', () => { From 7242d8521fa60ce3cd7b27543153877cdc3287ea Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 10:06:37 +0200 Subject: [PATCH 14/26] adjust package tags --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 097afd1..3ad35a0 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,9 @@ "private": false, "keywords": [ "react", + "query", "pagination", - "hooks" + "zod" ], "author": "About Bits", "license": "MIT", From a72d134f415cce2946b3bf2cd206752ebac642b2 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 10:33:28 +0200 Subject: [PATCH 15/26] use an entry point for each router --- package.json | 42 +++++++++++++++++------ src/index.ts | 2 -- src/routers/__test__/inMemory.test.ts | 26 +++++++------- src/routers/__test__/nextRouter.test.ts | 35 ++++++++----------- src/routers/__test__/reactRouter.test.tsx | 30 ++++++++-------- src/routers/inMemory.ts | 8 ++--- src/routers/index.ts | 3 -- src/routers/nextRouter.ts | 10 +++--- src/routers/reactRouter.ts | 10 +++--- src/zod/index.ts | 1 - src/zod/routers/inMemory.ts | 18 +++++----- src/zod/routers/index.ts | 3 -- src/zod/routers/nextRouter.ts | 18 +++++----- src/zod/routers/reactRouter.ts | 18 +++++----- 14 files changed, 114 insertions(+), 110 deletions(-) delete mode 100644 src/index.ts delete mode 100644 src/routers/index.ts delete mode 100644 src/zod/index.ts delete mode 100644 src/zod/routers/index.ts diff --git a/package.json b/package.json index 3ad35a0..4d8012c 100644 --- a/package.json +++ b/package.json @@ -2,19 +2,41 @@ "name": "@aboutbits/react-pagination", "version": "1.0.2", "description": "Pagination hooks for React", - "main": "dist/cjs/index.js", - "module": "dist/esm/index.js", - "types": "dist/types/index.d.ts", "exports": { ".": { - "types": "./dist/types/index.d.ts", - "import": "./dist/esm/index.js", - "require": "./dist/cjs/index.js" + "types": "./dist/types/engine/index.d.ts", + "import": "./dist/esm/engine/index.js", + "require": "./dist/cjs/engine/index.js" }, - "./zod": { - "types": "./dist/types/zod/index.d.ts", - "import": "./dist/esm/zod/index.js", - "require": "./dist/cjs/zod/index.js" + "./next-router": { + "types": "./dist/types/routers/nextRouter.d.ts", + "import": "./dist/esm/routers/nextRouter.js", + "require": "./dist/cjs/routers/nextRouter.js" + }, + "./in-memory": { + "types": "./dist/types/routers/inMemory.d.ts", + "import": "./dist/esm/routers/inMemory.js", + "require": "./dist/cjs/routers/inMemory.js" + }, + "./react-router": { + "types": "./dist/types/routers/reactRouter.d.ts", + "import": "./dist/esm/routers/reactRouter.js", + "require": "./dist/cjs/routers/reactRouter.js" + }, + "./next-router/zod": { + "types": "./dist/types/zod/routers/nextRouter.d.ts", + "import": "./dist/esm/zod/routers/nextRouter.js", + "require": "./dist/cjs/zod/routers/nextRouter.js" + }, + "./in-memory/zod": { + "types": "./dist/types/zod/routers/inMemory.d.ts", + "import": "./dist/esm/zod/routers/inMemory.js", + "require": "./dist/cjs/zod/routers/inMemory.js" + }, + "./react-router/zod": { + "types": "./dist/types/zod/routers/reactRouter.d.ts", + "import": "./dist/esm/zod/routers/reactRouter.js", + "require": "./dist/cjs/zod/routers/reactRouter.js" } }, "engines": { diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 6199638..0000000 --- a/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './engine' -export * from './routers' diff --git a/src/routers/__test__/inMemory.test.ts b/src/routers/__test__/inMemory.test.ts index 27a6c18..9c1e895 100644 --- a/src/routers/__test__/inMemory.test.ts +++ b/src/routers/__test__/inMemory.test.ts @@ -2,10 +2,10 @@ import { act, renderHook } from '@testing-library/react' import { z } from 'zod' import { NonNullableRecord } from '../../utils' import { - useInMemoryQuery, - useInMemoryPagination, - useInMemoryQueryAndPagination, -} from '../../zod' + useQuery, + usePagination, + useQueryAndPagination, +} from '../../zod/routers/inMemory' describe('InMemory', () => { const searchSchema = z.object({ @@ -14,12 +14,12 @@ describe('InMemory', () => { const useInMemoryQueryWithSearch = ( defaultQuery: NonNullableRecord> - ) => useInMemoryQuery(defaultQuery, searchSchema) + ) => useQuery(defaultQuery, searchSchema) test('should set default page and size', () => { const page = 0 const size = 15 - const { result } = renderHook(() => useInMemoryPagination({ page, size })) + const { result } = renderHook(() => usePagination({ page, size })) expect(result.current.page).toBe(page) expect(result.current.size).toBe(size) @@ -28,7 +28,7 @@ describe('InMemory', () => { test('should change page', () => { const page = 2 - const { result } = renderHook(() => useInMemoryPagination()) + const { result } = renderHook(() => usePagination()) act(() => { result.current.setPage(page) @@ -56,7 +56,7 @@ describe('InMemory', () => { const page = 2 const { result } = renderHook(() => - useInMemoryQueryAndPagination({ search: '' }, searchSchema) + useQueryAndPagination({ search: '' }, searchSchema) ) act(() => { @@ -80,7 +80,7 @@ describe('InMemory', () => { test('change default parameters', () => { const page = 1 const size = 10 - const { result } = renderHook(() => useInMemoryPagination({ page, size })) + const { result } = renderHook(() => usePagination({ page, size })) expect(result.current.page).toBe(page) expect(result.current.size).toBe(size) @@ -93,7 +93,7 @@ describe('InMemory', () => { const page = 2 const { result } = renderHook(() => - useInMemoryQueryAndPagination({ search: defaultSearch }, searchSchema) + useQueryAndPagination({ search: defaultSearch }, searchSchema) ) act(() => { @@ -128,7 +128,7 @@ describe('InMemory', () => { const page = 2 const { result } = renderHook(() => - useInMemoryQueryAndPagination({ search: defaultSearch }, searchSchema) + useQueryAndPagination({ search: defaultSearch }, searchSchema) ) act(() => { @@ -162,7 +162,7 @@ describe('InMemory', () => { }) const { result } = renderHook(() => - useInMemoryQuery({ search: '', department: '' }, schema) + useQuery({ search: '', department: '' }, schema) ) act(() => { @@ -181,7 +181,7 @@ describe('InMemory', () => { const search = '' const { result } = renderHook(() => - useInMemoryQuery({ search: 'Default search' }, searchSchema) + useQuery({ search: 'Default search' }, searchSchema) ) act(() => { diff --git a/src/routers/__test__/nextRouter.test.ts b/src/routers/__test__/nextRouter.test.ts index 4823af2..31f05c4 100644 --- a/src/routers/__test__/nextRouter.test.ts +++ b/src/routers/__test__/nextRouter.test.ts @@ -4,10 +4,10 @@ import { z } from 'zod' import { vi } from 'vitest' import { NonNullableRecord } from '../../utils' import { - useNextRouterQuery, - useNextRouterPagination, - useNextRouterQueryAndPagination, -} from '../../zod' + useQuery, + usePagination, + useQueryAndPagination, +} from '../../zod/routers/nextRouter' vi.mock('next/router', () => require('next-router-mock')) @@ -22,12 +22,12 @@ describe('NextRouter', () => { const useNextRouterQueryWithSearch = ( defaultQuery: NonNullableRecord> - ) => useNextRouterQuery(defaultQuery, searchSchema) + ) => useQuery(defaultQuery, searchSchema) test('should set default page and size', () => { const page = 0 const size = 15 - const { result } = renderHook(() => useNextRouterPagination({ page, size })) + const { result } = renderHook(() => usePagination({ page, size })) expect(result.current.page).toBe(page) expect(result.current.size).toBe(size) @@ -36,7 +36,7 @@ describe('NextRouter', () => { test('should change page', () => { const page = 2 - const { result } = renderHook(() => useNextRouterPagination()) + const { result } = renderHook(() => usePagination()) act(() => { result.current.setPage(page) @@ -66,7 +66,7 @@ describe('NextRouter', () => { const page = 2 const { result } = renderHook(() => - useNextRouterQueryAndPagination({ search: '' }, searchSchema) + useQueryAndPagination({ search: '' }, searchSchema) ) act(() => { @@ -93,7 +93,7 @@ describe('NextRouter', () => { test('change default parameters', () => { const page = 1 const size = 10 - const { result } = renderHook(() => useNextRouterPagination({ page, size })) + const { result } = renderHook(() => usePagination({ page, size })) expect(result.current.page).toBe(page) expect(result.current.size).toBe(size) @@ -106,7 +106,7 @@ describe('NextRouter', () => { const page = 2 const { result } = renderHook(() => - useNextRouterQueryAndPagination({ search: defaultSearch }, searchSchema) + useQueryAndPagination({ search: defaultSearch }, searchSchema) ) act(() => { @@ -148,7 +148,7 @@ describe('NextRouter', () => { const page = 2 const { result } = renderHook(() => - useNextRouterQueryAndPagination({ search: defaultSearch }, searchSchema) + useQueryAndPagination({ search: defaultSearch }, searchSchema) ) act(() => { @@ -189,10 +189,7 @@ describe('NextRouter', () => { }) const { result } = renderHook(() => - useNextRouterQuery( - { search: '', department: '', age: defaultAge }, - schema - ) + useQuery({ search: '', department: '', age: defaultAge }, schema) ) act(() => { @@ -212,9 +209,7 @@ describe('NextRouter', () => { const greeting = 'hello' router.query = { greeting } - const { result } = renderHook(() => - useNextRouterQuery({ search: '' }, searchSchema) - ) + const { result } = renderHook(() => useQuery({ search: '' }, searchSchema)) act(() => { result.current.setQuery({ search: 'Max' }) @@ -230,7 +225,7 @@ describe('NextRouter', () => { router.query = { search: 'Max' } const { result } = renderHook(() => - useNextRouterQuery({ search: defaultSearch }, searchSchema) + useQuery({ search: defaultSearch }, searchSchema) ) act(() => { @@ -245,7 +240,7 @@ describe('NextRouter', () => { const search = '' const { result } = renderHook(() => - useNextRouterQuery({ search: 'Default search' }, searchSchema) + useQuery({ search: 'Default search' }, searchSchema) ) act(() => { diff --git a/src/routers/__test__/reactRouter.test.tsx b/src/routers/__test__/reactRouter.test.tsx index bf4d904..4ffcf62 100644 --- a/src/routers/__test__/reactRouter.test.tsx +++ b/src/routers/__test__/reactRouter.test.tsx @@ -3,10 +3,10 @@ import { z } from 'zod' import { BrowserRouter } from 'react-router-dom' import { NonNullableRecord } from '../../utils' import { - useReactRouterQuery, - useReactRouterPagination, - useReactRouterQueryAndPagination, -} from '../../zod' + useQuery, + usePagination, + useQueryAndPagination, +} from '../../zod/routers/reactRouter' const renderHookWithContext = ( render: (initialProps: Props) => Result @@ -28,13 +28,13 @@ describe('ReactRouter', () => { const useReactRouterQueryWithSearch = ( defaultQuery: NonNullableRecord> - ) => useReactRouterQuery(defaultQuery, searchSchema) + ) => useQuery(defaultQuery, searchSchema) test('should set default page and size', () => { const page = 0 const size = 15 const { result } = renderHookWithContext(() => - useReactRouterPagination({ page, size }) + usePagination({ page, size }) ) expect(result.current.page).toBe(page) @@ -44,7 +44,7 @@ describe('ReactRouter', () => { test('should change page', () => { const page = 2 - const { result } = renderHookWithContext(() => useReactRouterPagination()) + const { result } = renderHookWithContext(() => usePagination()) act(() => { result.current.setPage(page) @@ -74,7 +74,7 @@ describe('ReactRouter', () => { const page = 2 const { result } = renderHookWithContext(() => - useReactRouterQueryAndPagination({ search: '' }, searchSchema) + useQueryAndPagination({ search: '' }, searchSchema) ) act(() => { @@ -100,7 +100,7 @@ describe('ReactRouter', () => { const page = 1 const size = 10 const { result } = renderHookWithContext(() => - useReactRouterPagination({ page, size }) + usePagination({ page, size }) ) expect(result.current.page).toBe(page) @@ -114,7 +114,7 @@ describe('ReactRouter', () => { const page = 2 const { result } = renderHookWithContext(() => - useReactRouterQueryAndPagination({ search: defaultSearch }, searchSchema) + useQueryAndPagination({ search: defaultSearch }, searchSchema) ) act(() => { @@ -152,7 +152,7 @@ describe('ReactRouter', () => { const page = 2 const { result } = renderHookWithContext(() => - useReactRouterQueryAndPagination({ search: defaultSearch }, searchSchema) + useQueryAndPagination({ search: defaultSearch }, searchSchema) ) act(() => { @@ -189,7 +189,7 @@ describe('ReactRouter', () => { }) const { result } = renderHookWithContext(() => - useReactRouterQuery({ search: '', department: '' }, schema) + useQuery({ search: '', department: '' }, schema) ) act(() => { @@ -210,7 +210,7 @@ describe('ReactRouter', () => { window.history.pushState({}, '', `/?greeting=${greeting}`) const { result } = renderHookWithContext(() => - useReactRouterQuery({ search: '' }, searchSchema) + useQuery({ search: '' }, searchSchema) ) act(() => { @@ -229,7 +229,7 @@ describe('ReactRouter', () => { window.history.pushState({}, '', `/?search=Max`) const { result } = renderHookWithContext(() => - useReactRouterQuery({ search: defaultSearch }, searchSchema) + useQuery({ search: defaultSearch }, searchSchema) ) act(() => { @@ -244,7 +244,7 @@ describe('ReactRouter', () => { const search = '' const { result } = renderHookWithContext(() => - useReactRouterQuery({ search: 'Default search' }, searchSchema) + useQuery({ search: 'Default search' }, searchSchema) ) act(() => { diff --git a/src/routers/inMemory.ts b/src/routers/inMemory.ts index 3baeb01..6e110b2 100644 --- a/src/routers/inMemory.ts +++ b/src/routers/inMemory.ts @@ -29,7 +29,7 @@ const useInMemoryRouter = (): Router => { } } -export const useInMemoryQuery = ( +export const useQuery = ( defaultQuery: T, parseQuery: ParseQuery, options?: Partial @@ -38,7 +38,7 @@ export const useInMemoryQuery = ( return useAbstractQuery(defaultQuery, parseQuery, router, options) } -export const useInMemoryQueryAndPagination = ( +export const useQueryAndPagination = ( defaultQuery: T, parseQuery: ParseQuery, defaultPagination?: PaginationQuery @@ -52,8 +52,8 @@ export const useInMemoryQueryAndPagination = ( ) } -export const useInMemoryPagination = (defaultPagination?: PaginationQuery) => { +export const usePagination = (defaultPagination?: PaginationQuery) => { const { page, size, setPage, setSize, setPagination, resetPagination } = - useInMemoryQueryAndPagination({}, () => ({}), defaultPagination) + useQueryAndPagination({}, () => ({}), defaultPagination) return { page, size, setPage, setSize, setPagination, resetPagination } } diff --git a/src/routers/index.ts b/src/routers/index.ts deleted file mode 100644 index a58cc7f..0000000 --- a/src/routers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './nextRouter' -export * from './inMemory' -export * from './reactRouter' diff --git a/src/routers/nextRouter.ts b/src/routers/nextRouter.ts index 14f25be..5c6e90c 100644 --- a/src/routers/nextRouter.ts +++ b/src/routers/nextRouter.ts @@ -37,7 +37,7 @@ const useNextRouter = (): Router => { } } -export const useNextRouterQuery = ( +export const useQuery = ( defaultQuery: T, parseQuery: ParseQuery, options?: Partial @@ -46,7 +46,7 @@ export const useNextRouterQuery = ( return useAbstractQuery(defaultQuery, parseQuery, router, options) } -export const useNextRouterQueryAndPagination = ( +export const useQueryAndPagination = ( defaultQuery: T, parseQuery: ParseQuery, defaultPagination?: PaginationQuery @@ -60,10 +60,8 @@ export const useNextRouterQueryAndPagination = ( ) } -export const useNextRouterPagination = ( - defaultPagination?: PaginationQuery -) => { +export const usePagination = (defaultPagination?: PaginationQuery) => { const { page, size, setPage, setSize, setPagination, resetPagination } = - useNextRouterQueryAndPagination({}, () => ({}), defaultPagination) + useQueryAndPagination({}, () => ({}), defaultPagination) return { page, size, setPage, setSize, setPagination, resetPagination } } diff --git a/src/routers/reactRouter.ts b/src/routers/reactRouter.ts index b31f9b2..5eb0026 100644 --- a/src/routers/reactRouter.ts +++ b/src/routers/reactRouter.ts @@ -53,7 +53,7 @@ const useReactRouter = (): Router => { } } -export const useReactRouterQuery = ( +export const useQuery = ( defaultQuery: T, parseQuery: ParseQuery, options?: Partial @@ -62,7 +62,7 @@ export const useReactRouterQuery = ( return useAbstractQuery(defaultQuery, parseQuery, router, options) } -export const useReactRouterQueryAndPagination = ( +export const useQueryAndPagination = ( defaultQuery: T, parseQuery: ParseQuery, defaultPagination?: PaginationQuery @@ -76,10 +76,8 @@ export const useReactRouterQueryAndPagination = ( ) } -export const useReactRouterPagination = ( - defaultPagination?: PaginationQuery -) => { +export const usePagination = (defaultPagination?: PaginationQuery) => { const { page, size, setPage, setSize, setPagination, resetPagination } = - useReactRouterQueryAndPagination({}, () => ({}), defaultPagination) + useQueryAndPagination({}, () => ({}), defaultPagination) return { page, size, setPage, setSize, setPagination, resetPagination } } diff --git a/src/zod/index.ts b/src/zod/index.ts deleted file mode 100644 index 8a7a318..0000000 --- a/src/zod/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './routers' diff --git a/src/zod/routers/inMemory.ts b/src/zod/routers/inMemory.ts index 25b410c..adad4e6 100644 --- a/src/zod/routers/inMemory.ts +++ b/src/zod/routers/inMemory.ts @@ -1,13 +1,13 @@ import { ZodType, ZodTypeDef } from 'zod' import { PaginationQuery, AbstractQuery } from '../../engine' import { - useInMemoryQuery as useInMemoryQueryVanilla, - useInMemoryPagination as useInMemoryPaginationVanilla, - useInMemoryQueryAndPagination as useInMemoryQueryAndPaginationVanilla, + useQuery as useQueryVanilla, + usePagination as usePaginationVanilla, + useQueryAndPagination as useQueryAndPaginationVanilla, } from '../../routers/inMemory' import { zodParser } from '../util' -export const useInMemoryQuery = < +export const useQuery = < TQuery extends AbstractQuery, // TODO: `TSchemaOutput` should not just extend `Partial`, but be exactly `Partial`, with not additional keys TSchemaOutput extends Partial = Partial, @@ -16,9 +16,9 @@ export const useInMemoryQuery = < >( defaultQuery: TQuery, schemaQuery: ZodType -) => useInMemoryQueryVanilla(defaultQuery, zodParser(schemaQuery)) +) => useQueryVanilla(defaultQuery, zodParser(schemaQuery)) -export const useInMemoryQueryAndPagination = < +export const useQueryAndPagination = < TQuery extends AbstractQuery, TSchemaOutput extends Partial = Partial, TSchemaDef extends ZodTypeDef = ZodTypeDef, @@ -28,11 +28,11 @@ export const useInMemoryQueryAndPagination = < schemaQuery: ZodType, defaultPagination?: PaginationQuery ) => - useInMemoryQueryAndPaginationVanilla( + useQueryAndPaginationVanilla( defaultQuery, zodParser(schemaQuery), defaultPagination ) -export const useInMemoryPagination = (defaultPagination?: PaginationQuery) => - useInMemoryPaginationVanilla(defaultPagination) +export const usePagination = (defaultPagination?: PaginationQuery) => + usePaginationVanilla(defaultPagination) diff --git a/src/zod/routers/index.ts b/src/zod/routers/index.ts deleted file mode 100644 index a58cc7f..0000000 --- a/src/zod/routers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './nextRouter' -export * from './inMemory' -export * from './reactRouter' diff --git a/src/zod/routers/nextRouter.ts b/src/zod/routers/nextRouter.ts index 9421660..a4195bd 100644 --- a/src/zod/routers/nextRouter.ts +++ b/src/zod/routers/nextRouter.ts @@ -1,13 +1,13 @@ import { ZodType, ZodTypeDef } from 'zod' import { PaginationQuery, AbstractQuery } from '../../engine' import { - useNextRouterQuery as useNextRouterQueryVanilla, - useNextRouterPagination as useNextRouterPaginationVanilla, - useNextRouterQueryAndPagination as useNextRouterQueryAndPaginationVanilla, + useQuery as useQueryVanilla, + usePagination as usePaginationVanilla, + useQueryAndPagination as useQueryAndPaginationVanilla, } from '../../routers/nextRouter' import { zodParser } from '../util' -export const useNextRouterQuery = < +export const useQuery = < TQuery extends AbstractQuery, // TODO: `TSchemaOutput` should not just extend `Partial`, but be exactly `Partial`, with not additional keys TSchemaOutput extends Partial = Partial, @@ -16,9 +16,9 @@ export const useNextRouterQuery = < >( defaultQuery: TQuery, schemaQuery: ZodType -) => useNextRouterQueryVanilla(defaultQuery, zodParser(schemaQuery)) +) => useQueryVanilla(defaultQuery, zodParser(schemaQuery)) -export const useNextRouterQueryAndPagination = < +export const useQueryAndPagination = < TQuery extends AbstractQuery, TSchemaOutput extends Partial = Partial, TSchemaDef extends ZodTypeDef = ZodTypeDef, @@ -28,11 +28,11 @@ export const useNextRouterQueryAndPagination = < schemaQuery: ZodType, defaultPagination?: PaginationQuery ) => - useNextRouterQueryAndPaginationVanilla( + useQueryAndPaginationVanilla( defaultQuery, zodParser(schemaQuery), defaultPagination ) -export const useNextRouterPagination = (defaultPagination?: PaginationQuery) => - useNextRouterPaginationVanilla(defaultPagination) +export const usePagination = (defaultPagination?: PaginationQuery) => + usePaginationVanilla(defaultPagination) diff --git a/src/zod/routers/reactRouter.ts b/src/zod/routers/reactRouter.ts index 9b90387..b604d5e 100644 --- a/src/zod/routers/reactRouter.ts +++ b/src/zod/routers/reactRouter.ts @@ -1,13 +1,13 @@ import { ZodType, ZodTypeDef } from 'zod' import { PaginationQuery, AbstractQuery } from '../../engine' import { - useReactRouterQuery as useReactRouterQueryVanilla, - useReactRouterPagination as useReactRouterPaginationVanilla, - useReactRouterQueryAndPagination as useReactRouterQueryAndPaginationVanilla, + useQuery as useQueryVanilla, + usePagination as usePaginationVanilla, + useQueryAndPagination as useQueryAndPaginationVanilla, } from '../../routers/reactRouter' import { zodParser } from '../util' -export const useReactRouterQuery = < +export const useQuery = < TQuery extends AbstractQuery, // TODO: `TSchemaOutput` should not just extend `Partial`, but be exactly `Partial`, with not additional keys TSchemaOutput extends Partial = Partial, @@ -16,9 +16,9 @@ export const useReactRouterQuery = < >( defaultQuery: TQuery, schemaQuery: ZodType -) => useReactRouterQueryVanilla(defaultQuery, zodParser(schemaQuery)) +) => useQueryVanilla(defaultQuery, zodParser(schemaQuery)) -export const useReactRouterQueryAndPagination = < +export const useQueryAndPagination = < TQuery extends AbstractQuery, TSchemaOutput extends Partial = Partial, TSchemaDef extends ZodTypeDef = ZodTypeDef, @@ -28,11 +28,11 @@ export const useReactRouterQueryAndPagination = < schemaQuery: ZodType, defaultPagination?: PaginationQuery ) => - useReactRouterQueryAndPaginationVanilla( + useQueryAndPaginationVanilla( defaultQuery, zodParser(schemaQuery), defaultPagination ) -export const useReactRouterPagination = (defaultPagination?: PaginationQuery) => - useReactRouterPaginationVanilla(defaultPagination) +export const usePagination = (defaultPagination?: PaginationQuery) => + usePaginationVanilla(defaultPagination) From e71e2d331fde26bd8064bf93a11479b78085d275 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 11:02:21 +0200 Subject: [PATCH 16/26] add options to pagination and routers, add replace history flag to react router and next router --- src/engine/pagination.ts | 14 ++++++++++--- src/routers/inMemory.ts | 13 ++++++++---- src/routers/nextRouter.ts | 43 ++++++++++++++++++++++++++++++-------- src/routers/reactRouter.ts | 42 +++++++++++++++++++++++++++++-------- 4 files changed, 87 insertions(+), 25 deletions(-) diff --git a/src/engine/pagination.ts b/src/engine/pagination.ts index 2ac8f87..85d69ea 100644 --- a/src/engine/pagination.ts +++ b/src/engine/pagination.ts @@ -5,6 +5,7 @@ import { ParseQuery, useAbstractQuery, Router, + AbstractQueryOptions, } from './query' export type PaginationQuery = { page: number; size: number } @@ -29,7 +30,8 @@ export const useAbstractQueryAndPagination = ( defaultQuery: T, parseQuery: ParseQuery, router: Router, - defaultPagination?: PaginationQuery + defaultPagination?: PaginationQuery, + options?: Partial ) => { const mergedDefaultPagination = { ...DEFAULT_PAGINATION, @@ -43,14 +45,20 @@ export const useAbstractQueryAndPagination = ( const { query, setQuery } = useAbstractQuery( mergedDefaultQueryAndPagination, parseQuery, - router + router, + options ) const { query: pagination, setQuery: setPagination, resetQuery: resetPagination, - } = useAbstractQuery(mergedDefaultPagination, parsePagination, router) + } = useAbstractQuery( + mergedDefaultPagination, + parsePagination, + router, + options + ) return { query, diff --git a/src/routers/inMemory.ts b/src/routers/inMemory.ts index 6e110b2..f18de91 100644 --- a/src/routers/inMemory.ts +++ b/src/routers/inMemory.ts @@ -41,19 +41,24 @@ export const useQuery = ( export const useQueryAndPagination = ( defaultQuery: T, parseQuery: ParseQuery, - defaultPagination?: PaginationQuery + defaultPagination?: PaginationQuery, + options?: Partial ) => { const router = useInMemoryRouter() return useAbstractQueryAndPagination( defaultQuery, parseQuery, router, - defaultPagination + defaultPagination, + options ) } -export const usePagination = (defaultPagination?: PaginationQuery) => { +export const usePagination = ( + defaultPagination?: PaginationQuery, + options?: Partial +) => { const { page, size, setPage, setSize, setPagination, resetPagination } = - useQueryAndPagination({}, () => ({}), defaultPagination) + useQueryAndPagination({}, () => ({}), defaultPagination, options) return { page, size, setPage, setSize, setPagination, resetPagination } } diff --git a/src/routers/nextRouter.ts b/src/routers/nextRouter.ts index 5c6e90c..e9de024 100644 --- a/src/routers/nextRouter.ts +++ b/src/routers/nextRouter.ts @@ -1,4 +1,5 @@ import { useRouter } from 'next/router' +import { useMemo } from 'react' import { Query, ParseQuery, @@ -12,9 +13,24 @@ import { useAbstractQueryAndPagination, } from '../engine/pagination' -const useNextRouter = (): Router => { +export type NextRouterOptions = { + setQueryMethod: 'replace' | 'push' +} + +const DEFAULT_NEXT_ROUTER_OPTIONS: NextRouterOptions = { + setQueryMethod: 'replace', +} + +const useNextRouter = ( + options: undefined | Partial +): Router => { const nextRouter = useRouter() + const mergedOptions = useMemo( + () => ({ ...DEFAULT_NEXT_ROUTER_OPTIONS, ...options }), + [options] + ) + return { getQuery: (defaultQuery) => { const query: Query = {} @@ -32,7 +48,11 @@ const useNextRouter = (): Router => { delete newQuery[key] } } - nextRouter.push({ query: newQuery }, undefined, { shallow: true }) + if (mergedOptions.setQueryMethod === 'push') { + nextRouter.push({ query: newQuery }, undefined, { shallow: true }) + } else { + nextRouter.replace({ query: newQuery }, undefined, { shallow: true }) + } }, } } @@ -40,28 +60,33 @@ const useNextRouter = (): Router => { export const useQuery = ( defaultQuery: T, parseQuery: ParseQuery, - options?: Partial + options?: Partial ) => { - const router = useNextRouter() + const router = useNextRouter(options) return useAbstractQuery(defaultQuery, parseQuery, router, options) } export const useQueryAndPagination = ( defaultQuery: T, parseQuery: ParseQuery, - defaultPagination?: PaginationQuery + defaultPagination?: PaginationQuery, + options?: Partial ) => { - const router = useNextRouter() + const router = useNextRouter(options) return useAbstractQueryAndPagination( defaultQuery, parseQuery, router, - defaultPagination + defaultPagination, + options ) } -export const usePagination = (defaultPagination?: PaginationQuery) => { +export const usePagination = ( + defaultPagination?: PaginationQuery, + options?: Partial +) => { const { page, size, setPage, setSize, setPagination, resetPagination } = - useQueryAndPagination({}, () => ({}), defaultPagination) + useQueryAndPagination({}, () => ({}), defaultPagination, options) return { page, size, setPage, setSize, setPagination, resetPagination } } diff --git a/src/routers/reactRouter.ts b/src/routers/reactRouter.ts index 5eb0026..ecbe816 100644 --- a/src/routers/reactRouter.ts +++ b/src/routers/reactRouter.ts @@ -1,4 +1,5 @@ import { useLocation, useNavigate } from 'react-router-dom' +import { useMemo } from 'react' import { Query, AbstractQuery, @@ -12,10 +13,25 @@ import { const QUERY_ARRAY_SEPARATOR = ',' -const useReactRouter = (): Router => { +export type ReactRouterOptions = { + setQueryMethod: 'replace' | 'push' +} + +const DEFAULT_REACT_ROUTER_OPTIONS: ReactRouterOptions = { + setQueryMethod: 'replace', +} + +const useReactRouter = ( + options: undefined | Partial +): Router => { const navigate = useNavigate() const { pathname, search } = useLocation() + const mergedOptions = useMemo( + () => ({ ...DEFAULT_REACT_ROUTER_OPTIONS, ...options }), + [options] + ) + return { getQuery: (defaultQuery) => { const query: Query = {} @@ -48,7 +64,10 @@ const useReactRouter = (): Router => { urlSearchParams.set(key, encodedValues) } } - navigate({ pathname, search: urlSearchParams.toString() }) + navigate( + { pathname, search: urlSearchParams.toString() }, + { replace: mergedOptions.setQueryMethod === 'replace' } + ) }, } } @@ -56,28 +75,33 @@ const useReactRouter = (): Router => { export const useQuery = ( defaultQuery: T, parseQuery: ParseQuery, - options?: Partial + options?: Partial ) => { - const router = useReactRouter() + const router = useReactRouter(options) return useAbstractQuery(defaultQuery, parseQuery, router, options) } export const useQueryAndPagination = ( defaultQuery: T, parseQuery: ParseQuery, - defaultPagination?: PaginationQuery + defaultPagination?: PaginationQuery, + options?: Partial ) => { - const router = useReactRouter() + const router = useReactRouter(options) return useAbstractQueryAndPagination( defaultQuery, parseQuery, router, - defaultPagination + defaultPagination, + options ) } -export const usePagination = (defaultPagination?: PaginationQuery) => { +export const usePagination = ( + defaultPagination?: PaginationQuery, + options?: Partial +) => { const { page, size, setPage, setSize, setPagination, resetPagination } = - useQueryAndPagination({}, () => ({}), defaultPagination) + useQueryAndPagination({}, () => ({}), defaultPagination, options) return { page, size, setPage, setSize, setPagination, resetPagination } } From f357484d55f2c5e7967a4c7147b94db208a6b741 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 12:52:23 +0200 Subject: [PATCH 17/26] add date and bigint to AbstractQueryValueElement --- src/engine/query.ts | 7 ++++++- src/routers/__test__/nextRouter.test.ts | 21 ++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/engine/query.ts b/src/engine/query.ts index 9494d63..1a875a9 100644 --- a/src/engine/query.ts +++ b/src/engine/query.ts @@ -2,7 +2,12 @@ import { useCallback, useMemo } from 'react' export type Query = Record -export type AbstractQueryValueElement = string | number | boolean +export type AbstractQueryValueElement = + | string + | number + | boolean + | Date + | bigint export type AbstractQuery = Record< string, diff --git a/src/routers/__test__/nextRouter.test.ts b/src/routers/__test__/nextRouter.test.ts index 31f05c4..e2daee0 100644 --- a/src/routers/__test__/nextRouter.test.ts +++ b/src/routers/__test__/nextRouter.test.ts @@ -181,15 +181,32 @@ describe('NextRouter', () => { const search = 'Max' const department = 'IT' const defaultAge = 42 + const defaultBirthDate = new Date() + defaultBirthDate.setMilliseconds(0) + const defaultNetWorth = BigInt(0xf8ffffffffffffffffffffffffff2fffn) + const defaultDarkMode: boolean | undefined = true const schema = z.object({ search: z.string().optional().catch(undefined), department: z.string().optional().catch(undefined), age: z.string().pipe(z.coerce.number().optional()).catch(undefined), + birthDate: z.string().pipe(z.coerce.date().optional()).catch(undefined), + netWorth: z.string().pipe(z.coerce.bigint().optional()).catch(undefined), + darkMode: z.string().pipe(z.coerce.boolean().optional()).catch(undefined), }) const { result } = renderHook(() => - useQuery({ search: '', department: '', age: defaultAge }, schema) + useQuery( + { + search: '', + department: '', + age: defaultAge, + birthDate: defaultBirthDate, + netWorth: defaultNetWorth, + darkMode: defaultDarkMode, + }, + schema + ) ) act(() => { @@ -203,6 +220,8 @@ describe('NextRouter', () => { expect(result.current.query.search).toBe(search) expect(result.current.query.department).toBe(department) expect(result.current.query.age).toBe(defaultAge) + expect(result.current.query.birthDate).toEqual(defaultBirthDate) + expect(result.current.query.netWorth).toEqual(defaultNetWorth) }) test('unspecified query keys should be left untouched', () => { From 20a6654abd4bcc8314f43fbf0bd4c49e4371d4e9 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 12:52:59 +0200 Subject: [PATCH 18/26] improve router types --- src/zod/routers/inMemory.ts | 10 +++++----- src/zod/routers/nextRouter.ts | 10 +++++----- src/zod/routers/reactRouter.ts | 10 +++++----- src/zod/util.ts | 9 ++------- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/zod/routers/inMemory.ts b/src/zod/routers/inMemory.ts index adad4e6..7f9d2a1 100644 --- a/src/zod/routers/inMemory.ts +++ b/src/zod/routers/inMemory.ts @@ -6,11 +6,11 @@ import { useQueryAndPagination as useQueryAndPaginationVanilla, } from '../../routers/inMemory' import { zodParser } from '../util' +import { NonNullableRecord } from '../../utils' export const useQuery = < - TQuery extends AbstractQuery, - // TODO: `TSchemaOutput` should not just extend `Partial`, but be exactly `Partial`, with not additional keys - TSchemaOutput extends Partial = Partial, + TQuery extends NonNullableRecord, + TSchemaOutput extends Partial, TSchemaDef extends ZodTypeDef = ZodTypeDef, TSchemaInput = TSchemaOutput >( @@ -19,8 +19,8 @@ export const useQuery = < ) => useQueryVanilla(defaultQuery, zodParser(schemaQuery)) export const useQueryAndPagination = < - TQuery extends AbstractQuery, - TSchemaOutput extends Partial = Partial, + TQuery extends NonNullableRecord, + TSchemaOutput extends Partial, TSchemaDef extends ZodTypeDef = ZodTypeDef, TSchemaInput = TSchemaOutput >( diff --git a/src/zod/routers/nextRouter.ts b/src/zod/routers/nextRouter.ts index a4195bd..8a26faa 100644 --- a/src/zod/routers/nextRouter.ts +++ b/src/zod/routers/nextRouter.ts @@ -6,11 +6,11 @@ import { useQueryAndPagination as useQueryAndPaginationVanilla, } from '../../routers/nextRouter' import { zodParser } from '../util' +import { NonNullableRecord } from '../../utils' export const useQuery = < - TQuery extends AbstractQuery, - // TODO: `TSchemaOutput` should not just extend `Partial`, but be exactly `Partial`, with not additional keys - TSchemaOutput extends Partial = Partial, + TQuery extends NonNullableRecord, + TSchemaOutput extends Partial, TSchemaDef extends ZodTypeDef = ZodTypeDef, TSchemaInput = TSchemaOutput >( @@ -19,8 +19,8 @@ export const useQuery = < ) => useQueryVanilla(defaultQuery, zodParser(schemaQuery)) export const useQueryAndPagination = < - TQuery extends AbstractQuery, - TSchemaOutput extends Partial = Partial, + TQuery extends NonNullableRecord, + TSchemaOutput extends Partial, TSchemaDef extends ZodTypeDef = ZodTypeDef, TSchemaInput = TSchemaOutput >( diff --git a/src/zod/routers/reactRouter.ts b/src/zod/routers/reactRouter.ts index b604d5e..9aa24f0 100644 --- a/src/zod/routers/reactRouter.ts +++ b/src/zod/routers/reactRouter.ts @@ -6,11 +6,11 @@ import { useQueryAndPagination as useQueryAndPaginationVanilla, } from '../../routers/reactRouter' import { zodParser } from '../util' +import { NonNullableRecord } from '../../utils' export const useQuery = < - TQuery extends AbstractQuery, - // TODO: `TSchemaOutput` should not just extend `Partial`, but be exactly `Partial`, with not additional keys - TSchemaOutput extends Partial = Partial, + TQuery extends NonNullableRecord, + TSchemaOutput extends Partial, TSchemaDef extends ZodTypeDef = ZodTypeDef, TSchemaInput = TSchemaOutput >( @@ -19,8 +19,8 @@ export const useQuery = < ) => useQueryVanilla(defaultQuery, zodParser(schemaQuery)) export const useQueryAndPagination = < - TQuery extends AbstractQuery, - TSchemaOutput extends Partial = Partial, + TQuery extends NonNullableRecord, + TSchemaOutput extends Partial, TSchemaDef extends ZodTypeDef = ZodTypeDef, TSchemaInput = TSchemaOutput >( diff --git a/src/zod/util.ts b/src/zod/util.ts index fab1924..625ef83 100644 --- a/src/zod/util.ts +++ b/src/zod/util.ts @@ -2,13 +2,8 @@ import { ZodType, ZodTypeDef } from 'zod' import { AbstractQuery, ParseQuery } from '../engine/query' export const zodParser = - < - TQuery extends AbstractQuery, - Output extends Partial, - Def extends ZodTypeDef, - Input - >( + , Def extends ZodTypeDef, Input>( schema: ZodType - ): ParseQuery => + ): ParseQuery => (q) => schema.parse(q) From 8461aef3398a0b498af388f9c54c502d96502816 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 23:37:04 +0200 Subject: [PATCH 19/26] update readme --- readme.md | 228 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 139 insertions(+), 89 deletions(-) diff --git a/readme.md b/readme.md index 9024ecb..6dffd63 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,4 @@ -React Pagination -============= +# 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) @@ -10,129 +9,180 @@ state or in the browser URL. ## Table of content - [Usage](#usage) - - [useQueryAndPagination](#usequeryandpagination) -- [Supported Implementations](#supported-implementations) - - [In Memory Pagination](#in-memory-pagination) - - [React-Router based pagination](#react-router-based-pagination) - - [NextJS Router based pagination](#nextjs-router-based-pagination) + - [useQuery](#usequery) + - [usePagination](#usepagination) + - [useQueryAndPagination](#usequeryandpagination) - [Build & Publish](#build--publish) - [Information](#information) ## Usage -First, you have to install the package: +Install the package: -```bash +```sh npm install @aboutbits/react-pagination ``` -Second, you can make use of the `useQueryAndPagination` hook. This package implements 3 versions of this hook: +There are a variety of entry points from which to import the hooks `useQuery`, `usePagiation` and `useQueryAndPagination`: -- [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 - your project is using React Router. -- [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. +- For the [Next.js](https://nextjs.org/) router: + - `@aboutbits/react-pagination/next-router` + - `@aboutbits/react-pagination/next-router/zod` +- For [React Router](https://reactrouter.com): +- `@aboutbits/react-pagination/react-router` +- `@aboutbits/react-pagination/react-router/zod` +- For an in-memory router that does not modify the browser history: + - `@aboutbits/react-pagination/in-memory` + - `@aboutbits/react-pagination/in-memory/zod` -### useQueryAndPagination +The hooks exported from `@aboutbits/react-pagination/*/zod` are more convenient when using [zod](https://github.com/colinhacks/zod) for the validation of the query. -This hook supports the combination of query parameters and pagination and manages the state of the query parameter values and the -pagination values. +`useQueryAndPagination` merges the functionality of `useQuery` and `usePagination`. Changing the query resets the page, but changing the page does not reset the query. -#### The hook supports following configuration parameter object: +Some examples follow, but we recommend having a look at the type definitions for more details about the API. -|value|type|default|description| -|---|---|---|---| -|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| -|---|---|---| -|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: updateQuery, setPage, clear| - -#### Example usage with NextJS +#### Example usage with Next.js ```tsx -import { useQueryAndPagination } from '@aboutbits/react-pagination/dist/nextRouterPagination' - -const users = [ - 'Alex', 'Simon', 'Natan', 'Nadia', 'Moritz', 'Marie' -] - -function UserList() { - const { page, size, queryParameters, actions } = useQueryAndPagination({pageSize: 2}) - - return ( -
- actions.updateQuery({search: value})}/> - - - -
    - {users.filter(user => user.startsWith(queryParameters.search)) - .slice(page, page + size) - .map(user =>
  • {user}
  • )} -
-
- ) +import { Query } from '@aboutbits/react-pagination' +import { useQueryAndPagination } from '@aboutbits/react-pagination/next-router' + +const users = ['Alex', 'Simon', 'Natan', 'Nadia', 'Moritz', 'Marie'] + +const parseSearch = (query: Query) => { + for (const [key, value] of Object.entries(query)) { + if (key === 'search' && !Array.isArray(value)) { + return { search: value } + } + } + return {} } -``` - -## Supported implementations - -This package includes 3 different implementations of the above hook. - -- [In Memory](#in-memory-pagination) -- [React Router](#react-router-based-pagination) -- [NextJS Router](#nextjs-router-based-pagination) -### In Memory Pagination - -Use this pagination hook if you want to keep track of the pagination in memory. This is very handy for dialogs. - -```tsx -import { useQueryAndPagination } from '@aboutbits/react-pagination/dist/inMemoryPagination' +export function UserList() { + const { page, size, query, setQuery, setPage, resetQuery } = + useQueryAndPagination({ search: '' }, parseSearch) + + return ( +
+ setQuery({ search: event.target.value })} + /> + + +
    + {users + .filter((user) => + user.toLowerCase().startsWith(query.search.toLowerCase()) + ) + .slice(page * size, (page + 1) * size) + .map((user) => ( +
  • {user}
  • + ))} +
+
+ ) +} ``` -### React-Router based pagination - -These are specific hooks for applications that use [React Router](https://reactrouter.com/) for routing. +### Example usage with React Router and zod ```tsx -import { useQueryAndPagination } from '@aboutbits/react-pagination/dist/reactRouterPagination' -``` +import { useQueryAndPagination } from '@aboutbits/react-pagination/react-router/zod' +import { z } from 'zod' + +const userSchema = z.object({ + name: z.string(), + // The input to the parser is going to be a string. + // We try to convert it to a number and default to undefined if the parsing fails. + // This continues the parsing of the remaining query. + // Another possibility would be to not catch errors, which would cancel the entire parsing + // if "age" cannot be converted to a number. + age: z.string().pipe(z.coerce.number().optional()).catch(undefined), +}) -### NextJS Router based pagination - -These are specific hooks for applications that use [NextJS Router](https://nextjs.org/docs/api-reference/next/router) -for routing. +const users = [ + { name: 'Alex', age: 10 }, + { name: 'Simon', age: 24 }, + { name: 'Natan', age: 88 }, + { name: 'Nadia', age: 42 }, + { name: 'Moritz', age: 35 }, + { name: 'Marie', age: 17 }, +] -```tsx -import { useQueryAndPagination } from '@aboutbits/react-pagination/dist/nextRouterPagination' +export function UserList() { + const { page, size, query, setQuery, setPage, resetQuery } = + useQueryAndPagination({ name: '', age: 0 }, userSchema, { + page: 0, + size: 4, + }) + + return ( +
+
+ Name: + setQuery({ name: event.target.value })} + /> +
+
+ Minimum age: + { + const value = event.target.value + const parsed = parseInt(value) + if (!isNaN(parsed)) { + setQuery({ age: parsed }) + } + }} + /> +
+ + +
    + {users + .filter( + (user) => + user.name.toLowerCase().startsWith(query.name.toLowerCase()) && + user.age >= query.age + ) + .slice(page * size, (page + 1) * size) + .map((user) => ( +
  • {user.name}
  • + ))} +
+
+ ) +} ``` ## Build & Publish To publish the package commit all changes and push them to main. Then run one of the following commands locally: -```bash +```sh npm version patch npm version minor npm version major ``` -## Information +## About -About Bits is a company based in South Tyrol, Italy. You can find more information about us +AboutBits is a company based in South Tyrol, Italy. You can find more information about us on [our website](https://aboutbits.it). ### Support From c66f26d20c5e73349ec9faf7f0535c9d0e8f76c4 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 23:40:14 +0200 Subject: [PATCH 20/26] fix readme --- readme.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index 6dffd63..20ee2b8 100644 --- a/readme.md +++ b/readme.md @@ -9,11 +9,10 @@ state or in the browser URL. ## Table of content - [Usage](#usage) - - [useQuery](#usequery) - - [usePagination](#usepagination) - - [useQueryAndPagination](#usequeryandpagination) + - [Example usage with Next.js](#example-usage-with-nextjs) + - [Example usage with React Router and zod](#example-usage-with-react-router-and-zod) - [Build & Publish](#build--publish) -- [Information](#information) +- [About](#about) ## Usage @@ -41,7 +40,7 @@ The hooks exported from `@aboutbits/react-pagination/*/zod` are more convenient Some examples follow, but we recommend having a look at the type definitions for more details about the API. -#### Example usage with Next.js +### Example usage with Next.js ```tsx import { Query } from '@aboutbits/react-pagination' From 457fb416c311ba330e148f2bec48529e523cb0a6 Mon Sep 17 00:00:00 2001 From: devgioele Date: Wed, 19 Jul 2023 23:43:48 +0200 Subject: [PATCH 21/26] improve readme --- readme.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 20ee2b8..e4651a9 100644 --- a/readme.md +++ b/readme.md @@ -3,8 +3,7 @@ [![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 query and pagination values in local -state or in the browser URL. +Query hooks for React with first-class support for TypeScript! Writing and reading the query, attached to the browser URL or in-memory, made easy! ## Table of content From ad4594c68baa80007ea5fd386d4a174ce8bf377f Mon Sep 17 00:00:00 2001 From: devgioele Date: Thu, 20 Jul 2023 08:38:43 +0200 Subject: [PATCH 22/26] fix readme --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index e4651a9..ac28222 100644 --- a/readme.md +++ b/readme.md @@ -21,7 +21,7 @@ Install the package: npm install @aboutbits/react-pagination ``` -There are a variety of entry points from which to import the hooks `useQuery`, `usePagiation` and `useQueryAndPagination`: +There are a variety of entry points from which to import the hooks `useQuery`, `usePagination` and `useQueryAndPagination`: - For the [Next.js](https://nextjs.org/) router: - `@aboutbits/react-pagination/next-router` @@ -178,7 +178,7 @@ npm version minor npm version major ``` -## About +## Information AboutBits is a company based in South Tyrol, Italy. You can find more information about us on [our website](https://aboutbits.it). From 4640931d9292bcffea04d1360ebfc3cbfc01a9e5 Mon Sep 17 00:00:00 2001 From: devgioele Date: Thu, 20 Jul 2023 08:44:16 +0200 Subject: [PATCH 23/26] let zod routers forward options --- src/zod/routers/inMemory.ts | 23 ++++++++++++++++------- src/zod/routers/nextRouter.ts | 24 +++++++++++++++++------- src/zod/routers/reactRouter.ts | 24 +++++++++++++++++------- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/zod/routers/inMemory.ts b/src/zod/routers/inMemory.ts index 7f9d2a1..f341102 100644 --- a/src/zod/routers/inMemory.ts +++ b/src/zod/routers/inMemory.ts @@ -1,5 +1,9 @@ import { ZodType, ZodTypeDef } from 'zod' -import { PaginationQuery, AbstractQuery } from '../../engine' +import { + PaginationQuery, + AbstractQuery, + AbstractQueryOptions, +} from '../../engine' import { useQuery as useQueryVanilla, usePagination as usePaginationVanilla, @@ -15,8 +19,9 @@ export const useQuery = < TSchemaInput = TSchemaOutput >( defaultQuery: TQuery, - schemaQuery: ZodType -) => useQueryVanilla(defaultQuery, zodParser(schemaQuery)) + schemaQuery: ZodType, + options?: Partial +) => useQueryVanilla(defaultQuery, zodParser(schemaQuery), options) export const useQueryAndPagination = < TQuery extends NonNullableRecord, @@ -26,13 +31,17 @@ export const useQueryAndPagination = < >( defaultQuery: TQuery, schemaQuery: ZodType, - defaultPagination?: PaginationQuery + defaultPagination?: PaginationQuery, + options?: Partial ) => useQueryAndPaginationVanilla( defaultQuery, zodParser(schemaQuery), - defaultPagination + defaultPagination, + options ) -export const usePagination = (defaultPagination?: PaginationQuery) => - usePaginationVanilla(defaultPagination) +export const usePagination = ( + defaultPagination?: PaginationQuery, + options?: Partial +) => usePaginationVanilla(defaultPagination, options) diff --git a/src/zod/routers/nextRouter.ts b/src/zod/routers/nextRouter.ts index 8a26faa..a6c1a83 100644 --- a/src/zod/routers/nextRouter.ts +++ b/src/zod/routers/nextRouter.ts @@ -1,9 +1,14 @@ import { ZodType, ZodTypeDef } from 'zod' -import { PaginationQuery, AbstractQuery } from '../../engine' +import { + PaginationQuery, + AbstractQuery, + AbstractQueryOptions, +} from '../../engine' import { useQuery as useQueryVanilla, usePagination as usePaginationVanilla, useQueryAndPagination as useQueryAndPaginationVanilla, + NextRouterOptions, } from '../../routers/nextRouter' import { zodParser } from '../util' import { NonNullableRecord } from '../../utils' @@ -15,8 +20,9 @@ export const useQuery = < TSchemaInput = TSchemaOutput >( defaultQuery: TQuery, - schemaQuery: ZodType -) => useQueryVanilla(defaultQuery, zodParser(schemaQuery)) + schemaQuery: ZodType, + options?: Partial +) => useQueryVanilla(defaultQuery, zodParser(schemaQuery), options) export const useQueryAndPagination = < TQuery extends NonNullableRecord, @@ -26,13 +32,17 @@ export const useQueryAndPagination = < >( defaultQuery: TQuery, schemaQuery: ZodType, - defaultPagination?: PaginationQuery + defaultPagination?: PaginationQuery, + options?: Partial ) => useQueryAndPaginationVanilla( defaultQuery, zodParser(schemaQuery), - defaultPagination + defaultPagination, + options ) -export const usePagination = (defaultPagination?: PaginationQuery) => - usePaginationVanilla(defaultPagination) +export const usePagination = ( + defaultPagination?: PaginationQuery, + options?: Partial +) => usePaginationVanilla(defaultPagination, options) diff --git a/src/zod/routers/reactRouter.ts b/src/zod/routers/reactRouter.ts index 9aa24f0..bba5044 100644 --- a/src/zod/routers/reactRouter.ts +++ b/src/zod/routers/reactRouter.ts @@ -1,9 +1,14 @@ import { ZodType, ZodTypeDef } from 'zod' -import { PaginationQuery, AbstractQuery } from '../../engine' +import { + PaginationQuery, + AbstractQuery, + AbstractQueryOptions, +} from '../../engine' import { useQuery as useQueryVanilla, usePagination as usePaginationVanilla, useQueryAndPagination as useQueryAndPaginationVanilla, + ReactRouterOptions, } from '../../routers/reactRouter' import { zodParser } from '../util' import { NonNullableRecord } from '../../utils' @@ -15,8 +20,9 @@ export const useQuery = < TSchemaInput = TSchemaOutput >( defaultQuery: TQuery, - schemaQuery: ZodType -) => useQueryVanilla(defaultQuery, zodParser(schemaQuery)) + schemaQuery: ZodType, + options?: Partial +) => useQueryVanilla(defaultQuery, zodParser(schemaQuery), options) export const useQueryAndPagination = < TQuery extends NonNullableRecord, @@ -26,13 +32,17 @@ export const useQueryAndPagination = < >( defaultQuery: TQuery, schemaQuery: ZodType, - defaultPagination?: PaginationQuery + defaultPagination?: PaginationQuery, + options?: Partial ) => useQueryAndPaginationVanilla( defaultQuery, zodParser(schemaQuery), - defaultPagination + defaultPagination, + options ) -export const usePagination = (defaultPagination?: PaginationQuery) => - usePaginationVanilla(defaultPagination) +export const usePagination = ( + defaultPagination?: PaginationQuery, + options?: Partial +) => usePaginationVanilla(defaultPagination, options) From ecdd7198aa7f550f29a03bbbebf0d004e1187d66 Mon Sep 17 00:00:00 2001 From: devgioele Date: Thu, 20 Jul 2023 09:00:13 +0200 Subject: [PATCH 24/26] document options and their default values --- src/engine/pagination.ts | 44 ++++++++++++++++++++++------------ src/engine/query.ts | 5 ++++ src/routers/nextRouter.ts | 15 +++++------- src/routers/reactRouter.ts | 17 +++++-------- src/routers/shared.ts | 10 ++++++++ src/zod/routers/nextRouter.ts | 8 +++---- src/zod/routers/reactRouter.ts | 8 +++---- 7 files changed, 64 insertions(+), 43 deletions(-) create mode 100644 src/routers/shared.ts diff --git a/src/engine/pagination.ts b/src/engine/pagination.ts index 85d69ea..2a6c38d 100644 --- a/src/engine/pagination.ts +++ b/src/engine/pagination.ts @@ -8,15 +8,33 @@ import { AbstractQueryOptions, } from './query' -export type PaginationQuery = { page: number; size: number } - -export type ChangeQueryOptions = { resetPage: boolean } +export type PaginationQuery = { + /** + * The page index. + */ + page: number + /** + * The size of one page. + */ + size: number +} const DEFAULT_PAGINATION: PaginationQuery = { page: 0, size: 15, } +export type ChangeQueryOptions = { + /** + * Whether the page should be reset to its default value when the query is changed. + * + * @default `true` + */ + resetPage: boolean +} + +const DEFAULT_CHANGE_QUERY_OPTIONS: ChangeQueryOptions = { resetPage: true } + const parsePagination = (query: Query): Partial => { return { page: queryValueToIntOrUndefined(query.page), @@ -24,8 +42,6 @@ const parsePagination = (query: Query): Partial => { } } -const DEFAULT_RESET_PAGE = true - export const useAbstractQueryAndPagination = ( defaultQuery: T, parseQuery: ParseQuery, @@ -63,23 +79,21 @@ export const useAbstractQueryAndPagination = ( return { query, setQuery: (query: Partial, options?: Partial) => { - const resetPage = - options?.resetPage === undefined - ? DEFAULT_RESET_PAGE - : options.resetPage + const mergedOptions = { ...DEFAULT_CHANGE_QUERY_OPTIONS, ...options } setQuery({ ...query, - page: resetPage ? mergedDefaultPagination.page : undefined, + page: mergedOptions.resetPage + ? mergedDefaultPagination.page + : undefined, }) }, resetQuery: (options?: Partial) => { - const resetPage = - options?.resetPage === undefined - ? DEFAULT_RESET_PAGE - : options.resetPage + const mergedOptions = { ...DEFAULT_CHANGE_QUERY_OPTIONS, ...options } setQuery({ ...defaultQuery, - page: resetPage ? mergedDefaultPagination.page : undefined, + page: mergedOptions.resetPage + ? mergedDefaultPagination.page + : undefined, }) }, page: pagination.page, diff --git a/src/engine/query.ts b/src/engine/query.ts index 1a875a9..3c5cba6 100644 --- a/src/engine/query.ts +++ b/src/engine/query.ts @@ -50,6 +50,11 @@ export const useQuery = (defaultQuery: T, router: Router) => { } export type AbstractQueryOptions = { + /** + * How the abstract query is converted to an actual query. + * + * @default Each value that is not undefined is converted to a string by calling `.toString()`. + */ convertToQuery: (abstractQuery: Partial) => Query } diff --git a/src/routers/nextRouter.ts b/src/routers/nextRouter.ts index e9de024..5ce2ff7 100644 --- a/src/routers/nextRouter.ts +++ b/src/routers/nextRouter.ts @@ -12,17 +12,14 @@ import { PaginationQuery, useAbstractQueryAndPagination, } from '../engine/pagination' +import { RouterWithHistoryOptions } from './shared' -export type NextRouterOptions = { - setQueryMethod: 'replace' | 'push' -} - -const DEFAULT_NEXT_ROUTER_OPTIONS: NextRouterOptions = { +const DEFAULT_NEXT_ROUTER_OPTIONS: RouterWithHistoryOptions = { setQueryMethod: 'replace', } const useNextRouter = ( - options: undefined | Partial + options: undefined | Partial ): Router => { const nextRouter = useRouter() @@ -60,7 +57,7 @@ const useNextRouter = ( export const useQuery = ( defaultQuery: T, parseQuery: ParseQuery, - options?: Partial + options?: Partial ) => { const router = useNextRouter(options) return useAbstractQuery(defaultQuery, parseQuery, router, options) @@ -70,7 +67,7 @@ export const useQueryAndPagination = ( defaultQuery: T, parseQuery: ParseQuery, defaultPagination?: PaginationQuery, - options?: Partial + options?: Partial ) => { const router = useNextRouter(options) return useAbstractQueryAndPagination( @@ -84,7 +81,7 @@ export const useQueryAndPagination = ( export const usePagination = ( defaultPagination?: PaginationQuery, - options?: Partial + options?: Partial ) => { const { page, size, setPage, setSize, setPagination, resetPagination } = useQueryAndPagination({}, () => ({}), defaultPagination, options) diff --git a/src/routers/reactRouter.ts b/src/routers/reactRouter.ts index ecbe816..fee5529 100644 --- a/src/routers/reactRouter.ts +++ b/src/routers/reactRouter.ts @@ -10,19 +10,14 @@ import { useAbstractQueryAndPagination, Router, } from '../engine' +import { QUERY_ARRAY_SEPARATOR, RouterWithHistoryOptions } from './shared' -const QUERY_ARRAY_SEPARATOR = ',' - -export type ReactRouterOptions = { - setQueryMethod: 'replace' | 'push' -} - -const DEFAULT_REACT_ROUTER_OPTIONS: ReactRouterOptions = { +const DEFAULT_REACT_ROUTER_OPTIONS: RouterWithHistoryOptions = { setQueryMethod: 'replace', } const useReactRouter = ( - options: undefined | Partial + options: undefined | Partial ): Router => { const navigate = useNavigate() const { pathname, search } = useLocation() @@ -75,7 +70,7 @@ const useReactRouter = ( export const useQuery = ( defaultQuery: T, parseQuery: ParseQuery, - options?: Partial + options?: Partial ) => { const router = useReactRouter(options) return useAbstractQuery(defaultQuery, parseQuery, router, options) @@ -85,7 +80,7 @@ export const useQueryAndPagination = ( defaultQuery: T, parseQuery: ParseQuery, defaultPagination?: PaginationQuery, - options?: Partial + options?: Partial ) => { const router = useReactRouter(options) return useAbstractQueryAndPagination( @@ -99,7 +94,7 @@ export const useQueryAndPagination = ( export const usePagination = ( defaultPagination?: PaginationQuery, - options?: Partial + options?: Partial ) => { const { page, size, setPage, setSize, setPagination, resetPagination } = useQueryAndPagination({}, () => ({}), defaultPagination, options) diff --git a/src/routers/shared.ts b/src/routers/shared.ts new file mode 100644 index 0000000..13971dd --- /dev/null +++ b/src/routers/shared.ts @@ -0,0 +1,10 @@ +export const QUERY_ARRAY_SEPARATOR = ',' + +export type RouterWithHistoryOptions = { + /** + * Whether the router should push a new URL to the browser history or replace the current URL. + * + * @default 'replace' + */ + setQueryMethod: 'replace' | 'push' +} diff --git a/src/zod/routers/nextRouter.ts b/src/zod/routers/nextRouter.ts index a6c1a83..13e7972 100644 --- a/src/zod/routers/nextRouter.ts +++ b/src/zod/routers/nextRouter.ts @@ -8,10 +8,10 @@ import { useQuery as useQueryVanilla, usePagination as usePaginationVanilla, useQueryAndPagination as useQueryAndPaginationVanilla, - NextRouterOptions, } from '../../routers/nextRouter' import { zodParser } from '../util' import { NonNullableRecord } from '../../utils' +import { RouterWithHistoryOptions } from '../../routers/shared' export const useQuery = < TQuery extends NonNullableRecord, @@ -21,7 +21,7 @@ export const useQuery = < >( defaultQuery: TQuery, schemaQuery: ZodType, - options?: Partial + options?: Partial ) => useQueryVanilla(defaultQuery, zodParser(schemaQuery), options) export const useQueryAndPagination = < @@ -33,7 +33,7 @@ export const useQueryAndPagination = < defaultQuery: TQuery, schemaQuery: ZodType, defaultPagination?: PaginationQuery, - options?: Partial + options?: Partial ) => useQueryAndPaginationVanilla( defaultQuery, @@ -44,5 +44,5 @@ export const useQueryAndPagination = < export const usePagination = ( defaultPagination?: PaginationQuery, - options?: Partial + options?: Partial ) => usePaginationVanilla(defaultPagination, options) diff --git a/src/zod/routers/reactRouter.ts b/src/zod/routers/reactRouter.ts index bba5044..6b9dffe 100644 --- a/src/zod/routers/reactRouter.ts +++ b/src/zod/routers/reactRouter.ts @@ -8,10 +8,10 @@ import { useQuery as useQueryVanilla, usePagination as usePaginationVanilla, useQueryAndPagination as useQueryAndPaginationVanilla, - ReactRouterOptions, } from '../../routers/reactRouter' import { zodParser } from '../util' import { NonNullableRecord } from '../../utils' +import { RouterWithHistoryOptions } from '../../routers/shared' export const useQuery = < TQuery extends NonNullableRecord, @@ -21,7 +21,7 @@ export const useQuery = < >( defaultQuery: TQuery, schemaQuery: ZodType, - options?: Partial + options?: Partial ) => useQueryVanilla(defaultQuery, zodParser(schemaQuery), options) export const useQueryAndPagination = < @@ -33,7 +33,7 @@ export const useQueryAndPagination = < defaultQuery: TQuery, schemaQuery: ZodType, defaultPagination?: PaginationQuery, - options?: Partial + options?: Partial ) => useQueryAndPaginationVanilla( defaultQuery, @@ -44,5 +44,5 @@ export const useQueryAndPagination = < export const usePagination = ( defaultPagination?: PaginationQuery, - options?: Partial + options?: Partial ) => usePaginationVanilla(defaultPagination, options) From 7ae917f4fe5b91c2a5a79b6b99c73c1f975a7a1e Mon Sep 17 00:00:00 2001 From: devgioele Date: Thu, 20 Jul 2023 09:40:07 +0200 Subject: [PATCH 25/26] document default values for page and size --- src/engine/pagination.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/engine/pagination.ts b/src/engine/pagination.ts index 2a6c38d..61a3ee0 100644 --- a/src/engine/pagination.ts +++ b/src/engine/pagination.ts @@ -11,10 +11,14 @@ import { export type PaginationQuery = { /** * The page index. + * + * @default 0 */ page: number /** * The size of one page. + * + * @default 15 */ size: number } @@ -28,7 +32,7 @@ export type ChangeQueryOptions = { /** * Whether the page should be reset to its default value when the query is changed. * - * @default `true` + * @default true */ resetPage: boolean } From ac91a4702eb8c138f36be57d438a479c4e1a3c54 Mon Sep 17 00:00:00 2001 From: devgioele Date: Thu, 20 Jul 2023 18:18:35 +0200 Subject: [PATCH 26/26] rm usePagination from zod routers --- src/routers/__test__/inMemory.test.ts | 7 ++----- src/routers/__test__/nextRouter.test.ts | 7 ++----- src/routers/__test__/reactRouter.test.tsx | 7 ++----- src/zod/routers/inMemory.ts | 6 ------ src/zod/routers/nextRouter.ts | 6 ------ src/zod/routers/reactRouter.ts | 6 ------ 6 files changed, 6 insertions(+), 33 deletions(-) diff --git a/src/routers/__test__/inMemory.test.ts b/src/routers/__test__/inMemory.test.ts index 9c1e895..a8ea949 100644 --- a/src/routers/__test__/inMemory.test.ts +++ b/src/routers/__test__/inMemory.test.ts @@ -1,11 +1,8 @@ import { act, renderHook } from '@testing-library/react' import { z } from 'zod' import { NonNullableRecord } from '../../utils' -import { - useQuery, - usePagination, - useQueryAndPagination, -} from '../../zod/routers/inMemory' +import { useQuery, useQueryAndPagination } from '../../zod/routers/inMemory' +import { usePagination } from '../inMemory' describe('InMemory', () => { const searchSchema = z.object({ diff --git a/src/routers/__test__/nextRouter.test.ts b/src/routers/__test__/nextRouter.test.ts index e2daee0..dadc446 100644 --- a/src/routers/__test__/nextRouter.test.ts +++ b/src/routers/__test__/nextRouter.test.ts @@ -3,11 +3,8 @@ import router from 'next/router' import { z } from 'zod' import { vi } from 'vitest' import { NonNullableRecord } from '../../utils' -import { - useQuery, - usePagination, - useQueryAndPagination, -} from '../../zod/routers/nextRouter' +import { useQuery, useQueryAndPagination } from '../../zod/routers/nextRouter' +import { usePagination } from '../nextRouter' vi.mock('next/router', () => require('next-router-mock')) diff --git a/src/routers/__test__/reactRouter.test.tsx b/src/routers/__test__/reactRouter.test.tsx index 4ffcf62..f02a2ea 100644 --- a/src/routers/__test__/reactRouter.test.tsx +++ b/src/routers/__test__/reactRouter.test.tsx @@ -2,11 +2,8 @@ import { act, renderHook } from '@testing-library/react' import { z } from 'zod' import { BrowserRouter } from 'react-router-dom' import { NonNullableRecord } from '../../utils' -import { - useQuery, - usePagination, - useQueryAndPagination, -} from '../../zod/routers/reactRouter' +import { useQuery, useQueryAndPagination } from '../../zod/routers/reactRouter' +import { usePagination } from '../reactRouter' const renderHookWithContext = ( render: (initialProps: Props) => Result diff --git a/src/zod/routers/inMemory.ts b/src/zod/routers/inMemory.ts index f341102..7eef00b 100644 --- a/src/zod/routers/inMemory.ts +++ b/src/zod/routers/inMemory.ts @@ -6,7 +6,6 @@ import { } from '../../engine' import { useQuery as useQueryVanilla, - usePagination as usePaginationVanilla, useQueryAndPagination as useQueryAndPaginationVanilla, } from '../../routers/inMemory' import { zodParser } from '../util' @@ -40,8 +39,3 @@ export const useQueryAndPagination = < defaultPagination, options ) - -export const usePagination = ( - defaultPagination?: PaginationQuery, - options?: Partial -) => usePaginationVanilla(defaultPagination, options) diff --git a/src/zod/routers/nextRouter.ts b/src/zod/routers/nextRouter.ts index 13e7972..7878d40 100644 --- a/src/zod/routers/nextRouter.ts +++ b/src/zod/routers/nextRouter.ts @@ -6,7 +6,6 @@ import { } from '../../engine' import { useQuery as useQueryVanilla, - usePagination as usePaginationVanilla, useQueryAndPagination as useQueryAndPaginationVanilla, } from '../../routers/nextRouter' import { zodParser } from '../util' @@ -41,8 +40,3 @@ export const useQueryAndPagination = < defaultPagination, options ) - -export const usePagination = ( - defaultPagination?: PaginationQuery, - options?: Partial -) => usePaginationVanilla(defaultPagination, options) diff --git a/src/zod/routers/reactRouter.ts b/src/zod/routers/reactRouter.ts index 6b9dffe..4ffe8fe 100644 --- a/src/zod/routers/reactRouter.ts +++ b/src/zod/routers/reactRouter.ts @@ -6,7 +6,6 @@ import { } from '../../engine' import { useQuery as useQueryVanilla, - usePagination as usePaginationVanilla, useQueryAndPagination as useQueryAndPaginationVanilla, } from '../../routers/reactRouter' import { zodParser } from '../util' @@ -41,8 +40,3 @@ export const useQueryAndPagination = < defaultPagination, options ) - -export const usePagination = ( - defaultPagination?: PaginationQuery, - options?: Partial -) => usePaginationVanilla(defaultPagination, options)