diff --git a/src/htmx.js b/src/htmx.js index a8d36b64c..fde56fe18 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -343,34 +343,23 @@ var htmx = (() => { } __determineMethodAndAction(elt, evt) { - if (this.__isBoosted(elt)) { - return this.__boostedMethodAndAction(elt, evt) - } else { - let method = this.__attributeValue(elt, "hx-method") || "GET" - 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; - } + 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; } } - method = method.toUpperCase() - return {action, method} } - } - - __boostedMethodAndAction(elt, evt) { - if (elt.matches("a")) { - return {action: elt.getAttribute("href"), method: "GET"} - } else { - let action = evt.submitter?.getAttribute?.("formAction") || elt.getAttribute("action"); - let method = evt.submitter?.getAttribute?.("formMethod") || elt.getAttribute("method") || "GET"; - return {action, method: method.toUpperCase()} + 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/test.html b/test/test.html index 0713192e4..6b126751a 100644 --- a/test/test.html +++ b/test/test.html @@ -131,6 +131,7 @@ + diff --git a/test/tests/attributes/hx-action.js b/test/tests/attributes/hx-action.js new file mode 100644 index 000000000..1c2834199 --- /dev/null +++ b/test/tests/attributes/hx-action.js @@ -0,0 +1,56 @@ +describe('hx-action attribute', function() { + + beforeEach(() => { + setupTest(this.currentTest) + }) + + afterEach(() => { + cleanupTest(this.currentTest) + }) + + it('hx-action alone defaults to GET', async function() { + mockResponse('GET', '/test', 'Clicked!') + let btn = createProcessedHTML('') + btn.click() + await forRequest() + fetchMock.calls[0].request.method.should.equal('GET') + btn.innerHTML.should.equal('Clicked!') + }) + + it('hx-action with hx-method uses specified method', async function() { + mockResponse('POST', '/test', 'Posted!') + let btn = createProcessedHTML('') + btn.click() + await forRequest() + fetchMock.calls[0].request.method.should.equal('POST') + btn.innerHTML.should.equal('Posted!') + }) + + it('hx-action on form picks up native method attribute', async function() { + mockResponse('POST', '/test', 'Posted!') + let form = createProcessedHTML('
') + form.requestSubmit() + await forRequest() + fetchMock.calls[0].request.method.should.equal('POST') + playground().innerHTML.should.equal('Posted!') + }) + + it('hx-action on form with submitter formmethod uses formmethod', async function() { + mockResponse('POST', '/test', 'Posted!') + let form = createProcessedHTML('') + find('#b1').click() + await forRequest() + fetchMock.calls[0].request.method.should.equal('POST') + playground().innerHTML.should.equal('Posted!') + }) + + it('hx-action with hx-method takes priority over native method attribute', async function() { + mockResponse('PUT', '/test', 'Put!') + let form = createProcessedHTML('') + form.requestSubmit() + await forRequest() + fetchMock.calls[0].request.method.should.equal('PUT') + playground().innerHTML.should.equal('Put!') + }) + +}) diff --git a/www/src/content/docs.mdx b/www/src/content/docs.mdx index db180cd70..ae208a4ea 100644 --- a/www/src/content/docs.mdx +++ b/www/src/content/docs.mdx @@ -400,8 +400,8 @@ Still available: `htmx.ajax()`, `htmx.config`, `htmx.find()`, `htmx.findAll()`, | Attribute | Purpose | |----------------------------------------------------|-----------------------------------------------------------------------| -| [`hx-action`](/reference/attributes/hx-action) | Specify URL (use with [`hx-method`](/reference/attributes/hx-method)) | -| [`hx-method`](/reference/attributes/hx-method) | Specify HTTP method | +| [`hx-action`](/reference/attributes/hx-action) | Specify URL, with optional [`hx-method`](/reference/attributes/hx-method). Supports progressive enhancement via native `action`/`method` fallback | +| [`hx-method`](/reference/attributes/hx-method) | Specify HTTP method (overrides native `method` and `formmethod`) | | [`hx-config`](/reference/attributes/hx-config) | Per-element request config (JSON or `key:value` syntax) | | [`hx-ignore`](/reference/attributes/hx-ignore) | Disable htmx processing (was `hx-disable`) | | [`hx-validate`](/reference/attributes/hx-validate) | Control form validation behavior | @@ -1027,7 +1027,7 @@ Use different attributes for different operations: ``` -Each attribute combines the URL and HTTP method. +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. #### Common Patterns diff --git a/www/src/content/reference/01-attributes/30-hx-action.md b/www/src/content/reference/01-attributes/30-hx-action.md index 327fa91bd..83b9968a9 100644 --- a/www/src/content/reference/01-attributes/30-hx-action.md +++ b/www/src/content/reference/01-attributes/30-hx-action.md @@ -3,7 +3,7 @@ title: "hx-action" description: "Specifies the URL to receive the request" --- -The `hx-action` attribute specifies the URL that will receive the request. +The `hx-action` attribute specifies the URL that will receive the request. It mirrors the native `action` attribute on forms, making it familiar to HTML authors and enabling progressive enhancement. ## Syntax @@ -13,22 +13,78 @@ The `hx-action` attribute specifies the URL that will receive the request. ``` +## Progressive Enhancement + +Like [`hx-boost`](/reference/attributes/hx-boost), `hx-action` supports progressive enhancement — the browser falls back to native `action`/`method` when JavaScript is unavailable. Where `hx-boost` provides page-navigation defaults (target body, scroll to top, push URL) for `` and ` +``` + +Unlike `hx-boost` (which always requests the same URL as the native `href`/`action`), `hx-action` can optionally point to a *different* URL. The recommended approach is still to detect htmx request headers and return fragments from the same URL, but in cases where separate endpoints are preferred, `hx-action` makes that possible: + +```html + +``` + +## Method Resolution + +When `hx-action` is used without [`hx-method`](/reference/attributes/hx-method), htmx resolves the HTTP method using the same fallback chain browsers use: + +1. [`hx-method`](/reference/attributes/hx-method) attribute on the element +2. `formmethod` attribute on the submitter button +3. Native `method` attribute on the form +4. Defaults to `GET` + +This means submitter buttons with `formmethod` work as expected: + +```html + +``` + +Clicking "View" sends a GET, clicking "Delete" sends a DELETE — matching native form semantics. + +## When to Use + +| Pattern | Use case | +|---------|----------| +| `hx-get`, `hx-post`, etc. | You know the method at authoring time and don't need native fallback | +| `hx-action` + `hx-method` | Method is dynamic (e.g. server-rendered) or you want a clear separation of URL and verb | +| `hx-action` alone | Progressive enhancement — method from native attributes, with option to use a separate AJAX endpoint | +| `hx-boost` | Progressive enhancement with page-navigation defaults (target body, push URL) | + ## Notes * `hx-action` is typically used with [`hx-method`](/reference/attributes/hx-method) to specify both the URL and HTTP method * The shorthand attributes like [`hx-get`](/reference/attributes/hx-get), [`hx-post`](/reference/attributes/hx-post), etc. combine both URL and method -* Use `hx-action` + `hx-method` when you need dynamic method selection ## Examples ```html - + + + + - - + diff --git a/www/src/content/reference/01-attributes/31-hx-method.md b/www/src/content/reference/01-attributes/31-hx-method.md index bd4117651..49bfaaaa2 100644 --- a/www/src/content/reference/01-attributes/31-hx-method.md +++ b/www/src/content/reference/01-attributes/31-hx-method.md @@ -3,7 +3,7 @@ title: "hx-method" description: "Specifies the HTTP method for the request" --- -The `hx-method` attribute specifies the HTTP method (verb) to use for the request. +The `hx-method` attribute specifies the HTTP method (verb) to use for the request. It is typically paired with [`hx-action`](/reference/attributes/hx-action) to separate the URL from the method. ## Syntax @@ -13,29 +13,42 @@ The `hx-method` attribute specifies the HTTP method (verb) to use for the reques ``` +## Priority + +`hx-method` takes the highest priority in the [method resolution chain](/reference/attributes/hx-action#method-resolution). It overrides both `formmethod` on submitter buttons and the native `method` attribute on forms: + +```html + + +``` + +When `hx-method` is omitted, htmx falls back to `formmethod`, then native `method`, then `GET`. See [`hx-action` Method Resolution](/reference/attributes/hx-action#method-resolution) for the full chain. + ## Notes -* Valid values: `get`, `post`, `put`, `patch`, `delete` -* If no method is specified, defaults to `get` -* `hx-method` is typically used with [`hx-action`](/reference/attributes/hx-action) -* The shorthand attributes like [`hx-get`](/reference/attributes/hx-get), [`hx-post`](/reference/attributes/hx-post), etc. can be used - instead +* Valid values: `get`, `post`, `put`, `patch`, `delete` (case-insensitive) +* If no method can be determined from any source, defaults to `GET` +* The shorthand attributes [`hx-get`](/reference/attributes/hx-get), [`hx-post`](/reference/attributes/hx-post), etc. combine URL and method into one attribute ## Examples ```html - + - + - - + + ```