Skip to content

HTMX

PyJinHx components work seamlessly with HTMX for building interactive web applications with minimal JavaScript.

Setup

Install PyJinHx:

pip install pyjinhx

Include HTMX in your HTML:

<script src="https://unpkg.com/htmx.org@2.0.3"></script>

Project Structure

my_app/
├── components/
│   └── ui/
│       ├── button.py
│       ├── button.html
│       ├── counter.py
│       ├── counter.html
│       └── counter.js
└── index.html

Basic Example

Component Class

# components/ui/button.py
from pyjinhx import BaseComponent


class Button(BaseComponent):
    id: str
    text: str
    endpoint: str = "/clicked"

Component Template with HTMX

<!-- components/ui/button.html -->
<button
    id="{{ id }}"
    hx-post="{{ endpoint }}"
    hx-vals='{"button_id": "{{ id }}"}'
    hx-target="#result"
    hx-swap="innerHTML"
>
    {{ text }}
</button>

HTML Page

<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/htmx.org@2.0.3"></script>
</head>
<body>
    <Button id="click-me" text="Click Me" endpoint="/clicked"></Button>
    <div id="result"></div>
</body>
</html>

Use PyJinHx's Renderer to process the HTML:

from pyjinhx import Renderer

Renderer.set_default_environment("./components")

with open("index.html", "r") as f:
    source = f.read()

html = Renderer.get_default_renderer().render(source)

Counter Example

A complete example showing state management with HTMX.

Counter Component

# components/ui/counter.py
from pyjinhx import BaseComponent


class Counter(BaseComponent):
    id: str
    value: int = 0
<!-- components/ui/counter.html -->
<div id="{{ id }}" class="counter">
    <button
        hx-post="/counter/decrement"
        hx-vals='{"counter_id": "{{ id }}", "value": "{{ value }}"}'
        hx-target="#{{ id }}"
        hx-swap="outerHTML"
    >
        -
    </button>

    <span class="value">{{ value }}</span>

    <button
        hx-post="/counter/increment"
        hx-vals='{"counter_id": "{{ id }}", "value": "{{ value }}"}'
        hx-target="#{{ id }}"
        hx-swap="outerHTML"
    >
        +
    </button>
</div>

Component JavaScript

// components/ui/counter.js
document.body.addEventListener('htmx:afterSwap', (event) => {
    if (event.detail.target.classList.contains('counter')) {
        console.log('Counter was updated!');
        // Add any additional client-side logic here
    }
});

HTMX Patterns with PyJinHx

Target a component by id and swap outerHTML

When a route returns a full component, pass its id in the request (so the server can target the right element) and use hx-swap="outerHTML" to replace the whole element with the response:

<!-- components/ui/item.html -->
<div id="{{ id }}" class="item">
    <h3>{{ title }}</h3>
    <button
        hx-post="/items/{{ id }}/update"
        hx-vals='{"item_id": "{{ id }}"}'
        hx-target="#{{ id }}"
        hx-swap="outerHTML"
    >
        Update
    </button>
</div>

Conditional HTMX Attributes

Use Jinja conditionals to control HTMX behavior:

<!-- components/ui/button.html -->
<button
    id="{{ id }}"
    {% if endpoint %}
    hx-post="{{ endpoint }}"
    hx-target="#result"
    hx-swap="innerHTML"
    {% endif %}
>
    {{ text }}
</button>

PJXPanel outside the swap target

PJXPanel holds multiple slots (e.g. chat vs. other tools) while PJXPanelTrigger controls can live in a navbar or sidebar. To keep in-DOM state (messages, inputs) when other UI updates, mount the PJXPanel root outside the element you pass to hx-target for those swaps. Only swap inner fragments that should reload; avoid replacing the entire PJXPanel wrapper unless you intend to reset that state.

Bundled pjx-panel.js re-runs its init on htmx:afterSwap and htmx:afterSettle, so triggers and panels stay in sync after partial HTML updates.

Loading states

For ad-hoc cases you can use htmx's own hx-indicator with a .htmx-indicator element. For reactive components, PyJinHx ships built-in indicators instead: add data-pjx-loading="skeleton" or data-pjx-loading="spinner" to the element(s) that should show an in-flight effect — no extra markup or CSS needed. A trigger can also name extra regions to light with data-pjx-loading-extra="<css-selector>", and every effect is themable through --pjx-* CSS custom properties. See Loading indicators.

How PyJinHx talks to htmx

pjx.js (auto-included with reactive components) hooks a few htmx events. You don't call these yourself, but it helps to know what's on the wire:

  • On htmx:configRequest it stamps three request headers the server reads to decide what to re-render: X-PJX-Mounted (the manifest of mounted components), X-PJX-Assets (already-loaded script/stylesheet URLs, so they aren't re-sent), and X-PJX-Trigger (the component that triggered the request).
  • The loading-indicator lifecycle is driven from htmx events: it lights indicators on htmx:beforeRequest, re-applies them across swaps on htmx:afterSettle, and clears them on htmx:afterOnLoad (plus the error/abort events htmx:responseError, htmx:timeout, htmx:sendError, and htmx:abort).

Tips

Component JavaScript with HTMX Events

If your component has JavaScript that needs to run after HTMX swaps, use HTMX events:

// components/ui/widget.js
document.body.addEventListener('htmx:afterSwap', (event) => {
    if (event.detail.target.classList.contains('widget')) {
        // Initialize widget after swap
        initializeWidget(event.detail.target);
    }
});

Server-Sent Events and WebSockets

These aren't PyJinHx-specific. In htmx 2 the old hx-sse / hx-ws core attributes are gone — SSE and WebSockets are now opt-in extensions (hx-ext="sse" with sse-connect, hx-ext="ws" with ws-connect). See htmx's SSE and WebSocket extension docs, and render the streamed fragments with PyJinHx components as usual.

Dependency-aware updates (reactive OOB)

The patterns above use manual hx-target / hx-swap for each interaction. With PyJinHx reactivity, a mutation route simply returns Cls.render() and every dependent region rides along as an hx-swap-oob fragment — no per-swap wiring. This is the path to reach for when one mutation updates multiple regions (counter, list, totals):

  • Declare react={...} + load() on ReactiveComponent subclasses
  • Return Cls.render() from mutation routes — dependent regions ride along as hx-swap-oob fragments
  • Wire ClientBackend via setup() so routes call Cls.render() with no framework kwargs

See Reactivity, Usage tiers, and the reactive todo example.