Pulse

Forms

Form helpers and types

Form

Server-registered HTML form component. Automatically wires up form submission to a Python handler.

def Form(
    *children: Node,
    key: str,
    onSubmit: EventHandler1[FormData] | None = None,
    **props: Unpack[PulseFormProps],
) -> Node

Parameters

ParameterTypeDefaultDescription
childrenNode-Form content (inputs, buttons, etc.)
keystrrequiredUnique form identifier
onSubmitEventHandler1[FormData] | NoneNoneSubmit handler
**propsPulseFormProps-Standard HTML form props (except action, method, encType, onSubmit)

Usage

async def handle_submit(data: ps.FormData):
    name = data.get("name")  # str
    file = data.get("avatar")  # UploadFile
    await save_user(name, file)

def my_form():
    return ps.Form(
        m.TextInput(name="name", label="Name"),
        m.FileInput(name="avatar", label="Avatar"),
        m.Button("Submit", type="submit"),
        key="user-form",
        onSubmit=handle_submit,
    )

Notes

  • key must be a non-empty string unique within the render
  • Cannot override action, method, encType, or onSubmit via props
  • Uses multipart/form-data encoding (supports file uploads)
  • Handler receives parsed form data as a FormData dict

ManualForm

Low-level form handler for custom form implementations.

class ManualForm(Disposable):
    def __init__(self, on_submit: EventHandler1[FormData] | None = None)

Properties

PropertyTypeDescription
is_submittingboolWhether form is currently submitting
registrationFormRegistrationForm registration info (raises if disposed)

Methods

props() -> GeneratedFormProps

Get form props for manual binding to a form element.

class GeneratedFormProps(TypedDict):
    action: str       # Form submission URL
    method: str       # "POST"
    encType: str      # "multipart/form-data"
    onSubmit: Callable[[], None]  # Submission trigger

update(on_submit: EventHandler1[FormData] | None) -> None

Update the submit handler.

dispose() -> None

Unregister the form and clean up.

__call__(*children, key: str | None, **props) -> Node

Render as a form element with children.

Usage

def custom_form():
    manual = ManualForm(on_submit=handle_data)

    # Option 1: Render directly
    return manual(
        m.TextInput(name="field"),
        m.Button("Submit", type="submit"),
        key="my-form",
    )

    # Option 2: Use props manually
    form_props = manual.props()
    return m.form(
        m.TextInput(name="field"),
        m.Button("Submit", type="submit"),
        **form_props,
    )

Types

FormData

Parsed form submission data.

FormData = dict[str, FormValue]

Native fields are strings or uploads. Repeated native fields are lists. Structured integrations can also submit nested values.

FormScalar

A non-container form value.

FormScalar = str | int | float | bool | date | datetime | UploadFile | None

FormValue

A scalar, sequence, mapping, or set submitted by a form integration.

FormValue = (
    FormScalar
    | Sequence[FormValue]
    | Mapping[str, FormValue]
    | AbstractSet[FormValue]
)

UploadFile

Re-exported from Starlette. Represents an uploaded file.

from starlette.datastructures import UploadFile

class UploadFile:
    filename: str | None
    size: int | None
    content_type: str | None

    async def read(self, size: int = -1) -> bytes: ...
    async def write(self, data: bytes) -> int: ...
    async def seek(self, offset: int) -> int: ...
    async def close() -> None: ...

Example with Files

async def handle_upload(data: ps.FormData):
    file = data.get("document")
    if isinstance(file, UploadFile):
        content = await file.read()
        filename = file.filename or "unnamed"
        await save_file(filename, content)

Structured form data

Form integrations can submit nested values through Pulse's structured multipart transport. The transport serializes non-file values, sends files as binary multipart parts, and reconstructs the complete value in FormRegistry before calling your handler.

The transport reserves these multipart field names:

  • __pulse_data__ contains the Pulse-serialized value.
  • __pulse_files__ contains the file manifest.
  • __pulse_files__.N contains the binary file referenced by manifest entry N.

__pulse_data__ and __pulse_files__ are reserved. Using either as a top-level form value raises an error. These fields are an internal transport detail; don't construct them manually.

Nested objects, empty or single-element lists, and uploaded files retain their structure when your onSubmit handler receives them.

Malformed structured payloads return HTTP 400 and don't call the submit handler.


FormRegistry

Internal class managing form registrations. Not typically used directly.

class FormRegistry(Disposable):
    def register(
        render_id: str,
        route_id: str,
        session_id: str,
        on_submit: Callable[[FormData], Awaitable[None]],
    ) -> FormRegistration

    def unregister(form_id: str) -> None

    async def handle_submit(
        form_id: str,
        request: Request,
        session: UserSession,
    ) -> Response

FormStorage

Internal hook state for managing form lifecycle within renders. Not typically used directly.


Complete Example

import pulse as ps
from pulse_mantine import components as m

async def create_post(data: ps.FormData):
    title = data.get("title", "")
    body = data.get("body", "")
    image = data.get("image")

    if not isinstance(title, str) or not title:
        raise ValueError("Title required")

    image_url = None
    if isinstance(image, ps.UploadFile) and image.filename:
        content = await image.read()
        image_url = await upload_to_s3(image.filename, content)

    await db.posts.create(title=title, body=body, image_url=image_url)
    ps.navigate("/posts")

def create_post_form():
    return ps.Form(
        m.Stack([
            m.TextInput(name="title", label="Title", required=True),
            m.Textarea(name="body", label="Body", rows=5),
            m.FileInput(name="image", label="Image", accept="image/*"),
            m.Button("Create Post", type="submit"),
        ]),
        key="create-post",
        onSubmit=create_post,
    )

On this page