-
Notifications
You must be signed in to change notification settings - Fork 0
JSCode and JS from Python
HTeaLeaf includes a built-in transcompiler that allows you to generate and embed JavaScript directly from Python. This makes it possible to interact with the browser DOM or call client-side APIs without leaving Python syntax.
HTeaLeaf offers two distinct mechanisms for this, each with different power and scope:
| Mechanism | Use case | Limitations |
|---|---|---|
JSCode |
Simple inline expressions, DOM access, event handlers | No control flow, expression-only |
@js |
Full functions with logic, loops, conditionals | No OOP, no exceptions (yet) |
JSCode represents a reference to a JavaScript expression or object.
It can be used anywhere inside HTeaLeaf elements, event handlers, or custom scripts.
from HTeaLeaf.Magic.Common import JSCode
document = JSCode("document")
button("Show Value").attr(onclick=document.getElementById("input1").value)JSCode implements __getattr__ and __call__ internally, which means attribute access and method calls on a JSCode object don't execute Python — they accumulate into a JS expression string that gets rendered into the page. The chaining above produces:
<button onclick="document.getElementById('input1').value">Show Value</button>Because JSCode is expression-only, it has no support for control flow (if, loops, etc.) or negation via not. For negation use the Not() helper described below.
window = JSCode("window")
button("Logout").attr(onclick=window.location.replace("/logout"))Resulting HTML:
<button onclick="window.location.replace('/logout')">Logout</button>@js is a decorator that marks a Python function for transpilation to JavaScript.
The function body is compiled by HTeaLeaf's PY2JS transpiler and injected into the page as a <script> block automatically.
from HTeaLeaf.JS import js
def Home():
@js
def show_alert(message):
if message != "":
alert(message)
else:
console.log("No message provided")This transpiles to:
function show_alert(message) {
if (message != "") {
alert(message)
} else {
console.log("No message provided")
}
}Once declared, a @js function is a JSFunction object and can be called from .attr() like any JSCode expression:
button("Show Alert").attr(onclick=show_alert("Hello"))The transpiler currently handles:
Phase 1 — Basic expressions
- Function calls
- Arithmetic (
+,-,*,/,//,%,**) - Comparisons (
==,!=,<,>,<=,>=) - Boolean operators (
and→&&,or→||,not→!) - String literals, including f-strings → template literals
-
None/True/False→null/true/false
Phase 2 — Flow control and variables
- Variable declaration (
let) and reassignment - Augmented assignment (
+=,-=, etc.) - Function declaration (
def) return-
if/elif/else whilefor x in iterable-
break/continue
Phase 3 — Data structures
- List literals → Array
- Dict literals → Object
- Tuple literals → Array
- Subscript / indexing (
a[0],d["key"]) - Attribute access (
obj.attr) - Scope tracking (variables by level)
The following are not yet supported and will raise an error if used inside a @js function:
- OOP (
class,self, inheritance) - Exception handling (
try/except) - Default arguments,
*args,**kwargs - Lambda expressions
- Imports (any
importstatement inside a@jsbody is out of scope and will error)
For the full roadmap of planned phases see the transpiler internals doc.
The recommended and fully supported pattern is to declare @js functions inside a route handler:
def Home():
@js
def my_function():
...When declared inside a route handler, HTeaLeaf automatically injects the transpiled function into the page via the on_render hook — no manual wiring required. Store references are also resolved and substituted correctly at this point.
⚠️ Beta / unsupported: Declaring@jsfunctions at module level is not officially supported. The function will not be injected automatically — you would need to inject it manually withscript(my_function)— and store ID substitution may not behave as expected. Avoid this pattern unless you know what you are doing.
script() lets you embed raw JavaScript strings directly into the rendered page.
This predates the @js transpiler and is still supported for cases where you need to drop down to handwritten JS — for example, integrating third-party libraries or writing code that uses features the transpiler does not yet support.
from HTeaLeaf.Html.Elements import script
script("""
function addTodoIfNotEmpty(inputId, store) {
let val = document.getElementById(inputId).value;
if (val.trim() !== "") {
store.set("todo", {"done": false, "value": val});
document.getElementById(inputId).value = "";
} else {
alert("empty task");
}
}
""")Note that script() content is not processed by the transpiler — it is injected verbatim. Store ID substitution and other framework transformations do not apply here.
HTeaLeaf includes convenience helpers that return JSCode objects for common patterns.
These are intended for use with JSCode-style inline expressions, not inside @js bodies (where you can use native Python syntax instead).
Shorthand for document.querySelector():
from HTeaLeaf.JS.Common import Dom
Dom("#myInput") # -> JSCode("document.querySelector(`#myInput`)")Negates a JavaScript expression. Use this when working with JSCode objects, where Python's not cannot be intercepted:
from HTeaLeaf.JS.Common import Not
Not(JSCode("isVisible")) # -> JSCode("!isVisible")Inside a @js function, use not directly — the transpiler converts it to ! automatically.
Assigns a JS expression:
from HTeaLeaf.JS.Common import Set
Set(JSCode("count"), 10) # -> JSCode("count = 10")