swap
swap(target: Element, content: SwapContent, options?: SwapOptions): Promise<void>Replaces the content of an element and resolves once the framework has caught up.
type SwapContent = string | Element | DocumentFragment;
interface SwapOptions {
mode?: 'replace' | 'prepend' | 'append' | 'morph';
wrap?: (mutate: () => void) => void | Promise<unknown>;
self?: boolean;
}Usage
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
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
valueof 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:
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, anElementcontent is the replacement rather than a container — the only way its own attributes reach the page. A string or aDocumentFragmentcontributes its first top-level element. replaceputs the replacement in the place of the target, so the target leaves the document.morphmorphs the target from the replacement withoutchildrenOnly, so the element and its identity survive while its attributes are updated.appendandprependadd to the children by definition, soselfwith either is aswap.self-ignoredwarning — 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:
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,
idmatching 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.