# fetchable-element

Composition base that owns the `fetch()` lifecycle for Neutron elements —
build the request from attributes, a `<form>`, or a custom override, then
track loading / success / error state automatically.

## Features

- **Shared lifecycle** States, `provision` and events come from [loadable-element](/packages/loadable-element)

- **Attribute-driven requests** URL, method, headers, redirect, and
  credentials all configurable declaratively
- **Form-aware** Point `form-ref` at a `<form>` to source action, method,
  enctype, and field values
- **Merged payloads** Attributes, form, and custom args deep-merge
  (lowest → highest priority)
- **Lifecycle state** `is-loading` / `is-success` / `is-error` managed
  for you
- **Provision** `provision` is the parsed response (or error payload) for
  Quark `prop("provision")` — not a reflected attribute
- **Cancel-safe** Superseded or disconnected requests are aborted via
  `AbortableElement`

## Installation


`@excom/fetchable-element` v0.3.0

```bash
pnpm add @excom/fetchable-element
```

```bash
npm install @excom/fetchable-element
```

```bash
yarn add @excom/fetchable-element
```

### Import

```ts
import { /* … */ } from "@excom/fetchable-element";
```



## Usage

Compose `FetchableElement`, then call `doFetch([url, requestInit])` —
usually built via `getFetchArgs(customFetchArgs?)` — whenever the
subclass decides a request should run. Concrete consumers include
`<provider-fetch>` (fetch on attribute change), `<super-form>` (fetch on
submit), and `<web-authn>` (WebAuthn ceremonies that still round-trip to
a server).

```ts
import { Neutron } from "@excom/neutron";
import { FetchableElement } from "@excom/fetchable-element";

export const RefreshOnClick = Neutron.compose([
  FetchableElement,
  Neutron({ tag: "refresh-on-click" }),
])
  .onEvent("click", ({ getFetchArgs }) => ({
    doFetch: [getFetchArgs()],
  }));

RefreshOnClick.define();
```

```html
<refresh-on-click api-url="/api/status"></refresh-on-click>
```

Every prop, state field, and event documented below is inherited
verbatim by any element that composes `FetchableElement` — it flattens
directly into that element's own generated docs, so `<provider-fetch>`
and friends don't redeclare it.

### API Reference


#### Attributes

| Name | Surface | Type | Default | Values | Description | Inherited from |
| --- | --- | --- | --- | --- | --- | --- |
| `form-ref` | option | `string` |  | `<CSS Selector>` | CSS selector for a `<form>` to source the request from — its `action` (URL), `method`, `enctype` (Content-Type), and field values (as the JSON payload) all take priority over the matching attributes below. Omit to build the request entirely from attributes / custom `doFetch()` args. |  |
| `has-body` | option | `boolean` |  |  | Force a request body even for methods that don't imply one (`GET` / `HEAD`). Already implied for `POST` / `PUT` / `PATCH`. |  |
| `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. |  |
| `api-method` | option | `string` | `"GET"` |  | HTTP method. Always uppercased before the request is sent. |  |
| `header-accept` | option | `string` | `"application/json"` |  | `Accept` request header. |  |
| `header-content-type` | option | `string` | `"application/json"` |  | `Content-Type` request header. Dropped entirely when the request has no body. |  |
| `header-cache-control` | option | `string` |  |  | `Cache-Control` request header. Unset by default (browser default caching applies). |  |
| `fetch-redirect` | option | `string` |  | `"follow"` \| `"error"` \| `"manual"` | `RequestInit.redirect` mode. Unset defers to the browser default (`follow`). |  |
| `fetch-credentials` | option | `string` | `"include"` | `"omit"` \| `"same-origin"` \| `"include"` | `RequestInit.credentials` mode. |  |
| `is-loading` | state | `boolean` |  |  | A request is currently in flight. |  |
| `is-success` | state | `boolean` |  |  | The most recent request resolved successfully. Mutually exclusive with `is-loading` and `is-error`. |  |
| `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. |  |
| `did-load` | state | `boolean` |  |  | Work has succeeded and no failure followed. Stays set while a refresh loads, so content can stay on screen (stale-while-revalidate): gate it on `[did-load]` rather than `[is-success]`. Cleared on error. | `@excom/loadable-element` |

#### Provision

| Name | Type | Description |
| --- | --- | --- |
| `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. |

#### Fires

| Name | Type | Description |
| --- | --- | --- |
| `{tag}-loading` | `FetchableLoadingEvent` (`CustomEvent & { type: "{tag}-loading"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched immediately before the request is sent. |
| `{tag}-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`). |
| `{tag}-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. |



There are no demos for this package — see
[provider-fetch](/packages/provider-fetch) for `FetchableElement` in
action against a real endpoint.

## Release notes

### 0.3.0 (2026-10-07)

- Add hydration of prerendered pages: an identical GET or HEAD request is answered from the page's hydration island, as often as the prerender made it, so the element announces `loading` then `success` without a network request

### 0.2.0 (2026-09-30)

- Add `did-load` from `loadable-element`: set on the first success, kept while a refresh runs, cleared on an error
- Update failed-request logging to one line: an error status (400 or above) logs a warning, `<tag>: request failed` with the response, which the default `KitLogger` level hides; a network or parse failure logs an error; an abort logs nothing

### 0.1.2 (2026-09-23)

- Fix typo in error message.

Older releases: https://nucleus.excom.dev/packages/fetchable-element
