superformdata
v0.1.0
Published
Serialize rich JavaScript values into form-style entries and decode them back again
Readme
superformdata
Serialize rich JavaScript values into form-style entries and decode them back again.
superformdata is useful when you want to move data through FormData, URL-encoded forms, or request bodies without flattening everything to strings yourself.
Installation
npm install superformdataWhat It Handles
- Plain objects and arrays
DateSetMapbigintRegExpURLErrornull,undefined,NaN,Infinity,-Infinity,-0
Basic Usage
import { decode, encode } from "superformdata";
const input = {
user: {
name: "Alice",
joined: new Date("2024-01-01T00:00:00.000Z"),
},
tags: new Set(["admin", "editor"]),
count: 42,
};
const entries = encode(input);
const value = decode<typeof input>(entries);encode()
Serialize a rich value into form-style entries.
import { encode } from "superformdata";
const entries = encode({
title: "Quarterly report",
publishedAt: new Date("2024-01-01T00:00:00.000Z"),
views: 1200,
featured: true,
});
console.log(entries);
// [
// ["title", "Quarterly report"],
// ["publishedAt", "2024-01-01T00:00:00.000Z"],
// ["views", "1200"],
// ["featured", "true"],
// ["$types", "{\"publishedAt\":\"Date\",\"views\":\"number\",\"featured\":\"boolean\"}"]
// ]decode()
Decode FormData entries back into a typed value.
import { decode } from "superformdata";
const entries = [
["user.name", "Alice"],
["user.joinedAt", "2024-01-01T00:00:00.000Z"],
["active", "true"],
["$types", '{"user.joinedAt":"Date","active":"boolean"}'],
] satisfies [string, string][];
const value = decode<{
user: {
name: string;
joinedAt: Date;
};
active: boolean;
}>(entries);When the same decoded field path appears more than once, decode() preserves every value by collecting them into an array in entry order.
decode([
["tags", "a"],
["tags", "b"],
]);
// => { tags: ["a", "b"] }Decode a Request
import { decodeRequest } from "superformdata";
export async function POST(request: Request) {
const data = await decodeRequest<{
name: string;
count: number;
createdAt: Date;
}>(request);
return Response.json(data);
}Custom Type Handlers
Pass custom handlers to encode() and decode() when you need to round-trip domain-specific values.
import { decode, encode, type TypeHandler } from "superformdata";
interface Money {
readonly cents: number;
}
const moneyHandler: TypeHandler<Money> = {
id: "Money",
test: (value): value is Money =>
typeof value === "object" &&
value !== null &&
"cents" in value &&
typeof value.cents === "number",
serialize: (value) => String(value.cents),
deserialize: (raw) => ({ cents: Number(raw) }),
};
const entries = encode({ price: { cents: 1299 } }, { typeHandlers: [moneyHandler] });
const value = decode<{ price: Money }>(entries, { typeHandlers: [moneyHandler] });Custom handler IDs must be unique and cannot use built-in or structural IDs such as number, Date, set, or map.
decodeRequest()
Decode a Request body directly.
import { decodeRequest } from "superformdata";
export async function action(request: Request) {
const data = await decodeRequest<{
email: string;
subscribed: boolean;
}>(request);
return Response.json({
email: data.email,
subscribed: data.subscribed,
});
}Browser Form Helpers
Use the change handlers to annotate form inputs with type metadata before submit.
import {
onBigIntChange,
onBooleanChange,
onChange,
onDateChange,
onNumberChange,
onURLChange,
} from "superformdata";
document.querySelector('input[name="count"]')?.addEventListener("change", onNumberChange);
document.querySelector('input[name="createdAt"]')?.addEventListener("change", onDateChange);
document.querySelector('input[name="isAdmin"]')?.addEventListener("change", onBooleanChange);
document.querySelector('input[name="accountId"]')?.addEventListener("change", onBigIntChange);
document.querySelector('input[name="website"]')?.addEventListener("change", onURLChange);
// Use onChange() when you want to set the type id yourself.
document.querySelector('input[name="pattern"]')?.addEventListener("change", onChange("RegExp"));Then pass the form to encode():
import { encode } from "superformdata";
const form = document.querySelector("form");
const entries = encode(form!);data-sf-* Attributes
The package currently recognizes data-sf-type.
data-sf-typetellsencode(form)which type handler to use for that field.- The helper functions like
onDateChange()andonNumberChange()setdata-sf-typefor you. - You can also set it manually in your HTML if the type is already known.
<form id="post-form">
<input name="publishedAt" value="2024-01-01T00:00:00.000Z" data-sf-type="Date" />
<input name="views" value="1200" data-sf-type="number" />
<input name="homepage" value="https://example.com" data-sf-type="URL" />
</form>import { encode } from "superformdata";
const form = document.querySelector<HTMLFormElement>("#post-form")!;
const entries = encode(form);For checkboxes, data-sf-type="boolean" has special handling: the field is always included and encoded as "true" or "false" based on checked.
<input name="published" type="checkbox" data-sf-type="boolean" />No other data-sf-* attributes are currently read by the package.
onChange()
Attach your own type id to an input.
import { onChange } from "superformdata";
const patternInput = document.querySelector<HTMLInputElement>('input[name="pattern"]')!;
patternInput.addEventListener("change", onChange("RegExp"));onDateChange()
import { onDateChange } from "superformdata";
document.querySelector('input[name="publishedAt"]')?.addEventListener("change", onDateChange);onNumberChange()
import { onNumberChange } from "superformdata";
document.querySelector('input[name="price"]')?.addEventListener("change", onNumberChange);onBooleanChange()
import { onBooleanChange } from "superformdata";
document.querySelector('input[name="completed"]')?.addEventListener("change", onBooleanChange);onBigIntChange()
import { onBigIntChange } from "superformdata";
document.querySelector('input[name="orderId"]')?.addEventListener("change", onBigIntChange);onURLChange()
import { onURLChange } from "superformdata";
document.querySelector('input[name="homepage"]')?.addEventListener("change", onURLChange);API
encode<T>(input: T, options?: { typesKey?: string; types?: Record<string, string>; typeHandlers?: readonly TypeHandler[] }): [string, string][]
decode<T = unknown>(data: FormData | Iterable<[string, FormDataEntryValue]>, options?: { typesKey?: string; typeHandlers?: readonly TypeHandler[] }): T
decodeRequest<T = unknown>(request: Request, options?: { typesKey?: string; typeHandlers?: readonly TypeHandler[] }): Promise<T>
onChange(typeId: string, options?: { typesKey?: string }): (event: Event) => voidLicense
MIT
