zod-form-data
Advanced tools
Comparing version 0.0.1-beta.0 to 1.0.0
@@ -8,11 +8,21 @@ import { z, ZodArray, ZodEffects, ZodNumber, ZodString, ZodTypeAny } from "zod"; | ||
export declare const numeric: InputType<ZodNumber>; | ||
declare type BooleanCheckboxOpts = { | ||
declare type CheckboxOpts = { | ||
trueValue?: string; | ||
}; | ||
export declare const booleanCheckbox: ({ trueValue, }?: BooleanCheckboxOpts) => z.ZodUnion<[z.ZodEffects<z.ZodLiteral<string>, boolean, string>, z.ZodEffects<z.ZodLiteral<undefined>, boolean, undefined>]>; | ||
declare type RepeatableField = { | ||
(): ZodEffects<ZodArray<ZodString>>; | ||
<ProvidedType extends ZodTypeAny>(schema: ProvidedType): ZodEffects<ZodArray<ProvidedType>>; | ||
}; | ||
export declare const repeatableField: RepeatableField; | ||
export declare const checkbox: ({ trueValue }?: CheckboxOpts) => z.ZodUnion<[z.ZodEffects<z.ZodLiteral<string>, boolean, string>, z.ZodEffects<z.ZodLiteral<undefined>, boolean, undefined>]>; | ||
export declare const repeatable: InputType<ZodArray<any>>; | ||
export declare const repeatableOfType: <T extends z.ZodTypeAny>(schema: T) => z.ZodEffects<z.ZodArray<T, "many">, T["_output"][], T["_input"][]>; | ||
export declare const formData: (shape: z.ZodRawShape) => z.ZodEffects<z.ZodObject<z.ZodRawShape, "strip", z.ZodTypeAny, { | ||
[x: string]: any; | ||
[x: number]: any; | ||
}, { | ||
[x: string]: any; | ||
[x: number]: any; | ||
}>, { | ||
[x: string]: any; | ||
[x: number]: any; | ||
}, { | ||
[x: string]: any; | ||
[x: number]: any; | ||
}>; | ||
export {}; |
@@ -17,10 +17,39 @@ import { z } from "zod"; | ||
])), schema); | ||
export const booleanCheckbox = ({ trueValue = "on", } = {}) => z.union([ | ||
export const checkbox = ({ trueValue = "on" } = {}) => z.union([ | ||
z.literal(trueValue).transform(() => true), | ||
z.literal(undefined).transform(() => false), | ||
]); | ||
export const repeatableField = (schema = z.string()) => z.preprocess((val) => { | ||
if (Array.isArray(val)) | ||
return val; | ||
return [val]; | ||
}, z.array(schema)); | ||
export const repeatable = (schema = z.array(text())) => { | ||
return z.preprocess((val) => { | ||
if (Array.isArray(val)) | ||
return val; | ||
if (val === undefined) | ||
return []; | ||
return [val]; | ||
}, schema); | ||
}; | ||
export const repeatableOfType = (schema) => repeatable(z.array(schema)); | ||
const entries = z.array(z.tuple([z.string(), z.any()])); | ||
export const formData = (shape) => z.preprocess(preprocessIfValid( | ||
// We're avoiding using `instanceof` here because different environments | ||
// won't necessarily have `FormData` or `URLSearchParams` | ||
z | ||
.any() | ||
.refine((val) => Symbol.iterator in val) | ||
.transform((val) => [...val]) | ||
.refine((val) => entries.safeParse(val).success) | ||
.transform((data) => { | ||
const map = new Map(); | ||
for (const [key, value] of data) { | ||
if (map.has(key)) { | ||
map.get(key).push(value); | ||
} | ||
else { | ||
map.set(key, [value]); | ||
} | ||
} | ||
return [...map.entries()].reduce((acc, [key, value]) => { | ||
acc[key] = value.length === 1 ? value[0] : value; | ||
return acc; | ||
}, {}); | ||
})), z.object(shape)); |
@@ -8,11 +8,21 @@ import { z, ZodArray, ZodEffects, ZodNumber, ZodString, ZodTypeAny } from "zod"; | ||
export declare const numeric: InputType<ZodNumber>; | ||
declare type BooleanCheckboxOpts = { | ||
declare type CheckboxOpts = { | ||
trueValue?: string; | ||
}; | ||
export declare const booleanCheckbox: ({ trueValue, }?: BooleanCheckboxOpts) => z.ZodUnion<[z.ZodEffects<z.ZodLiteral<string>, boolean, string>, z.ZodEffects<z.ZodLiteral<undefined>, boolean, undefined>]>; | ||
declare type RepeatableField = { | ||
(): ZodEffects<ZodArray<ZodString>>; | ||
<ProvidedType extends ZodTypeAny>(schema: ProvidedType): ZodEffects<ZodArray<ProvidedType>>; | ||
}; | ||
export declare const repeatableField: RepeatableField; | ||
export declare const checkbox: ({ trueValue }?: CheckboxOpts) => z.ZodUnion<[z.ZodEffects<z.ZodLiteral<string>, boolean, string>, z.ZodEffects<z.ZodLiteral<undefined>, boolean, undefined>]>; | ||
export declare const repeatable: InputType<ZodArray<any>>; | ||
export declare const repeatableOfType: <T extends z.ZodTypeAny>(schema: T) => z.ZodEffects<z.ZodArray<T, "many">, T["_output"][], T["_input"][]>; | ||
export declare const formData: (shape: z.ZodRawShape) => z.ZodEffects<z.ZodObject<z.ZodRawShape, "strip", z.ZodTypeAny, { | ||
[x: string]: any; | ||
[x: number]: any; | ||
}, { | ||
[x: string]: any; | ||
[x: number]: any; | ||
}>, { | ||
[x: string]: any; | ||
[x: number]: any; | ||
}, { | ||
[x: string]: any; | ||
[x: number]: any; | ||
}>; | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.repeatableField = exports.booleanCheckbox = exports.numeric = exports.text = void 0; | ||
exports.formData = exports.repeatableOfType = exports.repeatable = exports.checkbox = exports.numeric = exports.text = void 0; | ||
const zod_1 = require("zod"); | ||
@@ -22,12 +22,43 @@ const stripEmpty = zod_1.z.literal("").transform(() => undefined); | ||
exports.numeric = numeric; | ||
const booleanCheckbox = ({ trueValue = "on", } = {}) => zod_1.z.union([ | ||
const checkbox = ({ trueValue = "on" } = {}) => zod_1.z.union([ | ||
zod_1.z.literal(trueValue).transform(() => true), | ||
zod_1.z.literal(undefined).transform(() => false), | ||
]); | ||
exports.booleanCheckbox = booleanCheckbox; | ||
const repeatableField = (schema = zod_1.z.string()) => zod_1.z.preprocess((val) => { | ||
if (Array.isArray(val)) | ||
return val; | ||
return [val]; | ||
}, zod_1.z.array(schema)); | ||
exports.repeatableField = repeatableField; | ||
exports.checkbox = checkbox; | ||
const repeatable = (schema = zod_1.z.array((0, exports.text)())) => { | ||
return zod_1.z.preprocess((val) => { | ||
if (Array.isArray(val)) | ||
return val; | ||
if (val === undefined) | ||
return []; | ||
return [val]; | ||
}, schema); | ||
}; | ||
exports.repeatable = repeatable; | ||
const repeatableOfType = (schema) => (0, exports.repeatable)(zod_1.z.array(schema)); | ||
exports.repeatableOfType = repeatableOfType; | ||
const entries = zod_1.z.array(zod_1.z.tuple([zod_1.z.string(), zod_1.z.any()])); | ||
const formData = (shape) => zod_1.z.preprocess(preprocessIfValid( | ||
// We're avoiding using `instanceof` here because different environments | ||
// won't necessarily have `FormData` or `URLSearchParams` | ||
zod_1.z | ||
.any() | ||
.refine((val) => Symbol.iterator in val) | ||
.transform((val) => [...val]) | ||
.refine((val) => entries.safeParse(val).success) | ||
.transform((data) => { | ||
const map = new Map(); | ||
for (const [key, value] of data) { | ||
if (map.has(key)) { | ||
map.get(key).push(value); | ||
} | ||
else { | ||
map.set(key, [value]); | ||
} | ||
} | ||
return [...map.entries()].reduce((acc, [key, value]) => { | ||
acc[key] = value.length === 1 ? value[0] : value; | ||
return acc; | ||
}, {}); | ||
})), zod_1.z.object(shape)); | ||
exports.formData = formData; |
{ | ||
"name": "zod-form-data", | ||
"version": "0.0.1-beta.0", | ||
"version": "1.0.0", | ||
"browser": "./browser/index.js", | ||
@@ -5,0 +5,0 @@ "main": "./build/index.js", |
194
README.md
# zod-form-data | ||
Helpers for using [zod](https://github.com/colinhacks/zod) to parse `FormData` or `URLSearchParams`. | ||
Validation helpers for [zod](https://github.com/colinhacks/zod) | ||
specifically for parsing `FormData` or `URLSearchParams`. | ||
This is particularly useful when using [remix](https://github.com/remix-run/remix) | ||
and combos well with [remix-validated-form](https://github.com/airjp73/remix-validated-form). | ||
The main goal of this library is deal with the pain point that everything in `FormData` is a string. | ||
Sometimes, properly validating this kind of data requires a lot of extra hoop jumping and preprocessing. | ||
With the helpers in `zod-form-data`, you can write your types closer to how you want to. | ||
## Example | ||
```tsx | ||
import { zfd } from 'zod-form-data'; | ||
const schema = zfd.formData({ | ||
name: zfd.text(), | ||
age: zfd.numeric( | ||
z.number().min(25).max(50) | ||
), | ||
likesPizza: zfd.checkbox() | ||
}) | ||
// This example is using `remix`, but it will work | ||
// with any `FormData` or `URLSearchParams` no matter where you get it from. | ||
export const action = async ({ request }) => { | ||
const { name, age, likesPizza } = schema.parse(await request.formData()) | ||
// do something with parsed data | ||
} | ||
``` | ||
## Installation | ||
```bash | ||
npm install zod-form-data | ||
``` | ||
## Contributing | ||
The eventual goal is to have a helper to preprocess and/or validate most types of native inputs. | ||
If you have a helper for an input type that isn't in this library, feel free to open a PR to add it! | ||
## API Reference | ||
Contents | ||
* [formData](#formData) | ||
* [text](#text) | ||
* [numeric](#numeric) | ||
* [checkbox](#checkbox) | ||
* [repeatable](#repeatable) | ||
* [repeatableOfType](#repeatableOfType) | ||
### formData | ||
This helper takes the place of the `z.object` at the root of your schema. | ||
It wraps your schema in a `z.preprocess` that extracts all the data out of a `FormData` | ||
and transforms it into a regular object. | ||
If the `FormData` contains multiple entries with the same field name, | ||
it will automatically turn that field into an array. | ||
(If you're expecting multiple values for a field, use [repeatable](#repeatable).) | ||
The primary use-case for this helper is to accept `FormData`, | ||
but it works with any iterable that returns entries. | ||
This means it can accept `URLSearchParams` or regular objects as well. | ||
#### Usage | ||
You can use this the same way you would use `z.object`. | ||
```ts | ||
const schema = zfd.formData({ | ||
field1: zfd.text(), | ||
field2: zfd.text(), | ||
}) | ||
const someFormData = new FormData(); | ||
const dataObject = schema.parse(someFormData); | ||
``` | ||
### text | ||
Transforms any empty strings to `undefined` before validating. | ||
This makes it so empty strings will fail required checks, | ||
allowing you to use `optional` for optional fields instead of `nonempty` for required fields. | ||
If you call `zfd.text` with no arguments, it will assume the field is a required string by default. | ||
If you want to customize the schema, you can pass that as an argument. | ||
#### Usage | ||
```ts | ||
const const schema = zfd.formData({ | ||
requiredString: zfd.text(), | ||
stringWithMinLength: zfd.text(z.string().min(10)), | ||
optional: zfd.text(z.string().optional()), | ||
}) | ||
``` | ||
### numeric | ||
Coerces numerical strings to numbers transforms empty strings to `undefined` before validating. | ||
If you call `zfd.number` with no arguments, | ||
it will assume the field is a required number by default. | ||
If you want to customize the schema, you can pass that as an argument. | ||
_Note:_ The preprocessing only _coerces_ the value into a number. It doesn't use `parseInt`. | ||
Something like `"24px"` will not be transformed and will be treated as a string. | ||
#### Usage | ||
```ts | ||
const schema = zfd.formData({ | ||
requiredNumber: zfd.numeric(), | ||
numberWithMin: zfd.numeric(z.number().min(13)), | ||
optional: zfd.numeric(z.number().optional()), | ||
}) | ||
``` | ||
### checkbox | ||
Validates a checkbox field as a boolean. | ||
Unlike other helpers, this is not a preprocesser, | ||
but a complete schema that should do everything you need. | ||
By default, it will treat `"on"` as true and `undefined` as false, | ||
but you can customize the true value. | ||
If you have a checkbox group and you want to leave the values as strings, | ||
[repeatableField](#repeatableField) might be what you want. | ||
#### Usage | ||
```ts | ||
const schema = zfd.formData({ | ||
defaultCheckbox: zfd.checkbox(), | ||
checkboxWithValue: zfd.checkbox({ trueValue: "true" }), | ||
mustBeTrue: zfd.checkbox().refine(val => val, "Please check this box") | ||
}) | ||
``` | ||
#### Background on native checkbox behavior | ||
If you're used to using client-side form libraries and haven't dealt with native form behavior much, | ||
the native checkbox behavior might be non-intuitive (it was for me). | ||
Take this checkbox: | ||
```tsx | ||
<input name="myCheckbox" type="checkbox" /> | ||
``` | ||
If you check this checkbox and submit the form, the value in the `FormData` will be `"on"`. | ||
If you add a value prop: | ||
```tsx | ||
<input name="myCheckbox" type="checkbox" value="someValue" /> | ||
``` | ||
Then the checked value of the checkbox will be `"someValue"` instead of `"on"`. | ||
If you leave the checkbox unchecked, | ||
the `FormData` will not include an entry for `myCheckbox` at all. | ||
([Further reading](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#value)) | ||
### repeatable | ||
Preprocesses a field where you expect multiple values could be present for the same field name | ||
and transforms the value of that field to always be an array. | ||
This is specifically meant to work with data transformed by `zfd.formData` | ||
(or by `remix-validated-form`). | ||
If you don't provide a schema, it will assume the field is an array of [zfd.text](#text) fields. | ||
If you want to customize the type of the item, but don't care about validations on the array itself, | ||
you can use [repeatableOfType](#repeatableOfType). | ||
#### Usage | ||
```ts | ||
const schema = zfd.formData({ | ||
myCheckboxGroup: zfd.repeatable(), | ||
atLeastOneItem: zfd.repeatable(z.array(zfd.text()).min(1)), | ||
}) | ||
``` | ||
### repeatableOfType | ||
A convenience wrapper for [repeatable](#repeatable). | ||
Instead of passing the schema for an entire array, you pass in the schema for the item type. | ||
#### Usage | ||
```ts | ||
const schema = zfd.formData({ | ||
repeatableNumberField: zfd.repeatableOfType(zfd.numeric()) | ||
}) | ||
``` |
@@ -35,9 +35,7 @@ import { z, ZodArray, ZodEffects, ZodNumber, ZodString, ZodTypeAny } from "zod"; | ||
type BooleanCheckboxOpts = { | ||
type CheckboxOpts = { | ||
trueValue?: string; | ||
}; | ||
export const booleanCheckbox = ({ | ||
trueValue = "on", | ||
}: BooleanCheckboxOpts = {}) => | ||
export const checkbox = ({ trueValue = "on" }: CheckboxOpts = {}) => | ||
z.union([ | ||
@@ -48,13 +46,48 @@ z.literal(trueValue).transform(() => true), | ||
type RepeatableField = { | ||
(): ZodEffects<ZodArray<ZodString>>; | ||
<ProvidedType extends ZodTypeAny>(schema: ProvidedType): ZodEffects< | ||
ZodArray<ProvidedType> | ||
>; | ||
export const repeatable: InputType<ZodArray<any>> = ( | ||
schema = z.array(text()) | ||
) => { | ||
return z.preprocess((val) => { | ||
if (Array.isArray(val)) return val; | ||
if (val === undefined) return []; | ||
return [val]; | ||
}, schema); | ||
}; | ||
export const repeatableField: RepeatableField = (schema = z.string()) => | ||
z.preprocess((val) => { | ||
if (Array.isArray(val)) return val; | ||
return [val]; | ||
}, z.array(schema)); | ||
export const repeatableOfType = <T extends ZodTypeAny>( | ||
schema: T | ||
): ZodEffects<ZodArray<T>> => repeatable(z.array(schema)); | ||
const entries = z.array(z.tuple([z.string(), z.any()])); | ||
export const formData = (shape: z.ZodRawShape) => | ||
z.preprocess( | ||
preprocessIfValid( | ||
// We're avoiding using `instanceof` here because different environments | ||
// won't necessarily have `FormData` or `URLSearchParams` | ||
z | ||
.any() | ||
.refine((val) => Symbol.iterator in val) | ||
.transform((val) => [...val]) | ||
.refine( | ||
(val): val is z.infer<typeof entries> => | ||
entries.safeParse(val).success | ||
) | ||
.transform((data): Record<string, unknown | unknown[]> => { | ||
const map: Map<string, unknown[]> = new Map(); | ||
for (const [key, value] of data) { | ||
if (map.has(key)) { | ||
map.get(key)!.push(value); | ||
} else { | ||
map.set(key, [value]); | ||
} | ||
} | ||
return [...map.entries()].reduce((acc, [key, value]) => { | ||
acc[key] = value.length === 1 ? value[0] : value; | ||
return acc; | ||
}, {} as Record<string, unknown | unknown[]>); | ||
}) | ||
), | ||
z.object(shape) | ||
); |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
18934
17
314
1
197