# super-form

Submit AJAX requests with HTML forms. Pair it with Quark to render the response.


```html
<section>
  <super-form>
    <form action="/api/echo" method="post">
      <input type="text" name="name" placeholder="Your name" required>
      <button type="submit">Submit</button>
    </form>
    <h4></h4>
    <span></span>
  </super-form>
  <quark-sheet>
    super-form[is-success] {
      $res: prop("provision");
      h4 { content: "Echoed name: #{$res.body.json.name}"; }
    }
    super-form {
      $res: prop("provision");
      span { content: "HTTP status code: #{$res.status}"; }
    }
  </quark-sheet>
  <style>
    #demo-super-form-simple super-form[is-loading] {
      animation: pulse 1s linear infinite;
      pointer-events: none;
    }
    @keyframes pulse {
      0% { opacity: 0.2; }
      50% { opacity: 0.6; }
      100% { opacity: 0.2; }
    }
  </style>
</section>
```


## Features

- **Makes AJAX requests** JSON payload is built from each input's `name` and `type` attributes
- **Progressively enhanced** Doesn't replace the native `form`; it enhances it. Everything you know about `form` and `input` still applies.
- **Provides data** Use Quark to render the response
- **Submit command** `--submit` submits programmatically (`<button command="--submit" commandfor="…">`)
- **Highly configurable** Headers, credentials, redirect, etc

## Installation


`@excom/super-form` v0.1.8

```bash
pnpm add @excom/super-form
```

```bash
npm install @excom/super-form
```

```bash
yarn add @excom/super-form
```

### Import

```ts
import "@excom/super-form";
```



## Usage

Just wrap a regular form. Form fields become a JSON payload via their `name`: dot-separated names nest, and a trailing `[]` collects same-named fields into an array. `input[type]` determines the type conversion.

```html
<super-form>
  <form action="/api/signup" method="post">
    <input name="isAvailable" type="checkbox"> <!-- -> { isAvailable: true } -->
    <input name="address.city" value="Anytown"> <!-- -> { address: { city: "Anytown" } } -->
    <input name="tags[]" value="smart">
    <input name="tags[]" value="kind"> <!-- -> { tags: ["smart", "kind"] } -->
    <button type="submit">Sign up</button>
  </form>
</super-form>
```

Hook the lifecycle state with CSS:

```css
super-form[is-loading] { /* form currently submitting, show loading spinner */ }
super-form[is-error]::before { content: "An error occurred." }
```

`did-load` is set on the first success, kept through a resubmit (where `is-loading` replaces `is-success`) and cleared on an error: gate content on `[did-load]` to keep it on screen during a resubmit.

Or Quark:

```quark
super-form[is-success] {
  $res: prop("provision").body;
  span { content: $res.json.email; }
}
```

### API Reference


#### Attributes

| Name | Surface | Type | Default | Values | Description | Inherited from |
| --- | --- | --- | --- | --- | --- | --- |
| `form-ref` | option | `string` | `":scope form"` | `<CSS Selector>` | CSS selector for the `<form>` to intercept. The form's `action` / `method` / `enctype` take priority over `api-url` / `api-method` below. Must be a descendant to be heard directly — point elsewhere and invoke the `--submit` command instead. |  |
| `has-body` | option | `boolean` |  |  | Force a request body even for methods that don't imply one (`GET` / `HEAD`). Already implied for `POST` / `PUT` / `PATCH`. | `@excom/fetchable-element` |
| `api-url` | option | `string` | `""` |  | Endpoint URL. When the request has no body, the JSON payload (from `form-ref` or custom `doFetch()` args) is merged in as query params instead. | `@excom/fetchable-element` |
| `api-method` | option | `string` | `"GET"` |  | HTTP method. Always uppercased before the request is sent. | `@excom/fetchable-element` |
| `header-accept` | option | `string` | `"application/json"` |  | `Accept` request header. | `@excom/fetchable-element` |
| `header-content-type` | option | `string` | `"application/json"` |  | `Content-Type` request header. Dropped entirely when the request has no body. | `@excom/fetchable-element` |
| `header-cache-control` | option | `string` |  |  | `Cache-Control` request header. Unset by default (browser default caching applies). | `@excom/fetchable-element` |
| `fetch-redirect` | option | `string` |  | `"follow"` \| `"error"` \| `"manual"` | `RequestInit.redirect` mode. Unset defers to the browser default (`follow`). | `@excom/fetchable-element` |
| `fetch-credentials` | option | `string` | `"include"` | `"omit"` \| `"same-origin"` \| `"include"` | `RequestInit.credentials` mode. | `@excom/fetchable-element` |
| `is-loading` | state | `boolean` |  |  | A request is currently in flight. | `@excom/fetchable-element` |
| `is-success` | state | `boolean` |  |  | The most recent request resolved successfully. Mutually exclusive with `is-loading` and `is-error`. | `@excom/fetchable-element` |
| `is-error` | state | `boolean` |  |  | The most recent request failed (status 400 or above, network error, or a thrown error other than `AbortError`). Fires with the `error` event. | `@excom/fetchable-element` |

#### Provision

| Name | Type | Description | Inherited from |
| --- | --- | --- | --- |
| `provision` | `FetchResponse` (`{ bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; }`) | Response payload on success, or error payload on failure. Success shape: `{ status, statusText, ok, headers, url, redirected, bodyUsed, type, body }`. Failure shape is either that same response shape (server responded with an error status) or `{ message, stack }` (request never completed). Not reflected as an attribute. | `@excom/fetchable-element` |

#### Fires

| Name | Type | Description | Inherited from |
| --- | --- | --- | --- |
| `super-form-submit` | `SuperFormSubmitEvent` (`CustomEvent & { type: "super-form-submit"; detail: [url: string, requestInit: RequestInit]; bubbles: true; cancelable: true; composed: true }`) | Internal — dispatched whenever a submit is about to run (real `submit` or the `--submit` command). Built by `getFetchArgs()`. Cancelable; default action calls `doFetch()`. |  |
| `super-form-loading` | `FetchableLoadingEvent` (`CustomEvent & { type: "{tag}-loading"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched immediately before the request is sent. | `@excom/fetchable-element` |
| `super-form-success` | `FetchableSuccessEvent` (`CustomEvent & { type: "{tag}-success"; detail: { bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; }; bubbles: true; cancelable: true; composed: true }`) | Dispatched when the request resolves successfully. `event.detail` is the parsed response (see `provision`). | `@excom/fetchable-element` |
| `super-form-error` | `FetchableErrorEvent` (`CustomEvent & { type: "{tag}-error"; detail: { bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; } \| { message: string; stack?: string }; bubbles: true; cancelable: true; composed: true }`) | Dispatched when the request fails — status 400 or above, network error, or a thrown error. `event.detail` is the error payload (see `provision`). Not dispatched for aborted requests. | `@excom/fetchable-element` |

#### Listens for

| Name | Type | Description |
| --- | --- | --- |
| `submit` | `SuperFormNativeSubmitEvent` (`SubmitEvent & { type: "submit"; bubbles: true; cancelable: true; composed: false; }`) | The default action of the `<form>` matched by `form-ref` (or any descendant `<form>`); prevented, then converted into `super-form-submit`. |

#### Commands

| Command | Action |
| --- | --- |
| `--submit` | Submits programmatically (`<button command="--submit" commandfor="…">`) — the only option when `form-ref` points to a form that isn't a descendant, since this element can't hear its `submit` event directly. |

#### Default actions

| Event | Default behavior (unless preventDefault() is called) |
| --- | --- |
| `super-form-submit` | Calls `doFetch([url, requestInit])` with the event's detail. |



### Examples

#### Comprehensive

This example shows loading state, error state, rendering, and triggering from outside the form.


```html
<section>
  <super-form id="demo-super-form-external" class="tag-article grid">
    <form action="/api/echo" method="post">
      <label>
        Nickname
        <input type="text" name="nickname" placeholder="John" required>
      </label>
    </form>
    <ul></ul>
    <ul></ul>
    <template id="demo-super-form-item">
      <li class="tag-code"></li>
    </template>
  </super-form>
  <button type="button" command="--submit" commandfor="demo-super-form-external">
    Save (outside the form)
  </button>
  <quark-sheet>
    @use "quark:util" as *;

    super-form[is-success] {
      $res: prop("provision");
      li { content: "#{index}: #{to-json(item)}"; }
      ul:first-of-type { content: iterate($res.body.json, "#demo-super-form-item"); }
      ul:last-of-type { content: iterate($res, "#demo-super-form-item"); }
    }
  </quark-sheet>
  <style>
    #demo-super-form-external-trigger { max-width: none; }
    #demo-super-form-external-trigger super-form {
      width: 100%;
      &[is-loading] {
        opacity: 0.5;
        border: 1px dashed yellow;
      }
      &[is-success] { border: 1px solid green; }
      &[is-error] {
        border: 1px solid red;
        &::before { content: "An error occurred."; }
      }
      ul:first-of-type::before { content: "Submitted JSON:"; }
      ul:last-of-type::before { content: "Response data:"; }
      li { display: block; }
    }
  </style>
</section>
```

## Release notes

### 0.1.1 (2026-09-23)

- Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources
