Skip to content

swap

ts
swap(target: Element, content: SwapContent, options?: SwapOptions): Promise<void>

Replaces the content of an element and resolves once the framework has caught up.

ts
type SwapContent = string | Element | DocumentFragment;

interface SwapOptions {
  mode?: 'replace' | 'prepend' | 'append' | 'morph';
  wrap?: (mutate: () => void) => void | Promise<unknown>;
  self?: boolean;
}

Usage

ts
import { 
swap
} from '@studiometa/js-toolkit';
async function
load
(
target
: Element) {
const
response
= await
fetch
('/fragment');
await
swap
(
target
, await
response
.
text
());
// The mutation is applied, the new components are mounted, the old ones unmounted. }

The returned promise 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 as a string is parsed in the parsing context of the target, so <tr>, <li> and <option> survive. As an Element or a DocumentFragment it is read as the incoming counterpart of the target, and its children become the new content.

The modes

js
import { 
SWAP_MODES
} from '@studiometa/js-toolkit';
SWAP_MODES
.
REPLACE
; // 'replace' — the default
SWAP_MODES
.
PREPEND
; // 'prepend'
SWAP_MODES
.
APPEND
; // 'append'
SWAP_MODES
.
MORPH
; // 'morph'

SwapMode is derived from the frozen object — the pattern for every closed set of strings in the framework.

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

morph

Identity survives — nodes, focus, expandos, instances — 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.

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

self

Replaces the target element itself, attributes included, instead of its children:

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

It 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 holding 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.

wrap

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()swap() itself throws rather than swallowing.

What is deliberately not in core

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

It 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