Events
Now that you know how to create state, let's look at how users can interact with your application. In Pulse, you respond to user actions through event handlers.
What are Event Handlers?
Event handlers are Python functions that run when something happens in the browser - a button click, a form input, a key press. You've already seen onClick in action:
ps.button("Click me", onClick=st.increment)When the user clicks this button, Pulse calls your increment function on the server. This is the bridge between user interaction and state changes.
Passing Event Handlers
Event handlers can be any callable - state methods, standalone functions, or lambdas:
class Counter(ps.State):
count: int = 0
def inc(self):
self.count += 1
@ps.component
def counter():
with ps.init():
st = Counter()
# State method
def local_dec():
st.count -= 1
return ps.div(
ps.button("-", onClick=local_dec), # Local function
ps.span(f" {st.count} "),
ps.button("+", onClick=st.inc), # State method
ps.button("Reset", onClick=lambda: setattr(st, 'count', 0)), # Lambda
)All three approaches work. Use whichever makes your code clearest.
Reading Event Data
Many events carry useful information - what key was pressed, what text was entered, which element was clicked. You can access this through the event argument:
@ps.component
def input_demo():
with ps.init():
st = Counter()
def on_change(evt):
# evt contains the full event object from the browser
value = evt["target"]["value"]
st.count = int(value) if value else 0
return ps.div(
ps.input(
type="number",
value=st.count,
onChange=on_change,
className="border p-2 rounded",
),
ps.p(f"Current value: {st.count}"),
)The event object mirrors the JavaScript event. Common patterns:
- Input values:
evt["target"]["value"] - Checkbox state:
evt["target"]["checked"] - Key presses:
evt["key"]
The Zero-or-All Rule
Here's an important rule: a callback can take zero arguments or all arguments passed by the client.
This means you can write:
# Takes no arguments - ignores event data
ps.button("Click", onClick=st.inc)
# Takes the event - receives full event object
ps.button("Click", onClick=lambda evt: print(evt))But you cannot pick and choose which arguments to receive. If the client sends two arguments, your function must accept zero or two - not one.
Async Event Handlers
Event handlers can be async. This is useful for operations like API calls or database queries:
class SearchState(ps.State):
query: str = ""
results: list = []
async def search(self):
# Simulate an API call
import asyncio
await asyncio.sleep(0.5)
self.results = [f"Result for '{self.query}'"]Pulse handles the async nature automatically. State updates are batched between await points for efficiency.
Practical Example: Form Input
Here's a complete example showing a form with text input:
import pulse as ps
class FormState(ps.State):
name: str = ""
submitted_name: str = ""
def update_name(self, value: str):
self.name = value
def submit(self):
self.submitted_name = self.name
self.name = ""
@ps.component
def greeting_form():
with ps.init():
st = FormState()
return ps.div(className="p-4 max-w-md")[
ps.h2("Enter Your Name", className="text-xl font-bold mb-4"),
ps.div(className="flex gap-2 mb-4")[
ps.input(
type="text",
value=st.name,
onChange=lambda evt: st.update_name(evt["target"]["value"]),
placeholder="Your name...",
className="border p-2 rounded flex-1",
),
ps.button(
"Submit",
onClick=st.submit,
className="px-4 py-2 bg-blue-500 text-white rounded",
),
],
ps.p(
f"Hello, {st.submitted_name}!" if st.submitted_name else "Enter a name above.",
className="text-lg",
),
]
app = ps.App([ps.Route("/", greeting_form)])This pattern - using a lambda to extract the value and pass it to a state method - keeps your state methods clean and focused on business logic.