Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Atomic move operation for element reparenting & reordering #1255

Open
domfarolino opened this issue Feb 14, 2024 · 61 comments
Open

Atomic move operation for element reparenting & reordering #1255

domfarolino opened this issue Feb 14, 2024 · 61 comments
Assignees
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest stage: 2 Iteration

Comments

@domfarolino
Copy link
Member

domfarolino commented Feb 14, 2024

What problem are you trying to solve?

Chrome (@domfarolino, @noamr, @mfreed7) is interested in pursuing the addition of an atomic move primitive in the DOM Standard. This would allow an element to be re-parented or re-ordered without today's side effects of first being removed and then inserted.

Here are all of the prior issues/PRs I could find related to this problem space:

Problem

Without an atomic move operation, re-parenting or re-ordering elements involves first removing them and then re-inserting them. With the DOM Standard's current removal/insertion model, this resets lots of state on various elements, including iframe document state, selection/focus on <input>s, and more. See @josepharhar's reparenting demo for a more exhaustive list of state that gets reset.

This causes lots of developer pain, as recently voiced on X by frameworks like HTMX, and other companies such as Wix, Microsoft, and internally at Google.

This state-resetting is in part caused by the DOM Standard's current insertion & removal model. While well-defined, its model of insertion and removal steps has two issues, both captured by #808:

  1. Undesirable model: The current DOM Standard allows for the non-atomic insertion of multiple nodes at a time. In practice, this means when appending e.g., a DocumentFragment, script can run in between each individual child insertion, thus observing DOM state before the entire fragment insertion is complete.
  2. Interop issues: While Safari matches the spec, Chromium & Gecko have a model that ensures all DOM mutations are synchronously performed before any script runs as a result of the mutations.

What solutions exist today?

One very limited partial solution that does not actually involve any DOM tree manipulation, is this shadow DOM example that @emilio had posted a while back: whatwg/html#5484 (comment) (see my brief recreation of it below).

Screen Recording 2024-01-29 at 5 00 26 PM

But as mentioned, this does not seem to perform any real DOM mutations; rather, the slot mutation seems to just visually compose the element in the right place. Throughout this example, the iframe's actual parent does not change.


Otherwise, we know there is some historical precedent for trying to solve this problem with WebKit's since-rolled-back "magic iframes". See whatwg/html#5484 (comment) and https://bugs.webkit.org/show_bug.cgi?id=13574#c12. We believe that the concerns from that old approach can be ameliorated by:

How would you solve it?

Solution

To lay the groundwork for an atomic move primitive in the DOM Standard, we plan on resolving #808 by introducing a model desired by @annevk, @domfarolino, @noamr, and @mfreed7, that resembles Gecko & Chromium's model of handling all script-executing insertion/removal side-effects after all DOM mutations are done, for any given insertion.

With this in place, we believe it will be much easier to separate out the cases where we can simply skip the invocation of insertion/removal side-effects for nodes that are atomically moved in the DOM. This will make us, and implementers, confident that there won't be any way to observe an inconsistent DOM state while atomically moving an element, or experience other nasty unknown side-effects.

The API shape for this new primitive is an open question. Below are a few ideas:

  • A new DOM API like replaceChildAtomic()/replaceChildrenAtomic() that can take a connected node and atomically re-parent it without removal/insertion side-effects.
    • One limitation here is that we'd have to pick and choose which existing DOM APIs we want to mirror with atomic counterparts. For example, if we ever wanted append() or appendChild() to ever be able to also atomically move already-connected nodes, we'd have to introduce appendAtomic() and appendChildAtomic(), and so on.
  • A setting for existing DOM APIs, e.g., append(node, {atomic: true}), replaceChild(node, {atomic: true})
  • A scoped, declarative attribute that changes the behavior of DOM mutation APIs in a subtree
    • This could be an element attribute that makes all existing DOM mutation APIs behave "atomically" when operating on already-connected nodes under the element's subtree
    • This could also be a property on the document overall, set via a header/meta tag, or some other mechanism

Compatibility issues here take the form relying on insertion/removal side-effects which no longer happen during an atomic move. They vary depending on the shape of our final design.

  1. With a new DOM API/setting that developers have to affirmatively opt-into, you could atomically move fragments/subtrees constructed by other library code that's unaware it's being atomically moved. Those fragments may be built in a way that relies on non-atomic move side-effects (though we haven't heard of such concerns directly yet).
  2. Consider an element attribute that changes the behavior of all DOM mutation APIs to behave atomically on already-connected nodes in its subtree. You could minimize compat concerns by externally-constructed portions of the subtree to opt-out of atomic moves with the same attribute. But what would that mean exactly, to have part of a subtree move atomically and part of it not?

A non-exhaustive list of additional complexities that would be nice to track/discuss before a formal design:

  • How to handle mutation events? There was discussion at the TPAC 2023 about suppressing mutation events when new-ish DOM features are used, so we could probably get away with simply suppressing mutation events whenever an atomic move is being performed??
  • Handling things like focus/selection properly (need to land on desired behavior)
  • Fixing up things like live ranges; the way DOM handles this today might already be suitable for atomic moves, but unclear

Anything else?

No response

@domfarolino domfarolino added needs implementer interest Moving the issue forward requires implementers to express interest addition/proposal New features or enhancements labels Feb 14, 2024
@domfarolino domfarolino self-assigned this Feb 14, 2024
@domfarolino domfarolino added the agenda+ To be discussed at a triage meeting label Feb 14, 2024
@WebReflection
Copy link

WebReflection commented Feb 14, 2024

First of all, thank you! I've been vocal about this issue about forever and part of one of the biggest discussions you've linked.

As author of various "reactive" libraries and somehow veteran of the "DOM diffing field", I'd like to add an idea:

The API shape for this new primitive is an open question. Below are a few ideas:

I understand a node can be moved from <main> to an <aside> element and this proposal should still work but I think we should not discard the Range API:

  • most modern libraries have a concept of fragments, inevitably represented as virtual because there's no persistent fragment whatsoever yet on the DOM (I've been vocal about this too)
  • in a classic table sort mechanism there could be only few TRs moved within a specific place and taht's the same for LIs and others ... if any proposed API consider only parentNode to work that would not satisfy most fragment based requirements where areas are confined within Virtual DOM or comment nodes to confine those special cases while the Range api could instead simply select a node start, a node end, and update atomically inner nodes

On top of this I hope whatever solution comes to mind works well with DOM diffing, so that new nodes can even pass through the usual DOM dance when the parent is changed or they become live, removed nodes that won't land anywhere else would eventually invoke disconnectedCallback if Custom Elements, but nodes already present in that container and moved around basically do nothing in terms of state, they are just shuffled in the layout, if they do.

As quick idea to eventually signal a node is going to be moved in an atomic way, and assuming it's targeting also a live parent, I think something like parent.insertBeforeAtomic(node[, reference]) could be an interesting approach to consider as that basically solves everything, from append to prepend to any other case insertBefore works wonderfully well and it hints that such node should:

  • do nothing if the parent is the same as before (or the node was already live) ... just move it and skip all the things
  • trigger connectedCallback if the node was not live
  • ... that's it?

As insertBefore covers append, appendChild, prepend, before and after with ease, it might be the easiest starting point to have something working and useful for the variety of virtual fragments based solutions and diffing APIs out there.

I hope this answer of mine makes sense and maybe trigger some even better idea / API.

edit on after thoughts another companion of the API should be reflected in MutationObserver, or better, MutationRecord ... so far we have addedNodes and removedNodes but nothing about movedNodes which will still be desired for most convoluted edge cases.

The movedNodes record will contain, beside of course the target, a from parent container and a to parent container which might be the same if moved internally but it would signal previous parent and new parent otherwise that something different is within their content.

@1cg
Copy link

1cg commented Feb 18, 2024

This would be a fantastic addition of functionality for web development in general and for web libraries in particular. Currently if developers want to preserve the state of a node when updating the DOM they need to be extremely careful not to remove that node from the DOM.

Morphing (https://github.com/patrick-steele-idem/morphdom) is an idea that has developed around addressing this. I have created an extension to the original morphdom algorithm called idiomorph (https://github.com/bigskysoftware/idiomorph/) and the demo for idiomorph shows how it preserves a video in a situation when morphdom cannot. 37Signals has recently integrated idiomorph into Turbo 8 & Rails (https://radanskoric.com/articles/turbo-morphing-deep-dive-idiomorph)

If you look at the details of the idiomorph demo you will see it's set up in a particular way: namely, the video cannot change the depth in the DOM at which it is placed, nor can any of the types of the parent nodes of the video change. This is a severe restriction on what sorts of UI changes idiomorph can handle. With the ability to reparent elements idiomorph could offer much better user experience, handling much more significant changes to the DOM without losing state such as video playback, input focus, etc.

Note that it's not only morphing algorithms like idiomorph that would benefit from this change: nearly any library that mutates the DOM would benefit from this ability. Even virtual DOM based libraries, when the rubber meets the road, need to update the actual DOM and move actual elements around. This change would benefit them tremendously.

Thank you for considering it!

@smaug----
Copy link
Collaborator

Anything else?

Add some complexity to selection/range: how to deal with Shadow DOM when the host moves around and selection is partially in shadow DOM?

@ydogandjiev
Copy link

This is a very exciting proposal! In the Microsoft Teams Platform, we extensively use iframes to host embedded apps in the Teams Web/Desktop Clients. When a user navigates away from an experience powered by one of these embedded apps and comes back to it later, we provide the ability for them to keep their iframe cached in the DOM (in a hidden state) and then re-show it later when it's needed again. To implement this functionality, we had to resort to creating the embedded app frames under the body of our page and absolute position them in the right place within our UX. This approach has lots of obvious disadvantages (e.g. breaks the accessibility tree, requires us to run a bounds synchronization loop, etc.) and the only reason we had to resort to it was because moving the iframe in the DOM would reload the embedded app from scratch thus negating any benefits of caching the frame. This proposal would allow us to implement a much more ideal iframe caching solution!

Note the location of the iframe in the DOM and its absolute positioning in this recording:
https://github.com/whatwg/dom/assets/3357245/7fd4d2a7-2c2d-4bed-9a78-9c60f26a42f4

@infogulch
Copy link

The WHATNOT meetings that occurred after this issue was created deferred discussion about the topic. I wonder what next steps would be needed to move this issue forward. The next meeting is on March 28 (#10215).

@noamr
Copy link
Collaborator

noamr commented Mar 22, 2024

The WHATNOT meetings that occurred after this issue was created deferred discussion about the topic. I wonder what next steps would be needed to move this issue forward. The next meeting is on March 28 (#10215).

I hope we can get to it in the 28.3 WHATNOT. @domfarolino @past ?

@past
Copy link

past commented Mar 22, 2024

It's already on the agenda, so if the interested parties are attending we will discuss this.

@iteriani
Copy link

Are the imperative and declarative APIs meant to slowly replace the existing APIs over time? Or do we need to choose between one or the other because of potential overhead?

@noamr
Copy link
Collaborator

noamr commented Mar 26, 2024

Are the imperative and declarative APIs meant to slowly replace the existing APIs over time? Or do we need to choose between one or the other because of potential overhead?

If I understand the question, it's mainly for backwards compatibility. In some cases you might want the existing behavior or something subtle in your app relies on it, so we can't just change it under the hood.

@sebmarkbage
Copy link

This would be very nice for React since we currently basically just live with things sometimes incorrectly resetting. A couple of notes on the API options:

  • Associating with the node that gets moved e.g. an option on the <iframe> doesn't make much sense because it can be deeply nested inside the tree that moves. The iframe doesn't know anything about which context it moves inside. At best maybe you'd just have to by default add it to all possible nodes that might contain any state - which is all nodes.
  • Associating with a subtree creates a kind of "mode". Basically for a React app we'd just add it to the entire document, but that also affects any subtrees embedded inside the document which might be an entire legacy app or a different framework. It forces us to basically break the whole app to opt into it. It'd basically be like a new doctype kind of mode.

The thing that does causes a change is the place where the move happens. But even then it's kind of random which one gets moved and which one implicitly moves by everything around it moving. We don't remove all children and then reinsert them. So sometimes things preserve state.

A new API for insertion/move seems like a better option.

We'd basically like to just always the same API for all moves - which can be thousands at a time. This means that this API would have to be really fast - similar to insertBefore. An API like append(node, {atomic: true}) doesn't seem good because the allocation and creation of potentially new objects and reading back the value from C++ to JS isn't exactly fast. Since this is a high performance API, this seems like a bad option.

Something new like replaceChildAtomic would be easy to adopt inside a library and faster.

@rniwa
Copy link
Collaborator

rniwa commented Mar 26, 2024

One thing that's nice to nail down is whether re-ordering of child nodes is enough or we need to support re-parenting (i.e. parent node changing from one node to another). Supporting the latter is a lot more challenging than just supporting re-ordering.

@1cg
Copy link

1cg commented Mar 26, 2024

Definitely would prefer full re-parenting. I gave an htmx demo of an morph-based swap at Github where you could flip back and forth between two pages and a video keeps working:

https://www.youtube.com/watch?v=Gj6Bez2182k&t=2100s

The dark secret of that demo was that I had to really carefully structure the HTML in the first and second pages to make sure that the video stayed at the same depth w/ the same parent element types to make the video playing keep working. Would be far better for HTML authors if they could change the HTML structure entirely, just build page 1 the way they want and build page 2 the way they want, and we could swap elements into their new spots by ID.

@domfarolino
Copy link
Member Author

(For the purpose of brevity, I will begin using the SPAM acronym that we've been toying around with internally, which means "state-preserving atomic move". The most obvious example is an iframe that gets SPAM-moved doesn't lose its document or otherwise get torn down).


  • Associating with a subtree [...] Basically for a React app we'd just add it to the entire document, but that also affects any subtrees embedded inside the document [...]. It forces us to basically break the whole app to opt into it.

The thing that does causes a change is the place where the move happens.
[...]
A new API for insertion/move seems like a better option.

@sebmarkbage I understand your hesitation around a new subtree-associated-HTML-attribute — in that it would be over-broad, affecting tons of nested content that a framework might not own, possibly breaking parts of an app that doesn't expect SPAM moves to happen. But I'm curious if a new DOM API really gets you out from under that over-broadness, while still being useful? What would you expect orderedList.replaceChildAtomic(newListItem, oldListItem) to do, where newListItem is an <li> with a bunch of app-specific (not framework-owned) child content, including <iframe>s?

I guess I had in mind that the imperative API would force-SPAM-move the "state-preservable" elements in the subtree that's moving, so that any nested iframes do not get their documents reset1. But if that API would not preserve nested iframe state, then the only way it would be possible to actually preserve that iframe's state in this case is if the application took care to apply an iframe-specific HTML attribute to it, specifying that it opts into SPAM moves:

  • Associating with the node that gets moved e.g. an option on the <iframe> doesn't make much sense because it can be deeply nested inside the tree that moves. [...]

But it sounded like that option didn't sit well with you because the application author would be one-by-one sprinkling these attributes to random iframes without understanding the context in which the SPAM move might actually take place, by a framework way higher up the stack.

So how can we best enable the scenario where an <li> that contains a deeply-nested iframe, gets SPAM-moved without the iframe being reset? My thought is that:

  • list.replaceChildAtomic(new, old) would force-SPAM-move iframes in the new subtree (if new is already connected in the DOM of course)
  • Good ole fashioned list.replaceChild(new, old) would only cause SPAM moves to happen on elements in the subtree with the HTML attribute directly applied to it (i.e., <iframe preserve=content>), and no other elements.

But I would love to get more thoughts on the subtree side-effects stuff in general.

Footnotes

  1. Possibly other state like focus/selection being preserved on other eligible elements; that bit would need to be figured out!

@rniwa
Copy link
Collaborator

rniwa commented Mar 27, 2024

I don't think we can make this happen automatically based on a content attribute on an iframe. It most certainly needs to be a completely new DOM API.

@domfarolino
Copy link
Member Author

I don't think we can make this happen automatically based on a content attribute on an iframe. It most certainly needs to be a completely new DOM API.

I am very much open to that, I'm just trying to consider what subtree side-effects are acceptable. That is, if parent.appendAtomic(connectedDivWithChildIframe) should preserve the "child iframe" state or not? I think it has to, for the API to be useful at all. But I'm also sympathetic to compat concerns that it might cause a preserving-move to happen on deeply-nested iframes in a subtree built by another application/framework than the one performing the move in the first place. (And maybe that could break things if parts of the app relies on preserving moves not happening on nodes in the subtree).

@domfarolino
Copy link
Member Author

An attribute + DOM API could work together in this case a bit, to ameliorate some of the compat concerns. For example:

const nodeToAtomicallyMove = document.querySelector('......');
// Never trigger atomic moves on *this* specific sub-subtree, that was built by "old" content.
nodeToAtomicallyMove.querySelector('.built-by-legacy-app').preserve = 'none';
newParent.appendAtomic(nodeToAtomicallyMove);

In this case, all <iframe>s inside nodeToAtomicallyMove could be SPAM moved except ones that exist inside the subtree .built-by-legacy-app. Those ones are specifically opted-out, because maybe they can't handle preserving-moves... Just an idea!

@rniwa
Copy link
Collaborator

rniwa commented Mar 27, 2024

That sounds like something that could be built by a user hand library, not something that needs to be built into browser's native API. We really need to keep this API proposal as simple & succinct as much as possible.

@noamr
Copy link
Collaborator

noamr commented Mar 27, 2024

I don't think we can make this happen automatically based on a content attribute on an iframe. It most certainly needs to be a completely new DOM API.

Can you expand on why this is impossible? I can see the point why it might be preferable, but I think both directions are possible.

@noamr
Copy link
Collaborator

noamr commented Mar 27, 2024

and +1 to not limiting it to reordering. We'll end up just scratching the surface of the use-cases, coming back to where we started where we still need a full solution for reparenting.

@annevk
Copy link
Member

annevk commented Mar 27, 2024

I'm also a bit at a loss as to why we'd discuss new attributes. That seems like a pretty severe layering violation? The way I see it:

  1. https://dom.spec.whatwg.org/#mutation-algorithms needs to gain a new "move" operation that encapsulates argument validation, new mutation observer records, new callback steps for specifications to hook into, etc.
  2. We figure out what API is best suitable for that new primitive, e.g., parent.moveBefore(node, before). (Possibly multiple APIs, but best to start small and give it time to bake in multiple implementations.)

@noamr
Copy link
Collaborator

noamr commented Mar 27, 2024

I'm also a bit at a loss as to why we'd discuss new attributes. That seems like a pretty severe layering violation? The way I see it:

  1. https://dom.spec.whatwg.org/#mutation-algorithms needs to gain a new "move" operation that encapsulates argument validation, new mutation observer records, new callback steps for specifications to hook into, etc.
  2. We figure out what API is best suitable for that new primitive, e.g., parent.moveBefore(node, before). (Possibly multiple APIs, but best to start small and give it time to bake in multiple implementations.)

I tend to agree with the conclusion, but I want to explain why the main reason to consider things like an iframe attribute, in case it raises something else.

Outside "keep iframes from reloading", it's unclear exactly what the effects of this would be. For focus, we need to blur and refocus anyway, e.g. in case you're moving the element to an inert tree. We can decide to do that and just suppress the events. Similar provisions have to be taken for selection. So if we add moveBefore, we have to decide if it does all these things, if so, how exactly, or just the iframes thing for start.

@gnoff
Copy link

gnoff commented Mar 27, 2024

@domfarolino

I guess I had in mind that the imperative API would force-SPAM-move the "state-preservable" elements in the subtree that's moving

I think what Seb is saying is that React can decide if a move should be state preserving but if React added a "preserve-state" attribute to <html /> and then some embedded application deep in the DOM does an append expecting the append to be non-state-preserving we've just altered the moves that the other application owns.

Our perspective is that the mover decides the move semantics rather than the tree. So any moves done by this embedded application won't preserve state b/c that is what the application was expecting and any moves done by React would preserve state becuase React was updated to signal this intent by using a novel API

@anthonyhoegberg
Copy link

This would open up a whole bag of worms in terms of complexity because the slotted element would now be styled based on two locations...

What about not rendering the element at, only when it is slotted? Kind of like a template just to let the element live somewhere but be rendered elsewhere. So the style will only be based in one location where it is slotted? (Maybe the slotted conversation should be done elsewhere? I love slots so if there is a appropriate place to have this conversation please tell me.)

Even if slotted is updated/changed to be able to be used outside of shadow-DOM i still think that Node.prototype.moveBefore etc should be implemented.

@noamr
Copy link
Collaborator

noamr commented Jul 12, 2024

Even if slotted is updated/changed to be able to be used outside of shadow-DOM i still think that Node.prototype.moveBefore etc should be implemented.

I agree, it's a worthwhile and separate feature! Would you mind opening a separate issue on the HTML repo for this?

@tsutsu
Copy link

tsutsu commented Aug 23, 2024

While on the subject of atomic reparenting, I would like to point out a stumbling block that would continue to cause non-atomic reparenting: DocumentFragments.

In many real-world use-cases currently, DocumentFragments are used purely with the intent to gather up and pre-arrange a collection of maybe-connected nodes to submit to batch reparenting operations (appendChildren/replaceChildren). Yet in order to do this, by the (already-set-in-stone) semantics of DocumentFragment, all the nodes must first be non-atomically reparented to the DocumentFragment, triggering disconnects. And because DocumentFragments can't themselves be reified child nodes such that their children would stay connected to the DocumentFragment's parent document (at least unless DocumentFragments are somehow unified with the "DOM Parts" proposal), there's no way for an operation that involves temporarily assigning nodes to live under a DocumentFragment, to ever be atomic.

IMHO a fully-general solution to atomic element reparenting, needs to deal with the case where the developer's goal is to reparent a batch of nodes, without any of those nodes ever becoming disconnected.

I'm not totally sure what a solution that addresses that use-case could look like. Some thoughts:

  • Maybe a DocumentFragmentView that acts like a DOM node — in terms of being accepted by DOM APIs anywhere a DocumentFragment is accepted — but is really just a data structure, just holding borrowed references to nodes rather than reparenting them under itself. (Alternately, make DocumentFragments themselves able to act like this if you set some property on them.)
  • Or maybe a DOMAtomicMutationTransaction operation, either:
    • implicit, existing for the lifetime of a callback submitted to a document.atomicMutation call
    • or reified as an accessible object returned by e.g. document.beginAtomicMutation, then passable as an option into Node mutation operations, and then offering a commit method

I mention these potential approaches here not because I think they're particularly well-thought-out / beyond need for further refinement under WICG; but rather because these ideas seem to have a common thread. It would seem that any solution that addresses the "atomic reparenting despite a use-case that would warrant use of DocumentFragment" problem, seems to also be a fully-general solution for atomic reorder/reparent generally, batched or not.

  • In the DocumentFragmentView case, the semantics could be specified such that mutation operations that consume a DocumentFragmentView are always performed atomically on the contents of the view. So, rather than changing any existing APIs to introduce atomic variants, anyone who wants atomic reorder/reparent even a single node could do it by wrapping the node first in a DocumentFragmentView and then (re)placing it somewhere.
  • In the DOMAtomicMutationTransaction case, single operations could be wrapped in a transaction just as well as multiple operations could. (If using the reified form, document.beginAtomicMutation could have an optional parameter singleOperation, or an alternate form document.singleAtomicMutation, where in either case, you don't need to call commit because it's done implicitly at the end of the consuming operation — allowing the caller to just do e.g. node.appendChild(newChild, document.singleAtomicMutation()).

Because these approaches seem to be general, I would suggest paying attention to the "atomic reparenting despite a use-case that would warrant use of DocumentFragment" problem as a generator for potential approaches for the higher-level reparenting/reordering API-design problem. (Feel free to ignore the particular potential approaches outlined above, though; I just wanted to use them to make this point.)

@1cg
Copy link

1cg commented Aug 23, 2024

@tsutsu yes, I faced this issue when using the moveBefore() method to improve the hx-preserve attribute in htmx.

My solution was to create a hidden "pantry" div that was connected to the main document, move all the preserved elements to that pantry element with moveBefore(), merge in the nodes created against a DocumentFragment, then swap the preserved elements into the new content with another moveBefore().

This prevents the preserved elements from ever becoming disconnected and losing their state. Maybe not the most elegant approach, but it works!

@1cg
Copy link

1cg commented Aug 23, 2024

@WebReflection
Copy link

@tsutsu I proposed 5 years ago a persistent DocumentFragment that would solve your "need to re-parent before moving" issue but the proposal went "crickets" despite the full implementation provided https://github.com/WebReflection/document-persistent-fragment

@whatwg whatwg deleted a comment Aug 26, 2024
@noamr
Copy link
Collaborator

noamr commented Aug 26, 2024

@tsutsu thanks for the detailed use case and proposals!
I can see how the moveBefore proposal is constrained and still requires doing some things in Userland, however what makes it feasible to implement is the fact that the JS call itself is atomic. The range of things that can happen between starting and ending a multi-call transaction is way too vast.

I think that the path to support something more complex would be to first implement something along the lines of #270, where you can create a batch of DOM changes and apply them in a single JS call. Then the semantics of moveBefore can be an option there.

But I don't see this as a dependency, rather a potential future enhancement.

@domfarolino domfarolino added the agenda+ To be discussed at a triage meeting label Aug 26, 2024
@rniwa
Copy link
Collaborator

rniwa commented Aug 26, 2024

What happens to ongoing (CSS or SVG) animations? Do they behave like the node was removed then inserted back? If not, if they're expected to behave as if they were never removed, then we need to define what happens to them.

@domfarolino
Copy link
Member Author

The animation state is preserved. It does not act as if the node were removed and then inserted back. Spec'ing this is one of the tasks we've added to the list in the base PR (#1307), and we also have WPTs for this: https://github.com/web-platform-tests/wpt/tree/master/dom/nodes/moveBefore/tentative

@rniwa
Copy link
Collaborator

rniwa commented Aug 26, 2024

It seems like this API raises a lot of open questions.

Presumably animation & transition states should only be preserved if the element that got atomically moved continues to have the same style which initiated animation applied. It would be nonsensical for an element to retain animations from style rules that no longer apply (otherwise, we can end up with dozens of obsolete styles which once applied to an element and still ongoing due to animation / transition).

What does it mean, for example, for focus to be preserved. Would it mean that the focused element will continue to retain the focus regardless of whether it's moved to? It would mean that a bunch of old ancestors will need to receive focusout events & new ancestors will need to receive focusin events.

Preserving selection will be even funkier. Since Blink & WebKit don't support discontiguous selection so we can probably only able to preserve the end points of selection. Then the a bunch of nodes in-between old positions of start and end will be deselected, and a bunch of nodes in-between new positions of start and end will be newly selected. We most certainly need to fire selecionchange event in that scenario because the bunch of nodes have changed its selection state.

@WebReflection
Copy link

What does it mean, for example, for focus to be preserved. Would it mean that the focused element will continue to retain the focus regardless of whether it's moved to?

I personally expect that to happen even if I find it a really edge case to consider (but it's a valid point)

Since Blink & WebKit don't support discontiguous selection so we can probably only able to preserve the end points of selection.

Again, extremely edge case ... I wouldn't expect any input to move elsewhere while typing (focus) or selecting things and, to be honest, I would be perfectly fine if these cases would throw an error, as moving form fields while typing feels like a scam paradise to dive into, not a feature of the Web ... and accessibility concerns should be taken into account.

@WebReflection
Copy link

WebReflection commented Aug 26, 2024

amend ... I've said throw an error but for fallback sake I would say: if any input, select, etc, is involved in that operation while focused or typed, whatever insertBefore does now should be reflected in moveBefore too so that it's clear that an actively typed element should never be moved while users are operating on it.

throwing an error might be undesired due side-effects caused by slow/lazy asynchronous operations, everything should still work but maybe that page was too greedy to move those elements around during action.

@domfarolino
Copy link
Member Author

Presumably animation & transition states should only be preserved if the element that got atomically moved continues to have the same style which initiated animation applied.

This is correct.

@noamr
Copy link
Collaborator

noamr commented Aug 27, 2024

It seems like this API raises a lot of open questions.

Sure, happy to work through them and flesh them out.

What does it mean, for example, for focus to be preserved. Would it mean that the focused element will continue to retain the focus regardless of whether it's moved to? It would mean that a bunch of old ancestors will need to receive focusout events & new ancestors will need to receive focusin events.

In the blink prototype we currently don't send these events. But it's a choice, and we can spec and implement firing these events. There's not much complexity in this, just some judgement calls.

Preserving selection will be even funkier. Since Blink & WebKit don't support discontiguous selection so we can probably only able to preserve the end points of selection. Then the a bunch of nodes in-between old positions of start and end will be deselected, and a bunch of nodes in-between new positions of start and end will be newly selected. We most certainly need to fire selecionchange event in that scenario because the bunch of nodes have changed its selection state.

You mean if a node that's in the beginning/end of the range is moved to where the range now extends to additional/fewer elements? Yes we should probably fire selectionchange at that time.

@titoBouzout
Copy link

For solid, and possibly many other frameworks what would be welcome is an atomic replaceChildren
Such parent.replaceChildrenAtomic([node1, node2, ...]) which will remove any not on the list, and move nodes in one operation while also avoiding all the issues described in this thread.

@WebReflection
Copy link

@titoBouzout that's a diffing operation and it still addresses all the issue discussed in here as "moved nodes" might, or might not have, left the DOM before, or be fresh new. I think it's a wonderful idea and I've been asking already for that in the past but in here we need the right primitive to just move nodes (sort tables, as example, or LIs) without involving all the overhead that disconnecting and reconnecting moved nodes involve.

Once we have that, we can think about a parent.replaceChildrenAtomic operation as all mechanisms to do so will be already in place, imho.

@domfarolino domfarolino added stage: 2 Iteration and removed agenda+ To be discussed at a triage meeting stage: 1 Incubation labels Sep 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest stage: 2 Iteration
Development

No branches or pull requests