From b628d950493a83653d70077f89580a991f52d83f Mon Sep 17 00:00:00 2001 From: Jeppe Hasseriis Date: Wed, 4 Dec 2024 11:36:54 +0100 Subject: [PATCH 01/11] Add `parseAsPageIndex` --- packages/nuqs/src/parsers.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/nuqs/src/parsers.ts b/packages/nuqs/src/parsers.ts index 9a94681b2..ca818b8af 100644 --- a/packages/nuqs/src/parsers.ts +++ b/packages/nuqs/src/parsers.ts @@ -155,6 +155,17 @@ export const parseAsInteger = createParser({ serialize: v => Math.round(v).toFixed() }) +export const parseAsPageIndex = createParser({ + parse: (v) => { + const int = parseInt(v); + if (Number.isNaN(int)) { + return null; + } + return int - 1; + }, + serialize: (v) => Math.round(v + 1).toFixed(), +}); + export const parseAsHex = createParser({ parse: v => { const int = parseInt(v, 16) From cee7401ff2b1d979b040704f552bf250fd2068c4 Mon Sep 17 00:00:00 2001 From: Jeppe Hasseriis Date: Fri, 7 Feb 2025 23:36:41 +0100 Subject: [PATCH 02/11] feat: Rename to `parseAsIndex` --- packages/nuqs/src/parsers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuqs/src/parsers.ts b/packages/nuqs/src/parsers.ts index 103041215..266f306f4 100644 --- a/packages/nuqs/src/parsers.ts +++ b/packages/nuqs/src/parsers.ts @@ -155,8 +155,8 @@ export const parseAsInteger = createParser({ serialize: v => Math.round(v).toFixed() }) -export const parseAsPageIndex = createParser({ - parse: (v) => { +export const parseAsIndex = createParser({ + parse: v => { const int = parseInt(v); if (Number.isNaN(int)) { return null; From b74e17fb276d7088f1385c59ca2350503c0e4238 Mon Sep 17 00:00:00 2001 From: Jeppe Hasseriis Date: Fri, 7 Feb 2025 23:36:56 +0100 Subject: [PATCH 03/11] test: Add test for `parseAsIndex` --- packages/nuqs/src/parsers.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/nuqs/src/parsers.test.ts b/packages/nuqs/src/parsers.test.ts index 74d9d3d0c..8495ebfcd 100644 --- a/packages/nuqs/src/parsers.test.ts +++ b/packages/nuqs/src/parsers.test.ts @@ -7,7 +7,8 @@ import { parseAsIsoDateTime, parseAsIsoDate, parseAsString, - parseAsTimestamp + parseAsTimestamp, + parseAsIndex } from './parsers' describe('parsers', () => { @@ -27,6 +28,14 @@ describe('parsers', () => { // https://0.30000000000000004.com/ expect(parseAsFloat.serialize(0.1 + 0.2)).toBe('0.30000000000000004') }) + test('parseAsIndex', () => { + expect(parseAsIndex.parse('')).toBeNull() + expect(parseAsIndex.parse('1')).toBe(0) + expect(parseAsIndex.parse('3.14')).toBe(2) + expect(parseAsIndex.parse('3,14')).toBe(2) + expect(parseAsIndex.serialize(0)).toBe('1') + expect(parseAsIndex.serialize(3.14)).toBe('4') + }) test('parseAsHex', () => { expect(parseAsHex.parse('')).toBeNull() expect(parseAsHex.parse('1')).toBe(1) From 7f210e9493db6ed2e20b3ca07b7fe78f9915a828 Mon Sep 17 00:00:00 2001 From: Jeppe Hasseriis Date: Sat, 8 Feb 2025 12:08:06 +0100 Subject: [PATCH 04/11] doc: Add `parseAsIndex` to documentation --- .../docs/content/docs/parsers/built-in.mdx | 20 +++++++++++-- packages/docs/content/docs/parsers/demos.tsx | 29 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/docs/content/docs/parsers/built-in.mdx b/packages/docs/content/docs/parsers/built-in.mdx index 43d2be5f9..b2b98a28d 100644 --- a/packages/docs/content/docs/parsers/built-in.mdx +++ b/packages/docs/content/docs/parsers/built-in.mdx @@ -9,6 +9,7 @@ import { StringParserDemo, FloatParserDemo, HexParserDemo, + IndexParserDemo, BooleanParserDemo, StringLiteralParserDemo, DateISOParserDemo, @@ -105,6 +106,20 @@ useQueryState('hex', parseAsHex.withDefault(0x00)) Check out the [Hex Colors](/playground/hex-colors) playground for a demo. +### Index + +Same as integer, but adds a `+1` offset to the query value. Useful for pagination indexes. + +```ts +import { parseAsIndex } from 'nuqs' + +useQueryState('page', parseAsIndex.withDefault(0)) +``` + +}> + + + ## Boolean ```ts @@ -203,8 +218,9 @@ import { parseAsIsoDate } from 'nuqs' - The Date is parsed without the time zone offset, making it at 00:00:00 UTC.
- _Support: introduced in version 2.1.0._ + The Date is parsed without the time zone offset, making it at 00:00:00 UTC. +
+ _Support: introduced in version 2.1.0._
### Timestamp diff --git a/packages/docs/content/docs/parsers/demos.tsx b/packages/docs/content/docs/parsers/demos.tsx index b4bf4d44f..b62da9624 100644 --- a/packages/docs/content/docs/parsers/demos.tsx +++ b/packages/docs/content/docs/parsers/demos.tsx @@ -13,6 +13,7 @@ import { parseAsBoolean, parseAsFloat, parseAsHex, + parseAsIndex, parseAsInteger, parseAsIsoDate, parseAsIsoDateTime, @@ -171,6 +172,34 @@ export function HexParserDemo() { ) } +export function IndexParserDemo() { + const [value, setValue] = useQueryState('page', parseAsIndex) + return ( + + { + if (e.target.value === '') { + setValue(null) + } else { + setValue(e.target.valueAsNumber) + } + }} + placeholder="What page are you on?" + /> + + + ) +} + export function BooleanParserDemo() { const [value, setValue] = useQueryState( 'bool', From aceb4edaf3969dd33afa37164e7e876fdfdff198 Mon Sep 17 00:00:00 2001 From: Jeppe Hasseriis Date: Sat, 8 Feb 2025 13:34:00 +0100 Subject: [PATCH 05/11] test: Update e2e tests for Next.js --- packages/e2e/next/cypress/e2e/cache.cy.js | 5 +++- .../e2e/next/cypress/e2e/useQueryState.cy.js | 23 ++++++++++++++++ .../e2e/next/cypress/e2e/useQueryStates.cy.js | 27 +++++++++++++------ packages/e2e/next/src/app/app/cache/all.tsx | 3 ++- packages/e2e/next/src/app/app/cache/get.tsx | 2 ++ packages/e2e/next/src/app/app/cache/page.tsx | 3 ++- .../next/src/app/app/cache/searchParams.ts | 2 ++ packages/e2e/next/src/app/app/cache/set.tsx | 3 ++- .../next/src/app/app/useQueryState/page.tsx | 27 +++++++++++++++++++ .../next/src/app/app/useQueryStates/page.tsx | 5 ++++ .../src/pages/pages/useQueryState/index.tsx | 21 +++++++++++++++ .../src/pages/pages/useQueryStates/index.tsx | 5 ++++ 12 files changed, 114 insertions(+), 12 deletions(-) diff --git a/packages/e2e/next/cypress/e2e/cache.cy.js b/packages/e2e/next/cypress/e2e/cache.cy.js index 2344833db..5d4da7602 100644 --- a/packages/e2e/next/cypress/e2e/cache.cy.js +++ b/packages/e2e/next/cypress/e2e/cache.cy.js @@ -2,19 +2,22 @@ describe('cache', () => { it('works in app router', () => { - cy.visit('/app/cache?str=foo&num=42&bool=true&multi=foo&multi=bar') + cy.visit('/app/cache?str=foo&num=42&idx=1&bool=true&multi=foo&multi=bar') cy.get('#parse-str').should('have.text', 'foo') cy.get('#parse-num').should('have.text', '42') + cy.get('#parse-idx').should('have.text', '0') cy.get('#parse-bool').should('have.text', 'true') cy.get('#parse-def').should('have.text', 'default') cy.get('#parse-nope').should('have.text', 'null') cy.get('#all-str').should('have.text', 'foo') cy.get('#all-num').should('have.text', '42') + cy.get('#all-idx').should('have.text', '0') cy.get('#all-bool').should('have.text', 'true') cy.get('#all-def').should('have.text', 'default') cy.get('#all-nope').should('have.text', 'null') cy.get('#get-str').should('have.text', 'foo') cy.get('#get-num').should('have.text', '42') + cy.get('#get-idx').should('have.text', '0') cy.get('#get-bool').should('have.text', 'true') cy.get('#get-def').should('have.text', 'default') cy.get('#get-nope').should('have.text', 'null') diff --git a/packages/e2e/next/cypress/e2e/useQueryState.cy.js b/packages/e2e/next/cypress/e2e/useQueryState.cy.js index 8536defe7..df17d4432 100644 --- a/packages/e2e/next/cypress/e2e/useQueryState.cy.js +++ b/packages/e2e/next/cypress/e2e/useQueryState.cy.js @@ -79,6 +79,29 @@ function runTest(pathname) { cy.get('#bool_value').should('be.empty') } + // Index + { + cy.get('#index_value').should('be.empty') + cy.get('#index_increment').click() + cy.location('search').should('eq', '?index=2') + cy.get('#index_value').should('have.text', '1') + cy.get('#index_increment').click() + cy.location('search').should('eq', '?index=3') + cy.get('#index_value').should('have.text', '2') + cy.get('#index_decrement').click() + cy.location('search').should('eq', '?index=2') + cy.get('#index_value').should('have.text', '1') + cy.get('#index_decrement').click() + cy.location('search').should('eq', '?index=1') + cy.get('#index_value').should('have.text', '0') + cy.get('#index_decrement').click() + cy.location('search').should('eq', '?index=0') + cy.get('#index_value').should('have.text', '-1') + cy.get('#index_clear').click() + cy.location('search').should('be.empty') + cy.get('#index_value').should('be.empty') + } + // todo: Add tests for: // Timestamp // ISO DateTime diff --git a/packages/e2e/next/cypress/e2e/useQueryStates.cy.js b/packages/e2e/next/cypress/e2e/useQueryStates.cy.js index 36edff358..8a1b919c7 100644 --- a/packages/e2e/next/cypress/e2e/useQueryStates.cy.js +++ b/packages/e2e/next/cypress/e2e/useQueryStates.cy.js @@ -4,11 +4,12 @@ function runTest() { cy.contains('#hydration-marker', 'hydrated').should('be.hidden') cy.get('#json').should( 'have.text', - '{"string":null,"int":null,"float":null,"bool":null}' + '{"string":null,"int":null,"float":null,"index":null,"bool":null}' ) cy.get('#string').should('be.empty') cy.get('#int').should('be.empty') cy.get('#float').should('be.empty') + cy.get('#index').should('be.empty') cy.get('#bool').should('be.empty') cy.location('search').should('be.empty') @@ -17,7 +18,7 @@ function runTest() { cy.get('#string').should('have.text', 'Hello') cy.get('#json').should( 'have.text', - '{"string":"Hello","int":null,"float":null,"bool":null}' + '{"string":"Hello","int":null,"float":null,"index":null,"bool":null}' ) cy.contains('Set int').click() @@ -25,7 +26,7 @@ function runTest() { cy.get('#int').should('have.text', '42') cy.get('#json').should( 'have.text', - '{"string":"Hello","int":42,"float":null,"bool":null}' + '{"string":"Hello","int":42,"float":null,"index":null,"bool":null}' ) cy.contains('Set float').click() @@ -33,7 +34,15 @@ function runTest() { cy.get('#float').should('have.text', '3.14159') cy.get('#json').should( 'have.text', - '{"string":"Hello","int":42,"float":3.14159,"bool":null}' + '{"string":"Hello","int":42,"float":3.14159,"index":null,"bool":null}' + ) + + cy.contains('Set index').click() + cy.location('search').should('include', 'index=9') + cy.get('#index').should('have.text', '8') + cy.get('#json').should( + 'have.text', + '{"string":"Hello","int":42,"float":3.14159,"index":8,"bool":null}' ) cy.contains('Toggle bool').click() @@ -41,14 +50,14 @@ function runTest() { cy.get('#bool').should('have.text', 'true') cy.get('#json').should( 'have.text', - '{"string":"Hello","int":42,"float":3.14159,"bool":true}' + '{"string":"Hello","int":42,"float":3.14159,"index":8,"bool":true}' ) cy.contains('Toggle bool').click() cy.location('search').should('include', 'bool=false') cy.get('#bool').should('have.text', 'false') cy.get('#json').should( 'have.text', - '{"string":"Hello","int":42,"float":3.14159,"bool":false}' + '{"string":"Hello","int":42,"float":3.14159,"index":8,"bool":false}' ) cy.get('#clear-string').click() @@ -56,21 +65,23 @@ function runTest() { cy.get('#string').should('be.empty') cy.get('#json').should( 'have.text', - '{"string":null,"int":42,"float":3.14159,"bool":false}' + '{"string":null,"int":42,"float":3.14159,"index":8,"bool":false}' ) cy.get('#clear').click() cy.location('search').should('not.include', 'string') cy.location('search').should('not.include', 'int') cy.location('search').should('not.include', 'float') + cy.location('search').should('not.include', 'index') cy.location('search').should('not.include', 'bool') cy.get('#json').should( 'have.text', - '{"string":null,"int":null,"float":null,"bool":null}' + '{"string":null,"int":null,"float":null,"index":null,"bool":null}' ) cy.get('#string').should('be.empty') cy.get('#int').should('be.empty') cy.get('#float').should('be.empty') + cy.get('#index').should('be.empty') cy.get('#bool').should('be.empty') cy.location('search').should('be.empty') } diff --git a/packages/e2e/next/src/app/app/cache/all.tsx b/packages/e2e/next/src/app/app/cache/all.tsx index 038a70835..6d0ce7939 100644 --- a/packages/e2e/next/src/app/app/cache/all.tsx +++ b/packages/e2e/next/src/app/app/cache/all.tsx @@ -1,13 +1,14 @@ import { cache } from './searchParams' export function All() { - const { bool, num, str, def, nope } = cache.all() + const { bool, num, str, def, nope, idx } = cache.all() return ( <>

From all:

{str} {num} + {String(idx)} {String(bool)} {def} {String(nope)} diff --git a/packages/e2e/next/src/app/app/cache/get.tsx b/packages/e2e/next/src/app/app/cache/get.tsx index 0552f3026..52b7a06e0 100644 --- a/packages/e2e/next/src/app/app/cache/get.tsx +++ b/packages/e2e/next/src/app/app/cache/get.tsx @@ -3,6 +3,7 @@ import { cache } from './searchParams' export function Get() { const bool = cache.get('bool') const num = cache.get('num') + const idx = cache.get('idx') const str = cache.get('str') const def = cache.get('def') const nope = cache.get('nope') @@ -12,6 +13,7 @@ export function Get() {

{str} {num} + {String(idx)} {String(bool)} {def} {String(nope)} diff --git a/packages/e2e/next/src/app/app/cache/page.tsx b/packages/e2e/next/src/app/app/cache/page.tsx index f4d509205..944d81211 100644 --- a/packages/e2e/next/src/app/app/cache/page.tsx +++ b/packages/e2e/next/src/app/app/cache/page.tsx @@ -10,7 +10,7 @@ type Props = { } export default async function Page({ searchParams }: Props) { - const { str, bool, num, def, nope } = await cache.parse(searchParams) + const { str, bool, num, def, nope, idx } = await cache.parse(searchParams) return ( <>

Root page

@@ -18,6 +18,7 @@ export default async function Page({ searchParams }: Props) {

{str} {num} + {String(idx)} {String(bool)} {def} {String(nope)} diff --git a/packages/e2e/next/src/app/app/cache/searchParams.ts b/packages/e2e/next/src/app/app/cache/searchParams.ts index d8b7b5ad5..91ecbe9a1 100644 --- a/packages/e2e/next/src/app/app/cache/searchParams.ts +++ b/packages/e2e/next/src/app/app/cache/searchParams.ts @@ -2,12 +2,14 @@ import { createSearchParamsCache, parseAsBoolean, parseAsInteger, + parseAsIndex, parseAsString } from 'nuqs/server' export const parsers = { str: parseAsString, num: parseAsInteger, + idx: parseAsIndex, bool: parseAsBoolean, def: parseAsString.withDefault('default'), nope: parseAsString diff --git a/packages/e2e/next/src/app/app/cache/set.tsx b/packages/e2e/next/src/app/app/cache/set.tsx index c608b3e0e..4e6a6331a 100644 --- a/packages/e2e/next/src/app/app/cache/set.tsx +++ b/packages/e2e/next/src/app/app/cache/set.tsx @@ -4,7 +4,7 @@ import { useQueryStates } from 'nuqs' import { parsers } from './searchParams' export function Set() { - const [{ bool, num, str, def, nope }, set] = useQueryStates(parsers, { + const [{ bool, num, str, def, nope, idx }, set] = useQueryStates(parsers, { shallow: false }) return ( @@ -16,6 +16,7 @@ export function Set() {

{str} {num} + {String(idx)} {String(bool)} {def} {String(nope)} diff --git a/packages/e2e/next/src/app/app/useQueryState/page.tsx b/packages/e2e/next/src/app/app/useQueryState/page.tsx index 9d1e7d8cd..b90231dd3 100644 --- a/packages/e2e/next/src/app/app/useQueryState/page.tsx +++ b/packages/e2e/next/src/app/app/useQueryState/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation' import { parseAsBoolean, parseAsFloat, + parseAsIndex, parseAsInteger, parseAsString, useQueryState @@ -64,6 +65,7 @@ const Pane = () => { + @@ -144,6 +146,31 @@ const FloatSection = () => { ) } +const IndexSection = () => { + const [index, setIndex] = useQueryState('index', parseAsIndex) + return ( +

+

Index

+ + + +

{index}

+
+ ) +} + const BoolSection = () => { const [bool, setBool] = useQueryState('bool', parseAsBoolean) return ( diff --git a/packages/e2e/next/src/app/app/useQueryStates/page.tsx b/packages/e2e/next/src/app/app/useQueryStates/page.tsx index 06bc62f4d..b99d35103 100644 --- a/packages/e2e/next/src/app/app/useQueryStates/page.tsx +++ b/packages/e2e/next/src/app/app/useQueryStates/page.tsx @@ -4,6 +4,7 @@ import Link from 'next/link' import { parseAsBoolean, parseAsFloat, + parseAsIndex, parseAsInteger, parseAsString, useQueryStates @@ -23,6 +24,7 @@ function IntegrationPage() { string: parseAsString, int: parseAsInteger, float: parseAsFloat, + index: parseAsIndex, bool: parseAsBoolean }) return ( @@ -30,6 +32,7 @@ function IntegrationPage() { + @@ -43,6 +46,7 @@ function IntegrationPage() { string: null, int: null, float: null, + index: null, bool: null }) } @@ -57,6 +61,7 @@ function IntegrationPage() {

{state.string}

{state.int}

{state.float}

+

{state.index}

{state.bool === null ? null : state.bool ? 'true' : 'false'}

diff --git a/packages/e2e/next/src/pages/pages/useQueryState/index.tsx b/packages/e2e/next/src/pages/pages/useQueryState/index.tsx index a487a7c0c..78995cd24 100644 --- a/packages/e2e/next/src/pages/pages/useQueryState/index.tsx +++ b/packages/e2e/next/src/pages/pages/useQueryState/index.tsx @@ -4,6 +4,7 @@ import { usePathname } from 'next/navigation' import { parseAsBoolean, parseAsFloat, + parseAsIndex, parseAsInteger, parseAsString, useQueryState @@ -25,6 +26,7 @@ const IntegrationPage = () => { const [string, setString] = useQueryState('string') const [int, setInt] = useQueryState('int', parseAsInteger) const [float, setFloat] = useQueryState('float', parseAsFloat) + const [index, setIndex] = useQueryState('index', parseAsIndex) const [bool, setBool] = useQueryState('bool', parseAsBoolean) const [text, setText] = useQueryState( 'text', @@ -112,6 +114,25 @@ const IntegrationPage = () => { />

{float}

+
+

Index

+ + + +

{index}

+

Boolean

+ @@ -35,6 +38,7 @@ const IntegrationPage = () => { string: null, int: null, float: null, + index: null, bool: null })) } @@ -49,6 +53,7 @@ const IntegrationPage = () => {

{state.string}

{state.int}

{state.float}

+

{state.index}

{state.bool === null ? null : state.bool ? 'true' : 'false'}

From 2e9a2b4ebf69830283350465f051e98c5348c19b Mon Sep 17 00:00:00 2001 From: Jeppe Hasseriis Date: Sat, 8 Feb 2025 13:59:21 +0100 Subject: [PATCH 06/11] doc: Update Community > TanStack Table Parsers --- .../community/tanstack-table.generator.tsx | 34 +++---------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/packages/docs/content/docs/parsers/community/tanstack-table.generator.tsx b/packages/docs/content/docs/parsers/community/tanstack-table.generator.tsx index 0353b938e..eab5c3f28 100644 --- a/packages/docs/content/docs/parsers/community/tanstack-table.generator.tsx +++ b/packages/docs/content/docs/parsers/community/tanstack-table.generator.tsx @@ -20,7 +20,7 @@ import { } from '@/src/components/ui/select' import { Separator } from '@/src/components/ui/separator' import { - createParser, + parseAsIndex, parseAsInteger, parseAsString, useQueryState @@ -29,19 +29,6 @@ import { useDeferredValue } from 'react' const NUM_PAGES = 5 -// The page index parser is zero-indexed internally, -// but one-indexed when rendered in the URL, -// to align with your UI and what users might expect. -const pageIndexParser = createParser({ - parse: query => { - const page = parseAsInteger.parse(query) - return page === null ? null : page - 1 - }, - serialize: value => { - return parseAsInteger.serialize(value + 1) - } -}) - export function TanStackTablePagination() { const [pageIndexUrlKey, setPageIndexUrlKey] = useQueryState( 'pageIndexUrlKey', @@ -53,7 +40,7 @@ export function TanStackTablePagination() { ) const [page, setPage] = useQueryState( pageIndexUrlKey, - pageIndexParser.withDefault(0) + parseAsIndex.withDefault(0) ) const [pageSize, setPageSize] = useQueryState( pageSizeUrlKey, @@ -61,27 +48,14 @@ export function TanStackTablePagination() { ) const parserCode = useDeferredValue(`import { - createParser, + parseAsIndex, parseAsInteger, parseAsString, useQueryStates } from 'nuqs' -// The page index parser is zero-indexed internally, -// but one-indexed when rendered in the URL, -// to align with your UI and what users might expect. -const pageIndexParser = createParser({ - parse: query => { - const page = parseAsInteger.parse(query) - return page === null ? null : page - 1 - }, - serialize: value => { - return parseAsInteger.serialize(value + 1) - } -}) - const paginationParsers = { - pageIndex: pageIndexParser.withDefault(0), + pageIndex: parseAsIndex.withDefault(0), pageSize: parseAsInteger.withDefault(10) } const paginationUrlKeys = { From 30df519df586e9facee68d99f7e29a00c61ed7d1 Mon Sep 17 00:00:00 2001 From: Jeppe Hasseriis Date: Sat, 8 Feb 2025 15:50:40 +0100 Subject: [PATCH 07/11] style: Formatting issues --- packages/nuqs/src/parsers.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/nuqs/src/parsers.ts b/packages/nuqs/src/parsers.ts index 266f306f4..df30ed19b 100644 --- a/packages/nuqs/src/parsers.ts +++ b/packages/nuqs/src/parsers.ts @@ -157,14 +157,14 @@ export const parseAsInteger = createParser({ export const parseAsIndex = createParser({ parse: v => { - const int = parseInt(v); + const int = parseInt(v) if (Number.isNaN(int)) { - return null; + return null } - return int - 1; + return int - 1 }, - serialize: (v) => Math.round(v + 1).toFixed(), -}); + serialize: v => Math.round(v + 1).toFixed() +}) export const parseAsHex = createParser({ parse: v => { From 3a5ea3be2d3eb1a9561cda6a811fbd0e3fd93d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Sun, 9 Feb 2025 10:45:13 +0100 Subject: [PATCH 08/11] ref: Reuse parseAsInteger logic, add positive check --- packages/nuqs/src/parsers.test.ts | 8 +++++--- packages/nuqs/src/parsers.ts | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/nuqs/src/parsers.test.ts b/packages/nuqs/src/parsers.test.ts index 8495ebfcd..c2048561d 100644 --- a/packages/nuqs/src/parsers.test.ts +++ b/packages/nuqs/src/parsers.test.ts @@ -3,12 +3,12 @@ import { parseAsArrayOf, parseAsFloat, parseAsHex, + parseAsIndex, parseAsInteger, - parseAsIsoDateTime, parseAsIsoDate, + parseAsIsoDateTime, parseAsString, - parseAsTimestamp, - parseAsIndex + parseAsTimestamp } from './parsers' describe('parsers', () => { @@ -33,6 +33,8 @@ describe('parsers', () => { expect(parseAsIndex.parse('1')).toBe(0) expect(parseAsIndex.parse('3.14')).toBe(2) expect(parseAsIndex.parse('3,14')).toBe(2) + expect(parseAsIndex.parse('0')).toBeNull() + expect(parseAsIndex.parse('-1')).toBeNull() expect(parseAsIndex.serialize(0)).toBe('1') expect(parseAsIndex.serialize(3.14)).toBe('4') }) diff --git a/packages/nuqs/src/parsers.ts b/packages/nuqs/src/parsers.ts index df30ed19b..3f863901f 100644 --- a/packages/nuqs/src/parsers.ts +++ b/packages/nuqs/src/parsers.ts @@ -157,13 +157,13 @@ export const parseAsInteger = createParser({ export const parseAsIndex = createParser({ parse: v => { - const int = parseInt(v) - if (Number.isNaN(int)) { + const int = parseAsInteger.parse(v) + if (int === null || int <= 0) { return null } return int - 1 }, - serialize: v => Math.round(v + 1).toFixed() + serialize: v => parseAsInteger.serialize(v + 1) }) export const parseAsHex = createParser({ From 201a597abad8717428a934dc6350e9a6ee3a7bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Sun, 9 Feb 2025 10:56:22 +0100 Subject: [PATCH 09/11] chore: Remove negative index test --- packages/e2e/next/cypress/e2e/useQueryState.cy.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/e2e/next/cypress/e2e/useQueryState.cy.js b/packages/e2e/next/cypress/e2e/useQueryState.cy.js index df17d4432..3b371edc2 100644 --- a/packages/e2e/next/cypress/e2e/useQueryState.cy.js +++ b/packages/e2e/next/cypress/e2e/useQueryState.cy.js @@ -95,8 +95,6 @@ function runTest(pathname) { cy.location('search').should('eq', '?index=1') cy.get('#index_value').should('have.text', '0') cy.get('#index_decrement').click() - cy.location('search').should('eq', '?index=0') - cy.get('#index_value').should('have.text', '-1') cy.get('#index_clear').click() cy.location('search').should('be.empty') cy.get('#index_value').should('be.empty') From 524a63133ab1f5c96e126b66eb05f3841b971bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Sun, 9 Feb 2025 13:14:58 +0100 Subject: [PATCH 10/11] chore: Allow negative indices again --- packages/nuqs/src/parsers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuqs/src/parsers.ts b/packages/nuqs/src/parsers.ts index 3f863901f..bde81495b 100644 --- a/packages/nuqs/src/parsers.ts +++ b/packages/nuqs/src/parsers.ts @@ -158,7 +158,7 @@ export const parseAsInteger = createParser({ export const parseAsIndex = createParser({ parse: v => { const int = parseAsInteger.parse(v) - if (int === null || int <= 0) { + if (int === null) { return null } return int - 1 From a17f509914dac3a0c3b1d2c87aa7fe5bffc1370b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Sun, 9 Feb 2025 13:16:02 +0100 Subject: [PATCH 11/11] test: Fix tests --- packages/nuqs/src/parsers.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuqs/src/parsers.test.ts b/packages/nuqs/src/parsers.test.ts index c2048561d..dcbf52f08 100644 --- a/packages/nuqs/src/parsers.test.ts +++ b/packages/nuqs/src/parsers.test.ts @@ -33,8 +33,8 @@ describe('parsers', () => { expect(parseAsIndex.parse('1')).toBe(0) expect(parseAsIndex.parse('3.14')).toBe(2) expect(parseAsIndex.parse('3,14')).toBe(2) - expect(parseAsIndex.parse('0')).toBeNull() - expect(parseAsIndex.parse('-1')).toBeNull() + expect(parseAsIndex.parse('0')).toBe(-1) + expect(parseAsIndex.parse('-1')).toBe(-2) expect(parseAsIndex.serialize(0)).toBe('1') expect(parseAsIndex.serialize(3.14)).toBe('4') })