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
Comments
This comment was marked as spam.
This comment was marked as spam.
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:
I understand a node can be moved from
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 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
As 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 The |
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! |
Add some complexity to selection/range: how to deal with Shadow DOM when the host moves around and selection is partially in shadow DOM? |
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: |
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 ? |
It's already on the agenda, so if the interested parties are attending we will discuss this. |
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. |
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:
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 Something new like |
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. |
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. |
(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).
@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 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:
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
But I would love to get more thoughts on the subtree side-effects stuff in general. Footnotes
|
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 |
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 |
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. |
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. |
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. |
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:
|
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 |
To briefly summarize the WHATNOT meeting discussion about this issue, we tentatively landed on:
|
if something like |
Catching up with the conversation and adding Angular's perspective here. tl;dr;
Given the above I think that Angular's position is very well aligned with the current direction of the proposal 🎉 Some more details below. BackgroundWe regularly see issues caused by the "logical move" operation implemented as a pair of remove + add and loosing state as the consequence. This mostly comes up when using loops (@for or ngFor) that re-order lists - those loops have perfect understanding / distinction of insert vs. move so loosing state with moves is a real concern. We mostly see people complain about state loos in form controls (selection, focus) and iframes. Despite seeing those issues we never attempted implementing any work-around but rather were counting on a solution from the platform - in this sense very supportive of those efforts. API proposaldeclarative vs. imperativeWe mostly move nodes around when re-ordering list items in loops. Those framework constructs are executing JavaScript logic to understand lists re-ordering and move DOM nodes around accordingly. In this sense this logic is already very imperative and thus we would require imperative API to make use of the new platform capability. Declarative attributes could be used to opt out of the state preserving behaviour but using them to indicate that a state of a given node should be preserved would be problematic - we would have to pretty much add those new attributes to all the nodes created by the framework. API shapeTechnically speaking most of the proposed API signatures ( listing state to preserveWe would rather not explicitly list state to preserve as proposed with re-parentingNot strictly necessary in the core of the framework (we move nodes under the same parent) so could see a 2-phase approach were this simpler use-case is tackled first. Breaking changesWe do understand that the existing code might somehow depend on the fact that state is reset when the corresponding DOM nodes are moved but we see this more like a bug. Other commentsThe framework is usually moving a set of nodes so would love to see some thinking on this - similar consideration to the one expressed by @WebReflection in #1255 (comment) |
I'm a month late to the party, but I too am excited about this proposal! For certain scenarios -- especially ones involving an iframe -- the ability to re-root an iframe could vastly improve perceived performance and overall user experience. For the specific product that I am working on, the specific cases that we are envisioning using this for are:
Looking forward to seeing this proposal come to life! |
See whatwg/dom#1255 Animations & transitions should attempt to continue from where they left off, if possible. This is done in the following way: - Animations are not cancelled on removal when in a state-preserving atomic move. - We don't reset the computed style when removing the element in preparation for an atomic move. - We don't clear the layout/style flags, so that the layout is recomputed and reattached on the next style recalc. Bug: 40150299 Change-Id: I559e69e75df14df589485cb024da0f0f28b1e1ec
See whatwg/dom#1255 Animations & transitions should attempt to continue from where they left off, if possible. This is done in the following way: - Animations are not cancelled on removal when in a state-preserving atomic move. - We don't reset the computed style when removing the element in preparation for an atomic move. - We don't clear the layout/style flags, so that the layout is recomputed and reattached on the next style recalc. Bug: 40150299 Change-Id: I559e69e75df14df589485cb024da0f0f28b1e1ec
I didn't see this explicitly stated so asking directly: is it intended that atomic moves can be observed via APIs like And if so, does the atomic nature of the move need to be exposed somehow? Not sure if it does, but wanted to note that this would probably be easier to do in a |
Yes, it needs to be. See #1255 (comment). |
See also #1270 where we track the individual side effects of atmoc moves. |
See whatwg/dom#1255 Animations & transitions should attempt to continue from where they left off, if possible. This is done in the following way: - Animations are not cancelled on removal when in a state-preserving atomic move. - We don't reset the computed style when removing the element in preparation for an atomic move. - We don't clear the layout/style flags, so that the layout is recomputed and reattached on the next style recalc. Bug: 40150299 Change-Id: I559e69e75df14df589485cb024da0f0f28b1e1ec
See whatwg/dom#1255 Animations & transitions should attempt to continue from where they left off, if possible. This is done in the following way: - Animations are not cancelled on removal when in a state-preserving atomic move. - We don't reset the computed style when removing the element in preparation for an atomic move. - We don't clear the layout/style flags, so that the layout is recomputed and reattached on the next style recalc. Bug: 40150299 Change-Id: I559e69e75df14df589485cb024da0f0f28b1e1ec
See whatwg/dom#1255 Animations & transitions should attempt to continue from where they left off, if possible. This is done in the following way: - Animations are not cancelled on removal when in a state-preserving atomic move. - We don't reset the computed style when removing the element in preparation for an atomic move. - We don't clear the layout/style flags, so that the layout is recomputed and reattached on the next style recalc. Bug: 40150299 Change-Id: I559e69e75df14df589485cb024da0f0f28b1e1ec
See whatwg/dom#1255 Animations & transitions should attempt to continue from where they left off, if possible. This is done in the following way: - Animations are not cancelled on removal when in a state-preserving atomic move. - We don't reset the computed style when removing the element in preparation for an atomic move. - We don't clear the layout/style flags, so that the layout is recomputed and reattached on the next style recalc. Bug: 40150299 Change-Id: I559e69e75df14df589485cb024da0f0f28b1e1ec Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5458120 Commit-Queue: Noam Rosenthal <nrosenthal@chromium.org> Reviewed-by: Rune Lillesveen <futhark@chromium.org> Cr-Commit-Position: refs/heads/main@{#1291811}
See whatwg/dom#1255 Animations & transitions should attempt to continue from where they left off, if possible. This is done in the following way: - Animations are not cancelled on removal when in a state-preserving atomic move. - We don't reset the computed style when removing the element in preparation for an atomic move. - We don't clear the layout/style flags, so that the layout is recomputed and reattached on the next style recalc. Bug: 40150299 Change-Id: I559e69e75df14df589485cb024da0f0f28b1e1ec Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5458120 Commit-Queue: Noam Rosenthal <nrosenthal@chromium.org> Reviewed-by: Rune Lillesveen <futhark@chromium.org> Cr-Commit-Position: refs/heads/main@{#1291811}
…veBefore, a=testonly Automatic update from web-platform-tests Don't reset animations/transitions on moveBefore See whatwg/dom#1255 Animations & transitions should attempt to continue from where they left off, if possible. This is done in the following way: - Animations are not cancelled on removal when in a state-preserving atomic move. - We don't reset the computed style when removing the element in preparation for an atomic move. - We don't clear the layout/style flags, so that the layout is recomputed and reattached on the next style recalc. Bug: 40150299 Change-Id: I559e69e75df14df589485cb024da0f0f28b1e1ec Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5458120 Commit-Queue: Noam Rosenthal <nrosenthal@chromium.org> Reviewed-by: Rune Lillesveen <futhark@chromium.org> Cr-Commit-Position: refs/heads/main@{#1291811} -- wpt-commits: 7a801529e3cb3f2146fb0e7f9732734eb12eb580 wpt-pr: 45743
…veBefore, a=testonly Automatic update from web-platform-tests Don't reset animations/transitions on moveBefore See whatwg/dom#1255 Animations & transitions should attempt to continue from where they left off, if possible. This is done in the following way: - Animations are not cancelled on removal when in a state-preserving atomic move. - We don't reset the computed style when removing the element in preparation for an atomic move. - We don't clear the layout/style flags, so that the layout is recomputed and reattached on the next style recalc. Bug: 40150299 Change-Id: I559e69e75df14df589485cb024da0f0f28b1e1ec Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5458120 Commit-Queue: Noam Rosenthal <nrosenthal@chromium.org> Reviewed-by: Rune Lillesveen <futhark@chromium.org> Cr-Commit-Position: refs/heads/main@{#1291811} -- wpt-commits: 7a801529e3cb3f2146fb0e7f9732734eb12eb580 wpt-pr: 45743
This is awesome, my only input here is to kindly ask to remember not to implement this behaviour for when elements cross realms (such as when an element is being reparented under a different document than its current one). I believe this was referred in the descritpion:
Which is great, just want to emphasize that this is important from the security angle. Also - cross-document should mean all ways to achieve that, whether by iframe or popups: const child = document.createElement('a');
document.body.appendChild(document.createElement('iframe')).contentDocument.body.append(child);
// OR
open('').document.body.append(child); I assume this is clear already, but thought it's worth mentioning. |
This comment was marked as spam.
This comment was marked as spam.
Thanks Gal, yes the scope of this is same-document moves. Magic iframes are a separate can of worms 🐛. |
I think it's a very good feature, one of the current problems of signals is to manage lists, but with this you could reorganize the lists keeping the signals on it without the need of a diff DOM algorithm right? I love it. |
Great initiative!
|
Regarding the pointer capture issue, I want to make sure I understand the scenario. From experimenting with this demo I made, it seems that moving an element around the DOM (with classic
Our proposal is limited to the same-document move case for now. The cross-document case is significantly more complicated, and most of the big use cases are unlocked with the narrower-scoped approach, so we are starting out with that. |
Yes, pointer capture itself should stay active and the I guess that for this special case we could actually check if the element has pointer capture active and in that case do the swap the other way around ( |
Yep, I had made the same exact code change to your codepen on my own to experiment with. So yeah, while this sort of thing can be worked around by moving different nodes around the DOM with the classic APIs, encouraging this is bad because it just pushes the side effects of state-resetting to other nodes that are further away from the original interaction. It looks like this is all captured in #1270, which points to the relevant parts of the pointer events spec (example), so I think we're all on track for this! |
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:
insertBefore
vsappendChild
and transitions #880Problem
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:
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).
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:
append(node, {atomic: true})
,replaceChild(node, {atomic: true})
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.
A non-exhaustive list of additional complexities that would be nice to track/discuss before a formal design:
Anything else?
No response
The text was updated successfully, but these errors were encountered: