feat: add htmx server-side plumbing

This commit is contained in:
fromme committed 2026-09-13 14:47:21 +02:00
1 parent 3bfb5afef6
commit ef8a88ed2a
9 files changed
+450

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Htmx;
use Symfony\Component\HttpFoundation\Response;
/**
* Response that makes htmx perform a full page navigation.
*
* htmx never follows a normal 3xx itself -- the browser does, transparently, and htmx then swaps
* whatever the final destination returned, which is a whole page in the wrong place. So a redirect
* has to be handed to htmx as an instruction instead: a 200 carrying HX-Redirect.
*
* Anything returning this from an event listener must also call
* ExceptionEvent::allowCustomResponseCode(), or the kernel overwrites the 200.
*/
class HxRedirectResponse extends Response
{
public function __construct(string $url)
{
parent::__construct('', Response::HTTP_OK, ['HX-Redirect' => $url]);
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Htmx;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* htmx helpers for controllers. Expects to be used inside an AbstractController.
*
* Detecting an htmx request stays a plain `$request->headers->has('HX-Request')` -- no bundle,
* no argument resolver, no helper service.
*/
trait HxTrait
{
/**
* Renders several Twig blocks into one response, for an htmx out-of-band swap.
*
* This is how one interaction updates more than one region: the response body carries the
* primary fragment plus any number of others marked `hx-swap-oob`, and htmx dispatches each
* to its own target by id. `htmx_oob_swap` is injected into the context so a block can emit
* that attribute only when rendered through this path, and stay a plain fragment otherwise.
*
* @param string[] $blockNames block names to render, in order
* @param array<string, mixed> $context
* @param string|null $pushUrl pushed onto browser history via HX-Push-Url
*/
protected function htmxOobResponse(string $templateName, array $blockNames, array $context = [], ?string $pushUrl = null): Response
{
$html = '';
$oobContext = $context + ['htmx_oob_swap' => true];
foreach ($blockNames as $blockName) {
$html .= $this->renderBlockView($templateName, $blockName, $oobContext);
}
$response = new Response($html);
if (null !== $pushUrl) {
$response->headers->set('HX-Push-Url', $pushUrl);
}
return $response;
}
/**
* Redirects in a way htmx understands, falling back to a normal redirect for ordinary requests.
*
* Note this is a genuine full page navigation. Prefer swapping the affected fragment (plus an
* out-of-band toast) and keep this for real destination changes.
*/
protected function htmxRedirect(Request $request, string $url): Response
{
if ($request->headers->has('HX-Request')) {
return new HxRedirectResponse($url);
}
return $this->redirect($url);
}
}