feat: boost pagination and sorting links with htmx
This commit is contained in:
1 parent
493a43d952
commit
13e5b6c3af
8 files changed
+37
-11
No files matched your search
@@ -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!
|
||||
|
||||
Generated
+4
-4
@@ -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": {
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<select name="limit"
|
||||
class="text-sm py-1 border-gray-300"
|
||||
hx-get="{{ path(route, query|merge({(pageParameterName): 1})) }}"
|
||||
hx-replace-url="{{ path(route, query|merge({(pageParameterName): 1})) }}"
|
||||
hx-replace-url="true"
|
||||
hx-target="body">
|
||||
<option value="10"{% if numItemsPerPage == 10 %} selected{% endif %}>10 pro Seite</option>
|
||||
<option value="20"{% if numItemsPerPage == 20 %} selected{% endif %}>20 pro Seite</option>
|
||||
@@ -16,7 +16,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="relative z-0 inline-flex">
|
||||
<nav class="relative z-0 inline-flex" hx-boost="true">
|
||||
{% if pageCount > 1 %}
|
||||
{% if first is defined and current != first %}
|
||||
<a href="{{ path(route, query|merge({(pageParameterName): first})) }}" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium hover:bg-secondary hover:text-white">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<p class="text-sm leading-5 text-gray-700">{{ 'paginator.info'|trans({ '{from}': firstItemNumber, '{to}': lastItemNumber, '{total}': totalCount })|raw }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="relative z-0 inline-flex">
|
||||
<nav class="relative z-0 inline-flex" hx-boost="true">
|
||||
{% if pageCount > 1 %}
|
||||
{% if previous is defined %}
|
||||
<a href="{{ path(route, query|merge({(pageParameterName): previous})) }}" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium hover:bg-secondary hover:text-white" aria-label="Previous">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<a href="{{ options.href }}" class="flex items-center space-x-2">
|
||||
<a href="{{ options.href }}" class="flex items-center space-x-2" hx-boost="true">
|
||||
<span>{{ title|raw }}</span>
|
||||
{%- if sorted -%}
|
||||
{%- if direction == 'desc' -%}
|
||||
|
||||
@@ -169,6 +169,19 @@ class BpnAuthenticatorTest extends TestCase
|
||||
$this->assertFalse($request->getSession()->has('_security.main.target_path'));
|
||||
}
|
||||
|
||||
public function testABoostedHtmxRequestIsKeptAsTheDeeplink(): void
|
||||
{
|
||||
// a paginator or sort link fetches a whole page, so it is worth landing on after the login
|
||||
$request = $this->createRequest();
|
||||
$request->headers->set('HX-Request', 'true');
|
||||
$request->headers->set('HX-Boosted', 'true');
|
||||
$request->getSession()->set('_security.main.target_path', 'https://team.example.com/administrative/assignment?page=3');
|
||||
|
||||
$this->createAuthenticator()->start($request);
|
||||
|
||||
$this->assertSame('https://team.example.com/administrative/assignment?page=3', $request->getSession()->get('_security.main.target_path'));
|
||||
}
|
||||
|
||||
public function testAPageRequestIsKeptAsTheDeeplink(): void
|
||||
{
|
||||
$request = $this->createRequest();
|
||||
|
||||
Reference in new issue
Block a user