Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/gauben/formgator
A validation library for FormData objects
https://github.com/gauben/formgator
Last synced: 11 days 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 (about 1 month ago)
- Default Branch: main
- Last Pushed: 2024-10-21T16:26:02.000Z (17 days ago)
- Last Synced: 2024-10-22T06:03:06.910Z (17 days ago)
- Language: TypeScript
- Size: 487 KB
- Stars: 19
- 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.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 }`.## 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
}
);
```For now this requires adding `satisfies PageLoad` after `loadgate()` but [a pull request should solve this issue](https://github.com/sveltejs/language-tools/pull/2540).
## 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.