Skip to content

Swapping content

swap() replaces the content of an element with new markup and resolves once the framework has caught up: the mutation is applied, the new components are mounted and the old ones are unmounted.

The call

ts
import { 
swap
} from '@studiometa/js-toolkit';
async function
load
(
target
: Element) {
const
response
= await
fetch
('/fragment');
await
swap
(
target
, await
response
.
text
());
// Every eager component in the new markup has mounted. }

swap(target, content, options?) returns a promise that resolves after whenDOMSettled().

What survives

By default the element itself is never replaced, so the caller's reference, its id and any instance on it survive. Only its children change.

content is a markup string parsed in the parsing context of the target, so <tr>, <li> and <option> survive. It can also be an Element or a DocumentFragment, read as the incoming counterpart of the target, whose children become the new content.

The four modes

js
import { swap, SWAP_MODES } from '@studiometa/js-toolkit';

await swap(el, html); // 'replace' — the default
await swap(el, html, { mode: SWAP_MODES.PREPEND });
await swap(el, html, { mode: SWAP_MODES.APPEND });
await swap(el, html, { mode: SWAP_MODES.MORPH });
ModeEffect
replacethe children are replaced
prependthe new content goes before the existing children
appendthe new content goes after them
morphthe children are morphed in place, keeping identity

prepend and append stay in core because they need the same before-and-after script diff as the other two.

morph keeps identity

Nodes, focus, expandos and instances survive a morph — but markup that is not sent does not. Two consequences are morphdom's policy, not swap()'s:

  • an element the incoming markup does not contain is discarded even from a preserved parent;
  • morphdom syncs the value of an input from the incoming markup.

Core passes childrenOnly: true without self, so attributes are not synced on the target: data-component and data-mount are lifecycle declarations of the target, not content.

self — replacing the element itself

js
await swap(el, html, { mode: 'replace', self: true });

This is the axis a caller that matches an element of a response to an element of the page needs: an id-matched section otherwise keeps the classes of the element it replaces.

  • With self, an Element content is the replacement rather than a container — the only way its own attributes reach the page. A string or a DocumentFragment contributes its first top-level element.
  • replace puts the replacement in the place of the target, so the target leaves the document.
  • morph morphs the target from the replacement without childrenOnly, so the element and its identity survive while its attributes are updated.
  • append and prepend add to the children by definition, so self with either is a swap.self-ignored warning — as is content that holds no element.

Scripts

A <script> produced by the fragment parser is flagged as already started by the HTML specification and stays inert wherever it is moved. It runs only if it is recreated, and a script that was already in the page runs twice if it is recreated.

swap() owns that rule, once. Script adoption follows whatever ends up in the document, so a replacement carrying a script still runs it exactly once.

Wrapping the mutation

wrap is the whole seam. It receives the single mutation of the swap and decides when it runs:

ts
import { 
swap
,
viewTransition
} from '@studiometa/js-toolkit';
async function
load
(
el
: Element,
html
: string) {
await
swap
(
el
,
html
, {
wrap
: (
mutate
) =>
viewTransition
(
mutate
) });
}

domUpdate() produces that wrapper and nothing else, so an ancestor can claim the lane. Resilience policy stays with domUpdate().

What is deliberately not in core

  • Attribute syncing in a default morph — see above.
  • Transitions, view transitions, history, id matching and response parsing. All of these are caller policy.

swap() is a free function, not a Base method. morphdom is a real, statically imported dependency, and the subpath layout contains the cost: it is reachable through swap() only, so a page that never swaps never downloads it.

MIT Licensed