Skip to content

HTMX and Interactive Interfaces

First draft!

Please treat this as a very early draft, and be careful with anything that this chapter says! We welcome your pull requests to help refine the material so it actually becomes useful.

HTMX allows you to create dynamic, interactive web applications without writing JavaScript.

Installing HTMX Support

HTMX is included by default in Air's built-in layouts. Let's create an interactive example:

@app.page
def counter_demo():
    """Demo of HTMX counter."""
    return air.layouts.mvpcss(
        air.Title("HTMX Counter"),
        air.H1("HTMX Counter Demo"),
        air.Div(
            air.Button("Increment", hx_post="/increment", hx_target="#counter", hx_swap="innerHTML"),
            air.Button("Decrement", hx_post="/decrement", hx_target="#counter", hx_swap="innerHTML"),
            air.Button("Reset", hx_post="/reset", hx_target="#counter", hx_swap="innerHTML"),
            air.Div(0, id="counter", style="font-size: 2em; margin: 1rem 0;"),
        ),
        air.A("← Back to Home", href="/")
    )


# Store counter value in memory (in production, use database or Redis)
counter_value = 0

@app.post("/increment")
def increment():
    global counter_value
    counter_value += 1
    return air.Div(counter_value, id="counter", style="font-size: 2em; margin: 1rem 0;")

@app.post("/decrement")
def decrement():
    global counter_value
    counter_value -= 1
    return air.Div(counter_value, id="counter", style="font-size: 2em; margin: 1rem 0;")

@app.post("/reset")
def reset():
    global counter_value
    counter_value = 0
    return air.Div(counter_value, id="counter", style="font-size: 2em; margin: 1rem 0;")

Advanced HTMX Features

HTMX attributes can be added to Air Tags:

air.Div(
    "Content",
    hx_get="/api/data",           # Make GET request to /api/data
    hx_target="#result",          # Update element with id="result"
    hx_swap="innerHTML",          # Replace innerHTML of target
    hx_trigger="click",           # Trigger on click
    hx_indicator="#spinner"       # Show spinner while loading
)

# Form with HTMX
air.Form(
    air.Input(name="search", placeholder="Search..."),
    air.Button("Search", type="submit"),
    hx_post="/search",            # POST to /search
    hx_target="#results",         # Update #results div
    hx_indicator=".htmx-indicator" # Show loading indicator
)

Server-Sent Events (SSE)

Air supports Server-Sent Events for real-time updates:

import random
from asyncio import sleep

import air

app = air.Air()

@app.page
def index():
    return air.layouts.mvpcss(
        air.Script(src="https://unpkg.com/htmx-ext-sse@2.2.1/sse.js"), 
        air.Title("Server Sent Event Demo"),
        air.H1("Server Sent Event Demo"),
        air.P("Lottery number generator"),
        air.Section(
            hx_ext="sse",  
            sse_connect="/lottery-numbers", 
            hx_swap="beforeend show:bottom", 
            sse_swap="message", 
        ),
    )

async def lottery_generator():  
    while True:
        lottery_numbers = ", ".join([str(random.randint(1, 40)) for x in range(6)])
        # Tags work seamlessly
        yield air.Aside(lottery_numbers) 
        await sleep(1)


@app.page
async def lottery_numbers():
    return air.SSEResponse(lottery_generator())

Now would be a good time to commit your work:

git add .
git commit -m "Add HTMX and interactive interface features"