These docs cover 0.2.0-beta.26. Each guide distinguishes supported beta behavior, explicit deployment boundaries, and retained 0.1 compatibility material.

Browse documentationAll guides

Build · application data

Forms and resources

Presolve 0.2.0-beta.26 gives applications a compiler-owned Form model. Field ownership, native control binding, validation, submission, cancellation, and resume behavior are compiled into exact products instead of delegated to a generic client form controller.

Current authoring API

Use defineForm() and field(). The compiler derives the complete Form and Field graph from those declarations.

Define an owned form

A DefinedForm belongs to the component instance that declares it. Nested field objects preserve their authored value shape, so the submission payload is typed from the field tree rather than reconstructed from DOM names.

import { Component, defineForm, email, field, required } from "presolve";export class ProfileForm extends Component {profile = defineForm({serialization: "form-data",fields: { identity: { name: field({ initial: "", validate: [required()] }), email: field({ initial: "", validate: [required(), email()] }), }, newsletter: field({ initial: false }), attachments: field<File[]>({ initial: [] }),},});}

Bind native controls

Bind controls to the compiler-proven Field object, not to a copied value. bind:value covers admitted text and scalar controls, bind:checked covers checkboxes, and bind:files produces a File[] on change.

render() {return <form form={this.profile}><input bind:value={this.profile.fields.identity.name} /><input type="email" bind:value={this.profile.fields.identity.email} /><input type="checkbox" bind:checked={this.profile.fields.newsletter} /><input type="file" multiple bind:files={this.profile.fields.attachments} /><button type="submit">Save profile</button></form>;}
File fields require form data

Use serialization: "form-data" when a form owns a File[] field. JSON and URL-encoded serialization cannot carry native File values.

Validation

Built-in validators are required(), numeric min()/max(), string or sequence minLength()/maxLength(), pattern(), and email(). Presolve validates after a binding update and again before submission.

A directly imported Standard Schema v1 validator can also appear in a Field’s validate array. The compiler resolves the exact export, emits a validator bundle, suppresses stale asynchronous results, and projects its issues to the owning Field. Browser validation improves interaction; an external server boundary must still validate untrusted input again.

export const displayNameSchema = {"~standard": {version: 1 as const, vendor: "acme",validate(value: unknown) { return typeof value === "string" && value.length >= 3 ? { value } : { issues: [{ message: "Use at least three characters" }] };},},};

Export the validator from an ordinary module, then use that exact named import directly in the Field definition. Local inline objects, default imports, namespace members, and schema factories are not authority-proven validator coordinates.

import { displayNameSchema } from "../validation/profile";import { defineForm, field } from "presolve";profile = defineForm({fields: {displayName: field({ initial: "", validate: [displayNameSchema] })}});

Use equals(otherField) and notEquals(otherField) for compiler-bound cross-Field checks. Both Fields must be statically recoverable members of the same canonical Form.

Submission and cancellation

The optional submit handler receives the compiler-built nested value and a submission-owned AbortSignal. Native submit events validate first, prevent duplicate in-flight calls, and publish explicit completed, failed, cancelled, invalid, and reset outcomes.

import { saveProfile } from "profile-service";profile = defineForm({// fields omittedasync submit({ value, signal }) {await saveProfile(value, signal);},});

An imported asynchronous submit function is executable only when its package publishes the exact integrity-bound Presolve form_submission capability contract. Presolve bundles that declared runtime export; an arbitrary imported function with the same TypeScript shape is rejected.

{"kind": "capability","type_signature": "(FormValue, AbortSignal) -> Promise<void>","runtime_module": "dist/save-profile.js","resume_policy": "cold_fallback","form_submission": {"execution_boundary": "client", "cancellation": "abort","input": "form_value", "result": "void"}}

Submit to Node

A Form can select an executable server action when its submission receives canonical formData and signal values and the complete callback body is one direct call to an admitted named package import.

import { saveProfile } from "profile-service";profile = defineForm({serialization: "form-data",// fields omittedsubmit: async ({ formData, signal }) => saveProfile(formData, signal),});

TypeScript authority proves the canonical Form, DOM FormData and AbortSignal parameters, Promise completion, and exact named import. The package must declare a server_action capability with one JSON or redirect response family and typed failure. The generated Node host owns request validation, decoding, cancellation, and stable responses; the browser never imports the server module.

Resume and file behavior

Serializable Field values, form flags, validation membership, and submission ownership participate in the exact Forms resume product. Native File objects are intentionally cold-only: Presolve clears and revalidates the file Field on resume while preserving the other serializable Fields. Pending work is cancelled during teardown, and malformed snapshots take one clean cold path.

Route-owned Resources

Use loader<Data, Error>() on a route component when the page needs server data. The field is a canonical Resource<Data, Error>; the Node host executes its admitted package endpoint, codec-validates the outcome, and bootstraps the exact Resource activation before browser dependencies run.

import { loader, type Resource, type RouteParameters } from "presolve";import { loadProfile } from "profile-service";profile: Resource<Profile, NotFound> = loader(async (params: RouteParameters, signal: AbortSignal) => loadProfile(params, signal),);

Loader endpoints declare public, private, or no-store caching and a typed error codec. Read the complete loader and server-action guide before adopting this server boundary.

Deployment boundaries

  • The Cloudflare Static Assets adapter rejects every loader and server-action handoff; use the generated Node host for those capabilities.
  • Client submission capabilities and Node server actions are distinct compiler records with distinct parameter and package contracts.
  • Arbitrary fetch calls do not become compiler-owned resources or resumable submissions.
  • Unsupported validators, bindings, serializers, and submission shapes fail closed instead of falling back to an opaque form library.