Form object
Read and write an HTMLFormElement as a nested plain object via field names.
FormObject turns an HTMLFormElement into a structured plain object — and writes one back — using the control name to shape the result. It is a static snapshot, like new FormData(), with no reactivity or dependencies. Type into the form below and watch the object update.
import { signal } from "elements-kit/signals";
import { FormObject } from "elements-kit/utilities/form-object";
/** JSON of the form's current values. */
export const snapshot = signal("{}");
export function update(form: HTMLFormElement) {
snapshot(JSON.stringify(new FormObject(form).toObject(), null, 2));
}
export function prefill(form: HTMLFormElement) {
new FormObject(form).fromObject({
user: { name: "Sara", email: "sara@example.com" },
address: { city: "Paris" },
interests: ["music", "travel"],
newsletter: "yes",
});
update(form);
}
Field names shape the object
The name attribute is a path. Dots nest objects, numeric segments build arrays, and a trailing [] marks an append array.
import { FormObject } from "elements-kit/utilities/form-object";
// <input name="user.name"> → { user: { name } }
// <input name="user.address.city">→ { user: { address: { city } } }
// <input name="tags.0"> → { tags: [ "…" ] } (explicit index)
// <input name="colors[]"> → { colors: [ "…" ] } (append, one per control)
const data = new FormObject(form).toObject();A trailing [] always produces an array — a single colors[] control gives ["x"], and a group with none checked still serializes to []. Use it whenever a field is conceptually a list.
Reading a form
toObject() walks the form's named controls and returns the nested object. It mirrors native form submission: disabled and unchecked controls are dropped, <select multiple> yields an array, and <input type="file"> yields File objects. toJSON() is an alias, so JSON.stringify(instance) works.
const form = document.querySelector("form")!;
const data = new FormObject(form).toObject();Bare repeated names: last value wins
When several controls share a bare name (no []), the last value wins — matching form-data-json. So the <input type="hidden" name="x" value="0"> + <input type="checkbox" name="x" value="1"> idiom yields "1" when checked and "0" when unchecked. Reach for name="x[]" when you actually want both values in an array.
Writing back
fromObject(data) assigns values from a nested object onto matching controls — setting .value, checking boxes and radios, and selecting options. Missing paths are left untouched. clear() resets every named control. Both return this for chaining.
new FormObject(form).fromObject({
user: { name: "Wael" },
colors: ["red", "blue"], // checks the matching colors[] boxes
});Transforms
Extraction runs each field through a pipeline of FormFieldTransforms. Each transform receives the field ({ control, name, value, checked? }) and returns it, a modified copy, or null to drop it. The default pipeline skips disabled controls, buttons, and unchecked boxes:
import {
FormObject,
defaultTransforms,
uncheckedAs,
skipEmpty,
} from "elements-kit/utilities/form-object";
// Replace the defaults — include unchecked checkboxes as `false`
new FormObject(form, {
transforms: [...defaultTransforms.slice(0, 2), uncheckedAs(false)],
}).toObject();
// Extend — drop empty fields and trim strings
new FormObject(form, {
transforms: [
...defaultTransforms,
skipEmpty,
(f) => ({ ...f, value: typeof f.value === "string" ? f.value.trim() : f.value }),
],
}).toObject();Built-ins: skipDisabled, skipButtons, dropUnchecked (the defaults), plus uncheckedAs(value) and skipEmpty. Passing transforms replaces the defaults — spread ...defaultTransforms to keep them.
An empty pipeline keeps everything
transforms: [] removes all filtering, so disabled controls, buttons, and unchecked checkbox values are all emitted. Inspect field.checked in a custom transform when you need the checkbox state.
Field names that walk an object's prototype (__proto__, prototype, constructor) are ignored, so a malicious name cannot pollute Object.prototype.
See also
- Primitives
- Signals
- Source & full behavior table: src/utilities/form-object.ts