https://github.com/gauben/formgator
A validation library for FormData objects
https://github.com/gauben/formgator
Last synced: about 1 month ago
JSON representation
A validation library for FormData objects
- Host: GitHub
- URL: https://github.com/gauben/formgator
- Owner: GauBen
- License: mit
- Created: 2024-10-06T16:39:15.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2025-04-02T19:21:56.000Z (about 1 month ago)
- Last Synced: 2025-04-05T03:41:30.177Z (about 1 month ago)
- Language: TypeScript
- Size: 358 KB
- Stars: 43
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# Formgator
A validation library for JavaScript `FormData` and `URLSearchParams` objects.
## Basic Usage
If you have a form with the following fields:
```html
User Name:
Birthday:
Subscribe to newsletter
Submit```
You can use `formgator` to validate the form data:
```ts
import * as fg from 'formgator';// Define a form schema
const schema = fg.form({
username: fg.text({ required: true }),
birthday: fg.date().asDate(),
newsletter: fg.checkbox(),
});async function handle(request: Request) {
// Retrieve the form data from the request
const form = await request.formData();// Validate the form data
const data = schema.parse(form);// data is now an object with the following shape:
// {
// username: string,
// birthday: Date | null,
// newsletter: boolean,
// }// If the form data is invalid, an error will be thrown
}
```## API
You can expect formgator to expose a validator for [all possible `` values](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) as well as `` and ``.
These validators will produce a coherent value for each input type:
- `number()` and `range()` produce `number` values.
- `checkbox()` produces `boolean` values.
- `file()` produces a `File` object.
- Other validators produce `string` values.All these validators take their common (and less common) HTML validation attributes as options:
- `text({ required: true, maxlength: 255 })`
- `number({ min: 10, max: 100, step: 10 })`
- `radio(["yes", "no"], { required: true })` to check against a list of possible values.
- `select(["apple", "banana", "cherry"], { multiple: true })` for `` elements.
- `file({ accept: [".jpg", ".jpeg"] })` for basic extension and MIME type validation.Some validators have additional methods to transform the value into a native JavaScript object:
- `datetimeLocal()`, `date()` and `month()` have `asDate()` to return a `Date` object, and `asNumber()` to return a timestamp.
- `color()` has `asRgb()` to return a `[number, number, number]` tuple.
- `textarea()` has `trim()` to remove leading and trailing whitespace.
- `url()` has `asURL()` to return a `URL` object.Validators can be chained with additional methods to transform the value:
- `transform(fn: (value: T) => U)` transforms the value using the provided function.
```ts
const schema = fg.form({
id: fg.text({ required: true, pattern: /^\d+$/ }).transform(BigInt),
});
// schema.parse(form) will produce an object with the shape { id: BigInt }
```A second argument can be provided to `transform` to produce a meaningful error message if the transformation fails.
- `refine(fn: (value: T) => boolean)` adds a custom validation step.
```ts
const schema = fg.form({
even: fg.number().refine((value) => value % 2 === 0),
});
// schema.parse(form) will throw an error if `even` is odd
```A second argument can be provided to `refine` to produce a meaningful error message if the refinement fails.
- `optional()` allows the field to be missing. This is useful when dynamically adding fields to a form. Missing and empty fields are different things, and `optional` does not allow empty fields.
```ts
const schema = fg.form({
contactChannel = fg.radio(['email', 'phone'], { required: true }),
email: fg.email({ required: true }).optional(),
phone: fg.tel({ required: true }).optional(),
});
// You should then check if at least one is properly defined
```You can provide a value to `optional` to replace `undefined` with a default value.
The schema produced by `fg.form()` has two methods:
- `.parse()` that returns the parsed form data or throws an error if the form data is invalid.
- `.safeParse()` that returns an object with this shape: `{ success: true, data: Output } | { success: false, error: Error }`.## Complete API
interface FormInput
Base interface for all form inputs.
```ts
interface FormInput {
/** Attributes given when creating the validator. */
attributes: Record;
/**
* Transforms the output of the validator into another value.
*
* @example
* text().transform((value) => value.length);
*
* @param fn - The transformation function.
* @param catcher - In case the transformation function throws, this function
* is called to generate an error message.
*/
transform(fn: (value: T) => U, catcher?: (error: unknown) => string): FormInput;
/** Adds a custom validation to the input. */
refine(fn: (value: T) => value is U, message?: string | ((value: T) => string)): FormInput;
refine(fn: (value: T) => unknown, message?: string | ((value: T) => string)): FormInput;
/**
* Makes the field optional, for inputs that may be removed or added
* dynamically to the form.
*
* It returns `undefined` instead of `null` to differentiate between a missing
* field (`undefined`) and a field with an empty value (`null`).
*
* You may provide a default value to be used when the field is missing.
*/
optional(): FormInput;
optional(value: U): FormInput;
/**
* Transforms the value using a [Standard Schema](https://github.com/standard-schema/standard-schema)
* compliant validator.
*
* Async validators are not supported.
*
* @example
* import z from 'zod';
* fg.text().pipe(z.string().email());
*/
pipe(schema: StandardSchemaV1): FormInput;
/** @private @internal */
[safeParse]: (data: ReadonlyFormData, name: string) => Result;
}
```
class FormgatorError
An error thrown when using `form.parse()`. It has two fields: `issues` and `accepted`,
containing the issues and accepted values respectively.Type-safety cannot be guaranteed when using exceptions. If you want type-safety, use
`form.safeParse()`.
type Issues
Transforms an object of form inputs into the issues object.
```ts
type Issues = Record>> = {
[K in keyof T]?: ValidationIssue;
} extends infer O ? {
[K in keyof O]: O[K];
} : never;
```
type Output
Transforms an object of form inputs into the success object.
```ts
type Output = Record>> = {
[K in keyof T]: T[K] extends FormInput ? U : never;
} extends infer O ? {
[K in keyof O]: O[K];
} : never;
```
type ValidationIssue
All possible validation issues that can be returned by a form input.
```ts
type ValidationIssue = {
code: "accept";
message: string;
} | {
code: "custom";
message: string;
} | {
code: "invalid";
message: string;
} | {
code: "max";
max: number | string;
message: string;
} | {
code: "maxlength";
maxlength: number;
message: string;
} | {
code: "min";
min: number | string;
message: string;
} | {
code: "minlength";
minlength: number;
message: string;
} | {
code: "pattern";
pattern: RegExp;
message: string;
} | {
code: "refine";
received: unknown;
message: string;
} | {
code: "required";
message: string;
} | {
code: "step";
step: number;
message: string;
} | {
code: "transform";
message: string;
} | {
code: "type";
message: string;
};
```
function checkbox()
`` form input validator.
Supported attributes:
- `required` - Whether the input is required.
The `value` attribute is not supported, use `select({ multiple: true })`
instead if you want to handle several checkboxes with the same name but
different values.
function color()
`` form input validator.
It does not support any attributes.
The output value is a string with the format `#rrggbb`.
function custom()
A custom validator, transformer, whatever, for you to implement if formgator falls short on
features.Returning a value will be considered a success, while throwing an error will be considered a
validation issue. The error message will be used as the issue message.
function date()
`` form input validator.
Supported attributes:
- `required` - Whether the input is required.
- `min` - Minimum date.
- `max` - Maximum date.The output value is a string with the format `yyyy-mm-dd`.
function datetimeLocal()
`` form input validator.
Supported attributes:
- `required` - Whether the input is required.
- `min` - Minimum date.
- `max` - Maximum date.The output value is a string with the format `yyyy-mm-ddThh:mm`.
function email()
`` form input validator.
Supported attributes:
- `required` - Whether the input is required.
- `maxlength` - Maximum length of the input.
- `minlength` - Minimum length of the input.
- `pattern` - Regular expression pattern to match by each email address.
- `multiple` - Whether the input allows multiple comma-separated email
addresses.
function file()
`` form input validator.
Supported attributes:
- `multiple` - Whether the input allows multiple files.
- `required` - Whether the input is required.
- `accept` - The accepted file types, as an array of MIME types (`image/png`),
MIME wildcards (`image/*`), or file extensions (`.png`).
function form()
Creates a form validator from a record of form inputs.
The return schema has two methods: `parse` and `safeParse`:
- `parse(data: FormData)` returns the data object if valid, throws a `FormgatorError` otherwise.
- `safeParse(data: FormData)` returns an object with `success` a success boolean flag, and
either `data` or `error` containing the parsed data or the issues respectively.
function hidden()
`` form input validator.
Not very useful, but included for completeness.
function image()
`` form input validator.
function month()
`` form input validator.
Supported attributes:
- `required` - Whether the input is required.
- `min` - Minimum date.
- `max` - Maximum date.The output value is a string with the format `yyyy-mm`.
function multi()
Validator for multiple inputs with the same name.
Supported attributes:
- `min` - Minimum number of values, defaults to 0.
- `max` - Maximum number of values, defaults to `Infinity`.
function number()
`` form input validator.
Supported attributes:
- `required` - Whether the input is required.
- `min` - Minimum value.
- `max` - Maximum value.
function password()
`` form input validator.
Does not accept new lines, use `textarea()` instead.
Supported attributes:
- `required` - Whether the input is required.
- `maxlength` - Maximum length of the input.
- `minlength` - Minimum length of the input.
- `pattern` - Regular expression pattern to match.
function radio()
`` form input validator.
Supported attributes:
- `required` - Whether the input is required.
function range()
`` form input validator.
Supported attributes:
- `min` - Minimum value, defaults to `0`.
- `max` - Maximum value, defaults to `100`.
- `step` - Step value, defaults to `1`.
function search()
`` form input validator.
Does not accept new lines, use `textarea()` instead.
Supported attributes:
- `required` - Whether the input is required.
- `maxlength` - Maximum length of the input.
- `minlength` - Minimum length of the input.
- `pattern` - Regular expression pattern to match.
function select()
`` form input validator.
Supported attributes:
- `multiple` - Whether the input allows multiple selections.
- `required` - Whether the input is required.
function splat()
Allows you to splat attributes into Svelte HTML template.
This feature is considered experimental and may be removed in the future.
function tel()
`` form input validator.
Does not accept new lines, use `textarea()` instead.
Supported attributes:
- `required` - Whether the input is required.
- `maxlength` - Maximum length of the input.
- `minlength` - Minimum length of the input.
- `pattern` - Regular expression pattern to match.
function text()
`` form input validator.
Does not accept new lines, use `textarea()` instead.
Supported attributes:
- `required` - Whether the input is required.
- `maxlength` - Maximum length of the input.
- `minlength` - Minimum length of the input.
- `pattern` - Regular expression pattern to match.
function textarea()
`` form input validator.
Supported attributes:
- `required` - Whether the input is required.
- `maxlength` - Maximum length of the input.
- `minlength` - Minimum length of the input.
function time()
`` form input validator.
Supported attributes:
- `required` - Whether the input is required.
- `min` - Minimum date.
- `max` - Maximum date.The output value is a string with the format `yyyy-mm-dd`.
function url()
`` form input validator.
Supported attributes:
- `required` - Whether the input is required.
- `maxlength` - Maximum length of the input.
- `minlength` - Minimum length of the input.
- `pattern` - Regular expression pattern to match.
function week()
`` form input validator.
Supported attributes:
- `required` - Whether the input is required.
- `min` - Minimum date.
- `max` - Maximum date.The output value is a string with the format `yyyy-Www` (e.g. `1999-W01`).
## Errors
An invalid form will produce an error with the same shape as your form schema:
```ts
const schema = fg.form({
username: fg.text({ required: true }),
birthday: fg.date().asDate(),
newsletter: fg.checkbox(),
});// Using `.parse()`:
try {
schema.parse(form);
} catch (error) {
if (error instanceof fg.FormgatorError) {
// error.issues is an object with this shape
// {
// username?: ValidationIssue
// birthday?: ValidationIssue
// newsletter?: ValidationIssue
// }
}
}// Using `.safeParse()`:
const result = schema.safeParse(form);
if (!result.success) {
// result.error.issues is an object with this shape
// {
// username?: ValidationIssue
// birthday?: ValidationIssue
// newsletter?: ValidationIssue
// }
}
```A `ValidationIssue` object has the following shape:
```ts
interface ValidationIssue {
code:
| 'type' // If the value is not of the expected type (e.g. string instead of File)
| 'invalid' // If the value does not have the right format (e.g. invalid email)
| 'required' // If the value is empty
| 'minlength' // If the value is too short
| 'maxlength' // If the value is too long
| 'pattern' // If the value does not match the pattern
| 'min' // If the value is too low
| 'max' // If the value is too high
| 'step' // If the value is not a multiple of the step
| 'accept' // If the value does not match the accept attribute
| 'transform' // If the `transform` callback throws an error
| 'refine'; // If the `refine` callback returns false
message: string;
}
```If some fields were accepted nonetheless, the `error` object will have an `accepted` property with all the accepted fields: `error.accepted` for `.parse()` and `result.error.accepted` for `.safeParse()`. This allows you to recover from partial form data.
## Usage with SvelteKit
Formgator can be used in SvelteKit to validate form data and query parameters. Because formgator imports `@sveltejs/kit` internally, you need to bundle it with your application to avoid weird runtime behaviors:
- Move `formgator` from `dependencies` to `devDependencies` in your `package.json`.
- Add `ssr: { noExternal: ['formgator'] }` to the root of `vite.config.{js|ts}`.This will ensure that formgator use the bundled version of `@sveltejs/kit` instead of an external one. This also means that formgator will be tree-shaken in your production build, and no longer imported from `node_modules` at runtime.
### Form actions
Formgator exposes a SvelteKit adapter that can be used to validate form data in SvelteKit [form actions](https://kit.svelte.dev/docs/form-actions).
```ts
// +page.server.ts
import * as fg from 'formgator';
import { formgate } from 'formgator/sveltekit';export const actions = {
login: formgate(
{
email: fg.email({ required: true }),
password: fg.password({ required: true }),
},
(data, event) => {
// data.email and data.password are guaranteed to be strings
// The form will be rejected as 400 Bad Request if they are missing or empty
// event is the object that would be your first argument without formgator
}
),
};
```The parsed form result is added at the beginning of the arguments list to ensure ascending compatibility with SvelteKit; extending the `event` object might clash with upcoming features.
If the form data is invalid, the form action will populate the `form` property of your `+page.svelte` component. Its shape will be as follows:
```ts
export let form: {
issues: {
// Contains the validation issues for each field
email?: ValidationIssue;
password?: ValidationIssue;
};
accepted: {
// Allows you to recover from partial form data
email?: string;
password?: string;
};
};
```If you have several forms on the same page, you can add a third argument to `formgate` to specify the form name: `formgate(..., { id: "login" })`. This id will be propagated to `form.id` in your page component.
### Page load
As formgator works on `URLSearchParams` objects and can run client-side, you can use it to validate query parameters in your SvelteKit page components.
```ts
// +page.ts
import * as fg from 'formgator';
import { loadgate } from 'formgator/sveltekit';export const load = loadgate(
{
page: fg.number({ min: 1, required: true }).optional(1),
search: fg.search().trim().optional(),
},
(data, event) => {
// data has the shape { page: number, search: string | undefined }
// event is the object that would be your first argument without formgator
// The page will load as 400 Bad Request if the query parameters are invalid
}
);
```## Disclaimer
This package is still in development and the API is subject to change. API will be stabilized in version 1.0.0.
## Design choices
- Why does `text()` produce `null` for an empty string?
This allows making the difference between _empty_ and _valid_. For instance, the field `` would accept both `''` and `'1234'` but not `'123'`; an empty field is considered valid as long as the `required` attribute is not set on the input. Therefore, `text()` produces `string` when valid and `null` when empty. To receive a `string` value, either use `text({ required: true })` to prevent empty inputs, `text().transform(v => v ?? '')` to transform `null` into `''`, or `text().trim()` to transform whitespace-only strings into `''`.
- Why use both `null` and `undefined`?
`null` is used to represent an empty field, while `undefined` is used to represent a missing field. JavaScript is a weird language with two different ways to represent the absence of a value, and we can use this to our advantage.
- Why? Just why?
I needed a way to mirror client-side validation on a server application. Most JavaScript form validation libraries are designed to work with native JS objects, not `FormData`, so I made my own.
## License
This package is licensed under the MIT license.
The project logo was generated by AI and is in the public domain.