Skip to content

Commit

Permalink
feat: Add parseAsISODate (#707)
Browse files Browse the repository at this point in the history
* feat: Add parseAsISODate

* fix: Comments from review

* fix: Remove GMT

Co-authored-by: François Best <github@francoisbest.com>

* fix: Query keys

---------

Co-authored-by: Tyler <26290074+thegitduck@users.noreply.github.com>
Co-authored-by: Tyler <tyler@secondspectrum.com>
Co-authored-by: François Best <github@francoisbest.com>
  • Loading branch information
4 people authored Oct 26, 2024
1 parent 0645c51 commit 107fcc1
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 32 deletions.
67 changes: 39 additions & 28 deletions packages/docs/content/docs/parsers/built-in.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
BooleanParserDemo,
StringLiteralParserDemo,
DateISOParserDemo,
DatetimeISOParserDemo,
DateTimestampParserDemo,
JsonParserDemo
} from '@/content/docs/parsers/demos'
Expand All @@ -29,8 +30,8 @@ This is where **parsers** come in.
import { parseAsString } from 'nuqs'
```

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

<Callout title="Type-safety tip">
Expand All @@ -39,6 +40,7 @@ and will accept **any** value.

If you're expecting a certain set of string values, like `'foo' | 'bar'{:ts}`,
see [Literals](#literals) for ensuring type-runtime safety.

</Callout>

If search params are strings by default, what's the point of this _"parser"_ ?
Expand Down Expand Up @@ -67,11 +69,10 @@ import { parseAsInteger } from 'nuqs'
useQueryState('int', parseAsInteger.withDefault(0))
```

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


### Floating point

Same as integer, but uses `parseFloat` under the hood.
Expand All @@ -82,11 +83,10 @@ import { parseAsFloat } from 'nuqs'
useQueryState('float', parseAsFloat.withDefault(0))
```

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


### Hexadecimal

Encodes integers in hexadecimal.
Expand All @@ -97,12 +97,12 @@ import { parseAsHex } from 'nuqs'
useQueryState('hex', parseAsHex.withDefault(0x00))
```

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

<Callout title="Going further">
Check out the [Hex Colors](/playground/hex-colors) playground for a demo.
Check out the [Hex Colors](/playground/hex-colors) playground for a demo.
</Callout>

## Boolean
Expand All @@ -113,8 +113,8 @@ import { parseAsBoolean } from 'nuqs'
useQueryState('bool', parseAsBoolean.withDefault(false))
```

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

## Literals
Expand All @@ -138,14 +138,13 @@ const sortOrder = ['asc', 'desc'] as const
parseAsStringLiteral(sortOrder)

// Optional: extract the type from them
type SortOrder = (typeof sortOrder)[number]; // 'asc' | 'desc'
type SortOrder = (typeof sortOrder)[number] // 'asc' | 'desc'
```
<Suspense fallback={<DemoFallback/>}>
<StringLiteralParserDemo/>
<Suspense fallback={<DemoFallback />}>
<StringLiteralParserDemo />
</Suspense>

### Numeric literals
```ts /as const/
Expand Down Expand Up @@ -174,23 +173,35 @@ parseAsStringEnum<Direction>(Object.values(Direction))
```

<Callout title="Note">
The query string value will be the **value** of the enum, not its name
(here: `?direction=UP`).
The query string value will be the **value** of the enum, not its name (here:
`?direction=UP`).
</Callout>

## Dates & timestamps

There are two parsers that give you a `Date` object, their difference is
on how they encode the value into the query string.

### ISO 8601
### ISO 8601 Datetime

```ts
import { parseAsIsoDateTime } from 'nuqs'
```

<Suspense>
<DateISOParserDemo/>
<DatetimeISOParserDemo />
</Suspense>

### ISO 8601 Date

Note: the Date is parsed without the time zone offset, making it at GMT 00:00:00 UTC.

```ts
import { parseAsIsoDate } from 'nuqs'
```

<Suspense>
<DateISOParserDemo />
</Suspense>

### Timestamp
Expand All @@ -202,7 +213,7 @@ import { parseAsTimestamp } from 'nuqs'
```

<Suspense>
<DateTimestampParserDemo/>
<DateTimestampParserDemo />
</Suspense>

## Arrays
Expand Down Expand Up @@ -241,14 +252,14 @@ const schema = z.object({
const [json, setJson] = useQueryState('json', parseAsJson(schema.parse))

setJson({
pkg: "nuqs",
pkg: 'nuqs',
version: 2,
worksWith: ["Next.js", "React", "Remix", "React Router", "and more"],
});
worksWith: ['Next.js', 'React', 'Remix', 'React Router', 'and more']
})
```

<Suspense>
<JsonParserDemo/>
<JsonParserDemo />
</Suspense>

Using other validation libraries is possible, as long as they throw an error
Expand All @@ -264,6 +275,6 @@ import { parseAsString } from 'nuqs/server'
```

<Callout title="Note">
It used to be available under the alias import `nuqs/parsers`,
which will be dropped in the next major version.
It used to be available under the alias import `nuqs/parsers`, which will be
dropped in the next major version.
</Callout>
27 changes: 23 additions & 4 deletions packages/docs/content/docs/parsers/demos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
parseAsFloat,
parseAsHex,
parseAsInteger,
parseAsIsoDate,
parseAsIsoDateTime,
parseAsJson,
parseAsStringLiteral,
Expand Down Expand Up @@ -246,18 +247,20 @@ export function StringLiteralParserDemo() {

export function DateParserDemo({
queryKey,
parser
parser,
type
}: {
queryKey: string
parser: ParserBuilder<Date>
type: 'date' | 'datetime-local'
}) {
const [value, setValue] = useQueryState(queryKey, parser)
return (
<DemoContainer className="@container" demoKey={queryKey}>
<div className="flex w-full flex-col items-stretch gap-2 @md:flex-row">
<div className="flex flex-1 items-center gap-2">
<input
type="datetime-local"
type={type}
className="flex h-10 flex-[2] 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 === null ? '' : value.toISOString().slice(0, -8)}
onChange={e => {
Expand Down Expand Up @@ -290,12 +293,28 @@ export function DateParserDemo({
)
}

export function DatetimeISOParserDemo() {
return (
<DateParserDemo
type="datetime-local"
queryKey="iso"
parser={parseAsIsoDateTime}
/>
)
}

export function DateISOParserDemo() {
return <DateParserDemo queryKey="iso" parser={parseAsIsoDateTime} />
return <DateParserDemo type="date" queryKey="date" parser={parseAsIsoDate} />
}

export function DateTimestampParserDemo() {
return <DateParserDemo queryKey="ts" parser={parseAsTimestamp} />
return (
<DateParserDemo
type="datetime-local"
queryKey="ts"
parser={parseAsTimestamp}
/>
)
}

const jsonParserSchema = z.object({
Expand Down
9 changes: 9 additions & 0 deletions packages/nuqs/src/parsers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
parseAsHex,
parseAsInteger,
parseAsIsoDateTime,
parseAsIsoDate,
parseAsString,
parseAsTimestamp
} from './parsers'
Expand Down Expand Up @@ -48,6 +49,14 @@ describe('parsers', () => {
ref
)
})
test('parseAsIsoDate', () => {
expect(parseAsIsoDate.parse('')).toBeNull()
expect(parseAsIsoDate.parse('not-a-date')).toBeNull()
const moment = '2020-01-01'
const ref = new Date(moment)
expect(parseAsIsoDate.parse(moment)).toStrictEqual(ref)
expect(parseAsIsoDate.serialize(ref)).toEqual(moment)
})
test('parseAsArrayOf', () => {
const parser = parseAsArrayOf(parseAsString)
expect(parser.serialize([])).toBe('')
Expand Down
19 changes: 19 additions & 0 deletions packages/nuqs/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,25 @@ export const parseAsIsoDateTime = createParser({
serialize: (v: Date) => v.toISOString()
})

/**
* Querystring encoded as an ISO-8601 string (UTC)
* without the time zone offset, and returned as
* a Date object.
*
* The Date is parsed without the time zone offset,
* making it at 00:00:00 UTC.
*/
export const parseAsIsoDate = createParser({
parse: v => {
const date = new Date(v.slice(0, 10))
if (Number.isNaN(date.valueOf())) {
return null
}
return date
},
serialize: (v: Date) => v.toISOString().slice(0, 10)
})

/**
* String-based enums provide better type-safety for known sets of values.
* You will need to pass the parseAsStringEnum function a list of your enum values
Expand Down

0 comments on commit 107fcc1

Please sign in to comment.