JS Interop
Pulse lets you build web apps entirely in Python, but sometimes you need to reach into JavaScript. Maybe you want to focus an input field, scroll to an element, use a browser API, or integrate a JavaScript library that doesn't have a Pulse wrapper. JavaScript interop makes all of this possible.
Wrapping React Components
One of the most common uses of JS interop is wrapping existing React components. If there's a React component library you want to use that doesn't have a Pulse package, you can wrap it yourself.
from pulse import react_component, Import
# Import a component from an npm package
DatePicker = react_component(
Import("DatePicker", "react-datepicker")
)
# Now use it like any other Pulse component
@ps.component
def BookingForm():
with ps.init():
state = BookingState()
return ps.div()[
ps.label()["Select a date:"],
DatePicker(
selected=state.date,
onChange=state.set_date,
),
]The Import specifies where to find the component:
- Named import:
Import("DatePicker", "react-datepicker") - Default import:
Import("Calendar", "react-calendar", kind="default")
Running JavaScript from Python
Use ps.run_js() to execute JavaScript in the browser. It takes a JavaScript expression—either from pulse.js exports or from calling a @ps.javascript function.
from pulse.js import window, document
class ScrollState(ps.State):
def scroll_to_top(self):
ps.run_js(window.scrollTo(0, 0))
def focus_email(self):
ps.run_js(document.querySelector("#email").focus())run_js does not accept strings—you must pass an expression object:
# Correct
ps.run_js(window.scrollTo(0, 0))
# Wrong - strings don't work
ps.run_js("window.scrollTo(0, 0)") # TypeErrorFor operations that return values, use result=True and await:
from pulse.js import window
async def get_window_size(self):
width = await ps.run_js(window.innerWidth, result=True)
height = await ps.run_js(window.innerHeight, result=True)
print(f"Window is {width}x{height}")The @ps.javascript Decorator
For logic beyond simple expressions, define Python functions that get transpiled to JavaScript:
from pulse.js import document
@ps.javascript
def focus_element(selector: str):
el = document.querySelector(selector)
if el:
el.focus()
@ps.javascript
def scroll_to_element(element_id: str):
el = document.getElementById(element_id)
if el:
el.scrollIntoView({"behavior": "smooth"})Call these functions with run_js:
class FormState(ps.State):
def on_error(self):
ps.run_js(focus_element("#email"))
def go_to_section(self, section_id: str):
ps.run_js(scroll_to_element(section_id))When you call ps.run_js(focus_element("#email")), Pulse:
- Transpiles the Python function to JavaScript
- Calls it with the provided arguments
- Executes it in the browser
This approach gives you:
- Syntax highlighting and editor support in your Python code
- Type checking for your parameters
- Reusable JavaScript functions you can call from multiple places
What You Can Do in @ps.javascript Functions
The transpiler supports a subset of Python that maps cleanly to JavaScript:
- Variables and assignments
- Conditionals (
if/else) - Loops (
for,while) - Function calls
- Object and array access
- Basic operators
You can access browser globals like document, window, console, localStorage, etc.
@ps.javascript
def save_to_storage(key: str, value: str):
localStorage.setItem(key, value)
@ps.javascript
def log_and_alert(message: str):
console.log(message)
window.alert(message)
@ps.javascript
def get_cookies():
return document.cookieHandling Complex Interop
Sometimes you need more control. Here are some patterns for complex scenarios.
Getting Results from JavaScript
from pulse.js import window
@ps.javascript
def get_selection():
selection = window.getSelection()
if selection:
return selection.toString()
return ""
class EditorState(ps.State):
async def copy_selection(self):
text = await ps.run_js(get_selection(), result=True)
if text:
print(f"User selected: {text}")Calling External Libraries
If you've added a JavaScript library to your project, you can call it:
@ps.javascript
def init_chart(container_id: str, data: list):
ctx = document.getElementById(container_id).getContext("2d")
Chart(ctx, {
"type": "bar",
"data": {
"labels": data.map(lambda d: d["label"]),
"datasets": [{
"data": data.map(lambda d: d["value"]),
}]
}
})Browser APIs
from pulse.js import window, navigator, Promise
@ps.javascript
def request_notification_permission():
if "Notification" in window:
Notification.requestPermission()
@ps.javascript
def copy_to_clipboard(text: str):
navigator.clipboard.writeText(text)
@ps.javascript
def get_geolocation():
return Promise(lambda resolve, reject:
navigator.geolocation.getCurrentPosition(
lambda pos: resolve({"lat": pos.coords.latitude, "lng": pos.coords.longitude}),
lambda err: reject(err)
)
)When to Use JS Interop
Use JS interop when you need to:
- Access browser APIs (clipboard, notifications, geolocation)
- Manipulate the DOM directly (focus, scroll, measure elements)
- Use JavaScript libraries without a Pulse wrapper
- Optimize performance for client-side-only operations
Prefer staying in Python when you can. Pulse's normal state and rendering system handles most cases, and keeping logic in Python means you get all the benefits of server-side execution (security, consistency, easier debugging).
Tips
-
Keep JS functions small and focused: They should do one thing.
-
Handle errors: JavaScript can fail. Use try/catch in complex functions.
-
Remember it's async:
ps.run_js()communicates with the browser over WebSocket. -
Test your transpiled code: The transpiler handles common patterns, but complex Python might not translate as expected.
See also
- Advanced: JS Interop
- Reference: JS Interop — Full API including
pulse.jsexports