Skip to content

Commit

Permalink
feat: Add parseAsPageIndex parser (#791)
Browse files Browse the repository at this point in the history
Co-authored-by: François Best <github@francoisbest.com>
  • Loading branch information
cenobitedk and franky47 authored Feb 9, 2025
1 parent b41c027 commit 11a46bf
Show file tree
Hide file tree
Showing 17 changed files with 186 additions and 45 deletions.
20 changes: 18 additions & 2 deletions packages/docs/content/docs/parsers/built-in.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
StringParserDemo,
FloatParserDemo,
HexParserDemo,
IndexParserDemo,
BooleanParserDemo,
StringLiteralParserDemo,
DateISOParserDemo,
Expand Down Expand Up @@ -105,6 +106,20 @@ useQueryState('hex', parseAsHex.withDefault(0x00))
Check out the [Hex Colors](/playground/hex-colors) playground for a demo.
</Callout>

### 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))
```

<Suspense fallback={<DemoFallback />}>
<IndexParserDemo />
</Suspense>

## Boolean

```ts
Expand Down Expand Up @@ -203,8 +218,9 @@ import { parseAsIsoDate } from 'nuqs'
</Suspense>

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

### Timestamp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from '@/src/components/ui/select'
import { Separator } from '@/src/components/ui/separator'
import {
createParser,
parseAsIndex,
parseAsInteger,
parseAsString,
useQueryState
Expand All @@ -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',
Expand All @@ -53,35 +40,22 @@ export function TanStackTablePagination() {
)
const [page, setPage] = useQueryState(
pageIndexUrlKey,
pageIndexParser.withDefault(0)
parseAsIndex.withDefault(0)
)
const [pageSize, setPageSize] = useQueryState(
pageSizeUrlKey,
parseAsInteger.withDefault(10)
)

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 = {
Expand Down
29 changes: 29 additions & 0 deletions packages/docs/content/docs/parsers/demos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
parseAsBoolean,
parseAsFloat,
parseAsHex,
parseAsIndex,
parseAsInteger,
parseAsIsoDate,
parseAsIsoDateTime,
Expand Down Expand Up @@ -171,6 +172,34 @@ export function HexParserDemo() {
)
}

export function IndexParserDemo() {
const [value, setValue] = useQueryState('page', parseAsIndex)
return (
<DemoContainer demoKey="page">
<input
type="number"
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"
value={value ?? ''} // Handle empty input
onChange={e => {
if (e.target.value === '') {
setValue(null)
} else {
setValue(e.target.valueAsNumber)
}
}}
placeholder="What page are you on?"
/>
<Button
variant="secondary"
onClick={() => setValue(null)}
className="ml-auto"
>
Clear
</Button>
</DemoContainer>
)
}

export function BooleanParserDemo() {
const [value, setValue] = useQueryState(
'bool',
Expand Down
5 changes: 4 additions & 1 deletion packages/e2e/next/cypress/e2e/cache.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
21 changes: 21 additions & 0 deletions packages/e2e/next/cypress/e2e/useQueryState.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,27 @@ 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.get('#index_clear').click()
cy.location('search').should('be.empty')
cy.get('#index_value').should('be.empty')
}

// todo: Add tests for:
// Timestamp
// ISO DateTime
Expand Down
27 changes: 19 additions & 8 deletions packages/e2e/next/cypress/e2e/useQueryStates.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -17,60 +18,70 @@ 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()
cy.location('search').should('include', 'int=42')
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()
cy.location('search').should('include', 'float=3.14159')
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()
cy.location('search').should('include', 'bool=true')
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()
cy.location('search').should('not.include', 'string=Hello')
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')
}
Expand Down
3 changes: 2 additions & 1 deletion packages/e2e/next/src/app/app/cache/all.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h2>From all:</h2>
<p style={{ display: 'flex', gap: '1rem' }}>
<span id="all-str">{str}</span>
<span id="all-num">{num}</span>
<span id="all-idx">{String(idx)}</span>
<span id="all-bool">{String(bool)}</span>
<span id="all-def">{def}</span>
<span id="all-nope">{String(nope)}</span>
Expand Down
2 changes: 2 additions & 0 deletions packages/e2e/next/src/app/app/cache/get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -12,6 +13,7 @@ export function Get() {
<p style={{ display: 'flex', gap: '1rem' }}>
<span id="get-str">{str}</span>
<span id="get-num">{num}</span>
<span id="get-idx">{String(idx)}</span>
<span id="get-bool">{String(bool)}</span>
<span id="get-def">{def}</span>
<span id="get-nope">{String(nope)}</span>
Expand Down
3 changes: 2 additions & 1 deletion packages/e2e/next/src/app/app/cache/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ 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 (
<>
<h1>Root page</h1>
<h2>From parse:</h2>
<p style={{ display: 'flex', gap: '1rem' }}>
<span id="parse-str">{str}</span>
<span id="parse-num">{num}</span>
<span id="parse-idx">{String(idx)}</span>
<span id="parse-bool">{String(bool)}</span>
<span id="parse-def">{def}</span>
<span id="parse-nope">{String(nope)}</span>
Expand Down
2 changes: 2 additions & 0 deletions packages/e2e/next/src/app/app/cache/searchParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/e2e/next/src/app/app/cache/set.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -16,6 +16,7 @@ export function Set() {
<p style={{ display: 'flex', gap: '1rem' }}>
<span id="set-str">{str}</span>
<span id="set-num">{num}</span>
<span id="set-idx">{String(idx)}</span>
<span id="set-bool">{String(bool)}</span>
<span id="set-def">{def}</span>
<span id="set-nope">{String(nope)}</span>
Expand Down
Loading

0 comments on commit 11a46bf

Please sign in to comment.