Skip to content
Draft
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
10 changes: 9 additions & 1 deletion editors/jetbrains/htmx.web-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,14 @@
},
"doc-url": "https://htmx.org/attributes/hx-put/"
},
{
"name": "hx-query",
"description": "The `hx-query` attribute will cause an element to issue a `QUERY` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n<button hx-query=\"/products\" hx-target=\"body\">\n Search for products\n</button>\n```",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-query/"
},
{
"name": "hx-replace-url",
"description": "The `hx-replace-url` attribute allows you to replace the current url of the browser [location history](https://developer.mozilla.org/en-US/docs/Web/API/History_API).\n\nThe possible values of this attribute are:\n\n1. `true`, which replaces the fetched URL in the browser navigation bar.\n2. `false`, which disables replacing the fetched URL if it would otherwise be replaced due to inheritance.\n3. A URL to be replaced into the location bar.\n This may be relative or absolute, as per [`history.replaceState()`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState).",
Expand Down Expand Up @@ -592,4 +600,4 @@
]
}
}
}
}
34 changes: 17 additions & 17 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ var htmx = (function() {
withExtensions
}

const VERBS = ['get', 'post', 'put', 'delete', 'patch']
const VERBS = ['get', 'post', 'put', 'delete', 'patch', 'query']
const VERB_SELECTOR = VERBS.map(function(verb) {
return '[hx-' + verb + '], [data-hx-' + verb + ']'
}).join(', ')
Expand Down Expand Up @@ -4039,11 +4039,11 @@ var htmx = (function() {

/**
* @param {XMLHttpRequest} xhr
* @param {RegExp} regexp
* @param {string} name
* @return {boolean}
*/
function hasHeader(xhr, regexp) {
return regexp.test(xhr.getAllResponseHeaders())
function hasHeader(xhr, name) {
return xhr.getResponseHeader(name) !== null
}

/**
Expand Down Expand Up @@ -4668,13 +4668,13 @@ var htmx = (function() {
//= ==========================================
let pathFromHeaders = null
let typeFromHeaders = null
if (hasHeader(xhr, /HX-Push:/i)) {
if (hasHeader(xhr, 'HX-Push')) {
pathFromHeaders = xhr.getResponseHeader('HX-Push')
typeFromHeaders = 'push'
} else if (hasHeader(xhr, /HX-Push-Url:/i)) {
} else if (hasHeader(xhr, 'HX-Push-Url')) {
pathFromHeaders = xhr.getResponseHeader('HX-Push-Url')
typeFromHeaders = 'push'
} else if (hasHeader(xhr, /HX-Replace-Url:/i)) {
} else if (hasHeader(xhr, 'HX-Replace-Url')) {
pathFromHeaders = xhr.getResponseHeader('HX-Replace-Url')
typeFromHeaders = 'replace'
}
Expand Down Expand Up @@ -4809,11 +4809,11 @@ var htmx = (function() {

if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return

if (hasHeader(xhr, /HX-Trigger:/i)) {
if (hasHeader(xhr, 'HX-Trigger')) {
handleTriggerHeader(xhr, 'HX-Trigger', elt)
}

if (hasHeader(xhr, /HX-Location:/i)) {
if (hasHeader(xhr, 'HX-Location')) {
let redirectPath = xhr.getResponseHeader('HX-Location')
/** @type {HtmxAjaxHelperContext&{path?:string}} */
var redirectSwapSpec = {}
Expand All @@ -4828,9 +4828,9 @@ var htmx = (function() {
return
}

const shouldRefresh = hasHeader(xhr, /HX-Refresh:/i) && xhr.getResponseHeader('HX-Refresh') === 'true'
const shouldRefresh = hasHeader(xhr, 'HX-Refresh') && xhr.getResponseHeader('HX-Refresh') === 'true'

if (hasHeader(xhr, /HX-Redirect:/i)) {
if (hasHeader(xhr, 'HX-Redirect')) {
responseInfo.keepIndicators = true
htmx.location.href = xhr.getResponseHeader('HX-Redirect')
shouldRefresh && htmx.location.reload()
Expand Down Expand Up @@ -4859,11 +4859,11 @@ var htmx = (function() {
}

// response headers override response handling config
if (hasHeader(xhr, /HX-Retarget:/i)) {
if (hasHeader(xhr, 'HX-Retarget')) {
responseInfo.target = resolveRetarget(elt, xhr.getResponseHeader('HX-Retarget'))
}

if (hasHeader(xhr, /HX-Reswap:/i)) {
if (hasHeader(xhr, 'HX-Reswap')) {
swapOverride = xhr.getResponseHeader('HX-Reswap')
}

Expand Down Expand Up @@ -4919,7 +4919,7 @@ var htmx = (function() {
selectOverride = responseInfoSelect
}

if (hasHeader(xhr, /HX-Reselect:/i)) {
if (hasHeader(xhr, 'HX-Reselect')) {
selectOverride = xhr.getResponseHeader('HX-Reselect')
}

Expand All @@ -4933,7 +4933,7 @@ var htmx = (function() {
anchor: responseInfo.pathInfo.anchor,
contextElement: elt,
afterSwapCallback: function() {
if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
if (hasHeader(xhr, 'HX-Trigger-After-Swap')) {
let finalElt = elt
if (!bodyContains(elt)) {
finalElt = getDocument().body
Expand All @@ -4942,7 +4942,7 @@ var htmx = (function() {
}
},
afterSettleCallback: function() {
if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
if (hasHeader(xhr, 'HX-Trigger-After-Settle')) {
let finalElt = elt
if (!bodyContains(elt)) {
finalElt = getDocument().body
Expand Down Expand Up @@ -5155,7 +5155,7 @@ var htmx = (function() {
return htmx
})()

/** @typedef {'get'|'head'|'post'|'put'|'delete'|'connect'|'options'|'trace'|'patch'} HttpVerb */
/** @typedef {'get'|'head'|'post'|'put'|'delete'|'connect'|'options'|'trace'|'patch'|'query'} HttpVerb */

/**
* @typedef {Object} SwapOptions
Expand Down
32 changes: 32 additions & 0 deletions test/attributes/hx-query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
describe('hx-query attribute', function() {
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
})
afterEach(function() {
this.server.restore()
clearWorkArea()
})

it('issues a QUERY request', function() {
this.server.respondWith('QUERY', '/test', function(xhr) {
xhr.respond(200, {}, 'Queried!')
})

var btn = make('<button hx-query="/test">Click Me!</button>')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Queried!')
})

it('issues a QUERY request w/ data-* prefix', function() {
this.server.respondWith('QUERY', '/test', function(xhr) {
xhr.respond(200, {}, 'Queried!')
})

var btn = make('<button data-hx-query="/test">Click Me!</button>')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Queried!')
})
})
16 changes: 16 additions & 0 deletions test/core/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,4 +533,20 @@ describe('Core htmx AJAX headers', function() {
htmx._('loadHistoryFromServer')('/test')
this.server.respond()
})

// Regression for #527: response headers whose names *contain* an htmx
// header name as a substring (e.g. X-HX-Trigger) must not be treated as
// the htmx header. The previous implementation tested a regex against
// getAllResponseHeaders() and false-positively matched the substring,
// then crashed when getResponseHeader returned null.
it('does not crash or fire trigger when X-HX-Trigger header is present without HX-Trigger', function() {
this.server.respondWith('GET', '/test', [200, { 'X-HX-Trigger': 'foo' }, ''])

var div = make('<div hx-get="/test"></div>')
var invokedEvent = false
div.addEventListener('foo', function() { invokedEvent = true })
div.click()
this.server.respond()
invokedEvent.should.equal(false)
})
})
8 changes: 8 additions & 0 deletions test/core/verbs.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@ describe('Core htmx AJAX Verbs', function() {
this.server.respond()
div.innerHTML.should.equal('delete')
})

it('handles basic query properly', function() {
this.server.respondWith('QUERY', '/test', 'query')
var div = make('<div hx-query="/test">click me</div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('query')
})
})
1 change: 1 addition & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ <h2>Mocha Test Suite</h2>
<script src="attributes/hx-prompt.js"></script>
<script src="attributes/hx-push-url.js"></script>
<script src="attributes/hx-put.js"></script>
<script src="attributes/hx-query.js"></script>
<script src="attributes/hx-replace-url.js"></script>
<script src="attributes/hx-request.js"></script>
<script src="attributes/hx-select.js"></script>
Expand Down
26 changes: 26 additions & 0 deletions www/content/attributes/hx-query.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
+++
title = "hx-query"
description = """\
The hx-query attribute in htmx will cause an element to issue a QUERY request to the specified URL and swap the returned \
HTML into the DOM using a swap strategy."""
+++

The `hx-query` attribute will cause an element to issue a `QUERY` to the specified URL and swap
the HTML into the DOM using a swap strategy:

```html
<button hx-query="/products" hx-target="body">
Search for products
</button>
```

This example will cause the `button` to issue a `QUERY` to `/products` and swap the returned HTML into
the `innerHTML` of the `body`.

## Notes

* `hx-query` is not inherited
* You can control the target of the swap using the [hx-target](@/attributes/hx-target.md) attribute
* You can control the swap strategy by using the [hx-swap](@/attributes/hx-swap.md) attribute
* You can control what event triggers the request with the [hx-trigger](@/attributes/hx-trigger.md) attribute
* You can control the data submitted with the request in various ways, documented here: [Parameters](@/docs.md#parameters)
1 change: 1 addition & 0 deletions www/content/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ The core of htmx is a set of attributes that allow you to issue AJAX requests di
| [hx-get](@/attributes/hx-get.md) | Issues a `GET` request to the given URL |
| [hx-post](@/attributes/hx-post.md) | Issues a `POST` request to the given URL |
| [hx-put](@/attributes/hx-put.md) | Issues a `PUT` request to the given URL |
| [hx-query](@/attributes/hx-query.md) | Issues a `QUERY` request to the given URL |
| [hx-patch](@/attributes/hx-patch.md) | Issues a `PATCH` request to the given URL |
| [hx-delete](@/attributes/hx-delete.md) | Issues a `DELETE` request to the given URL |

Expand Down
1 change: 1 addition & 0 deletions www/content/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ All other attributes available in htmx.
| [`hx-preserve`](@/attributes/hx-preserve.md) | specifies elements to keep unchanged between requests |
| [`hx-prompt`](@/attributes/hx-prompt.md) | shows a `prompt()` before submitting a request |
| [`hx-put`](@/attributes/hx-put.md) | issues a `PUT` to the specified URL |
| [`hx-query`](@/attributes/hx-query.md) | issues a `QUERY` to the specified URL |
| [`hx-replace-url`](@/attributes/hx-replace-url.md) | replace the URL in the browser location bar |
| [`hx-request`](@/attributes/hx-request.md) | configures various aspects of the request |
| [`hx-sync`](@/attributes/hx-sync.md) | control how requests made by different elements are synchronized |
Expand Down
32 changes: 32 additions & 0 deletions www/static/test/attributes/hx-query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
describe('hx-query attribute', function() {
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
})
afterEach(function() {
this.server.restore()
clearWorkArea()
})

it('issues a QUERY request', function() {
this.server.respondWith('QUERY', '/test', function(xhr) {
xhr.respond(200, {}, 'Queried!')
})

var btn = make('<button hx-query="/test">Click Me!</button>')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Queried!')
})

it('issues a QUERY request w/ data-* prefix', function() {
this.server.respondWith('QUERY', '/test', function(xhr) {
xhr.respond(200, {}, 'Queried!')
})

var btn = make('<button data-hx-query="/test">Click Me!</button>')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Queried!')
})
})
8 changes: 8 additions & 0 deletions www/static/test/core/verbs.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ describe('Core htmx AJAX Verbs', function() {
div.innerHTML.should.equal('patch')
})

it('handles basic query properly', function() {
this.server.respondWith('QUERY', '/test', 'query')
var div = make('<div hx-query="/test">click me</div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('query')
})

it('handles basic delete properly', function() {
this.server.respondWith('DELETE', '/test', 'delete')
var div = make('<div hx-delete="/test">click me</div>')
Expand Down
1 change: 1 addition & 0 deletions www/static/test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ <h2>Mocha Test Suite</h2>
<script src="attributes/hx-prompt.js"></script>
<script src="attributes/hx-push-url.js"></script>
<script src="attributes/hx-put.js"></script>
<script src="attributes/hx-query.js"></script>
<script src="attributes/hx-replace-url.js"></script>
<script src="attributes/hx-request.js"></script>
<script src="attributes/hx-select.js"></script>
Expand Down