diff --git a/src/htmx.js b/src/htmx.js index f87a39b6b..d2a47933c 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -4744,9 +4744,13 @@ var htmx = (function() { const requestPath = responseInfo.pathInfo.finalRequestPath const responsePath = responseInfo.pathInfo.responsePath - const pushUrl = responseInfo.etc.push || getClosestAttributeValue(elt, 'hx-push-url') - let replaceUrl = responseInfo.etc.replace || getClosestAttributeValue(elt, 'hx-replace-url') - if (replaceUrl === 'false') replaceUrl = null + let pushUrl = responseInfo.etc.push ?? getClosestAttributeValue(elt, 'hx-push-url') + let replaceUrl = responseInfo.etc.replace ?? getClosestAttributeValue(elt, 'hx-replace-url') + + const pushDisabled = pushUrl === 'false' || pushUrl === false + if (pushDisabled) pushUrl = null + + if (replaceUrl === 'false' || replaceUrl === false) replaceUrl = null const elementIsBoosted = getInternalData(elt).boosted let saveType = null @@ -4758,17 +4762,12 @@ var htmx = (function() { } else if (replaceUrl) { saveType = 'replace' path = replaceUrl - } else if (elementIsBoosted) { + } else if (elementIsBoosted && !pushDisabled) { saveType = 'push' path = responsePath || requestPath // if there is no response path, go with the original request path } 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 @@ -5293,8 +5292,8 @@ var htmx = (function() { * @property {Object|FormData} [values] * @property {Record} [headers] * @property {string} [select] - * @property {string} [push] - * @property {string} [replace] + * @property {string|false} [push] + * @property {string|false} [replace] * @property {string} [selectOOB] */ @@ -5342,8 +5341,8 @@ var htmx = (function() { * @property {Object|FormData} [values] * @property {boolean} [credentials] * @property {number} [timeout] - * @property {string} [push] - * @property {string} [replace] + * @property {string|false} [push] + * @property {string|false} [replace] * @property {string} [selectOOB] */ diff --git a/test/attributes/hx-replace-url.js b/test/attributes/hx-replace-url.js index 27a4ba73a..e47a85275 100644 --- a/test/attributes/hx-replace-url.js +++ b/test/attributes/hx-replace-url.js @@ -31,6 +31,48 @@ describe('hx-replace-url attribute', function() { cache[cache.length - 1].url.should.equal(normalizePath('/test')) }) + it('should replace URL when hx-push-url is false', function() { + var history = recordHistoryUpdates() + try { + this.server.respondWith('GET', '/test', 'Replaced!') + var div = make('
') + byId('b1').click() + this.server.respond() + div.innerHTML.should.equal('Replaced!') + history.updates.should.deep.equal([['htmx:replacedInHistory', '/test']]) + } finally { + history.stop() + } + }) + + it('boosted anchor should replace URL when hx-push-url is false', function() { + var history = recordHistoryUpdates() + try { + this.server.respondWith('GET', '/test', 'Replaced!') + var div = make('
Go
') + byId('a1').click() + this.server.respond() + div.innerHTML.should.equal('Replaced!') + history.updates.should.deep.equal([['htmx:replacedInHistory', '/custom']]) + } finally { + history.stop() + } + }) + + it('boosted anchor should still push URL when hx-replace-url is false', function() { + var history = recordHistoryUpdates() + try { + this.server.respondWith('GET', '/test', 'Pushed!') + var div = make('
Go
') + byId('a1').click() + this.server.respond() + div.innerHTML.should.equal('Pushed!') + history.updates.should.deep.equal([['htmx:pushedIntoHistory', '/test']]) + } finally { + history.stop() + } + }) + it('should handle HX-Replace-Url response header', function() { var path var handler = htmx.on('htmx:replacedInHistory', function(event) { diff --git a/test/core/api.js b/test/core/api.js index f9042c673..a8085a7e1 100644 --- a/test/core/api.js +++ b/test/core/api.js @@ -699,6 +699,36 @@ describe('Core htmx API test', function() { path.should.equal('/abc123') }) + it('ajax api push Url should not push an inherited Url when false', function() { + var history = recordHistoryUpdates() + try { + this.server.respondWith('GET', '/test', 'Loaded!') + make('
') + var div = byId('d1') + htmx.ajax('GET', '/test', { source: div, target: div, push: false }) + this.server.respond() + div.innerHTML.should.equal('Loaded!') + history.updates.should.deep.equal([]) + } finally { + history.stop() + } + }) + + it('ajax api replace Url should not replace an inherited Url when false', function() { + var history = recordHistoryUpdates() + try { + this.server.respondWith('GET', '/test', 'Loaded!') + make('
') + var div = byId('d1') + htmx.ajax('GET', '/test', { source: div, target: div, replace: false }) + this.server.respond() + div.innerHTML.should.equal('Loaded!') + history.updates.should.deep.equal([]) + } finally { + history.stop() + } + }) + it('ajax api replace Url should replace an element into the cache when true', function() { this.server.respondWith('POST', '/test123', 'Clicked!') diff --git a/test/core/headers.js b/test/core/headers.js index 056f826c8..7dc16abc2 100644 --- a/test/core/headers.js +++ b/test/core/headers.js @@ -442,6 +442,72 @@ describe('Core htmx AJAX headers', function() { }, 30) }) + it('should replace Url on HX-Location if push is string false', function() { + var history = recordHistoryUpdates() + try { + this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"path":"/test2", "target":"#testdiv", "push":"false", "replace":"true"}' }, '']) + this.server.respondWith('GET', '/test2', 'Redirected!') + var div = make('
') + div.click() + this.server.respond() + this.server.respond() + div.innerHTML.should.equal('Redirected!') + history.updates.should.deep.equal([['htmx:replacedInHistory', '/test2']]) + } finally { + history.stop() + } + }) + + it('should replace Url on HX-Location if push is false', function() { + var history = recordHistoryUpdates() + try { + this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"path":"/test2", "target":"#testdiv", "push":false, "replace":"/custom"}' }, '']) + this.server.respondWith('GET', '/test2', 'Redirected!') + var div = make('
') + div.click() + this.server.respond() + this.server.respond() + div.innerHTML.should.equal('Redirected!') + history.updates.should.deep.equal([['htmx:replacedInHistory', '/custom']]) + } finally { + history.stop() + } + }) + + it('should not update history on HX-Location from a boosted source if push and replace are string false', function() { + var history = recordHistoryUpdates() + try { + this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"path":"/test2", "source":"#a1", "target":"#output", "push":"false", "replace":"false"}' }, '']) + this.server.respondWith('GET', '/test2', 'Redirected!') + var output = make('
') + make('
Go
') + byId('a1').click() + this.server.respond() + this.server.respond() + output.innerHTML.should.equal('Redirected!') + history.updates.should.deep.equal([]) + } finally { + history.stop() + } + }) + + it('should not update history on HX-Location from a boosted source if push and replace are false', function() { + var history = recordHistoryUpdates() + try { + this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"path":"/test2", "source":"#a1", "target":"#output", "push":false, "replace":false}' }, '']) + this.server.respondWith('GET', '/test2', 'Redirected!') + var output = make('
') + make('
Go
') + byId('a1').click() + this.server.respond() + this.server.respond() + output.innerHTML.should.equal('Redirected!') + history.updates.should.deep.equal([]) + } finally { + history.stop() + } + }) + 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' diff --git a/test/util/util.js b/test/util/util.js index 363fdda78..4501d2a7b 100644 --- a/test/util/util.js +++ b/test/util/util.js @@ -108,6 +108,22 @@ function getParameters(xhr) { } } +function recordHistoryUpdates() { + var updates = [] + var handler = function(event) { + updates.push([event.type, event.detail.path]) + } + htmx.on('htmx:pushedIntoHistory', handler) + htmx.on('htmx:replacedInHistory', handler) + return { + updates, + stop: function() { + htmx.off('htmx:pushedIntoHistory', handler) + htmx.off('htmx:replacedInHistory', handler) + } + } +} + function log(val) { console.log(val) return val diff --git a/www/content/headers/hx-location.md b/www/content/headers/hx-location.md index a91c4228c..6240979bb 100644 --- a/www/content/headers/hx-location.md +++ b/www/content/headers/hx-location.md @@ -30,8 +30,14 @@ 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 +* `push` - set to `'false'`, `false`, or a path string to prevent or override the URL pushed to browser location history +* `replace` - set to `'true'` or a path string to replace the URL in the browser location history + +To replace the current history entry, disable the default push and specify `replace`: + +```html +HX-Location: {"path":"/test2", "push":"false", "replace":"true"} +``` ## Notes