obfo
@johnhenry/obfo (Object Form) converts HTML forms into nested JavaScript objects, and back. It reads the structure
you mark up with a few data-obfo-* attributes: {} and [] containers become objects and arrays, inputs become
values, and everything else (labels, headings, layout divs) is looked through. The same structure works in the other
direction: fill writes an object into a form, observe reports the form’s object every time it is edited, and
formFromObject builds a form for a JSON value.
Zero dependencies. ESM. Runs wherever there is a DOM: browsers, iframes (another realm), and Node 26 or newer
(engines) with happy-dom or a similar DOM.
Read these before the API. None of them is visible from the type signatures.
- The root needs a container. A form without
data-obfo-container(and without thecontaineroption) reads asnull, not as an object. Pass{ container: "{}" }for plain forms. - Every direct child of a
{}container needs a key. An input with neithernamenordata-obfo-namemakesobfothrow, and so does a nested container withoutdata-obfo-name. Elements in a[]container need no key; their names are ignored. - By default every value is a string, whatever the input. A checkbox reads its
value("on") whether it is checked or not, a radio group reads the last radio’s value, a number input reads"20". That is obfo’s original behavior and it stays the default; pass{ cast: "auto" }for numbers, booleans, radio groups and arrays (Typed values). data-obfo-castwith no value is notcast: "auto". An empty cast uses the input’stypeas the cast, so a date input becomes aDateand an empty number input becomes0;autokeeps dates as strings and reads an empty number asnull.fillcan’t write everythingobforeads. File inputs can’t be set by script, and a checkbox read without a boolean cast has no state to restore, so both are skipped. Read and fill with the same options.observehears events, not property writes.input.value = "x"from script fires nothing. Usefill(form, value, { dispatch: true })or dispatch aninputevent.
More in Limitations and traps.
Install
Section titled “Install”npm install @johnhenry/obfo<!-- In <head>, before any <script type="module"> or modulepreload. --><script type="importmap"> { "imports": { "@johnhenry/obfo": "https://cdn.jsdelivr.net/npm/@johnhenry/[email protected]/index.mjs" } }</script><script type="module"> import * as obfo from "@johnhenry/obfo";</script>Provenance: previously published as obfo, last unscoped version 0.0.9 (2024-07-27). @johnhenry/obfo restarts at
0.0.0 under the family’s convention for adopted packages, so the number means “new address”, not “new code”. Under
npm’s caret rules ^0.0.0 matches only 0.0.0, so pin exactly until a deliberate 0.1.0.
Quick start
Section titled “Quick start”<form id="profile" data-obfo-container="{}"> <label>Name <input name="name" value="Jon" /></label> <label>Age <input name="age" type="number" value="20" /></label> <fieldset data-obfo-container="[]" data-obfo-name="tags"> <input value="js" /> <input value="css" /> </fieldset></form>import obfo, { fill, observe } from "@johnhenry/obfo";
const form = document.getElementById("profile");
obfo(form); // { name: "Jon", age: "20", tags: ["js", "css"] }obfo(form, { cast: "auto" }); // { name: "Jon", age: 20, tags: ["js", "css"] }
fill(form, { age: 21 }, { cast: "auto" }); // writes one field, leaves the rest
const stop = observe(form, (value) => console.log(value), { cast: "auto" });// ...the user types; each edit logs the whole object oncestop();The pages here
Section titled “The pages here”- Reading a form: containers, keys,
data-obfo-value, everydata-obfo-cast, buttons and the submitter, andobfo’s options. - Typed values: what
cast: "auto"reads from each kind of input. - Writing a form:
fill, how each cast is inverted, and what it skips. - Live values:
observe, its timing, and its options. - Generating a form:
formFromObjectfor a plain JSON value. - Limitations and traps: untrusted forms, what isn’t read, and testing without a browser.
Examples
Section titled “Examples”The repository’s examples/ directory holds runnable examples that assert what
they show (npm run examples, Node 26 with happy-dom):
| Example | Demonstrates |
|---|---|
01-a-form-reads-as-a-nested-object.mjs |
{} and [] containers read as one nested object; labels, headings and a fieldset’s legend are looked through, and the submit button is ignored unless passed as options.submit. |
02-auto-cast-gives-numbers-booleans-and-arrays.mjs |
cast: "auto" reads numbers, booleans, a radio group’s checked value and a multiple select’s array, while an element’s own data-obfo-cast still wins; without it every value is the string obfo 0.0.9 returned. |
03-fill-is-the-inverse-of-obfo.mjs |
fill writes a value that obfo reads back unchanged, and a partial value changes only the fields it names. |
04-observe-reports-the-object-as-the-form-is-edited.mjs |
observe reports the typed object after each edit, makes one call for a checkbox’s input + change, and goes quiet after unsubscribing. |
05-a-form-generated-from-json-reads-back-as-the-same-json.mjs |
formFromObject builds a form that obfo reads back as the same JSON with no options; an HTML-looking string stays text. |
examples/demo.html is a larger browser page, read on submit and live with observe.
Family
Section titled “Family”obfo turns a form into a value the rest of a reactive page can use. It has no dependencies, and none of these packages depend on it; they meet in the app.
- signalle:
observe(form, (value) => { formValue.value = value }, { immediate: true })keeps a signallesignalequal to the form’s object, socomputeds andeffects downstream update as the user types. - dataflow: for a form that feeds other computations, store the value
observereports and callflow.invalidate(id)in the same callback; dataflow reruns the form’s dependents in order and drops stale runs.
Built for the form panes planned for miso, a natto.dev-style spatial notebook: a pane renders a form, and its value is the form’s object.
Source: github.com/johnhenry/obfo.