diff --git a/CHANGELOG.md b/CHANGELOG.md index 435a03d23..66988baac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/packages/docs/.vitepress/reference/public-contracts.ts b/packages/docs/.vitepress/reference/public-contracts.ts index b0f125f80..1558bbbc7 100644 --- a/packages/docs/.vitepress/reference/public-contracts.ts +++ b/packages/docs/.vitepress/reference/public-contracts.ts @@ -367,7 +367,7 @@ export const publicContractSymbols = [ status: 'stable', }, { - name: 'FetchEventBase', + name: 'FetchLifecycleDetail', kind: 'type', package: 'npm:@studiometa/ui', importPath: '@studiometa/ui', @@ -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', diff --git a/packages/docs/migration-guides/1.0-2.0/index.md b/packages/docs/migration-guides/1.0-2.0/index.md index b01718665..a06f6eb86 100644 --- a/packages/docs/migration-guides/1.0-2.0/index.md +++ b/packages/docs/migration-guides/1.0-2.0/index.md @@ -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 }` | @@ -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 }` | diff --git a/packages/docs/reference/items/Fetch/index.md b/packages/docs/reference/items/Fetch/index.md index 725e63d2b..489d7a4f3 100644 --- a/packages/docs/reference/items/Fetch/index.md +++ b/packages/docs/reference/items/Fetch/index.md @@ -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 @@ -223,7 +223,7 @@ The `Fetch` components catches request errors and emits a [`fetch-error` event]( ```html [index.html] {3}
+ data-on:fetch-error="alert(event.detail.error)"> Home
``` diff --git a/packages/docs/reference/items/Fetch/js-api.md b/packages/docs/reference/items/Fetch/js-api.md index 8be631249..36d2332e3 100644 --- a/packages/docs/reference/items/Fetch/js-api.md +++ b/packages/docs/reference/items/Fetch/js-api.md @@ -336,75 +336,128 @@ 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; + }; + response?: { + url: string; + status: number; + statusText: string; + ok: boolean; + redirected: boolean; + headers: Record; + }; + 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`): the query, with every value each name carries. A repeated name — a checkbox group, a ` + + +``` + + +`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` @@ -412,44 +465,36 @@ Emitted after the [`fetch-update` event](#fetch-update), right before the fetche **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 diff --git a/packages/docs/reference/items/Fetch/stories/abort/app.twig b/packages/docs/reference/items/Fetch/stories/abort/app.twig index 20518b822..ded1ffd68 100644 --- a/packages/docs/reference/items/Fetch/stories/abort/app.twig +++ b/packages/docs/reference/items/Fetch/stories/abort/app.twig @@ -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">