Skip to content

JSCode and JS from Python

Alberto Ruiz edited this page Apr 18, 2026 · 1 revision

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

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

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

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)
  • Default arguments, *args, **kwargs
  • Lambda expressions
  • Imports (any import statement inside a @js body is out of scope and will error)

For the full roadmap of planned phases see the transpiler internals doc.

Where to declare @js functions

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


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

Dom(query)

Shorthand for document.querySelector():

from HTeaLeaf.JS.Common import Dom

Dom("#myInput")  # -> JSCode("document.querySelector(`#myInput`)")

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.

Set(target, value)

Assigns a JS expression:

from HTeaLeaf.JS.Common import Set

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

Clone this wiki locally