diff --git a/src/htmx.js b/src/htmx.js index 024baa612..424dad0ea 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -685,12 +685,13 @@ var htmx = (() => { } if (ctx.hx.location) { // HX-Location let path = ctx.hx.location, opts = {}; - if (path[0] === '{' || /[\s,]/.test(path)) { - opts = HCON.parse(path); + let parsed = HCON.parse(path); + if (path[0] === '{' || parsed.path != null) { + opts = parsed; path = opts.path; delete opts.path; } - opts.push ??= 'true'; + if (opts.push == null && opts.replace == null) opts.push = 'true'; this.ajax('GET', path, opts); return true } diff --git a/test/tests/unit/__handleHxHeadersAndMaybeReturnEarly.js b/test/tests/unit/__handleHxHeadersAndMaybeReturnEarly.js index 2914ae1b1..89c1b2225 100644 --- a/test/tests/unit/__handleHxHeadersAndMaybeReturnEarly.js +++ b/test/tests/unit/__handleHxHeadersAndMaybeReturnEarly.js @@ -34,4 +34,73 @@ describe('__handleHxHeadersAndMaybeReturnEarly unit tests', function() { assert.isNotOk(result) }) + it('keeps commas in plain HX-Location paths', function() { + let originalAjax = htmx.ajax + let request + htmx.ajax = (...args) => request = args + + try { + let result = htmx.__handleHeadersAndMaybeReturnEarly({hx: {location: '/files/a,b'}}) + + assert.isTrue(result) + assert.deepEqual(request, ['GET', '/files/a,b', {push: 'true'}]) + } finally { + htmx.ajax = originalAjax + } + }) + + it('parses an HCON HX-Location value', function() { + let originalAjax = htmx.ajax + let request + htmx.ajax = (...args) => request = args + + try { + let result = htmx.__handleHeadersAndMaybeReturnEarly({hx: {location: 'path:/search'}}) + + assert.isTrue(result) + assert.deepEqual(request, ['GET', '/search', {push: 'true'}]) + } finally { + htmx.ajax = originalAjax + } + }) + + it('honors HX-Location replace without pushing', async function() { + mockResponse('GET', '/test', 'ignored', { + headers: { + 'HX-Location': '{"path":"/location-replaced","target":"#destination","replace":"/location-replaced"}' + } + }) + mockResponse('GET', '/location-replaced', 'Located') + let source = createProcessedHTML('
') + .querySelector('#source') + let requestFinished = new Promise(resolve => { + find('#destination').addEventListener('htmx:finally:request', resolve, {once: true}) + }) + let originalPushState = history.pushState + let originalReplaceState = history.replaceState + let pushes = 0 + let replaces = 0 + + history.pushState = function(...args) { + pushes++ + return originalPushState.apply(history, args) + } + history.replaceState = function(...args) { + replaces++ + return originalReplaceState.apply(history, args) + } + + try { + source.click() + await requestFinished + + assert.equal(find('#destination').textContent, 'Located') + assert.equal(pushes, 0) + assert.equal(replaces, 1) + } finally { + history.pushState = originalPushState + history.replaceState = originalReplaceState + } + }) + }); diff --git a/www/src/content/reference/02-headers/11-HX-Location.md b/www/src/content/reference/02-headers/11-HX-Location.md index 876926145..1272639a9 100644 --- a/www/src/content/reference/02-headers/11-HX-Location.md +++ b/www/src/content/reference/02-headers/11-HX-Location.md @@ -1,51 +1,34 @@ --- title: "HX-Location" -description: "Navigates with `htmx.ajax()`" +description: "Redirect without a full page load" --- -The `HX-Location` response header navigates to a new URL without a full page reload. +The `HX-Location` response header redirects without reloading the page. -Like clicking a boosted link—htmx fetches the content and updates the page via AJAX. +## Usage -## Simple Usage - -Redirect to a path: +Return a path: ```http HX-Location: /dashboard ``` -## Advanced Usage - -Specify target and other options: +`HX-Location` calls `htmx.ajax()`. The header above is equivalent to: -```http -HX-Location: {"path":"/search", "target":"#results", "push":"false"} +```js +htmx.ajax('GET', '/dashboard', { push: 'true' }) ``` -## Options +Use any serializable [`htmx.ajax()` option](/reference/methods/htmx-ajax#context). Include `path`: -The JSON value mirrors the htmx ajax API. All fields except `path` are optional. +```text +# HCON +HX-Location: path:/search target:#results select:#matches -- `path` - URL to load the response from (required) -- `target` - Element to swap the response into (defaults to `document.body`) -- `source` - The source element of the request -- `event` - Event that triggered the request -- `handler` - Callback to handle the response HTML -- `swap` - How to swap the response relative to the target -- `values` - Values to submit with the request -- `headers` - Headers to submit with the request -- `select` - Selects content from the response to swap -- `push` - Prevents or overrides the URL pushed to history (`'false'` or a path string) -- `replace` - Path to replace in browser history instead of pushing +# JSON +HX-Location: {"path":"/search","target":"#results","select":"#matches"} +``` ## Notes -Response headers are not processed on 3xx response codes. Return a 2xx status when using this header. - -## Example - -```python -headers = {'HX-Location': '/profile'} -return Response(content, headers=headers) -``` +`HX-Location` is not processed on 3xx responses. Return a 2xx response instead.