Skip to content

JSCode and JS from Python

LoboGuardian 🐺 edited this page Jul 11, 2026 · 8 revisions

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: Writing JavaScript inline

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.js 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.

Example: Using JSCode in event handlers

window = JSCode("window")

button("Logout").attr(onclick=window.location.replace("/logout"))

Resulting HTML:

<button onclick="window.location.replace('/logout')">Logout</button>

@js: Transpiled Python functions

@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

async 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")
  }
}

(Python's == / != compile to strict === / !==.)

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"))

Transpiler support

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 / Falsenull / true / false

Phase 2 — Flow control and variables

  • Variable declaration (let) and reassignment
  • Augmented assignment (+=, -=, etc.)
  • Function declaration (def)
  • return
  • if / elif / else
  • while
  • for 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)
  • Lambda expressions
  • Imports (any import statement inside a @js body is out of scope and will error)

⚠️ Default arguments, *args and **kwargs do not raise — they are silently dropped: only the plain positional parameters survive transpilation. Avoid them in @js signatures.

For the transpiler internals see src/htealeaf/js/py2js.py.

Where to declare @js functions

The recommended and fully supported pattern is to declare @js functions inside a route handler (sync or async):

async def Home():
    @js
    def my_function():
        ...

When declared inside a route handler, HTeaLeaf registers the function in the active render context and the HTML renderer injects the transpiled <script> into <head> — no manual wiring required. Store references are also resolved and substituted correctly at this point.

⚠️ Beta / unsupported: Declaring @js functions at module level is not officially supported. The function will not be injected automatically — you would need to inject it manually with script(my_function) — and store ID substitution may not behave as expected. Avoid this pattern unless you know what you are doing.


script(): Raw JavaScript escape hatch

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.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.


Low-level JS helpers

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).

document

A JSCode wrapper class around the browser document. Outside a @js body it must be instantiated before chaining (the class itself has no live attributes):

from htealeaf.js.common import document

document().querySelector("#myInput")   # -> JSCode('document.querySelector("#myInput")')
document().getElementById("input1")    # -> JSCode('document.getElementById("input1")')

Inside a @js body, use document directly — the transpiler resolves the name for you.

console, window, alert, and event are available as ready-made JSCode instances in htealeaf.js.common, so they chain directly (console.log(...)).

Not(code)

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. On a JSCode object, Python's ~ operator is an equivalent shorthand: ~JSCode("isVisible") also produces !isVisible.

Set(target, value)

Assigns a JS expression:

from htealeaf.js.common import Set

Set(JSCode("count"), 10)  # -> JSCode("count = 10")

Other helpers

htealeaf.js.common also provides comparison builders Eq, Ne, Gt, Lt (e.g. Eq(code, 5)code == 5) and wait(code), which prefixes an expression with await. JSCode additionally overloads the arithmetic operators (+, -, *, /) and item access (code["key"] = x, del code["key"]), all of which accumulate into the generated expression.


Back to Home

Clone this wiki locally