Skip to content
109 changes: 35 additions & 74 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -4053,7 +4053,9 @@ var htmx = (function() {
targetOverride: resolvedTarget,
swapOverride: context.swap,
select: context.select,
returnPromise: true
returnPromise: true,
push: context.push,
replace: context.replace
})
}
} else {
Expand Down Expand Up @@ -4632,82 +4634,39 @@ var htmx = (function() {
* @return {HtmxHistoryUpdate}
*/
function determineHistoryUpdates(elt, responseInfo) {
const xhr = responseInfo.xhr
const { xhr, pathInfo, etc } = responseInfo

//= ==========================================
// First consult response headers
//= ==========================================
let pathFromHeaders = null
let typeFromHeaders = null
if (hasHeader(xhr, /HX-Push:/i)) {
pathFromHeaders = xhr.getResponseHeader('HX-Push')
typeFromHeaders = 'push'
} else if (hasHeader(xhr, /HX-Push-Url:/i)) {
pathFromHeaders = xhr.getResponseHeader('HX-Push-Url')
typeFromHeaders = 'push'
} else if (hasHeader(xhr, /HX-Replace-Url:/i)) {
pathFromHeaders = xhr.getResponseHeader('HX-Replace-Url')
typeFromHeaders = 'replace'
}
let push = xhr.getResponseHeader('HX-Push') || xhr.getResponseHeader('HX-Push-Url')
let replace = xhr.getResponseHeader('HX-Replace-Url')

// if there was a response header, that has priority
if (pathFromHeaders) {
if (pathFromHeaders === 'false') {
return {}
} else {
return {
type: typeFromHeaders,
path: pathFromHeaders
}
if (!push && !replace) {
// Next resolve via DOM values
push = getClosestAttributeValue(elt, 'hx-push-url') || etc.push
replace = getClosestAttributeValue(elt, 'hx-replace-url') || etc.replace
if (!push && !replace && getInternalData(elt).boosted) {
push = 'true'
}
}

//= ==========================================
// Next resolve via DOM values
//= ==========================================
const requestPath = responseInfo.pathInfo.finalRequestPath
const responsePath = responseInfo.pathInfo.responsePath

const pushUrl = getClosestAttributeValue(elt, 'hx-push-url')
const replaceUrl = getClosestAttributeValue(elt, 'hx-replace-url')
const elementIsBoosted = getInternalData(elt).boosted

let saveType = null
let path = null

if (pushUrl) {
saveType = 'push'
path = pushUrl
} else if (replaceUrl) {
saveType = 'replace'
path = replaceUrl
} else if (elementIsBoosted) {
saveType = 'push'
path = responsePath || requestPath // if there is no response path, go with the original request path
let path = push || replace
// unset or false indicates no push, return empty object
if (!path || path === 'false') {
return {}
}

if (path) {
// false indicates no push, return empty object
if (path === 'false') {
return {}
}

// true indicates we want to follow wherever the server ended up sending us
if (path === 'true') {
path = responsePath || requestPath // if there is no response path, go with the original request path
}

// restore any anchor associated with the request
if (responseInfo.pathInfo.anchor && path.indexOf('#') === -1) {
path = path + '#' + responseInfo.pathInfo.anchor
}
// true indicates we want to follow wherever the server ended up sending us
if (path === 'true') {
path = pathInfo.responsePath || pathInfo.finalRequestPath // if there is no response path, go with the original request path
}
// restore any anchor associated with the request
if (pathInfo.anchor && path.indexOf('#') === -1) {
path = path + '#' + pathInfo.anchor
}

return {
type: saveType,
path
}
} else {
return {}
return {
type: push ? 'push' : 'replace',
path
}
}

Expand Down Expand Up @@ -4788,19 +4747,17 @@ var htmx = (function() {
}

if (hasHeader(xhr, /HX-Location:/i)) {
saveCurrentPageToHistory()
let redirectPath = xhr.getResponseHeader('HX-Location')
/** @type {HtmxAjaxHelperContext&{path:string}} */
var redirectSwapSpec
/** @type {HtmxAjaxHelperContext&{path?:string}} */
var redirectSwapSpec = {}
if (redirectPath.indexOf('{') === 0) {
redirectSwapSpec = parseJSON(redirectPath)
// what's the best way to throw an error if the user didn't include this
redirectPath = redirectSwapSpec.path
delete redirectSwapSpec.path
}
ajaxHelper('get', redirectPath, redirectSwapSpec).then(function() {
pushUrlIntoHistory(redirectPath)
})
redirectSwapSpec.push = redirectSwapSpec.push || 'true'
ajaxHelper('get', redirectPath, redirectSwapSpec)
return
}

Expand Down Expand Up @@ -5216,6 +5173,8 @@ var htmx = (function() {
* @property {Object|FormData} [values]
* @property {Record<string,string>} [headers]
* @property {string} [select]
* @property {string} [push]
* @property {string} [replace]
*/

/**
Expand Down Expand Up @@ -5262,6 +5221,8 @@ var htmx = (function() {
* @property {Object|FormData} [values]
* @property {boolean} [credentials]
* @property {number} [timeout]
* @property {string} [push]
* @property {string} [replace]
*/

/**
Expand Down
60 changes: 60 additions & 0 deletions test/core/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -656,4 +656,64 @@ describe('Core htmx API test', function() {
var div = make('<div>textNode</div>')
htmx.process(div.firstChild)
})

it('ajax api push Url should push an element into the cache when true', function() {
this.server.respondWith('POST', '/test123', 'Clicked!')

var div = make("<div id='d1'></div>")
htmx.ajax('POST', '/test123', {
target: '#d1',
swap: 'innerHTML',
push: 'true'
})
this.server.respond()
div.innerHTML.should.equal('Clicked!')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/test123')
})

it('ajax api push Url should push an element into the cache when string', function() {
this.server.respondWith('POST', '/test', 'Clicked!')

var div = make("<div id='d1'></div>")
htmx.ajax('POST', '/test', {
target: '#d1',
swap: 'innerHTML',
push: '/abc123'
})
this.server.respond()
div.innerHTML.should.equal('Clicked!')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/abc123')
})

it('ajax api replace Url should replace an element into the cache when true', function() {
this.server.respondWith('POST', '/test123', 'Clicked!')

var div = make("<div id='d1'></div>")
htmx.ajax('POST', '/test123', {
target: '#d1',
swap: 'innerHTML',
replace: 'true'
})
this.server.respond()
div.innerHTML.should.equal('Clicked!')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/test123')
})

it('ajax api replace Url should replace an element into the cache when string', function() {
this.server.respondWith('POST', '/test', 'Clicked!')

var div = make("<div id='d1'></div>")
htmx.ajax('POST', '/test', {
target: '#d1',
swap: 'innerHTML',
replace: '/abc123'
})
this.server.respond()
div.innerHTML.should.equal('Clicked!')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/abc123')
})
})
50 changes: 50 additions & 0 deletions test/core/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,56 @@ describe('Core htmx AJAX headers', function() {
}, 30)
})

it('should push new Url on HX-Location', function(done) {
sessionStorage.removeItem('htmx-current-path-for-history')
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"path":"/test2", "target":"#work-area"}' }, ''])
this.server.respondWith('GET', '/test2', [200, {}, '<div>Yay! Welcome</div>'])
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
div.click()
this.server.respond()
this.server.respond()
setTimeout(function() {
getWorkArea().innerHTML.should.equal('<div>Yay! Welcome</div>')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/test2')
done()
}, 30)
})

it('should not push new Url on HX-Location if push Url false', function(done) {
sessionStorage.setItem('htmx-current-path-for-history', '/old')
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"push":"false", "path":"/test2", "target":"#work-area"}' }, ''])
this.server.respondWith('GET', '/test2', [200, {}, '<div>Yay! Welcome</div>'])
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
div.click()
this.server.respond()
this.server.respond()
setTimeout(function() {
getWorkArea().innerHTML.should.equal('<div>Yay! Welcome</div>')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/old')
done()
}, 30)
})

it('should push different Url on HX-Location if push Url is string', function(done) {
sessionStorage.removeItem('htmx-current-path-for-history')
var HTMX_HISTORY_CACHE_NAME = 'htmx-history-cache'
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"push":"/abc123", "path":"/test2", "target":"#work-area"}' }, ''])
this.server.respondWith('GET', '/test2', [200, {}, '<div>Yay! Welcome</div>'])
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
div.click()
this.server.respond()
this.server.respond()
setTimeout(function() {
getWorkArea().innerHTML.should.equal('<div>Yay! Welcome</div>')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/abc123')
done()
}, 30)
})

it('should refresh page on HX-Refresh', function() {
var refresh = false
htmx.location = { reload: function() { refresh = true } }
Expand Down
2 changes: 2 additions & 0 deletions www/content/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ or
* `values` - values to submit with the request
* `headers` - headers to submit with the request
* `select` - allows you to select the content you want swapped from a response
* `push` - can be `'true'` or a path to push a URL into browser location history
* `replace` - can be `'true'` or a path to replace the URL in the browser location history

##### Example

Expand Down
2 changes: 2 additions & 0 deletions www/content/headers/hx-location.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Path is required and is url to load the response from. The rest of the data mirr
* `values` - values to submit with the request
* `headers` - headers to submit with the request
* `select` - allows you to select the content you want swapped from a response
* `push` - set to `'false'` or a path string to prevent or override the URL pushed to browser location history
* `replace` - a path string to replace the URL in the browser location history

## Notes

Expand Down
5 changes: 3 additions & 2 deletions www/content/headers/hx-push-url.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ description = """\

The `HX-Push-Url` header allows you to push a URL into the browser [location history](https://developer.mozilla.org/en-US/docs/Web/API/History_API).
This creates a new history entry, allowing navigation with the browser’s back and forward buttons.
This is similar to the [`hx-push-url` attribute](@/attributes/hx-push-url.md).
It works the same as [`hx-push-url` attribute](@/attributes/hx-push-url.md).

If present, this header overrides any behavior defined with attributes.

The possible values for this header are:

1. A URL to be pushed into the location bar.
This may be relative or absolute, as per [`history.pushState()`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState).
2. `false`, which prevents the browser’s history from being updated.
2. `true`, which pushes the fetched URL into history and location bar.
3. `false`, which prevents the browser’s history from being updated.

## Notes

Expand Down
5 changes: 3 additions & 2 deletions www/content/headers/hx-replace-url.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ description = """\

The `HX-Replace-Url` header allows you to replace the current URL in the browser [location history](https://developer.mozilla.org/en-US/docs/Web/API/History_API).
This does not create a new history entry; in effect, it removes the previous current URL from the browser&rsquo;s history.
This is similar to the [`hx-replace-url` attribute](@/attributes/hx-replace-url.md).
It works the same as [`hx-replace-url` attribute](@/attributes/hx-replace-url.md).

If present, this header overrides any behavior defined with attributes.

The possible values for this header are:

1. A URL to replace the current URL in the location bar.
This may be relative or absolute, as per [`history.replaceState()`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState), but must have the same origin as the current URL.
2. `false`, which prevents the browser’s current URL from being updated.
2. `true`, which replaces the fetched URL into history and location bar.
3. `false`, which prevents the browser’s current URL from being updated.

## Notes

Expand Down