# Prefill and OpenForm

A form can open with values already filled in, some fields hidden, or running by itself. These options belong to one opening of the page; the function does not change.

GET /edit_user/?prefill={"name": "Ana", "age": 32}&hidden=["age"]
Option What it does
prefill a JSON object with initial values, by parameter name
hidden a JSON list of fields not to show; "config.token" and "items.*.token" reach nested fields
autorun submits the form once it is ready, if nothing is missing
hide_title, hide_description hide the function's heading and description
hide_submit hides the Submit button

The flags take 1, true, on or yes (and 0, false, off, no); anything else is a 422.

The prefill uses the same JSON as /invoke (dates as ISO text, an enum by its name, a file by its reference) and it can leave out parameters, which keep their defaults. It is validated against the signature before the page is served, so a wrong value is a 400, never a half-filled form:

prefill={"age": "two"}   → 400 age: expected int, got str
prefill={"nope": 1}      → 400 unknown prefill field: 'nope'

autorun is for a page opened to see a result, such as a report: the page presses its own button once. With hide_submit too, it shows only the result.

hidden hides, it does not lock. A hidden field still travels in the request, and anyone can call /invoke with another value. The prefill travels in the URL, so it ends up in the browser history and the server logs: it is not a place for secrets.

# OpenForm

A function can return the values another function opens with. Mark the return with OpenForm and the target function:

from dataclasses import dataclass
from typing import Annotated

from func_to_web import OpenForm, run


@dataclass
class Product:
    product_id: int
    name: str
    stock: int


def edit_product(product_id: int, name: str, stock: int) -> str:
    return "Product updated"


def select_product(
    product_id: int,
) -> Annotated[Product, OpenForm(edit_product, hidden=("product_id",))]:
    return Product(product_id, "Tuna", 12)


run([select_product, edit_product])

Running select_product takes the browser to edit_product, with its fields filled in and product_id hidden.

  • Return a dataclass or a dict; it may cover only some of the parameters.
  • The target must be defined first, and registered in the same space; it is checked when the application is built.
  • OpenForm marks the whole return, and does not mix with other outputs.
  • A file the function received can be passed on: the target opens with it already in place, without uploading it again.

resize_image opened from choose_image, with its size already filled in

# From Python: page_of()

page_of() builds the HTML of one opening, for a host that serves pages of its own:

page_of(
    web_function: WebFunction,
    *,
    prefill: Mapping[str, Any] | None = None,
    hidden: Iterable[str] | None = None,
    autorun: bool = False,
    hide_title: bool = False,
    hide_description: bool = False,
    hide_submit: bool = False,
    theme: Theme = "system",
) -> str
from func_to_web import WebFunction, page_of

html = page_of(WebFunction(edit_user), prefill={"name": "Ana", "age": 32})

Here the prefill holds real Python values (date, enum members, dataclasses). The page loads its assets from ../static/, so it needs a space mounted beside it.

How it works inside

A URL prefill goes through json.loads(), then decode() (which also resolves file references), then a temporary Signature with only the parameters present, whose build() produces exact Python values. Those go to page_of(), which is where the Python entry point starts. The prefill becomes temporary defaults, so its errors from Python carry the default: prefix; the function's own schema, plan and HTML are never modified.

An OpenForm target is resolved by identity, not by slug: a matching slug would not prove it is the same function. Its result reaches the browser as a relative URL with the usual query string, so nothing is stored on the server.