From 13e5b6c3af281875d5c022ff1032cbd4631209c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Fri, 16 Oct 2026 10:39:12 +0200 Subject: [PATCH] feat: boost pagination and sorting links with htmx --- assets/app.js | 9 +++++++++ package-lock.json | 8 ++++---- package.json | 2 +- src/Security/BpnAuthenticator.php | 8 ++++++-- templates/paginator/sliding.html.twig | 4 ++-- templates/paginator/sliding_boxes.html.twig | 2 +- templates/paginator/sortable_link.html.twig | 2 +- tests/Security/BpnAuthenticatorTest.php | 13 +++++++++++++ 8 files changed, 37 insertions(+), 11 deletions(-) diff --git a/assets/app.js b/assets/app.js index 114ed3f..d4bf91c 100644 --- a/assets/app.js +++ b/assets/app.js @@ -3,6 +3,15 @@ htmx.config.includeIndicatorStyles = false htmx.config.historyCacheSize = 0 htmx.config.allowScriptTags = false +// A boosted link is a page navigation: show the error page like the browser would, instead of +// swallowing the 4xx/5xx and leaving the click without effect. +document.addEventListener('htmx:beforeSwap', (event) => { + if (true === event.detail.boosted && event.detail.xhr.status >= 400) { + event.detail.shouldSwap = true + event.detail.isError = false + } +}) + import './bootstrap.js'; /* * Welcome to your app's main JavaScript file! diff --git a/package-lock.json b/package-lock.json index 0ed32c6..f8e5675 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "dropzone": "^6.0.0-beta.2", "file-loader": "^6.2.0", "flatpickr": "^4.6.13", - "htmx.org": "^1.9.10", + "htmx.org": "^2.0.11", "mjml": "^4.15.3", "postcss": "^8.4.29", "postcss-import": "^15.1.0", @@ -5139,9 +5139,9 @@ } }, "node_modules/htmx.org": { - "version": "1.9.12", - "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.12.tgz", - "integrity": "sha512-VZAohXyF7xPGS52IM8d1T1283y+X4D+Owf3qY1NZ9RuBypyu9l8cGsxUMAG5fEAb/DhT7rDoJ9Hpu5/HxFD3cw==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.11.tgz", + "integrity": "sha512-Thx/WtpeOQqSrqBCw/A1cwGJGg4UrVa3+sW0GmrM3p4gJgO89ecH4qtbnyzDDWFvBTqjnIMCgELTNt636dtamA==", "license": "0BSD" }, "node_modules/http-deceiver": { diff --git a/package.json b/package.json index 844d260..4eadd2c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "dropzone": "^6.0.0-beta.2", "file-loader": "^6.2.0", "flatpickr": "^4.6.13", - "htmx.org": "^1.9.10", + "htmx.org": "^2.0.11", "mjml": "^4.15.3", "postcss": "^8.4.29", "postcss-import": "^15.1.0", diff --git a/src/Security/BpnAuthenticator.php b/src/Security/BpnAuthenticator.php index cea7a67..61e1f21 100644 --- a/src/Security/BpnAuthenticator.php +++ b/src/Security/BpnAuthenticator.php @@ -51,11 +51,15 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent * The firewall saves the requested url as the target path right before it calls this. An * htmx request is a fragment - a modal, a table body - that cannot stand as a page, so * after the login it must not be where the user lands. htmx does not send the - * X-Requested-With header that would make Symfony skip it on its own. + * X-Requested-With header that would make Symfony skip it on its own. A boosted request + * (a paginator or sort link) is the exception: it fetches a whole page and stays the deeplink. */ public function start(Request $request, ?AuthenticationException $authException = null): Response { - if (true === $request->headers->has('HX-Request') && true === $request->hasSession()) { + if (true === $request->headers->has('HX-Request') + && false === $request->headers->has('HX-Boosted') + && true === $request->hasSession() + ) { $this->removeTargetPath($request->getSession(), self::FIREWALL_NAME); } diff --git a/templates/paginator/sliding.html.twig b/templates/paginator/sliding.html.twig index 008383f..5650282 100644 --- a/templates/paginator/sliding.html.twig +++ b/templates/paginator/sliding.html.twig @@ -8,7 +8,7 @@
-
-