Fetch JS
The Fetch component adds AJAX capabilities to your existing HTML, with support for the View Transition API to transition between states.
Usage
The Fetch component can be used to perform GET or POST requests without reloading the page.
import { registerComponents } from '@studiometa/js-toolkit';
import { Fetch } from '@studiometa/ui';
registerComponents(Fetch);Basic
Given the following HTML:
<a data-component="Fetch" href="/some-content">Click me</a>
<div id="content"></div>And the following response from the /some-content endpoint:
<div id="content">
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
</div>
Some extra content.Clicking on the link will dispatch a background fetch request and will replace the <div id="content"></div> element with the new content from the fetch response. The page will have the following HTML after:
<a data-component="Fetch" href="/some-content">Click me</a>
<div id="content">
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
</div>💡 Important
We use id attributes to detect which content from the response should be used and injected in the DOM. Any content from the response not nested in a parent with an id attribute will be discarded.
A click with a modifier key, a middle click and a link with a target other than _self are left to the browser. A link without a target of its own takes the target of the <base> element, as natively.
From any element
The Fetch component is not limited to <a> and <form> elements. On any other element, set the src option to the endpoint to request, and call the fetch() method, for example from an event through the Action component.
<div data-component="Action Fetch" data-option-src="/some-content" data-on:click="Fetch.fetch()">
Click me
</div>
<div id="content"></div>import { registerComponents } from '@studiometa/js-toolkit';
import { Action, Fetch } from '@studiometa/ui';
registerComponents(Action, Fetch);Such an element has no destination of its own: fetch() requests src, with the params option, and writes no history. Give fetch() a destination, such as Fetch.fetch('/other-content'), to request a page and write it to history.
Back and forward navigation
Set the history option to write the destination of each request to the browser history. Back and forward navigation then restore the content of each entry.
<a href="/projects?page=2" id="pager" data-component="Fetch" data-option-history>2</a>popstate listener of the page. It reads the recipe of the entry, finds who runs the restore, rebuilds one GET request from the restored URL and swaps the regions that the entries of the page name, so a region that a later entry changed comes back too. The owner is found by the id of the element that wrote the entry.- One owner per page. One history coordinator holds the only
popstatelistener of the page. TwoFetchelements withhistorynever race on one back navigation: it sends one request. - The entry keeps a recipe. Each entry stores plain data under the
fetchkey ofhistory.state: the regions to swap, the swap mode, theparamsandsrcoptions, and theidof the element that wrote it. Back rebuilds the request from the restored URL and that recipe. It works after the element has left the page. See history entries. - Push by default. Each request adds an entry, as a native navigation does. A GET to the URL the page already shows replaces the current entry, as natively. Set
historyModetoreplacefor a live search: the request replaces the current entry, so back leaves the search instead of going back one keystroke. - Entries of other scripts are kept. An entry without a
fetchkey is ignored, and the keys that other scripts put inhistory.stateare kept. - Anchors cost nothing. Back to an entry that differs from the content on the page only by its hash sends no request, as natively.
- A POST writes history only after a redirect. The entry is then the page the server redirected to, which back restores with a GET. A POST without a redirect writes nothing.
- Give the element an
id. The events of a restore reach the element that wrote the entry only when it has anid. Without one, they reach the first element that theselectorof the entry matches, and bubble from there. A listener on a wrapper of that region still sees the restore.
A lighter page: params or src
The address bar always shows the destination: the href of a link or the action of a form. The URL that is requested is derived from it, so back and forward navigation can rebuild it from the restored URL alone. Two options change it:
paramsadds query parameters to the page itself. Use it to request a lighter copy of the same page: pagination, Shopify sections, aview=fragmenttemplate.srcis a fixed endpoint. It gives the origin and the path, and the query of the destination is folded on. Use it for another endpoint, such as a live search that requests a search service.
- Origin and path: from
srcwhen it is set, otherwise from the destination. - Query: the query of
src, then the query of the destination folded on. Withoutsrc, the query of the destination. paramsare set last, so they win.
params: a lighter copy of the same pagepagination, Shopify sections, view=fragmentsrc: a fixed endpointlive search on another endpointformaction: the submitter names another destinationsrc and params are droppedKeep a lighter copy of the page out of src: src is the same for every page, so back to page 1 would request the copy of page 2. Use params instead.
Forms
A form submission sends what a native submission would send:
- The submitter. The body comes from
new FormData(form, submitter), so the name and the value of the button that submitted the form are sent. - Overrides. A submitter with a
formaction,formmethodorformenctypeattribute overrides the form for that submission.formactionalso wins over thesrcandparamsoptions, which do not apply to that submission. The parameters that a subclass adds for its transport, such as thesectionsofFetchShopifySection, still apply. - GET. The fields replace the query of the
action, as natively. - POST. The body is URL-encoded by default, as natively. A file control then sends only the name of its file, and the
fetch.file-not-uploadeddiagnostic is reported. Setenctype="multipart/form-data"to upload files.enctype="text/plain"sendsname=valuelines. In a query, a URL-encoded body and atext/plainbody, every line break of a name or a value is sent as CRLF, as natively. - Left to the browser. A
dialogmethod closes its dialog natively, and atargetorformtargetother than_selfopens another browsing context. A form without atargetof its own takes thetargetof the<base>element.Fetchdoes not intercept these submissions. - The Enter key. Pressing Enter in a field submits the form through its first submit button, as natively. A form that paginates with submit buttons sends the value of the first one.
The submitter makes pagination with no script of its own:
<form action="/projects" method="get" data-component="Fetch" data-option-history>
<input type="hidden" name="orderby" value="title" />
<button type="submit" name="page" value="1">1</button>
<button type="submit" name="page" value="2">2</button>
</form>
<div id="projects">…</div>The second button requests /projects?orderby=title&page=2, writes it to history and swaps #projects. See the pagination example.
With a loader
Use the Action and Transition components to display a loader while the request runs. Every request ends with exactly one fetch-after event, after the DOM change, so a loader bound to fetch-before and fetch-after covers the whole navigation, aborts included.
<a
href="/"
data-component="Fetch Action"
data-option-history
data-on:fetch-before="Transition(#foo) -> target.enter()"
data-on:fetch-after="Transition(#foo) -> target.leave()"
data-on:fetch-error="alert('error')">
Click me
</a>
<div
data-component="Transition"
data-option-enter-from="opacity-0"
data-option-leave-to="opacity-0"
class="opacity-0"
id="foo">
Loading...
</div>
<div id="content"></div>import { registerComponents } from '@studiometa/js-toolkit';
import { Action, Fetch, Transition } from '@studiometa/ui';
registerComponents(Action, Fetch, Transition);Global loader
The events sent by the Fetch component bubble up the DOM tree. This means that the Action component can be placed on a parent of the element where the Fetch component is configured.
<main
data-component="Action"
data-on:fetch-before="Transition(#fetch-loader) -> target.enter()"
data-on:fetch-after="Transition(#fetch-loader) -> target.leave()">
... ...
<a data-component="Fetch" href="/page/2">Next page</a>
... ...
</main>
<div
data-component="Transition"
data-option-enter-from="hidden"
data-option-leave-to="hidden"
data-option-leave-keep
class="hidden"
id="fetch-loader">
Loading...
</div>Back and forward also reach the loader: a restore emits its events on the element that wrote the entry, or on the region of the entry when that element has no id. A new request on the same element ends the previous one first, so the loader stays on. Requests that write history run one at a time on the page, and a new one ends the previous one first too. Other requests run one at a time per element only: two elements without history can run in parallel, and the first fetch-after would hide a shared loader. Count the requests in flight when several such elements share one loader.
Transitions
The Fetch component is compatible with the View Transition API in compatible browsers. By default, the browser chooses which transition to use between the two states of the DOM. If you want more fine-grained transition, use the View Transition CSS APIs.
::view-transition-old(root) {
animation: transition-out 1s ease-in-out;
}
::view-transition-new(root) {
animation: transition-in 1s ease-in-out;
}
@keyframes transition-out {
to {
opacity: 0;
}
}
@keyframes transition-in {
from {
opacity: 0;
}
}Disabling View Transitions
You can disable view transitions with the data-option-no-view-transition attribute.
To run the change with your own choreography, register a runner on the js-toolkit:dom:update event.
Error handling
The Fetch component catches request errors and emits a fetch-error event which can be used to display meaningful information. A response with a status that is not ok, a response that cannot be parsed and an error thrown by the DOM change are errors too. An aborted request is not.
<main
data-component="Action"
data-on:fetch-error="alert(event.detail.error)">
<a href="/" data-component="Fetch">Home</a>
</main>import { registerComponents } from '@studiometa/js-toolkit';
import { Action, Fetch } from '@studiometa/ui';
registerComponents(Action, Fetch);See the error handling example for detailed usage.
Cancelling a request
The abort method cancels the request in flight. It emits fetch-abort, then fetch-after with the outcome aborted.
<main
data-component="Action"
data-on:fetch-abort="alert('Request was aborted')">
<a href="/" data-component="Fetch">Home</a>
<button
data-component="Action"
data-on:click="Fetch -> target.abort()">
Cancel request
</button>
</main>import { registerComponents } from '@studiometa/js-toolkit';
import { Action, Fetch } from '@studiometa/ui';
registerComponents(Action, Fetch);See the cancelling a request example for detailed usage.
Handling JSON response
If you need to fetch an API whose content-type is application/json, you can use the data-option-response option to extract the content that will be inserted in the DOM from the JSON object.
<form
action="/api/msg"
data-component="Fetch"
data-option-response="response.json().then((data) => data.rendered)">
<input type="text" name="msg" value="Hello world!">
<button type="submit">
Submit
</button>
</form>
<div id="content">...</div>{
"rendered": "<div id=\"content\">Hello world!</div>"
}import { registerComponents } from '@studiometa/js-toolkit';
import { Fetch } from '@studiometa/ui';
registerComponents(Fetch);Tracking a request
The response of the fetch-update-after event is plain data with lower-case header names, so the Track component can read it from markup. See tracking the result of a request.