Skip to content
Open
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
37 changes: 13 additions & 24 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions test/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
<!-- ============================================ -->
<!-- Attribute Tests -->
<!-- ============================================ -->
<script src="./tests/attributes/hx-action.js"></script>
<script src="./tests/attributes/hx-boost.js"></script>
<script src="./tests/attributes/hx-config.js"></script>
<script src="./tests/attributes/hx-confirm.js"></script>
Expand Down
56 changes: 56 additions & 0 deletions test/tests/attributes/hx-action.js
Original file line number Diff line number Diff line change
@@ -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('<button hx-action="/test">Click Me!</button>')
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('<button hx-action="/test" hx-method="post">Click Me!</button>')
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 hx-action="/test" hx-swap="outerHTML" method="post"><button>Submit</button></form>')
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('<form hx-action="/test" hx-swap="outerHTML" method="get"><button id="b1" formmethod="post">Submit</button></form>')
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 hx-action="/test" hx-method="put" hx-swap="outerHTML" method="post"><button>Submit</button></form>')
form.requestSubmit()
await forRequest()
fetchMock.calls[0].request.method.should.equal('PUT')
playground().innerHTML.should.equal('Put!')
})

})
6 changes: 3 additions & 3 deletions www/src/content/docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -1027,7 +1027,7 @@ Use different attributes for different operations:
<button hx-delete="/users/1">Delete User</button>
```

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

Expand Down
66 changes: 61 additions & 5 deletions www/src/content/reference/01-attributes/30-hx-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

## 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 `<a>` and `<form>`, `hx-action` gives you full control over those behaviors and works on any element.

```html
<!-- Works natively AND with htmx -->
<form hx-action="/contacts" method="post" hx-target="#results">
<input name="name" required>
<button>Create Contact</button>
</form>
```

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
<form action="/contacts/new" <!-- full page (no-JS fallback) -->
hx-action="/fragments/contacts/new" <!-- separate partial endpoint -->
method="post"
hx-target="#contact-list">
<input name="name" required>
<button>Create</button>
</form>
```

## 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
<form hx-action="/contacts/123" method="get" hx-target="#detail">
<button>View</button>
<button formmethod="delete">Delete</button>
</form>
```

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
<!-- Using hx-action with hx-method -->
<!-- Server-rendered dynamic method -->
<button hx-action="/api/users/123" hx-method="${method}">
${action_label}
</button>

<!-- Equivalent to hx-post shorthand -->
<button hx-action="/api/users" hx-method="post">
Create User
</button>

<!-- Equivalent using hx-post shorthand -->
<!-- same as -->
<button hx-post="/api/users">
Create User
</button>
Expand Down
37 changes: 25 additions & 12 deletions www/src/content/reference/01-attributes/31-hx-method.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

## 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
<!-- Always sends PUT regardless of native method or submitter -->
<form hx-action="/items/1" hx-method="put" method="post">
<input name="name">
<button>Save</button>
</form>
```

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
<!-- Explicit method specification -->
<!-- Explicit method -->
<button hx-action="/api/users/123" hx-method="delete">
Delete User
</button>

<!-- Equivalent using hx-delete shorthand -->
<!-- Equivalent shorthand -->
<button hx-delete="/api/users/123">
Delete User
</button>

<!-- Default to GET if no method specified -->
<button hx-action="/api/users">
Get Users
</button>
<!-- Server-rendered dynamic method -->
<form hx-action="/resources/${id}" hx-method="${method}" hx-target="this">
<input name="name">
<button>${action_label}</button>
</form>
```
Loading