feat: reload the page with a flash when an htmx request fails
This commit is contained in:
1 parent
0be3c2ccde
commit
a2e4035889
14 files changed
+205
-210
No files matched your search
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
/**
|
||||
* Leaves a flash behind when an htmx request fails, for the page reload that follows.
|
||||
*
|
||||
* htmx is only used for modals and out-of-band refreshes, so there is always a whole page under a
|
||||
* failed request -- and every page is a GET. assets/app.js therefore answers a failed htmx request
|
||||
* by reloading that page, and this flash is what tells the user why it just reloaded.
|
||||
*
|
||||
* It only adds the flash. The response is left alone, so it keeps its real status and Symfony
|
||||
* logs the exception as usual. A listener that already produced a response stops propagation
|
||||
* before this runs -- notably the firewall's entry point, which sends an expired session to the
|
||||
* login page with HX-Redirect instead.
|
||||
*/
|
||||
#[AsEventListener(event: KernelEvents::EXCEPTION, priority: -10)]
|
||||
final class HtmxErrorFlashListener
|
||||
{
|
||||
public function __invoke(ExceptionEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
if (!$request->headers->has('HX-Request') || !$request->hasSession()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$session = $request->getSession();
|
||||
|
||||
if ($session instanceof FlashBagAwareSessionInterface) {
|
||||
$session->getFlashBag()->add('error', 'Da ist etwas schiefgelaufen. Bitte versuche es erneut.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* Renders an error modal when an htmx request blows up.
|
||||
*
|
||||
* htmx does not swap a 5xx by default, so an uncaught exception on a fragment request produces
|
||||
* no visible effect whatsoever -- the user clicks and nothing happens. This turns it into
|
||||
* something they can see: a 200 carrying the error modal, retargeted at <body> so it appends
|
||||
* regardless of what the original hx-target was.
|
||||
*
|
||||
* allowCustomResponseCode() is what keeps the 200; without it the kernel rewrites it to 500 and
|
||||
* htmx discards the body again.
|
||||
*/
|
||||
#[AsEventListener(event: KernelEvents::EXCEPTION, priority: -10)]
|
||||
final readonly class HtmxExceptionListener
|
||||
{
|
||||
public function __construct(private Environment $twig)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(ExceptionEvent $event): void
|
||||
{
|
||||
if (!$event->getRequest()->headers->has('HX-Request')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event->hasResponse()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = new Response($this->twig->render('htmx_error_modal.html.twig'), Response::HTTP_OK);
|
||||
$response->headers->set('HX-Retarget', 'body');
|
||||
$response->headers->set('HX-Reswap', 'beforeend');
|
||||
|
||||
$event->setResponse($response);
|
||||
$event->allowCustomResponseCode();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -16,6 +17,10 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface
|
||||
*
|
||||
* Deliberately the login *page* rather than straight into /auth/init: the page is where a failed
|
||||
* login's flash message is shown, and bouncing there instead would swallow it.
|
||||
*
|
||||
* An htmx request -- typically a click after the session expired -- gets HX-Redirect instead:
|
||||
* a plain 302 is followed invisibly by the browser, and htmx would swap the whole login page into
|
||||
* whatever the click targeted, a modal included.
|
||||
*/
|
||||
final readonly class LoginEntryPoint implements AuthenticationEntryPointInterface
|
||||
{
|
||||
@@ -25,6 +30,12 @@ final readonly class LoginEntryPoint implements AuthenticationEntryPointInterfac
|
||||
|
||||
public function start(Request $request, ?AuthenticationException $authException = null): Response
|
||||
{
|
||||
return new RedirectResponse($this->urlGenerator->generate('app_login'));
|
||||
$url = $this->urlGenerator->generate('app_login');
|
||||
|
||||
if ($request->headers->has('HX-Request')) {
|
||||
return new HxRedirectResponse($url);
|
||||
}
|
||||
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user