seaworn / symfony-htmx-bundle
Htmx integration for Symfony
Package info
github.com/seaworn/symfony-htmx
Type:symfony-bundle
pkg:composer/seaworn/symfony-htmx-bundle
dev-main
2026-03-26 05:28 UTC
Requires
- php: >=8.0
- ext-json: *
- symfony/framework-bundle: 6.*
- symfony/twig-bundle: 6.*
Requires (Dev)
This package is auto-updated.
Last update: 2026-03-26 05:28:42 UTC
README
Htmx integration for Symfony
- Symfony >=6.0
- PHP >=8.0
Install
$ composer require seaworn/symfony-htmx-bundle
Setup
Add the following snippet to the front controller. This will allow Symfony to use a custom Request class
/* public/index.php */ \Symfony\Component\HttpFoundation\Request::setFactory(function (...$args) { return new \Seaworn\HtmxBundle\Request\HtmxRequest(...$args); });
Usage
Extend Seaworn\HtmxBundle\Controller\AbstractController (extends \Symfony\Bundle\FrameworkBundle\Controller\AbstractController)
use Seaworn\HtmxBundle\Request\HtmxRequest; use Seaworn\HtmxBundle\Response\HtmxResponse; class HelloController extends \Seaworn\HtmxBundle\Controller\AbstractController{}
or use Seaworn\HtmxBundle\Controller\HtmxControllerTrait
use Seaworn\HtmxBundle\Request\HtmxRequest; use Seaworn\HtmxBundle\Response\HtmxResponse; class HelloController extends \Symfony\Bundle\FrameworkBundle\Controller\AbstractController { use \Seaworn\HtmxBundle\Controller\HtmxControllerTrait; }
Request headers
public function index(HtmxRequest $request): HtmxResponse { // whether the request is made via htmx $request->isHxRequest(); // whether the request is via an element using hx-boost attribute $request->isHxBoosted(); // the current browser url $request->getHxCurrentUrl(); // whether request is for history restoration after a miss in the local history cache $request->isHxHistoryRestoreRequest(); // the user response to an hx-prompt $request->getHxPrompt(); // the id of the target element if it exists $request->getHxTarget(); // the id of the triggered element if it exists $request->getHxTrigger(); // the name of the triggered element if it exists $request->getHxTriggerName(); // ... }
Response headers
public function index(HtmxRequest $request): HtmxResponse { // ... $response = $this->hxRender('index.html.twig'); // or new HtmxResponse() // Optional headers $response->setHxLocation( "/location", [ 'source' => '', 'event' => '', 'handler' => '', 'target' => '', 'swap' => '', 'select' => '', 'values' => [], 'headers' => [], ]) // set HX-Location header ->setHxPushUrl('/push') // set HX-Push-Url header ->setHxReplaceUrl('/replace') // set HX-Replace-Url header ->setHxReswap('outerHTML') // set HX-Reswap header ->setHxRetarget('#target') // set HX-Retarget header ->setHxReselect('#select') // set HX-Reselect header ->setHxTrigger('event') // set HX-Trigger header (simple) ->setHxTriggerAfterSwap('event1,event2') // set HX-Trigger-After-Swap header (multiple) ->setHxTriggerAfterSettle(['event' => ['key' => 'value']]); // set HX-Trigger-After-Settle header (with detail) return $response; }
Render template block
<!-- index.html.twig --> {% extends 'base.html.twig' %} {% block block1 %} <div id="block1"> Sample content... </div> {% endblock %}
public function index(HtmxRequest $request): HtmxResponse { return $this->hxRenderBlock('index.html.twig', 'block1'); }
Redirect
public function index(HtmxRequest $request): HtmxResponse { return $this->hxRedirect('https://htmx.org/'); }
Refresh
public function index(HtmxRequest $request): HtmxResponse { return $this->hxRefresh(); }
Stop polling
<!-- index.html.twig --> {% extends 'base.html.twig' %} {% block content %} <div hx-get="/poll" hx-vals="js:{pollingIndex}" hx-trigger="every 2s"></div> <script type="text/javascript"> var pollingIndex = 0; document.body.addEventListener("polling", function(e) { pollingIndex++; console.log("Polling Index:", pollingIndex); }); </script> {% endblock %}
public function poll(HtmxRequest $request): HtmxResponse { $index = $request->get('pollingIndex', 0); if ((int)$index >= 10) { return new HtmxStopPollingResponse(); } return (new HtmxResponse())->setHxTrigger('polling'); }