Forms
In plain words
Inputs, validation, submit: field values are reactive, errors are computed, the form is captured at the moment of submission.
A controlled input
bindInput binds a State<string> to a field both ways:
import { state } from "@continuum-js/frp";
import { bindInput } from "@continuum-js/dom";
const name = state("");
<input {...bindInput(name, name.set)} />;Under the hood it is just { value: name, onInput: (e) => name.set(…) } — no magic; write it by hand when you want to normalize input (name.set(v.trimStart())).
Validation is a derived value
Errors aren't stored — they are computed:
import { combine } from "@continuum-js/frp";
const emailError = email.map((v) =>
v === "" ? null : /@/.test(v) ? null : "an address with @ is required",
);
const formValid = combine(
emailError,
nameError,
(e1, e2) => e1 === null && e2 === null,
);
<span class="error">{emailError.map((e) => e ?? "")}</span>
<button disabled={formValid.map((v) => !v)}>Submit</button>;If you need "touched/dirty" states, they are folds over events: blurs.once().mapTo(true).hold(false).
Submit: at assembles the form
The button produces an event; it captures the field values at the moment of the click:
const submits = stream<void>();
const form = combine(name, email, (n, e) => ({ name: n, email: e }));
const payload = form.at(
submits.when(formValid), // ignore submits of an invalid form
);
const results = perform(payload, (form) => api.register(form));
<form
onSubmit={(e: SubmitEvent) => {
e.preventDefault();
submits.fire();
}}
>
…
</form>;Line by line:
when(formValid)— the event passes only while the state is true; the "disabled" is duplicated semantically, not just in the button attribute;attakes the field values at the moment of submit — no reading from the DOM or from state inside a handler;performcarries the form into IO; the result comes back as an event (see the async guide).
Clearing after success
The server response is an event; success is the trigger to reset the fields:
import { onCleanup } from "@continuum-js/dom";
const saved = results.filter((r) => r.ok);
onCleanup(
saved.listen(() => {
name.set("");
email.set("");
}),
);listen returns an unsubscribe function — hand it to onCleanup so the subscription dies with the subtree. (JSX bindings do this by themselves; an explicit listen is a manual hatch, and its cleanup is manual too.)
Unfamiliar term? Every piece of jargon in these docs is explained in the glossary.