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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- **Fetch:** add the `historyMode` option and keep the `src` separation on popstate ([#656](https://github.com/studiometa/ui/pull/656))
- **Fetch:** report `fetch.file-not-uploaded` when a file control cannot be sent as a file ([#656](https://github.com/studiometa/ui/pull/656))

### Changed

- ⚠️ **Fetch:** replace the `url` and `requestInit` event payload fields with one progressive lifecycle detail ([#657](https://github.com/studiometa/ui/pull/657))
- ⚠️ **Fetch:** drop the raw `Response` from the `fetch-response` payload ([#657](https://github.com/studiometa/ui/pull/657))

### Fixed

- **Fetch:** send a form submission's submitter and its `formaction`, `formmethod` and `formenctype` ([#656](https://github.com/studiometa/ui/pull/656))
- **Fetch:** await the DOM update so `fetch-update-after` settles it and a failed update reaches `fetch-error` ([#657](https://github.com/studiometa/ui/pull/657))

## [v2.0.0-alpha.0](https://github.com/studiometa/ui/compare/1.11.1..2.0.0-alpha.0) (2026-09-03)

Expand Down
26 changes: 25 additions & 1 deletion packages/docs/.vitepress/reference/public-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ export const publicContractSymbols = [
status: 'stable',
},
{
name: 'FetchEventBase',
name: 'FetchLifecycleDetail',
kind: 'type',
package: 'npm:@studiometa/ui',
importPath: '@studiometa/ui',
Expand All @@ -390,6 +390,30 @@ export const publicContractSymbols = [
href: '/reference/items/Fetch/js-api',
status: 'stable',
},
{
name: 'FetchRequestDetail',
kind: 'type',
package: 'npm:@studiometa/ui',
importPath: '@studiometa/ui',
href: '/reference/items/Fetch/js-api',
status: 'stable',
},
{
name: 'FetchResponseDetail',
kind: 'type',
package: 'npm:@studiometa/ui',
importPath: '@studiometa/ui',
href: '/reference/items/Fetch/js-api',
status: 'stable',
},
{
name: 'FetchShopifyPartialDetail',
kind: 'type',
package: 'npm:@studiometa/ui',
importPath: '@studiometa/ui',
href: '/reference/items/FetchShopifyPartial/js-api',
status: 'preview',
},
{
name: 'FetchShopifyPartialProps',
kind: 'type',
Expand Down
4 changes: 3 additions & 1 deletion packages/docs/migration-guides/1.0-2.0/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,8 @@ In v1 every `detail` was an array of the positional arguments. In v2 it is the p

This includes components whose payload was already an object: `Fetch` and `Draggable` were `[{ … }]` in v1 and are `{ … }` in v2.

`Fetch` also changes what that object holds: the `url` and `requestInit` fields are replaced by one plain `request` description, and the update events gain the response status and headers. See [the event detail](/reference/items/Fetch/js-api#the-event-detail).

| Component | Event | v1.x `detail` | v2.x `detail` |
| ----------------- | ---------------------------------- | ------------------------ | -------------------------- |
| `Carousel` | `progress` | `[progress]` | `{ progress }` |
Expand All @@ -654,7 +656,7 @@ This includes components whose payload was already an object: `Fetch` and `Dragg
| `DisclosureGroup` | `disclosure-group-open` / `-close` | `[item, index]` | `{ item, index }` |
| `DisclosureGroup` | `disclosure-group-change` | `[openItems]` | `{ items }` |
| `Draggable` | `drag-*` | `[props]` | `props` |
| `Fetch` | `fetch-*` | `[{ instance, url, … }]` | `{ instance, url, … }` |
| `Fetch` | `fetch-*` | `[{ instance, url, … }]` | `{ instance, request, … }` |
| `Indexable` | `index` | `[index]` | `{ index }` |
| `Prefetch` | `prefetched` | `[url]` | `{ url }` |
| `Sentinel` | `intersected` | `[entries]` | `{ isInView, entry }` |
Expand Down
6 changes: 3 additions & 3 deletions packages/docs/reference/items/Fetch/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ Use the [`Action`](../Action/index.md) and [`Transition`](../Transition/index.md
href="/"
data-component="Fetch Action"
data-option-history
data-on:before-fetch="Transition(#foo) -> transition.enter()"
data-on:after-fetch="Transition(#foo) -> transition.leave()"
data-on:fetch-before="Transition(#foo) -> transition.enter()"
data-on:fetch-after="Transition(#foo) -> transition.leave()"
data-on:fetch-error="alert('error')">
Click me
</a>
Expand Down Expand Up @@ -223,7 +223,7 @@ The `Fetch` components catches request errors and emits a [`fetch-error` event](
```html [index.html] {3}
<main
data-component="Action"
data-on:fetch-error="alert(event.detail[0].error)">
data-on:fetch-error="alert(event.detail.error)">
<a href="/" data-component="Fetch">Home</a>
</main>
```
Expand Down
153 changes: 99 additions & 54 deletions packages/docs/reference/items/Fetch/js-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,120 +336,165 @@ Every one is a development-only warning on the [toolkit diagnostic channel](http

All events from the `Fetch` component bubble up the DOM tree, so they can be listened to from any parent element.

### The event detail

Every `fetch-*` event carries a detail of the same shape, holding the fields known at that point in the lifecycle. `event.detail` **is** that object, so a listener reads a field by path with nothing to unwrap.

```ts
interface FetchLifecycleDetail {
instance: Fetch;
request: {
url: string;
method: string;
searchParams: Record<string, string[]>;
};
response?: {
url: string;
status: number;
statusText: string;
ok: boolean;
redirected: boolean;
headers: Record<string, string>;
};
content?: string;
fragment?: Document;
}
```

- `instance` (`Fetch`): the `Fetch` instance emitting the event.
- `request.url` (`string`): the absolute URL the request is sent to.
- `request.method` (`string`): the HTTP method, uppercase.
- `request.searchParams` (`Record<string, string[]>`): the query, with every value each name carries. A repeated name — a checkbox group, a `<select multiple>` — keeps all of its values, which is why each name maps to a list.
- `response` (`object`): the response description, present once the request has returned one.
- `response.headers` (`Record<string, string>`): the response headers, names lowercase.
- `content` (`string`): the string extracted from the response body by the [`response` option](#response), present once the body has been read.
- `fragment` (`Document`): `content` parsed with a [`DOMParser`](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser), present once the update starts.

Three events add one field of their own: [`fetch-after`](#fetch-after) and [`fetch-error`](#fetch-error) carry `error`, [`fetch-abort`](#fetch-abort) carries `reason`.

Everything except `instance` and `fragment` is plain data — no `URL`, no `Headers`, no `RequestInit`, no getters — so a declarative consumer resolves any field by path, with nothing to import:

<!-- prettier-ignore-start -->
```html
<main
data-component="Action"
data-on:fetch-update-after="console.log(event.detail.response.headers['x-search-result-count'])">
<form action="/search" method="get" data-component="Fetch">
<input type="search" name="q" />
</form>
</main>
```
<!-- prettier-ignore-end -->

`response` describes the response, it is not the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). A body reads once, and the component reads it to produce `content`, so the object itself is never handed to listeners: one of them consuming the body would leave the component with nothing to inject.

### Event order

A successful request emits, in this order:

| Order | Event | Detail added |
| ----- | --------------------------------------------- | ---------------- |
| 1 | [`fetch-before`](#fetch-before) | `request` |
| 2 | [`fetch-fetch`](#fetch-fetch) | |
| 3 | [`fetch-response`](#fetch-response) | `response` |
| 4 | [`fetch-after`](#fetch-after) | `content` |
| 5 | [`fetch-update-before`](#fetch-update-before) | |
| 6 | [`fetch-update`](#fetch-update) | `fragment` |
| 7 | [`dom-update`](#dom-update) | _protocol event_ |
| 8 | [`fetch-update-after`](#fetch-update-after) | |

The promise returned by [`fetch()`](#fetch-url-url-string-requestinit-requestinit-context-fetchrequestcontext) resolves after `fetch-update-after`, so awaiting it means every swap has settled.

A failed request replaces steps 4 to 8 with `fetch-after` carrying `error` instead of `content`, then [`fetch-error`](#fetch-error). `fetch-response` is emitted only when a response came back, so a network failure goes straight from `fetch-fetch` to `fetch-after`.

A failed update — a rejected swap, a rejected [`dom-update`](#the-dom-update-protocol-event) runner — emits `fetch-error` in place of `fetch-update-after`, carrying the `content` and the `fragment` it was applying. It does not emit a second `fetch-after`: the request succeeded, the update did not.

[`fetch-abort`](#fetch-abort) is emitted whenever the request in flight is aborted, which happens when a new request starts on the same instance or when [`abort()`](#abort-reason-any) is called.

### `fetch-before`

Emitted before the fetch request is sent.

**Payload**
**Detail**

- `ctx` (`Object`): context for the event with the following properties
- `instance` (`Fetch`): the `Fetch` instance emitting the event
- `url` (`URL`): the URL that will be fetched
- `requestInit` ([`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)): options for the `fetch` call
- `instance`, `request`.

### `fetch-fetch`

Emitted when the fetch request is sent.

**Payload**
**Detail**

- `ctx` (`Object`): context for the event with the following properties
- `instance` (`Fetch`): the `Fetch` instance emitting the event
- `url` (`URL`): the URL that will be fetched
- `requestInit` ([`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)): options for the `fetch` call
- `instance`, `request`.

### `fetch-response`

Emitted when the fetch request returned a response, before extracting its body, and before throwing if `response.ok !== true`.

**Payload**
**Detail**

- `ctx` (`Object`): context for the event with the following properties
- `response` (`Response`): the `Response` object returned by the `fetch` request
- `instance` (`Fetch`): the `Fetch` instance emitting the event
- `url` (`URL`): the URL that will be fetched
- `requestInit` ([`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)): options for the `fetch` call
- `instance`, `request`, `response`.

### `fetch-after`

Emitted after the fetch request is finished, whether it is successful or not.

**Payload**
**Detail**

- `ctx` (`Object`): context for the event with the following properties
- `instance` (`Fetch`): the `Fetch` instance emitting the event
- `url` (`URL`): the URL that was fetched
- `requestInit` ([`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)): options for the `fetch` call
- `content` (`string | void`): the content of the response if the request succeeded
- `instance`, `request`, `response` when a response came back, and either `content` when the request succeeded or `error` when it failed.

### `fetch-update-before`

Emitted before the DOM is updated.

**Payload**
**Detail**

- `ctx` (`Object`): context for the event with the following properties
- `instance` (`Fetch`): the `Fetch` instance emitting the event
- `url` (`URL`): the URL that was fetched
- `requestInit` ([`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)): options for the `fetch` call
- `content` (`string`): the content of the response
- `instance`, `request`, `response`, `content`.

### `fetch-update`

Emitted when the DOM is updated.

**Payload**
**Detail**

- `ctx` (`Object`): context for the event with the following properties
- `instance` (`Fetch`): the `Fetch` instance emitting the event
- `url` (`URL`): the URL that was fetched
- `requestInit` ([`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)): options for the `fetch` call
- `document` (`Document`): the content of the response, parsed with a [DOMParse](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser)
- `instance`, `request`, `response`, `content`, `fragment`.

### `dom-update`

Emitted after the [`fetch-update` event](#fetch-update), right before the fetched content is applied to the DOM. Unlike the `fetch-*` events, this is a shared protocol event announcing an imminent DOM change — see [the `dom-update` protocol event](#the-dom-update-protocol-event).

**Detail**

The event `detail` is a bare object (not an argument array) with the following property:
The event `detail` carries the same fields as `fetch-update`, plus the one the protocol is made of:

- `wrap` (`(runner: DomUpdateRunner) => void`): registers a runner or transitioner that substitutes the default update path

### `fetch-update-after`

Emitted when the DOM has been updated.
Emitted when the DOM has been updated and every swap has settled.

**Payload**
**Detail**

- `ctx` (`Object`): context for the event with the following properties
- `instance` (`Fetch`): the `Fetch` instance emitting the event
- `url` (`URL`): the URL that was fetched
- `requestInit` ([`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)): options for the `fetch` call
- `document` (`Document`): the content of the response, parsed with a [DOMParse](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser)
- `instance`, `request`, `response`, `content`, `fragment`.

### `fetch-error`

Emitted when the fetch request failed.
Emitted when the fetch request failed, or when the DOM update failed.

**Payload**
**Detail**

- `instance`, `request`, everything the lifecycle had learned when it failed, and:
- `error` (`Error`): the error thrown by the failing request or the failing update

- `ctx` (`Object`): context for the event with the following properties
- `instance` (`Fetch`): the `Fetch` instance emitting the event
- `url` (`URL`): the URL that was fetched
- `requestInit` ([`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)): options for the `fetch` call
- `error` (`Error`): the error object thrown by the failing request
A failed request carries `response` when one came back and nothing else: there was no content to apply. A failed update carries `response`, `content` and `fragment`, which is what was being applied when it failed.

### `fetch-abort`

Emitted when the fetch request has been aborted.

**Payload**
**Detail**

- `ctx` (`Object`): context for the event with the following properties
- `instance` (`Fetch`): the `Fetch` instance emitting the event
- `url` (`URL`): the URL that was fetched
- `requestInit` ([`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)): options for the `fetch` call
- `instance`, `request`, and:
- `reason` (`any`): the reason the request was aborted

## The `dom-update` protocol event
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/reference/items/Fetch/stories/abort/app.twig
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
data-component="Action"
data-on:fetch-before="Transition(#loader) -> target.enter()"
data-on:fetch-after="Transition(#loader) -> target.leave()"
data-on:fetch-abort="DataBind(#toaster) -> target.value += `${event.detail[0].reason}\n`"
data-on:fetch-abort="DataBind(#toaster) -> target.value += `${event.detail.reason}\n`"
class="flex flex-col gap-4">
<nav class="flex items-center gap-4">
{{
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/reference/items/Fetch/stories/error/app.twig
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
data-component="Action"
data-on:fetch-before="Transition(#loader) -> target.enter()"
data-on:fetch-after="Transition(#loader) -> target.leave()"
data-on:fetch-error="DataBind(#toaster) -> target.value += event.detail[0].error.message + '\n'"
data-on:fetch-error="DataBind(#toaster) -> target.value += event.detail.error.message + '\n'"
class="flex flex-col gap-8 max-w-xl">
<nav class="flex items-center gap-4">
{{
Expand Down
4 changes: 2 additions & 2 deletions packages/docs/reference/items/FetchShopifyPartial/js-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The package is loaded lazily on the first request, so it never needs to be bundl

`FetchShopifyPartial` emits the same [events as `Fetch`](../Fetch/js-api.md#events), with two differences on the partial rendering path:

- the [`fetch-response` event](../Fetch/js-api.md#fetch-response) is **not** emitted, as there is no `Response` object to expose;
- the [`fetch-update` event](../Fetch/js-api.md#fetch-update) payload carries the opaque partials `update` object (as `event.detail[0].update`) instead of a parsed `Document` fragment.
- the [`fetch-response` event](../Fetch/js-api.md#fetch-response) is **not** emitted, and no event carries a `response` description, as there is no `Response` on this path;
- every event carries the opaque partials `update` object (as `event.detail.update`) instead of the `content` string and the parsed `fragment`, which do not exist on this path.

On the fallback path, all events — including `fetch-response` — behave exactly like the base [`Fetch`](../Fetch/js-api.md#events) component.
4 changes: 3 additions & 1 deletion packages/docs/reference/items/FetchShopifySection/js-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Overrides the base [`fetch`](../Fetch/js-api.md#fetch-url-string-requestinit-req

Unwraps the Section Rendering JSON object (`{ [id]: html }`) into the concatenated section HTML, dropping any section returned as `null` through `filter(Boolean)`. Each section is then swapped in place by the inherited [`[id]` selector](../Fetch/js-api.md#selector). The unwrap is skipped — deferring to the base [`Fetch`](../Fetch/js-api.md), which evaluates the [`response`](#response) option — when no `sections` are configured (a normal HTML page is requested) or when a custom `response` option is supplied.

### `update(url, requestInit, content)`
### `update(url, requestInit, content, detail)`

Overrides the base `update` to remove the `sections` parameter from the URL before delegating to `Fetch`, so — when the [`history` option](../Fetch/js-api.md#history) is enabled — the address bar reflects the human-facing page and not the raw Section Rendering endpoint.

The stripping is a history concern only. The [event detail](../Fetch/js-api.md#the-event-detail) describes the request that was actually made, so `event.detail.request.url` carries the Section Rendering endpoint on every event.
Loading
Loading