HTMX¶
PyJinHx components work seamlessly with HTMX for building interactive web applications with minimal JavaScript.
Setup¶
Install PyJinHx:
Include HTMX in your HTML:
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:configRequestit 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), andX-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 onhtmx:afterSettle, and clears them onhtmx:afterOnLoad(plus the error/abort eventshtmx:responseError,htmx:timeout,htmx:sendError, andhtmx: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()onReactiveComponentsubclasses - Return
Cls.render()from mutation routes — dependent regions ride along ashx-swap-oobfragments - Wire ClientBackend via
setup()so routes callCls.render()with no framework kwargs
See Reactivity, Usage tiers, and the reactive todo example.