Skip to content
Merged
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
5 changes: 5 additions & 0 deletions src/editors/jetbrains/htmx.web-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
"description": "Issues a `DELETE` request to the specified URL and swaps the response into the DOM.",
"doc-url": "https://four.htmx.org/reference/attributes/hx-delete"
},
{
"name": "hx-query",
"description": "Issues a `QUERY` request to the specified URL and swaps the response into the DOM.",
"doc-url": "https://four.htmx.org/reference/attributes/hx-query"
},
{
"name": "hx-trigger",
"description": "Controls which event specifications trigger an element's request. Supports filters and modifiers such as `delay`, `throttle`, `once`, `changed`, `from`, and `target`.",
Expand Down
56 changes: 19 additions & 37 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ var htmx = (() => {
#ttPolicy = { createHTML: s => s, createScript: s => s };
#actionSelector
#boostSelector = "a,form";
#verbs = ["get", "post", "put", "patch", "delete", "query"];
#hxOnQuery
#transitionQueue
#historyAbort
Expand All @@ -148,7 +149,7 @@ var htmx = (() => {
constructor() {
this.__initHtmxConfig();
this.__initRequestIndicatorCss();
this.#actionSelector = this.__prefixSelector('[hx-action],[hx-get],[hx-post],[hx-put],[hx-patch],[hx-delete]');
this.#actionSelector = this.__prefixSelector('[hx-action],[hx-get],[hx-post],[hx-put],[hx-patch],[hx-delete],[hx-query]');
this.#hxOnQuery = new XPathEvaluator().createExpression(`.//*[@*[${this.__prefixes("hx-on").map(p => `starts-with(name(), "${p}")`).join(' or ')}]]`);
this.#internalAPI = {
HCON,
Expand Down Expand Up @@ -325,42 +326,23 @@ var htmx = (() => {
}

__determineMethodAndAction(elt, evt) {
let hxMethod = this.__attributeValue(elt, "hx-method");
let hxAction = this.__attributeValue(elt, "hx-action");

let hxGet = this.__attributeValue(elt, "hx-get");
let hxPost = this.__attributeValue(elt, "hx-post");
let hxPut = this.__attributeValue(elt, "hx-put");
let hxPatch = this.__attributeValue(elt, "hx-patch");
let hxDelete = this.__attributeValue(elt, "hx-delete");

let formMethod = evt.submitter?.getAttribute?.("formmethod") || elt.getAttribute("method");
let formAction = evt.submitter?.getAttribute?.("formAction") || elt.getAttribute("action");
let anchorHref = elt.getAttribute("href");

return {
action:
hxAction ||
(
hxGet ??
hxPost ??
hxPut ??
hxPatch ??
hxDelete
) ||
this.__isBoosted(elt) && (anchorHref || formAction),

method: (
hxMethod ||
(hxGet != null ? "GET" :
hxPost != null ? "POST" :
hxPut != null ? "PUT" :
hxPatch != null ? "PATCH" :
hxDelete != null && "DELETE") ||
formMethod ||
"GET"
).toUpperCase()
};
let method = this.__attributeValue(elt, "hx-method");
let action = this.__attributeValue(elt, "hx-action");
if (!action) {
for (let verb of this.#verbs) {
let verbAction = this.__attributeValue(elt, "hx-" + verb);
if (verbAction != null) {
action = verbAction;
method = verb;
break;
}
}
}
if (this.__isBoosted(elt)) {
action ||= evt.submitter?.getAttribute?.("formAction") || elt.getAttribute(elt.matches("a") ? "href" : "action");
}
method ||= evt.submitter?.getAttribute?.("formmethod") || elt.getAttribute("method") || "GET";
return {action, method: method.toUpperCase()};
}

__htmxProp(elt) {
Expand Down
47 changes: 47 additions & 0 deletions test/tests/attributes/hx-query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
describe('hx-query attribute', function() {

beforeEach(() => {
setupTest(this.currentTest)
})

afterEach(() => {
cleanupTest(this.currentTest)
})

it('issues a QUERY request on click and swaps content', async function () {
mockResponse('QUERY', '/test', 'Queried!')
let btn = createProcessedHTML('<button hx-query="/test">Click Me!</button>');
btn.click()
await forRequest()
btn.innerHTML.should.equal('Queried!')
})

it('issues a QUERY request with proper headers', async function() {
mockResponse('QUERY', '/test', 'Queried!')
let btn = createProcessedHTML('<button hx-query="/test">Click Me!</button>')
btn.click()
await forRequest()
fetchMock.calls[0].request.method.should.equal('QUERY');
btn.innerHTML.should.equal('Queried!')
})

it('QUERY on form includes its own data in request body', async function () {
mockResponse('QUERY', '/test', "Queried!")
let form = createProcessedHTML('<form hx-query="/test" hx-swap="outerHTML"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
form.requestSubmit()
await forRequest();
playground().innerHTML.should.equal('Queried!')
lastFetch().url.should.equal("/test");
})

it('QUERY sends parameters in body not URL (unlike GET)', async function () {
mockResponse('QUERY', '/test', 'Success')
let form = createProcessedHTML('<form hx-query="/test" hx-swap="outerHTML"><input name="i1" value="value"/><button>Submit</button></form>');
form.requestSubmit()
await forRequest();
lastFetch().url.should.equal("/test");
// URL should not have query params - they go in body
lastFetch().url.should.not.include("?");
})

})
15 changes: 12 additions & 3 deletions test/tests/unit/__determineMethodAndAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,20 @@ describe('__determineMethodAndAction unit tests', function() {
assert.equal(result.action, '/test')
})

it('hx-method overrides the shorthand request method', function() {
let div = createProcessedHTML('<div hx-post="/test" hx-method="delete"></div>')
let result = htmx.__determineMethodAndAction(div, new Event('click'))
it('hx-method:inherited on parent changes method of child hx-action', function() {
let html = createProcessedHTML('<div hx-method:inherited="delete"><button hx-action="/test"></button></div>')
let btn = html.querySelector('button')
let result = htmx.__determineMethodAndAction(btn, new Event('click'))
assert.equal(result.method, 'DELETE')
assert.equal(result.action, '/test')
})

it('hx-method:inherited on parent does not change method of child hx-verb', function() {
let html = createProcessedHTML('<div hx-method:inherited="delete"><button hx-post="/test"></button></div>')
let btn = html.querySelector('button')
let result = htmx.__determineMethodAndAction(btn, new Event('click'))
assert.equal(result.method, 'POST')
assert.equal(result.action, '/test')
})

})
1 change: 1 addition & 0 deletions www/src/content/docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,7 @@ Use different attributes for different operations:
<button hx-put="/users/1">Update User</button>
<button hx-patch="/users/1">Patch User</button>
<button hx-delete="/users/1">Delete User</button>
<button hx-query="/users">Query Users</button>
```

Each attribute combines the URL and HTTP method. Alternatively, use [`hx-action`](/reference/attributes/hx-action) and [`hx-method`](/reference/attributes/hx-method) to separate them — useful when the method is dynamic, or when you want progressive enhancement with native `action`/`method` fallback.
Expand Down
25 changes: 25 additions & 0 deletions www/src/content/reference/01-attributes/06-hx-query.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: "hx-query"
description: "Issues `QUERY` request to specified URL"
---

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.

## Syntax

```html
<button hx-query="/search">Search</button>
```

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

## Notes

* `QUERY` is a safe, idempotent method ([RFC 10008](https://datatracker.ietf.org/doc/html/rfc10008)) that sends content in the request body like `POST`
* You can control the target of the swap using the [`hx-target`](/reference/attributes/hx-target) attribute
* You can control the swap strategy by using the [`hx-swap`](/reference/attributes/hx-swap) attribute
* You can control what event triggers the request with the [`hx-trigger`](/reference/attributes/hx-trigger) attribute
* You can control the data submitted with the request in various ways, documented
here: [Parameters](/docs#parameters)
Loading