A small tool your app uses to bring htmx into its server-rendered pages — load the client, keep CSRF working, and hand your controllers helpers for responding with HTML fragments.
htmx is an HTML-attribute approach to AJAX: you
decorate your markup with attributes like
hx-get and hx-target,
and the htmx client turns those into requests that
swap fragments of the page — no JavaScript
framework required. ForgeHtmx is the piece that
makes that fit naturally into your app.
It lives as a capability in the
capabilities/ directory (the reference
layout for reusable building blocks), and is
scoped strictly to wiring: inject the client, keep
CSRF headers flowing, and give your controllers
concise helpers for returning partial HTML and the
htmx response headers your interactions need.
Prerequisites: ForgeHtmx
builds on the routing and view layers of your
app, so both need to be present. The page
source is in
capabilities/ForgeHtmx.
ForgeHtmx doesn't reimplement htmx. The attribute
language itself — every hx-* in your
markup and the DOM swaps on the page — is htmx's
own client, loaded verbatim. ForgeHtmx only handles
the server-side fit:
htmx.min.js into your
pages so the attributes work out of the box.
HX-Request header), so
your app can respond with just the fragment.
You write your markup the htmx way, and your controllers the way your app already works — the capability is a thin, predictable seam between the two.
php forge.php package:install-module --module=ForgeHtmx
Installing links the client asset so
htmx.min.js is served to your pages,
and registers the request middleware. From there
the client is injected and the helpers are
available in your controllers.
Once installed, decorate any element with htmx attributes and the client does the rest — a button or link that loads a fragment, a form posted in the background, or a region that refreshes on an interval:
<!-- click the button, swap a comment list into the target, off the request path -->
<button hx-get="/comments" hx-target="#comments" hx-swap="innerHTML">
Load comments
</button>
<div id="comments"></div>
<!-- submit a form over AJAX and refresh the page if it succeeds -->
<form hx-post="/newsletter" hx-swap="none" hx-on::after-request="location.reload()">
<input name="email" type="email" />
<button type="submit">Subscribe</button>
</form>
The attribute set is the standard htmx one —
hx-get, hx-post,
hx-target, hx-swap,
hx-trigger, hx-push-url,
and friends. ForgeHtmx is what makes those requests
reach your controllers and return clean fragments.
In a controller, compose the
HtmxResponseHelper trait to return the
response headers htmx understands. Instead of
hand-writing headers, use a method that says what
you mean:
use Capability\ForgeHtmx\Traits\HtmxResponseHelper;
final class CommentController
{
use HtmxResponseHelper;
public function index(): mixed
{
// a plain HTML fragment to swap into the page
return $this->htmxFragment($this->view('partials/comments', $data));
}
public function store(): mixed
{
// after a form post, send the browser to a new URL
return $this->htmxRedirect('/comments');
}
public function refresh(): mixed
{
// have htmx reload the current page
return $this->htmxRefresh();
}
}
The fuller set covers everything htmx can act on from response headers:
htmxTrigger(),
htmxTriggerAfterSwap(),
htmxTriggerAfterSettle() — emit
client-side events (with optional detail).
htmxLocation() — issue a client-side
navigation with context.
htmxPushUrl() / htmxReplaceUrl()
— update the browser history.
htmxRetarget() / htmxReswap()
— override where and how the swap happens.
htmxStopPolling() — tell a polling
region to stop.
A common pattern is one endpoint that serves both a
full page and the htmx fragment for it. The
HtmxViewHelper handles that in one
call — full view with layout for normal requests, a
focused partial without the layout when htmx asks:
use Capability\ForgeHtmx\Traits\HtmxViewHelper;
final class DashboardController
{
use HtmxViewHelper;
public function show()
{
$data = ['metrics' => $this->metrics()];
// full page normally; just the 'partials/dashboard' view on htmx
return $this->htmxView('pages/dashboard', $data, 'partials/dashboard');
}
}
When the request carries HX-Request,
the layout is suppressed and only the partial view
is returned; otherwise your full page renders as
usual.
htmx-driven requests would otherwise omit the CSRF
protection your app expects. ForgeHtmx handles this
automatically: it injects a small script that reads
your CSRF token from the page and attaches it as an
X-CSRF-TOKEN header on every htmx
request, so your existing verification keeps
working with no extra ceremony.
Both the client script and this CSRF wiring are injected only into real HTML pages — htmx partial responses and JSON responses pass through clean, without the surrounding page chrome.