Pulse

Forms

Forms are a fundamental part of web applications. Whether you're building a login page, a contact form, or a complex multi-step wizard, you'll need to collect and process user input. Pulse makes this straightforward with built-in form handling that works seamlessly with Python.

The Basics: Creating a Form

The simplest way to handle forms in Pulse is with ps.Form. It automatically collects all the input values and sends them to your Python handler when the user submits.

class SignupState(ps.State):
    message: str = ""

    async def submit(self, data: ps.FormData):
        # data is a dictionary containing all form field values
        email = data.get("email")
        password = data.get("password")
        self.message = f"Thanks for signing up, {email}!"


@ps.component
def signup_page():
    with ps.init():
        st = SignupState()

    return ps.div(className="max-w-md mx-auto mt-10")[
        ps.h1("Create an Account", className="text-2xl font-bold mb-4"),
        ps.Form(key="signup", onSubmit=st.submit, className="space-y-4")[
            ps.div(
                ps.label("Email", className="block text-sm font-medium"),
                ps.input(
                    name="email",
                    type="email",
                    required=True,
                    className="w-full border rounded px-3 py-2",
                ),
            ),
            ps.div(
                ps.label("Password", className="block text-sm font-medium"),
                ps.input(
                    name="password",
                    type="password",
                    required=True,
                    className="w-full border rounded px-3 py-2",
                ),
            ),
            ps.button(
                "Sign Up",
                type="submit",
                className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600",
            ),
        ],
        ps.p(st.message, className="mt-4 text-green-600") if st.message else None,
    ]

A few things to notice here:

  • The name attribute matters: Each input needs a name attribute. This becomes the key in the FormData dictionary.
  • The key prop: Forms need a unique key to identify them. This is used internally by Pulse to track the form.
  • Async handlers work great: Your onSubmit handler can be async, which is perfect for saving data to a database or calling an API.

Understanding FormData

When your form submits, Pulse collects all the named inputs and passes them to your handler as a FormData object. This is essentially a dictionary where keys are the input names and values are what the user entered.

async def submit(self, data: ps.FormData):
    # Access individual fields
    username = data.get("username")
    email = data.get("email")

    # Check what was submitted
    print(f"Received: {data}")

File Uploads

Need to handle file uploads? Pulse has you covered. File inputs are automatically handled and converted to UploadFile objects that you can read and process.

class UploadState(ps.State):
    filename: str = ""

    async def upload(self, data: ps.FormData):
        file = data.get("document")  # This is an UploadFile object
        if file:
            self.filename = file.filename
            contents = await file.read()
            # Process the file contents...


@ps.component
def upload_page():
    with ps.init():
        st = UploadState()

    return ps.Form(key="upload", onSubmit=st.upload)[
        ps.input(name="document", type="file"),
        ps.button("Upload", type="submit"),
        ps.p(f"Uploaded: {st.filename}") if st.filename else None,
    ]

The UploadFile object gives you access to:

  • filename: The original name of the uploaded file
  • content_type: The MIME type (like "image/png" or "application/pdf")
  • read(): An async method to read the file contents

Manual Form Handling

Sometimes you need more control over your form behavior—like tracking submission state, customizing form props, or integrating with components that need direct access to the form object. For these cases, you can use ManualForm directly.

ManualForm is the same mechanism that ps.Form uses internally. When you create one, you get access to additional features like the is_submitting property.

class ContactState(ps.State):
    form: ps.ManualForm | None = None
    submitted: bool = False

    async def handle_submit(self, data: ps.FormData):
        name = data.get("name")
        email = data.get("email")
        message = data.get("message")
        # Process the form data...
        self.submitted = True


@ps.component
def contact_form():
    with ps.init():
        st = ContactState()
        # Create the ManualForm during render and store it in state
        st.form = ps.ManualForm(on_submit=st.handle_submit)

    if st.submitted:
        return ps.div("Thanks for your message!", className="text-green-600")

    # Render the form by calling it directly
    return st.form(
        ps.input(
            name="name",
            placeholder="Your name",
            required=True,
            className="w-full border rounded px-3 py-2 mb-4",
        ),
        ps.input(
            name="email",
            placeholder="Your email",
            type="email",
            required=True,
            className="w-full border rounded px-3 py-2 mb-4",
        ),
        ps.textarea(
            name="message",
            placeholder="Your message",
            required=True,
            className="w-full border rounded px-3 py-2 mb-4",
        ),
        ps.button(
            "Sending..." if st.form.is_submitting else "Send Message",
            type="submit",
            disabled=st.form.is_submitting,
            className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50",
        ),
        key="contact",
        className="space-y-4",
    )

This approach gives you:

  • Submission state: Track when the form is being submitted with is_submitting
  • Full form props access: Use form.props() if you need to apply props to a custom form element
  • Same server-side handling: Your submit handler receives FormData just like ps.Form

Tips for Working with Forms

  1. Use ps.Form when you can: It handles the boilerplate for you and works well for most cases.

  2. Always set name on inputs: Without a name, the input won't be included in the form data.

  3. Handle loading states: If your submit handler does async work, consider showing a loading indicator.

  4. Validate on the server: Even with client-side validation, always validate data in your Python handler too.

See also

What to read next

On this page