> For the complete documentation index, see [llms.txt](https://analog.groupthought.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://analog.groupthought.com/reference/alpine/async-form.md).

# x-async-form

Directives and a magic for forms that submit with fetch instead of a page reload, with a request state you can render.

Directives and a magic for forms that submit with fetch instead of a page reload, with a request state you can render.

| Directive / magic                  | What it does                                                                                                                                                                                                                 |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-async-form`                     | The root, on the `<form>` or a parent. Binds `:context` and `:form` automatically                                                                                                                                            |
| `x-async-form="save"`              | Custom submit callback. Receives the submission info and returns a `Response` promise, or a partial submission-info object for the built-in fetch                                                                            |
| `x-async-form.wait-success.2000ms` | How long the success state shows before the form returns to idle (default 1000ms)                                                                                                                                            |
| `x-async-form.wait-error.3000ms`   | How long the error state shows before the form returns to idle. Without it the error state stays until the next submission or `reset()`                                                                                      |
| `x-async-form:context`             | Holds the form state. Put it on a wrapper to read `$asyncForm` outside the form                                                                                                                                              |
| `x-async-form:form`                | The `<form>` itself. Hijacks submit and sends the form data with fetch                                                                                                                                                       |
| `$asyncForm`                       | The state, readable anywhere inside the context: `state` (`'idle'`, `'pending'`, `'success'`, `'error'`), `formData`, `response`, `parsedBody`, `errorMessage`, `errorDescription`, `signal`, `submitAsync(info)`, `reset()` |

Without a callback, the form fetches its `async-action` attribute if present, otherwise its `action`, using the form's `method` and `enctype`. `GET` puts fields in the query string. `enctype="application/json"` sends a JSON body. Without JavaScript the form submits normally to `action`, so the page keeps working. A new submission cancels the pending request.

A 200 response means success. Any other status sets `errorMessage` from the body's `message` (or the status text) and `errorDescription` from the body's `description`, `error`, or `errors`. Field errors like `{email: ["is invalid"]}` flatten to "Email is invalid".

The form also dispatches `form:pending` (detail: the form data), `form:success` (detail: the parsed body), and `form:error` (detail: `{ message, description }`).

## Examples

### A basic async form

Use `x-async-form` on a `<form>` and render `$asyncForm.state` while the request runs:

```html
<form x-async-form action="{{ routes.cart_url }}/shipping_rates.json" method="GET">
  <select name="shipping_address[country]">…</select>
  <button type="submit" :disabled="$asyncForm.state === 'pending'">
    <span x-show="$asyncForm.state !== 'pending'">Estimate shipping</span>
    <span x-show="$asyncForm.state === 'pending'">Estimating…</span>
  </button>
</form>
```

### Reading the response

Use `$asyncForm.parsedBody` to render the response body. The shipping estimate block lists the returned rates this way:

```html
<template x-if="$asyncForm.parsedBody?.shipping_rates?.length">
  <ul>
    <template x-for="rate in $asyncForm.parsedBody?.shipping_rates">
      <li x-text="`${rate.name} — ${rate.price}`"></li>
    </template>
  </ul>
</template>
```

### Showing errors

Use `$asyncForm.errorMessage` for the summary and `$asyncForm.parsedBody` for per-field messages:

```html
<div role="alert" x-show="$asyncForm.state === 'error'" x-cloak>
  <p x-text="$asyncForm.errorMessage"></p>
</div>

<div :class="{ 'vars-scheme-error': $asyncForm.parsedBody?.zip }">
  <input type="text" name="shipping_address[zip]">
  <p x-show="$asyncForm.parsedBody?.zip" x-text="[$asyncForm.parsedBody?.zip ?? []].flat().join(', ')"></p>
</div>
```

### Splitting the context from the form

Use `x-async-form:context` on a wrapper and `x-async-form:form` on the form to render results outside the form element:

```html
<div x-async-form:context>
  <form x-async-form:form action="{{ routes.cart_url }}/shipping_rates.json" method="GET">
    …
  </form>

  <p x-show="$asyncForm.state === 'success'">Rates found.</p>
</div>
```

The bare `x-async-form` on a non-form element does this split automatically. It becomes the context and binds `:form` to the first `<form>` inside.

### A custom submit callback

Pass a function to `x-async-form` to run the request yourself:

```html
<form
  x-data="{
    async save(info) {
      const response = await fetch('/cart/add.js', { method: 'POST', body: info.formData })
      if (response.status === 422) {
        const body = await response.json()
        if (body.message) throw new Error(body.message)
      }
      return response
    }
  }"
  x-async-form="save"
>
  …
</form>
```

The callback can also return a partial submission-info object (`({ formData }) => ({ action: '/other/url.js', method: 'POST', formData })`) and the plugin runs the fetch for it. A thrown error becomes `errorMessage`.

## Customizing

`x-product-form`, `x-cart-form`, and `x-cart-item` wrap this plugin for Shopify cart endpoints. Use them instead of a custom callback when possible. See [x-product-form](/reference/alpine/product-form.md), [x-cart-form](/reference/alpine/cart-form.md), [x-cart-item](/reference/alpine/cart-item.md), and [Cart](/features/cart.md) for the merchant-facing behavior.
