Skip to content

Proposal: moveOrInsertBefore() #1406

Open

Description

@justinfagnani

What problem are you trying to solve?

In adding support for moveBefore() to libraries, we have to add checks that mirror the Move operation in the spec. Something like:

// Do once per range of sibling nodes that need to be moved
const sameRoot = oldParent.getRootNode({composed: true}) === newParent.getRootNode({composed: true});   

// This conditional attempts to re-implement the precondition checks that
// the Move operation has in addition to those the Insert operation has in the DOM spec
const moveMethod = sameRoot && (node.nodeType === 1 || node.nodeType === 3 || node.nodeType === 8)
    ? container.moveBefore
    : container.insertBefore;

moveMethod.call(container, node, refNode);

This is a bit tricky to get right because what preconditions you check depends on which are invariants for the particular type of move we're doing (many moves are within the same parent for instance, but some are not), and because the conditions are more complex than for methods like insertBefore().

It also is redundant, as moveBefore() is already performing these checks, just to throw. In fact, we could do:

try {
  container.moveBefore(node, refNode);
} catch (e) {
  container.insertBefore(node, refNode);
}

but since moveBefore already knows whether we can use an atomic move or not, why can't we have a method that just does this check and fallback for us? IOW, we know we want to move the node - there's no exception where we decide to just not move it - and we want to preserve state if possible, so do that with as little ceremony as possible:

container.moveOrInsertBefore(node, refNode);

Frameworks and libraries really don't want to have to carry around extra code for checks the DOM could do, or perform redundant checks that the DOM is already doing.

What solutions exist today?

No response

How would you solve it?

No response

Anything else?

Bulk mutation methods would help reduce repeated checks even in the DOM for ranges of nodes: #1369

Activity

  1. smaug---- commented on Oct 6, 2025

    @smaug----
    Contributor

    The example code is quite unclear. sameroot isn't used for anything, as an example.
    What is the problematic case you're trying to handle?

    moveOrInsertBefore certainly does smell a bit like a kitchen sink method ;)

  2. justinfagnani commented on Oct 7, 2025

    @justinfagnani
    Author

    @smaug---- sorry I was trying to simplify my actual code to make it more readable here. I'll clean it up.

    The point is that in order to safely use moveBefore() we have to reimplement the checks that it does and fall back to insertBefore() if our nodes don't meet the requirements. It would be a lot easier, and likely faster, if there was a DOM API that just did this for us. I don't see the benefit in forcing this conditional logic to be wrapped around so many `moveBefore() calls.

  3. justinfagnani commented on Oct 7, 2025

    @justinfagnani
    Author
  4. noamr commented on Oct 15, 2025

    @noamr
    Contributor

    This version is probably sufficient for any practical use (unless you really need the runtime is-element nodeType checks):

    function insertOrMoveBefore(parent: Element, child: Element, ref: Node?) {
      if (child.isConnected && parent.isConnected && 
          (child.ownerDocument === parent.ownerDocument))
        parent.moveBefore(child, ref);
      else
        parent.insertBefore(child, ref);
    }

    or:

    function insertOrMoveBefore(parent, child, ref) {
      try {
        parent.moveBefore(child, ref);
      } catch (e) {
        parent.insertBefore(child, ref);
      }
    }

    Some of the other premutations like moving CharacterData or moving elements in an inactive document are not that interesting if you're anyway falling back to insertBefore - those are cases where moveBefore would "succeed" but have the same effect as insertBefore.

    I don't mind having this as part of the platform (and it was discussed before) but the polyfill is perhaps not that bad?

  5. keithamus commented on Oct 15, 2025

    @keithamus
    Member

    We should consider the performance constraints of JS code like that. It's been a while for me looking at stuff like this but try/catch in a hot path could impact perf. If on the implementation side we're just checking a flag, then it might be enough to provide a reasonable justification to add to the platform - but I think emperical evidence is necessary.

  6. noamr commented on Oct 15, 2025

    @noamr
    Contributor

    We should consider the performance constraints of JS code like that. It's been a while for me looking at stuff like this but try/catch in a hot path could impact perf.

    That's just one of the options. The first one was just checking a couple of flags.

  7. trusktr commented on Sep 18, 2026

    @trusktr

    I strongly agree with Justin here.

    The thing is, it isn't the consumer of the APIs that should care about how elements/nodes handle their own state when moved.

    Consumers of this API (or any API that lets them move a child) just want to move the element/node.

    It is the responsibility of the moved element/node itself to determine what to do, exactly like custom elements can with and without connectedMoveCallback.

    On the other hand, native nodes throwing an error saying "I cannot be moved" is totally nonsensical to the consumer.

    This is an unnecessary leak of internal implementation detail.

    Consumer (even a framework's) thought process:

    "I'm gonna move this node" -> error -> "WTF?" -> tries other method -> it works -> "WTF?"

    (Where F stands for fudge cakes, of course 😊)

    It is

    • not useful for this API to throw like it does
    • is error prone due to replication of internal logic needed on the outside to avoid try/catch
    • the inversion of control is the wrong way: it isn't the element/node consumer that decides if an element is movable; it is the element (or node) that decides what to do (with its own state) when moved
    • and it is inconsistent across different types of nodes.

    When we want to move a node, just let us move the node.

    (Parent elements that care about childList changing observe children, which is not relevant to this.)

  8. trusktr commented on Sep 19, 2026

    @trusktr

    Another question is, what can spec authors do to get more of this sort of feedback before finalizing spec?

  9. annevk commented on Sep 19, 2026

    @annevk
    Member

    I think we just disagree. Moving is not an implementation detail. If you think it should be, I recommend not using the move primitive at all and always use the APIs that disconnect and connect.

  10. smaug---- commented on Sep 21, 2026

    @smaug----
    Contributor

    moveBefore is vastly different primitive to insertBefore. If those were the same method, then we'd need to know which path is taken. Say, for example if there is an iframe around, with move no document is destroyed and loaded, but with insert both happen, and now one might need to wait for the load event to fire before working on the iframe again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    addition/proposalNew features or enhancementsneeds implementer interestMoving the issue forward requires implementers to express interest

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions