From 28612d9ac17de5743224c52eece3f10a1718da0b Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Sat, 15 Aug 2026 10:15:08 +1200 Subject: [PATCH 1/2] add hx-query to support the new RFC method --- src/htmx.js | 56 +++++++------------ test/tests/attributes/hx-query.js | 47 ++++++++++++++++ test/tests/unit/__determineMethodAndAction.js | 15 ++++- www/src/content/docs.mdx | 1 + .../reference/01-attributes/06-hx-query.md | 25 +++++++++ 5 files changed, 104 insertions(+), 40 deletions(-) create mode 100644 test/tests/attributes/hx-query.js create mode 100644 www/src/content/reference/01-attributes/06-hx-query.md diff --git a/src/htmx.js b/src/htmx.js index 10f1fb516..485087a62 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -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 @@ -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, @@ -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) { diff --git a/test/tests/attributes/hx-query.js b/test/tests/attributes/hx-query.js new file mode 100644 index 000000000..230683fef --- /dev/null +++ b/test/tests/attributes/hx-query.js @@ -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(''); + 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('') + 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.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.requestSubmit() + await forRequest(); + lastFetch().url.should.equal("/test"); + // URL should not have query params - they go in body + lastFetch().url.should.not.include("?"); + }) + +}) diff --git a/test/tests/unit/__determineMethodAndAction.js b/test/tests/unit/__determineMethodAndAction.js index b4e52d6ce..991a074b5 100644 --- a/test/tests/unit/__determineMethodAndAction.js +++ b/test/tests/unit/__determineMethodAndAction.js @@ -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('
') - let result = htmx.__determineMethodAndAction(div, new Event('click')) + it('hx-method:inherited on parent changes method of child hx-action', function() { + let html = createProcessedHTML('
') + 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('
') + let btn = html.querySelector('button') + let result = htmx.__determineMethodAndAction(btn, new Event('click')) + assert.equal(result.method, 'POST') + assert.equal(result.action, '/test') + }) + }) diff --git a/www/src/content/docs.mdx b/www/src/content/docs.mdx index fcdc46142..f4f0b36a1 100644 --- a/www/src/content/docs.mdx +++ b/www/src/content/docs.mdx @@ -1032,6 +1032,7 @@ Use different attributes for different operations: + ``` 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. diff --git a/www/src/content/reference/01-attributes/06-hx-query.md b/www/src/content/reference/01-attributes/06-hx-query.md new file mode 100644 index 000000000..83e7568c6 --- /dev/null +++ b/www/src/content/reference/01-attributes/06-hx-query.md @@ -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 + +``` + +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) From 32df451d9820bc787dd4777dcdadee25accdd1e7 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Sun, 16 Aug 2026 03:02:19 +1200 Subject: [PATCH 2/2] web types --- src/editors/jetbrains/htmx.web-types.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/editors/jetbrains/htmx.web-types.json b/src/editors/jetbrains/htmx.web-types.json index 05cded34b..36ef644cb 100644 --- a/src/editors/jetbrains/htmx.web-types.json +++ b/src/editors/jetbrains/htmx.web-types.json @@ -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`.",