Skip to content

Commit 11a46bf

Browse files
cenobitedkfranky47
andauthored
feat: Add parseAsPageIndex parser (#791)
Co-authored-by: François Best <github@francoisbest.com>
1 parent b41c027 commit 11a46bf

File tree

17 files changed

+186
-45
lines changed

17 files changed

+186
-45
lines changed

packages/docs/content/docs/parsers/built-in.mdx

+18-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
StringParserDemo,
1010
FloatParserDemo,
1111
HexParserDemo,
12+
IndexParserDemo,
1213
BooleanParserDemo,
1314
StringLiteralParserDemo,
1415
DateISOParserDemo,
@@ -105,6 +106,20 @@ useQueryState('hex', parseAsHex.withDefault(0x00))
105106
Check out the [Hex Colors](/playground/hex-colors) playground for a demo.
106107
</Callout>
107108

109+
### Index
110+
111+
Same as integer, but adds a `+1` offset to the query value. Useful for pagination indexes.
112+
113+
```ts
114+
import { parseAsIndex } from 'nuqs'
115+
116+
useQueryState('page', parseAsIndex.withDefault(0))
117+
```
118+
119+
<Suspense fallback={<DemoFallback />}>
120+
<IndexParserDemo />
121+
</Suspense>
122+
108123
## Boolean
109124

110125
```ts
@@ -203,8 +218,9 @@ import { parseAsIsoDate } from 'nuqs'
203218
</Suspense>
204219

205220
<Callout>
206-
The Date is parsed without the time zone offset, making it at 00:00:00 UTC.<br/>
207-
<span className='block mt-1.5'>_Support: introduced in version 2.1.0._</span>
221+
The Date is parsed without the time zone offset, making it at 00:00:00 UTC.
222+
<br />
223+
<span className="mt-1.5 block">_Support: introduced in version 2.1.0._</span>
208224
</Callout>
209225

210226
### Timestamp

packages/docs/content/docs/parsers/community/tanstack-table.generator.tsx

+4-30
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from '@/src/components/ui/select'
2121
import { Separator } from '@/src/components/ui/separator'
2222
import {
23-
createParser,
23+
parseAsIndex,
2424
parseAsInteger,
2525
parseAsString,
2626
useQueryState
@@ -29,19 +29,6 @@ import { useDeferredValue } from 'react'
2929

3030
const NUM_PAGES = 5
3131

32-
// The page index parser is zero-indexed internally,
33-
// but one-indexed when rendered in the URL,
34-
// to align with your UI and what users might expect.
35-
const pageIndexParser = createParser({
36-
parse: query => {
37-
const page = parseAsInteger.parse(query)
38-
return page === null ? null : page - 1
39-
},
40-
serialize: value => {
41-
return parseAsInteger.serialize(value + 1)
42-
}
43-
})
44-
4532
export function TanStackTablePagination() {
4633
const [pageIndexUrlKey, setPageIndexUrlKey] = useQueryState(
4734
'pageIndexUrlKey',
@@ -53,35 +40,22 @@ export function TanStackTablePagination() {
5340
)
5441
const [page, setPage] = useQueryState(
5542
pageIndexUrlKey,
56-
pageIndexParser.withDefault(0)
43+
parseAsIndex.withDefault(0)
5744
)
5845
const [pageSize, setPageSize] = useQueryState(
5946
pageSizeUrlKey,
6047
parseAsInteger.withDefault(10)
6148
)
6249

6350
const parserCode = useDeferredValue(`import {
64-
createParser,
51+
parseAsIndex,
6552
parseAsInteger,
6653
parseAsString,
6754
useQueryStates
6855
} from 'nuqs'
6956
70-
// The page index parser is zero-indexed internally,
71-
// but one-indexed when rendered in the URL,
72-
// to align with your UI and what users might expect.
73-
const pageIndexParser = createParser({
74-
parse: query => {
75-
const page = parseAsInteger.parse(query)
76-
return page === null ? null : page - 1
77-
},
78-
serialize: value => {
79-
return parseAsInteger.serialize(value + 1)
80-
}
81-
})
82-
8357
const paginationParsers = {
84-
pageIndex: pageIndexParser.withDefault(0),
58+
pageIndex: parseAsIndex.withDefault(0),
8559
pageSize: parseAsInteger.withDefault(10)
8660
}
8761
const paginationUrlKeys = {

packages/docs/content/docs/parsers/demos.tsx

+29
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
parseAsBoolean,
1414
parseAsFloat,
1515
parseAsHex,
16+
parseAsIndex,
1617
parseAsInteger,
1718
parseAsIsoDate,
1819
parseAsIsoDateTime,
@@ -171,6 +172,34 @@ export function HexParserDemo() {
171172
)
172173
}
173174

175+
export function IndexParserDemo() {
176+
const [value, setValue] = useQueryState('page', parseAsIndex)
177+
return (
178+
<DemoContainer demoKey="page">
179+
<input
180+
type="number"
181+
className="flex h-10 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
182+
value={value ?? ''} // Handle empty input
183+
onChange={e => {
184+
if (e.target.value === '') {
185+
setValue(null)
186+
} else {
187+
setValue(e.target.valueAsNumber)
188+
}
189+
}}
190+
placeholder="What page are you on?"
191+
/>
192+
<Button
193+
variant="secondary"
194+
onClick={() => setValue(null)}
195+
className="ml-auto"
196+
>
197+
Clear
198+
</Button>
199+
</DemoContainer>
200+
)
201+
}
202+
174203
export function BooleanParserDemo() {
175204
const [value, setValue] = useQueryState(
176205
'bool',

packages/e2e/next/cypress/e2e/cache.cy.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@
22

33
describe('cache', () => {
44
it('works in app router', () => {
5-
cy.visit('/app/cache?str=foo&num=42&bool=true&multi=foo&multi=bar')
5+
cy.visit('/app/cache?str=foo&num=42&idx=1&bool=true&multi=foo&multi=bar')
66
cy.get('#parse-str').should('have.text', 'foo')
77
cy.get('#parse-num').should('have.text', '42')
8+
cy.get('#parse-idx').should('have.text', '0')
89
cy.get('#parse-bool').should('have.text', 'true')
910
cy.get('#parse-def').should('have.text', 'default')
1011
cy.get('#parse-nope').should('have.text', 'null')
1112
cy.get('#all-str').should('have.text', 'foo')
1213
cy.get('#all-num').should('have.text', '42')
14+
cy.get('#all-idx').should('have.text', '0')
1315
cy.get('#all-bool').should('have.text', 'true')
1416
cy.get('#all-def').should('have.text', 'default')
1517
cy.get('#all-nope').should('have.text', 'null')
1618
cy.get('#get-str').should('have.text', 'foo')
1719
cy.get('#get-num').should('have.text', '42')
20+
cy.get('#get-idx').should('have.text', '0')
1821
cy.get('#get-bool').should('have.text', 'true')
1922
cy.get('#get-def').should('have.text', 'default')
2023
cy.get('#get-nope').should('have.text', 'null')

packages/e2e/next/cypress/e2e/useQueryState.cy.js

+21
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,27 @@ function runTest(pathname) {
7979
cy.get('#bool_value').should('be.empty')
8080
}
8181

82+
// Index
83+
{
84+
cy.get('#index_value').should('be.empty')
85+
cy.get('#index_increment').click()
86+
cy.location('search').should('eq', '?index=2')
87+
cy.get('#index_value').should('have.text', '1')
88+
cy.get('#index_increment').click()
89+
cy.location('search').should('eq', '?index=3')
90+
cy.get('#index_value').should('have.text', '2')
91+
cy.get('#index_decrement').click()
92+
cy.location('search').should('eq', '?index=2')
93+
cy.get('#index_value').should('have.text', '1')
94+
cy.get('#index_decrement').click()
95+
cy.location('search').should('eq', '?index=1')
96+
cy.get('#index_value').should('have.text', '0')
97+
cy.get('#index_decrement').click()
98+
cy.get('#index_clear').click()
99+
cy.location('search').should('be.empty')
100+
cy.get('#index_value').should('be.empty')
101+
}
102+
82103
// todo: Add tests for:
83104
// Timestamp
84105
// ISO DateTime

packages/e2e/next/cypress/e2e/useQueryStates.cy.js

+19-8
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ function runTest() {
44
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
55
cy.get('#json').should(
66
'have.text',
7-
'{"string":null,"int":null,"float":null,"bool":null}'
7+
'{"string":null,"int":null,"float":null,"index":null,"bool":null}'
88
)
99
cy.get('#string').should('be.empty')
1010
cy.get('#int').should('be.empty')
1111
cy.get('#float').should('be.empty')
12+
cy.get('#index').should('be.empty')
1213
cy.get('#bool').should('be.empty')
1314
cy.location('search').should('be.empty')
1415

@@ -17,60 +18,70 @@ function runTest() {
1718
cy.get('#string').should('have.text', 'Hello')
1819
cy.get('#json').should(
1920
'have.text',
20-
'{"string":"Hello","int":null,"float":null,"bool":null}'
21+
'{"string":"Hello","int":null,"float":null,"index":null,"bool":null}'
2122
)
2223

2324
cy.contains('Set int').click()
2425
cy.location('search').should('include', 'int=42')
2526
cy.get('#int').should('have.text', '42')
2627
cy.get('#json').should(
2728
'have.text',
28-
'{"string":"Hello","int":42,"float":null,"bool":null}'
29+
'{"string":"Hello","int":42,"float":null,"index":null,"bool":null}'
2930
)
3031

3132
cy.contains('Set float').click()
3233
cy.location('search').should('include', 'float=3.14159')
3334
cy.get('#float').should('have.text', '3.14159')
3435
cy.get('#json').should(
3536
'have.text',
36-
'{"string":"Hello","int":42,"float":3.14159,"bool":null}'
37+
'{"string":"Hello","int":42,"float":3.14159,"index":null,"bool":null}'
38+
)
39+
40+
cy.contains('Set index').click()
41+
cy.location('search').should('include', 'index=9')
42+
cy.get('#index').should('have.text', '8')
43+
cy.get('#json').should(
44+
'have.text',
45+
'{"string":"Hello","int":42,"float":3.14159,"index":8,"bool":null}'
3746
)
3847

3948
cy.contains('Toggle bool').click()
4049
cy.location('search').should('include', 'bool=true')
4150
cy.get('#bool').should('have.text', 'true')
4251
cy.get('#json').should(
4352
'have.text',
44-
'{"string":"Hello","int":42,"float":3.14159,"bool":true}'
53+
'{"string":"Hello","int":42,"float":3.14159,"index":8,"bool":true}'
4554
)
4655
cy.contains('Toggle bool').click()
4756
cy.location('search').should('include', 'bool=false')
4857
cy.get('#bool').should('have.text', 'false')
4958
cy.get('#json').should(
5059
'have.text',
51-
'{"string":"Hello","int":42,"float":3.14159,"bool":false}'
60+
'{"string":"Hello","int":42,"float":3.14159,"index":8,"bool":false}'
5261
)
5362

5463
cy.get('#clear-string').click()
5564
cy.location('search').should('not.include', 'string=Hello')
5665
cy.get('#string').should('be.empty')
5766
cy.get('#json').should(
5867
'have.text',
59-
'{"string":null,"int":42,"float":3.14159,"bool":false}'
68+
'{"string":null,"int":42,"float":3.14159,"index":8,"bool":false}'
6069
)
6170

6271
cy.get('#clear').click()
6372
cy.location('search').should('not.include', 'string')
6473
cy.location('search').should('not.include', 'int')
6574
cy.location('search').should('not.include', 'float')
75+
cy.location('search').should('not.include', 'index')
6676
cy.location('search').should('not.include', 'bool')
6777
cy.get('#json').should(
6878
'have.text',
69-
'{"string":null,"int":null,"float":null,"bool":null}'
79+
'{"string":null,"int":null,"float":null,"index":null,"bool":null}'
7080
)
7181
cy.get('#string').should('be.empty')
7282
cy.get('#int').should('be.empty')
7383
cy.get('#float').should('be.empty')
84+
cy.get('#index').should('be.empty')
7485
cy.get('#bool').should('be.empty')
7586
cy.location('search').should('be.empty')
7687
}

packages/e2e/next/src/app/app/cache/all.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { cache } from './searchParams'
22

33
export function All() {
4-
const { bool, num, str, def, nope } = cache.all()
4+
const { bool, num, str, def, nope, idx } = cache.all()
55
return (
66
<>
77
<h2>From all:</h2>
88
<p style={{ display: 'flex', gap: '1rem' }}>
99
<span id="all-str">{str}</span>
1010
<span id="all-num">{num}</span>
11+
<span id="all-idx">{String(idx)}</span>
1112
<span id="all-bool">{String(bool)}</span>
1213
<span id="all-def">{def}</span>
1314
<span id="all-nope">{String(nope)}</span>

packages/e2e/next/src/app/app/cache/get.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { cache } from './searchParams'
33
export function Get() {
44
const bool = cache.get('bool')
55
const num = cache.get('num')
6+
const idx = cache.get('idx')
67
const str = cache.get('str')
78
const def = cache.get('def')
89
const nope = cache.get('nope')
@@ -12,6 +13,7 @@ export function Get() {
1213
<p style={{ display: 'flex', gap: '1rem' }}>
1314
<span id="get-str">{str}</span>
1415
<span id="get-num">{num}</span>
16+
<span id="get-idx">{String(idx)}</span>
1517
<span id="get-bool">{String(bool)}</span>
1618
<span id="get-def">{def}</span>
1719
<span id="get-nope">{String(nope)}</span>

packages/e2e/next/src/app/app/cache/page.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ type Props = {
1010
}
1111

1212
export default async function Page({ searchParams }: Props) {
13-
const { str, bool, num, def, nope } = await cache.parse(searchParams)
13+
const { str, bool, num, def, nope, idx } = await cache.parse(searchParams)
1414
return (
1515
<>
1616
<h1>Root page</h1>
1717
<h2>From parse:</h2>
1818
<p style={{ display: 'flex', gap: '1rem' }}>
1919
<span id="parse-str">{str}</span>
2020
<span id="parse-num">{num}</span>
21+
<span id="parse-idx">{String(idx)}</span>
2122
<span id="parse-bool">{String(bool)}</span>
2223
<span id="parse-def">{def}</span>
2324
<span id="parse-nope">{String(nope)}</span>

packages/e2e/next/src/app/app/cache/searchParams.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import {
22
createSearchParamsCache,
33
parseAsBoolean,
44
parseAsInteger,
5+
parseAsIndex,
56
parseAsString
67
} from 'nuqs/server'
78

89
export const parsers = {
910
str: parseAsString,
1011
num: parseAsInteger,
12+
idx: parseAsIndex,
1113
bool: parseAsBoolean,
1214
def: parseAsString.withDefault('default'),
1315
nope: parseAsString

packages/e2e/next/src/app/app/cache/set.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useQueryStates } from 'nuqs'
44
import { parsers } from './searchParams'
55

66
export function Set() {
7-
const [{ bool, num, str, def, nope }, set] = useQueryStates(parsers, {
7+
const [{ bool, num, str, def, nope, idx }, set] = useQueryStates(parsers, {
88
shallow: false
99
})
1010
return (
@@ -16,6 +16,7 @@ export function Set() {
1616
<p style={{ display: 'flex', gap: '1rem' }}>
1717
<span id="set-str">{str}</span>
1818
<span id="set-num">{num}</span>
19+
<span id="set-idx">{String(idx)}</span>
1920
<span id="set-bool">{String(bool)}</span>
2021
<span id="set-def">{def}</span>
2122
<span id="set-nope">{String(nope)}</span>

0 commit comments

Comments
 (0)