[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/pythonnative/pythonnative/main/docs/examples/forms.md [Back]  [Original]

# Forms

A sign-up form with controlled inputs, basic validation, and a submit
handler. Demonstrates [`TextInput`][pythonnative.TextInput],
controlled patterns with [`use_state`][pythonnative.use_state], and
how to wire submission.

Paste it into `app/main.py` of a project scaffolded with `pn init`,
then run `pn preview app.main.SignUp`.

## The code

```python
from collections.abc import Callable

import pythonnative as pn


def is_valid_email(s: str) -> bool:
    return "@" in s and "." in s.split("@", 1)[-1]


@pn.component
def LabeledInput(
    label: str,
    value: str,
    on_change: Callable[[str], None],
    placeholder: str = "",
    error: str | None = None,
    secure: bool = False,
) -> pn.Node:
    return pn.Column(
        pn.Text(label, style={"font_size": 14, "bold": True}),
        pn.TextInput(
            value=value,
            on_change=on_change,
            placeholder=placeholder,
            secure=secure,
        ),
        pn.Text(
            error or "",
            style={"font_size": 12, "color": "#ff3b30"},
        ),
        style={"gap": 4},
    )


@pn.component
def SignUp() -> pn.Node:
    name, set_name = pn.use_state("")
    email, set_email = pn.use_state("")
    password, set_password = pn.use_state("")
    submitting, set_submitting = pn.use_state(False)
    submitted, set_submitted = pn.use_state(False)

    name_error = "Name is required" if not name.strip() else None
    email_error = "Enter a valid email" if email and not is_valid_email(email) else None
    password_error = "At least 8 characters" if password and len(password) < 8 else None

    has_errors = bool(name_error or email_error or password_error)
    can_submit = bool(name and email and password) and not has_errors and not submitting

    def submit() -> None:
        if not can_submit:
            return
        set_submitting(True)
        # Simulate work; in a real app, post to an API here.
        set_submitted(True)
        set_submitting(False)

    if submitted:
        return pn.Column(
            pn.Text("Welcome!", style={"font_size": 28, "bold": True}),
            pn.Text(f"Account created for {name}.", style={"font_size": 16}),
            style={"gap": 8, "padding": 16},
        )

    return pn.ScrollView(
        pn.Column(
            pn.Text("Sign up", style={"font_size": 28, "bold": True}),
            LabeledInput(
                label="Name",
                value=name,
                on_change=set_name,
                placeholder="Ada Lovelace",
                error=name_error if name else None,
            ),
            LabeledInput(
                label="Email",
                value=email,
                on_change=set_email,
                placeholder="ada@example.com",
                error=email_error,
            ),
            LabeledInput(
                label="Password",
                value=password,
                on_change=set_password,
                placeholder="At least 8 characters",
                error=password_error,
                secure=True,
            ),
            pn.Button(
                "Submitting..." if submitting else "Create account",
                on_press=submit,
                disabled=not can_submit,
            ),
            style={"gap": 12, "padding": 16, "align_items": "stretch"},
        )
    )
```

## What's going on

- Each input is **controlled**: its `value` comes from state, and its
  `on_change` writes back into state. There is no separate "form
  state" object; the component itself is the source of truth.
- Validation is computed each render. Showing an error message only
  *after* the user has typed (the `if name` guard) avoids the "every
  field is red on first paint" trap.
- `can_submit` derives from the inputs and the in-flight state. The
  button is disabled while `submitting` is true, and the label
  reflects what's happening.
- The success view is just a different render path; no navigation is
  needed for a one-screen form.

## Async submission

For a real submission flow, hand the work to `asyncio`:

```python
import asyncio


def submit() -> None:
    if not can_submit:
        return
    set_submitting(True)

    async def go() -> None:
        try:
            await api.create_account(name=name, email=email, password=password)
            set_submitted(True)
        except Exception as exc:
            set_form_error(str(exc))
        finally:
            set_submitting(False)

    pn.run_async(go())
```

`pn.run_async` runs the coroutine on the application loop in the
component's task scope, so it's cancelled if the form unmounts first.
You can also pass an `async def` handler straight to `on_press`.

Wrap state updates that touch the same screen in a single setter when
possible (e.g., a single `dispatch` from a reducer) so the page only
re-renders once per user-visible change.

## Validation libraries

The validation here is hand-rolled for clarity. For more elaborate
forms, any pure-Python validation library (`pydantic`, `cerberus`,
`marshmallow`) drops in: validate from the input dict on each render,
and surface the resulting errors next to the inputs.

## Next steps

- Render a dynamic list: [Lists](lists.md).
- Wire forms into navigation: [Navigation](navigation.md).
- Make the form theme-aware: [Styling guide](../guides/styling.md).

Web Proxy Viewer  |  New URL  |  Original Page