Skip to content

domUpdate ​

ts
domUpdate(target: Node, mutate: () => void | Promise<void>, detail?: Record<string, unknown>): Promise<void>

Announces a DOM mutation before it happens, so anything above the node can take it over.

This is the take-over mode of the negotiated-event mechanism. The delay mode is emitExtendable().

Usage ​

The code that mutates announces instead of mutating:

js
import { domUpdate } from '@studiometa/js-toolkit';

await domUpdate(this.$el, () => this.$el.replaceChildren(fragment));

An ancestor claims it:

js
import { EVENTS, viewTransition } from '@studiometa/js-toolkit';

this.$on(EVENTS.dom.update, ({ detail }) => detail.wrap(viewTransition));

The detail ​

ts
interface DomUpdateDetail {
  wrap(runner: DomUpdateRunner): void;
}

type DomUpdateRunner =
  | ((apply: () => void | Promise<void>) => void | Promise<unknown>)
  | { update(mutate: () => void | Promise<void>): void | Promise<unknown> };

The event is EVENTS.dom.update — 'js-toolkit:dom:update' — dispatched on target as a bubbling, non-cancelable CustomEvent.

The detail is one object, like every other v4 event. A decorated handler reads { payload: { wrap } } for what a raw listener reads as event.detail.

What a claim composes with ​

ClaimEffect
wrap(viewTransition)the change plays as one batched native view transition
wrap((apply) => this.$write(apply).promise)the change lands in the write phase, batched, cancelled on unmount
wrap(motionView)any object with update(mutate) — which MotionView already is

Neither runner is the default. The ancestor chooses the lane, because it knows whether the region animates.

The rules ​

  • defaultPrevented is ignored. The step is announced, not proposed.
  • The last claim wins. Take-over mode keeps one runner.
  • A registration is valid only while the event dispatches. A listener that keeps wrap and calls it later is reported as protocol.late-registration and ignored.
  • The work of the emitter always completes. A runner that throws, rejects, or resolves without calling apply loses the animation, never the change — and that last case is reported as protocol.unapplied-dom-update.
  • An unclaimed domUpdate() is synchronous. With no listener, the mutation runs before the returned promise exists.
  • A failing runner is reported as callback.dom-update-runner-failed.

It has no Base path ​

The helper dispatches its own event and is absent from $emits, so the negotiation code and the optional view-transition import stay out of the Base module graph. Plain DOM code uses the same function with no instance.

With swap() ​

domUpdate() produces exactly the wrap that swap() takes, and nothing else. Resilience policy stays here.

MIT Licensed