Skip to content

Commit 236babc

Browse files
Unify method/action resolution and improve hx-action doco (#3883)
* Unify method/action resolution and improve hx-action doco * improve doco
1 parent 86417b7 commit 236babc

6 files changed

Lines changed: 159 additions & 44 deletions

File tree

src/htmx.js

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -343,34 +343,23 @@ var htmx = (() => {
343343
}
344344

345345
__determineMethodAndAction(elt, evt) {
346-
if (this.__isBoosted(elt)) {
347-
return this.__boostedMethodAndAction(elt, evt)
348-
} else {
349-
let method = this.__attributeValue(elt, "hx-method") || "GET"
350-
let action = this.__attributeValue(elt, "hx-action");
351-
if (!action) {
352-
for (let verb of this.#verbs) {
353-
let verbAction = this.__attributeValue(elt, "hx-" + verb);
354-
if (verbAction != null) {
355-
action = verbAction;
356-
method = verb;
357-
break;
358-
}
346+
let method = this.__attributeValue(elt, "hx-method");
347+
let action = this.__attributeValue(elt, "hx-action");
348+
if (!action) {
349+
for (let verb of this.#verbs) {
350+
let verbAction = this.__attributeValue(elt, "hx-" + verb);
351+
if (verbAction != null) {
352+
action = verbAction;
353+
method = verb;
354+
break;
359355
}
360356
}
361-
method = method.toUpperCase()
362-
return {action, method}
363357
}
364-
}
365-
366-
__boostedMethodAndAction(elt, evt) {
367-
if (elt.matches("a")) {
368-
return {action: elt.getAttribute("href"), method: "GET"}
369-
} else {
370-
let action = evt.submitter?.getAttribute?.("formAction") || elt.getAttribute("action");
371-
let method = evt.submitter?.getAttribute?.("formMethod") || elt.getAttribute("method") || "GET";
372-
return {action, method: method.toUpperCase()}
358+
if (this.__isBoosted(elt)) {
359+
action ||= evt.submitter?.getAttribute?.("formAction") || elt.getAttribute(elt.matches("a") ? "href" : "action");
373360
}
361+
method ||= evt.submitter?.getAttribute?.("formmethod") || elt.getAttribute("method") || "GET";
362+
return {action, method: method.toUpperCase()};
374363
}
375364

376365
__htmxProp(elt) {

test/test.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
<!-- ============================================ -->
132132
<!-- Attribute Tests -->
133133
<!-- ============================================ -->
134+
<script src="./tests/attributes/hx-action.js"></script>
134135
<script src="./tests/attributes/hx-boost.js"></script>
135136
<script src="./tests/attributes/hx-config.js"></script>
136137
<script src="./tests/attributes/hx-confirm.js"></script>

test/tests/attributes/hx-action.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
describe('hx-action attribute', function() {
2+
3+
beforeEach(() => {
4+
setupTest(this.currentTest)
5+
})
6+
7+
afterEach(() => {
8+
cleanupTest(this.currentTest)
9+
})
10+
11+
it('hx-action alone defaults to GET', async function() {
12+
mockResponse('GET', '/test', 'Clicked!')
13+
let btn = createProcessedHTML('<button hx-action="/test">Click Me!</button>')
14+
btn.click()
15+
await forRequest()
16+
fetchMock.calls[0].request.method.should.equal('GET')
17+
btn.innerHTML.should.equal('Clicked!')
18+
})
19+
20+
it('hx-action with hx-method uses specified method', async function() {
21+
mockResponse('POST', '/test', 'Posted!')
22+
let btn = createProcessedHTML('<button hx-action="/test" hx-method="post">Click Me!</button>')
23+
btn.click()
24+
await forRequest()
25+
fetchMock.calls[0].request.method.should.equal('POST')
26+
btn.innerHTML.should.equal('Posted!')
27+
})
28+
29+
it('hx-action on form picks up native method attribute', async function() {
30+
mockResponse('POST', '/test', 'Posted!')
31+
let form = createProcessedHTML('<form hx-action="/test" hx-swap="outerHTML" method="post"><button>Submit</button></form>')
32+
form.requestSubmit()
33+
await forRequest()
34+
fetchMock.calls[0].request.method.should.equal('POST')
35+
playground().innerHTML.should.equal('Posted!')
36+
})
37+
38+
it('hx-action on form with submitter formmethod uses formmethod', async function() {
39+
mockResponse('POST', '/test', 'Posted!')
40+
let form = createProcessedHTML('<form hx-action="/test" hx-swap="outerHTML" method="get"><button id="b1" formmethod="post">Submit</button></form>')
41+
find('#b1').click()
42+
await forRequest()
43+
fetchMock.calls[0].request.method.should.equal('POST')
44+
playground().innerHTML.should.equal('Posted!')
45+
})
46+
47+
it('hx-action with hx-method takes priority over native method attribute', async function() {
48+
mockResponse('PUT', '/test', 'Put!')
49+
let form = createProcessedHTML('<form hx-action="/test" hx-method="put" hx-swap="outerHTML" method="post"><button>Submit</button></form>')
50+
form.requestSubmit()
51+
await forRequest()
52+
fetchMock.calls[0].request.method.should.equal('PUT')
53+
playground().innerHTML.should.equal('Put!')
54+
})
55+
56+
})

www/src/content/docs.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,8 @@ Still available: `htmx.ajax()`, `htmx.config`, `htmx.find()`, `htmx.findAll()`,
400400

401401
| Attribute | Purpose |
402402
|----------------------------------------------------|-----------------------------------------------------------------------|
403-
| [`hx-action`](/reference/attributes/hx-action) | Specify URL (use with [`hx-method`](/reference/attributes/hx-method)) |
404-
| [`hx-method`](/reference/attributes/hx-method) | Specify HTTP method |
403+
| [`hx-action`](/reference/attributes/hx-action) | Specify URL, with optional [`hx-method`](/reference/attributes/hx-method). Supports progressive enhancement via native `action`/`method` fallback |
404+
| [`hx-method`](/reference/attributes/hx-method) | Specify HTTP method (overrides native `method` and `formmethod`) |
405405
| [`hx-config`](/reference/attributes/hx-config) | Per-element request config (JSON or `key:value` syntax) |
406406
| [`hx-ignore`](/reference/attributes/hx-ignore) | Disable htmx processing (was `hx-disable`) |
407407
| [`hx-validate`](/reference/attributes/hx-validate) | Control form validation behavior |
@@ -1027,7 +1027,7 @@ Use different attributes for different operations:
10271027
<button hx-delete="/users/1">Delete User</button>
10281028
```
10291029

1030-
Each attribute combines the URL and HTTP method.
1030+
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.
10311031

10321032
#### Common Patterns
10331033

www/src/content/reference/01-attributes/30-hx-action.md

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: "hx-action"
33
description: "Specifies the URL to receive the request"
44
---
55

6-
The `hx-action` attribute specifies the URL that will receive the request.
6+
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.
77

88
## Syntax
99

@@ -13,22 +13,78 @@ The `hx-action` attribute specifies the URL that will receive the request.
1313
</button>
1414
```
1515

16+
## Progressive Enhancement
17+
18+
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 `<a>` and `<form>`, `hx-action` gives you full control over those behaviors and works on any element.
19+
20+
```html
21+
<!-- Works natively AND with htmx -->
22+
<form hx-action="/contacts" method="post" hx-target="#results">
23+
<input name="name" required>
24+
<button>Create Contact</button>
25+
</form>
26+
```
27+
28+
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:
29+
30+
```html
31+
<form action="/contacts/new" <!-- full page (no-JS fallback) -->
32+
hx-action="/fragments/contacts/new" <!-- separate partial endpoint -->
33+
method="post"
34+
hx-target="#contact-list">
35+
<input name="name" required>
36+
<button>Create</button>
37+
</form>
38+
```
39+
40+
## Method Resolution
41+
42+
When `hx-action` is used without [`hx-method`](/reference/attributes/hx-method), htmx resolves the HTTP method using the same fallback chain browsers use:
43+
44+
1. [`hx-method`](/reference/attributes/hx-method) attribute on the element
45+
2. `formmethod` attribute on the submitter button
46+
3. Native `method` attribute on the form
47+
4. Defaults to `GET`
48+
49+
This means submitter buttons with `formmethod` work as expected:
50+
51+
```html
52+
<form hx-action="/contacts/123" method="get" hx-target="#detail">
53+
<button>View</button>
54+
<button formmethod="delete">Delete</button>
55+
</form>
56+
```
57+
58+
Clicking "View" sends a GET, clicking "Delete" sends a DELETE — matching native form semantics.
59+
60+
## When to Use
61+
62+
| Pattern | Use case |
63+
|---------|----------|
64+
| `hx-get`, `hx-post`, etc. | You know the method at authoring time and don't need native fallback |
65+
| `hx-action` + `hx-method` | Method is dynamic (e.g. server-rendered) or you want a clear separation of URL and verb |
66+
| `hx-action` alone | Progressive enhancement — method from native attributes, with option to use a separate AJAX endpoint |
67+
| `hx-boost` | Progressive enhancement with page-navigation defaults (target body, push URL) |
68+
1669
## Notes
1770

1871
* `hx-action` is typically used with [`hx-method`](/reference/attributes/hx-method) to specify both the URL and HTTP method
1972
* The shorthand attributes like [`hx-get`](/reference/attributes/hx-get), [`hx-post`](/reference/attributes/hx-post), etc. combine both URL
2073
and method
21-
* Use `hx-action` + `hx-method` when you need dynamic method selection
2274

2375
## Examples
2476

2577
```html
26-
<!-- Using hx-action with hx-method -->
78+
<!-- Server-rendered dynamic method -->
79+
<button hx-action="/api/users/123" hx-method="${method}">
80+
${action_label}
81+
</button>
82+
83+
<!-- Equivalent to hx-post shorthand -->
2784
<button hx-action="/api/users" hx-method="post">
2885
Create User
2986
</button>
30-
31-
<!-- Equivalent using hx-post shorthand -->
87+
<!-- same as -->
3288
<button hx-post="/api/users">
3389
Create User
3490
</button>

www/src/content/reference/01-attributes/31-hx-method.md

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: "hx-method"
33
description: "Specifies the HTTP method for the request"
44
---
55

6-
The `hx-method` attribute specifies the HTTP method (verb) to use for the request.
6+
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.
77

88
## Syntax
99

@@ -13,29 +13,42 @@ The `hx-method` attribute specifies the HTTP method (verb) to use for the reques
1313
</button>
1414
```
1515

16+
## Priority
17+
18+
`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:
19+
20+
```html
21+
<!-- Always sends PUT regardless of native method or submitter -->
22+
<form hx-action="/items/1" hx-method="put" method="post">
23+
<input name="name">
24+
<button>Save</button>
25+
</form>
26+
```
27+
28+
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.
29+
1630
## Notes
1731

18-
* Valid values: `get`, `post`, `put`, `patch`, `delete`
19-
* If no method is specified, defaults to `get`
20-
* `hx-method` is typically used with [`hx-action`](/reference/attributes/hx-action)
21-
* The shorthand attributes like [`hx-get`](/reference/attributes/hx-get), [`hx-post`](/reference/attributes/hx-post), etc. can be used
22-
instead
32+
* Valid values: `get`, `post`, `put`, `patch`, `delete` (case-insensitive)
33+
* If no method can be determined from any source, defaults to `GET`
34+
* The shorthand attributes [`hx-get`](/reference/attributes/hx-get), [`hx-post`](/reference/attributes/hx-post), etc. combine URL and method into one attribute
2335

2436
## Examples
2537

2638
```html
27-
<!-- Explicit method specification -->
39+
<!-- Explicit method -->
2840
<button hx-action="/api/users/123" hx-method="delete">
2941
Delete User
3042
</button>
3143

32-
<!-- Equivalent using hx-delete shorthand -->
44+
<!-- Equivalent shorthand -->
3345
<button hx-delete="/api/users/123">
3446
Delete User
3547
</button>
3648

37-
<!-- Default to GET if no method specified -->
38-
<button hx-action="/api/users">
39-
Get Users
40-
</button>
49+
<!-- Server-rendered dynamic method -->
50+
<form hx-action="/resources/${id}" hx-method="${method}" hx-target="this">
51+
<input name="name">
52+
<button>${action_label}</button>
53+
</form>
4154
```

0 commit comments

Comments
 (0)