diff --git a/src/ext/hx-preload.js b/src/ext/hx-preload.js index 0bd8a3449..38c4f700e 100644 --- a/src/ext/hx-preload.js +++ b/src/ext/hx-preload.js @@ -3,7 +3,7 @@ function initializePreload(elt) { let preloadSpec = api.attributeValue(elt, "hx-preload"); - if (!preloadSpec && !elt._htmx?.boosted) return; + if (preloadSpec == undefined && !elt._htmx?.boosted) return; let preloadEvents = [] let timeout = 5000; @@ -17,14 +17,15 @@ } } } else { - //only boosted links are supported - if (elt.tagName === "A") { - if(htmx.config?.preload?.boostTimeout) { - timeout = htmx.parseInterval(htmx.config.preload.boostTimeout) - } - preloadEvents.push(htmx.config?.preload?.boostEvent || "mousedown"); - preloadEvents.push("touchstart"); + let isBoostedAnchor = elt._htmx?.boosted && elt.tagName === "A"; + let isHxGet = api.attributeValue(elt, "hx-get") != null; + if (!isBoostedAnchor && !isHxGet) return; + if (isBoostedAnchor && htmx.config?.preload?.autoBoost === false) return; + if (htmx.config?.preload?.boostTimeout) { + timeout = htmx.parseInterval(htmx.config.preload.boostTimeout) } + preloadEvents.push(htmx.config?.preload?.boostEvent || "mousedown"); + preloadEvents.push("touchstart"); } let preloadListener = async (evt) => { @@ -43,7 +44,6 @@ let action = ctx.request.action.replace?.(/#.*$/, ''); - let params = new URLSearchParams(body); if (params.size) action += (/\?/.test(action) ? "&" : "?") + params; @@ -96,4 +96,4 @@ } } }); -})() \ No newline at end of file +})() diff --git a/test/tests/ext/hx-preload.js b/test/tests/ext/hx-preload.js index e516852d5..8d4e34056 100644 --- a/test/tests/ext/hx-preload.js +++ b/test/tests/ext/hx-preload.js @@ -25,6 +25,31 @@ describe('hx-preload attribute', function() { cleanupTest(this.currentTest) }) + it('preloads on mousedown with bare attribute on hx-get element', async function () { + mockResponse('GET', '/test', 'Preloaded') + let btn = createProcessedHTML(''); + btn.dispatchEvent(new Event('mousedown')) + await htmx.timeout(20) + assert.isDefined(btn._htmx.preload) + }) + + it('preloads on mousedown with bare attribute on boosted anchor', async function () { + mockResponse('GET', '/test', 'Preloaded') + let div = createProcessedHTML('
Link
'); + let a = div.querySelector('#a1') + a.dispatchEvent(new Event('mousedown')) + await htmx.timeout(20) + assert.isDefined(a._htmx.preload) + }) + + it('does not preload with bare attribute on non-hx-get element', async function () { + mockResponse('POST', '/test', 'Posted') + let btn = createProcessedHTML(''); + btn.dispatchEvent(new Event('mousedown')) + await htmx.timeout(20) + assert.isUndefined(btn._htmx?.preload) + }) + it('preloads on specified event', async function () { mockResponse('GET', '/test', 'Preloaded') let btn = createProcessedHTML(''); @@ -83,6 +108,29 @@ describe('hx-preload attribute', function() { assert.isDefined(btn._htmx.preload) }) + it('auto-preloads boosted anchors by default', async function () { + mockResponse('GET', '/test', 'Preloaded') + let div = createProcessedHTML('
Link
'); + let a = div.querySelector('#a1') + a.dispatchEvent(new Event('mousedown')) + await htmx.timeout(20) + assert.isDefined(a._htmx.preload) + }) + + it('does not auto-preload boosted anchors when autoBoost is false', async function () { + htmx.config.preload = { autoBoost: false } + try { + mockResponse('GET', '/test', 'Preloaded') + let div = createProcessedHTML('
Link
'); + let a = div.querySelector('#a1') + a.dispatchEvent(new Event('mousedown')) + await htmx.timeout(20) + assert.isUndefined(a._htmx?.preload) + } finally { + delete htmx.config.preload + } + }) + it('builds URL with form params', async function () { mockResponse('GET', '/test?name=test', 'Response') let form = createProcessedHTML('
'); diff --git a/www/src/content/docs/06-extensions/06-preload.md b/www/src/content/docs/06-extensions/06-preload.md index 603057ed2..f6957ed7d 100644 --- a/www/src/content/docs/06-extensions/06-preload.md +++ b/www/src/content/docs/06-extensions/06-preload.md @@ -17,50 +17,82 @@ The `preload` extension allows you to load HTML fragments into your browser's ca ## Usage -Add an `hx-preload` attribute to any hyperlinks and [`hx-get`](/reference/attributes/hx-get) elements you want to preload. By default, resources will be loaded as soon as the `mousedown` event begins, giving your application a roughly 100-200ms head start on serving responses. +Add an `hx-preload` attribute to any boosted hyperlinks and [`hx-get`](/reference/attributes/hx-get) elements you want to preload. By default, resources will be loaded as soon as the `mousedown` event begins, giving your application a roughly 100-200ms head start on serving responses. ```html -Preloaded on mousedown - + + ``` -All preload requests include an additional `"HX-Preloaded": "true"` header. +All preload requests include an additional `HX-Preloaded: true` header. -### Inheriting Preload Settings +## hx-boost Integration -You can add the `hx-preload` attribute to a parent element, and all links within it will be preloaded: +When the preload extension is loaded, all [`hx-boost`](/reference/attributes/hx-boost) anchor tags are automatically preloaded on `mousedown` without needing an explicit `hx-preload` attribute. To opt out of this behaviour, set `htmx.config.preload.autoBoost = false`. ```html - + + ``` -## Configuration +To disable auto-preloading of boosted links: + +```html + +``` + +Plain `` links without `hx-boost` cannot be preloaded, as preloading works by warming an htmx request — it does not intercept browser navigation. -### `hx-preload="mousedown"` (default) +## Trigger Events + +### `hx-preload` / `hx-preload="mousedown"` (default) Begins loading when the user presses the mouse down. Conservative — guarantees the user intends to click. Gives your server a 100-200ms head start. ### `hx-preload="mouseover"` -Preloads when the user's mouse hovers over the link. A 100ms delay prevents loading when the user scrolls past. More aggressive — gives your server several hundred milliseconds of head start. +Preloads when the user's mouse hovers over the element. More aggressive — gives your server several hundred milliseconds of head start. ### `hx-preload="custom-event-name"` -Preload can listen to any custom event. The extension generates a `preload:init` event that can trigger preloads as soon as an element is processed by htmx. +Preload can listen to any event name supported by htmx trigger specs. -### `hx-preload="always"` +```html + +``` + +## Configuration -By default, the extension preloads each element once. Use `hx-preload="always"` to preload on every trigger. Can be combined with other options: `hx-preload="always mouseover"`. +All options are set via `htmx.config.preload`: + +| Option | Default | Description | +|--------|---------|-------------| +| `autoBoost` | `true` | Automatically preload all `hx-boost` anchor tags on `mousedown` | +| `boostEvent` | `"mousedown"` | The event used to trigger preloading for auto-boosted links | +| `boostTimeout` | `5000` | Milliseconds before a preloaded response for a boosted link expires | + +```html + +``` + +## Timeout + +Preloaded responses expire after 5 seconds by default. Use the `timeout` modifier to override per element: + +```html + +``` ## Limitations -- Only `GET` requests can be preloaded (including `` and `hx-get=""`). POST, PUT, and DELETE will not be preloaded. -- When listening to `mouseover` events, preload waits 100ms before downloading. If the mouse leaves before the timeout, the resource is not preloaded. -- Preloaded responses will only be cached if the response headers allow it (e.g., `Cache-Control: private, max-age=60`). +- Only `GET` requests can be preloaded. `POST`, `PUT`, `PATCH` and `DELETE` will not be preloaded. +- Plain `` links without `hx-boost` or `hx-get` cannot be preloaded. +- Preloaded responses will only be reused if the actual request is made before the timeout expires. ## Upgrading from htmx 2.x - The `preload` attribute is now named `hx-preload`. +- Inherited preload via a parent element now requires the `:inherited` modifier: `hx-preload:inherited`.