Please note: this proposal is still being drafted and is not officially launched.
Allow HTML to specify HTTP requests that replace part of the page.
Proposal 3/3 in the Triptych Proposals.
Update the target attribute on the <form>to accept a CSS selector.
If the CSS selector matches an element in the document, the body content from the response should replace the element in the page.
The target attribute would thus accept the following possible values, in order of preference:
_self, _blank, etc.)
name attribute
In conjunction with Button Actions, add target to the <button> element as well.
It behaves identically.
<section role="feed"> <h2>News Feed</h2> <article>Speed Up Your Computer With This One Weird Trick</article> <article>24 Signs You're a Die-Hard Houston Astros Fan</article> <article>Is Smoking The New Drinking?</article> <button class="load" target=".load" action="/articles/1">Load More</button> </section>
If the user would like to read more content, they can click the "Load More" button.
The browser will issue a request to `/articles/1`, and the server could return a response like this:
<article>We Could All Learn From This Baby's Inspirational First Word</article> <article>Move Over Espresso Martini, The Aperol Spritz is Here</article> <button class="load" target=".load" action="/articles/2">Load More</button>
When the browser receives that response, it will be parsed as HTML and inserted into the DOM.
The button that triggered the request had target=".load" on it, and the first DOM element to match that CSS selector is itself.
So the browser will replace the button with the contents of the server's response, resulting in the following HTML:
<section role="feed"> <h2>News Feed</h2> <article>Speed Up Your Computer With This One Weird Trick</article> <article>24 Signs You're a Die-Hard Houston Astros Fan</article> <article>Is Smoking The New Drinking?</article> <article>We Could All Learn From This Baby's Inspirational First Word</article> <article>Move Over Espresso Martini, The Aperol Spritz is Here</article> <button class="load" target=".load" action="/articles/2">Load More</button> </section>Note how the server doesn't just return more content, it returns a new button as well, to replace the old one. The new button has a new URL to go with it—because the server is responding to a request at
/articles/1, it knows that the next button the user should see is one to fetch the articles at /articles/2.
This pattern easily generalizes to any kind of network-based content update that targets a subset of the page.
If the content is timely and fast-updating, a "refresh" button can delete the current list and load the most current one;
if the webpage is geared towards discovery, a "randomize" button can fetch a different batch.
"The Missing Mechanic: Behavioral Affordances as the Limiting Factor in Generalizing HTML Controls" (Petros et al.) identifies this limitation and investigates possibly ways to remedy it.
Imagine a web service that maintains a list of of its current users and gives an administrator the option to remove them. Here's a simple, non-functioning version of that feature:
| User | ||
|---|---|---|
| Alex | alex@example.com | |
| Grace | grace@example.com | |
| Pat | pat@example.com |
This table is described with the following HTML:
<table class="users">
<tr>
<th>User</th>
<th>Email</th>
<th></th>
</tr>
<tr class="user-1">
<td>Alex</td>
<td>alex@example.com</td>
<td><button target=".user-1" action="/users/1" method="DELETE">Delete</button></td>
</tr>
<tr class="user-2">
<td>Grace</td>
<td>grace@example.com</td>
<td><button target=".user-2" action="/users/2" method="DELETE">Delete</button></td>
</tr>
<tr class="user-3">
<td>Pat</td>
<td>pat@example.com</td>
<td><button target=".user-3" action="/users/3" method="DELETE">Delete</button></td>
</tr>
</table>
Note that the rows all have the same structure.
<tr class="user-1">
<td>Alex</td>
<td>alex@example.com</td>
<td>
<button target=".user-1" action="/users/1" method="DELETE">Delete</button>
</td>
</tr>
The user "Alex" can be found at the URL /users/1, "Grace" is at /users/2, and so on.
That URL is used in each button's action attribute, such as in the first one.
<button target=".user-1" action="/users/1" method="DELETE">Delete</button>When the user clicks that button, the browser issues a DELETE request to
/users/1.
The server then responds with HTML that describes what happened and
the browser will replace the DOM node described by .user-1 with the response body.
In this case that's the whole row.
This is an extremely expressive pattern.outerHTML swap.
It's not, however, the default replacement stragey in htmx, which defaults to an innerHTML replacement.
Our experience is that outerHTML should be the default and the JS ecosystem has also evolved in that direction.
For example, outerHTML is the only type of swap supported by htmz, a minimalist DOM replacement library whose overloading of the target attribute inspired the interface of this proposal.
They argue: replacement is a more powerful option. With replacement, you can replace, delete (replace with nothing), and insert-into (replace with the same container as original).
We agree.
Requests with a CSS target have their responses inserted into the page, replacing the first DOM element that matches the CSS target.targetNode and the HTML response is instantiated as responseNodes.
This feature works just as if targetNode.replaceWith(responseNodes) has been called.
As the name implies, responseNodes could contain multiple nodes and that would work just fine.
Document.querySelector() works.
Partial page replacement does not create a new navigation history entry.
"Forward" and "Back" navigations should treat partial page updates the same way they would treat ephemeral updates to the DOM that had been made with JavaScript.
By default, network requests with a partial page target should trigger the network progress bar.
The Sec-Fetch-Mode header should be set to replacement, a new value.navigate value is not appropriate as navigation is not taking place.
cors could be re-used but why not add a more-specific one?
Sec-Fetch-Mode: replacement
The Sec-Fetch-Dest header should be enhanced to include the replacement target of the of the request.document in Sec-Fetch-Dest.
Sec-Fetch-Dest: document Sec-Fetch-Dest: document/.target Sec-Fetch-Dest: document/section>div.profile
If using the existing document value raises any concerns, a substitute like replacement would be perfectly fine.
Sec-Fetch-Dest: replacement/.target jec-Fetch-Dest: replacement/section>div.profile
JavaScript is not a requirement to use partial page replacement, but having an event lifecycle allows further customization for those who want it. Events also make it possible for library authors to extend the HTML primitives, rather than replacing them.
We propose the following events to launch with Partial Page Replacement:beforerequest and afterrequest would also be reasonable alternatives.
| Event | Target | Phase | preventDefault action |
|---|---|---|---|
request |
Request-issuing element | Before the request is issued | Aborts the request and the swap |
requestsuccess |
Request-issuing element | After the request is issued, but before it is parsed | Aborts the swap |
requesterror |
Request-issuing element | After the request is issued, if it failed | None |
beforeswap |
Replacement target | Before the swap | Aborts the swap |
afterswap |
Parent of the (now-replaced) replacement target | After the swap completes | None |
Only five events are proposed for the first version of this feature, far more limited than what can be found in JavaScript libraries. This is done to limit scope. It is easy to add events and impossible to remove them; this proposal errs on the side of doing less. It will be easier to determine what additional events would be useful once the feature has real-world usage.
| Property Name | Value |
|---|---|
action |
The URL target of the request |
method |
The HTTP method of the request |
replacementtarget |
The CSS selector for the replacement target |
formData |
Any form data associated with the request |
abortSignal |
An AbortSignal object for the request |
Fires before the network request is issued.
Calling preventDefault() aborts the cycle; no request is sent, no further events fire.
| Property Name | Value |
|---|---|
action |
The URL target of the request |
method |
The HTTP method of the request |
replacementtarget |
The CSS selector for the replacement target |
response |
A ReadableStream of the response |
Fires when the request completes successfully, but before the response is parsed.
The event's details contains an object with the following properties:
Calling preventDefault() ends the cycle before the body content is parsed; beforeswap does not fire.
Keep in mind that response codes do not impact whether the request completed successfully.
A request that finishes with a 401 UNAUTHORIZED response will still fire this event.
An ErrorEvent that fires when the request does not complete successfully.
Following navigateerror, the event includes an error property with a JavaScript value representing the error associated with the event.
That error could be one of the following:
| Error | Meaning |
|---|---|
NetworkError |
The request failed at the transport layer |
AbortError |
The request was aborted via its AbortSignal |
TimeoutError |
The request timed out |
NotFoundError |
The CSS selector matched no element |
Calling preventDefault() has no effect; a swap will never take place if the request fails.
| Property Name | Value |
|---|---|
action |
The URL target of the request |
method |
The HTTP method of the request |
replacementtarget |
The CSS selector for the replacement target |
newelements |
NodeList of the elements to be inserted |
Fires on the replacement target after the response body is parsed but before the target is replaced.
The event's details contains an object with the following properties:
Calling preventDefault() aborts the swap; afterswap does not fire.
| Property Name | Value |
|---|---|
action |
The URL target of the request |
method |
The HTTP method of the request |
replacementtarget |
The CSS selector for the replacement target |
newelements |
NodeList of the elements to be inserted |
replacedelement |
The replaced element, now removed from the DOM |
Fires on the parent of the former target after the swap completes.
The event's details contains an object with the following properties:
Note that newelements will be empty if the response body was empty.
Partial page replacements are subject to CORS. All cross-origin requests require a preflight and there are no so-called "simple" cross-origin requests for partial page replacements. Same-origin requests proceed as normal.
This can be accomplished without changes to fetch spec, by adding a non-safelisted header to the request.
Fortunately, Paul Kinlan from the Chrome team has put together a demonstration of "htmx-style attributes," implemented using the new steaming-replacement functions from the second part of the Chrome proposal.id="out".
<section>
<button
data-get="/03/fragment/news"
data-target="#out"
data-swap="inner"
>load news</button>
</section>
<h3>Target #out</h3>
<section id="out"></section>
Kinan's demonstration is additional evidence of the demand for a declarative HTML API to access this exact functionality.
The
Recall the delete button from the editable table earlier:
With the
Although this is a minor convenience in isolation, it becomes a dramatic simpliciation for the server when applied to items that are generated progmatically from a list, because it removes the need to create iterative target/id pairs for each item.
Without it, there are three dynamic values in the following button:
TODO TODO
Web authors will want to customize the view of inflight request
Allow disabling the progress bar?
TODO Headers that let the server control the response location
Oob swaps
Don't re-use the Alternatives and Additions
_self keyword
target attribute has pre-existing keywords that refer to relative browsing contexts.
The addition of a _self keyword, which target the item that made the request, would significantly simplify the targets by removing the need for IDs in many common cases.
<button class="load" target=".load" action="/articles/1">Load More</button>
_this keyword, the code becomes trivial.
<button target="_this" action="/articles/1">Load More</button>
<button
class=".article-1"
target=".article-1"
action="/articles/1/upvote"
method="PUT">
Upvote
</button>
Instead, we could just have one.
<button
target="_self"
action="/articles/1/upvote"
method="PUT">
Upvote
</button>
Control response placement with headers
CSS Classes
Control Headers
New Attribute For CSS Targets
target attribute, instead add a new one specifically for CSS targets.
Footnotes