Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
69 changes: 69 additions & 0 deletions test/tests/unit/__handleHxHeadersAndMaybeReturnEarly.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div><div id="destination"></div><button id="source" hx-get="/test">Go</button></div>')
.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
}
})

});
47 changes: 15 additions & 32 deletions www/src/content/reference/02-headers/11-HX-Location.md
Original file line number Diff line number Diff line change
@@ -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.
Loading