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
nameattribute matters: Each input needs anameattribute. This becomes the key in theFormDatadictionary. - The
keyprop: Forms need a uniquekeyto identify them. This is used internally by Pulse to track the form. - Async handlers work great: Your
onSubmithandler 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 filecontent_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
FormDatajust likeps.Form
Tips for Working with Forms
-
Use
ps.Formwhen you can: It handles the boilerplate for you and works well for most cases. -
Always set
nameon inputs: Without a name, the input won't be included in the form data. -
Handle loading states: If your submit handler does async work, consider showing a loading indicator.
-
Validate on the server: Even with client-side validation, always validate data in your Python handler too.