Giter VIP home page Giter VIP logo

Comments (43)

WebReflection avatar WebReflection commented on June 12, 2024 15

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.

from dom.

1cg avatar 1cg commented on June 12, 2024 8

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!

from dom.

1cg avatar 1cg commented on June 12, 2024 7

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.

from dom.

past avatar past commented on June 12, 2024 5

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

from dom.

domfarolino avatar domfarolino commented on June 12, 2024 5

To briefly summarize the WHATNOT meeting discussion about this issue, we tentatively landed on:

  • Moving this to stage 1 🎉
  • Not pursuing an iframe-specific solution, but handling the big known use cases in https://reparent.jarhar.com/ up-front
  • Pursuing a generic DOM API that would use a new "move" DOM primitive operation
    • This API would not have a bag of options to specify what kind of state should be preserved, but would preserve all of "the big known things" captured in https://reparent.jarhar.com/
    • The compat risk of, in the future, adding possibly-new forms of preservable state that would begin to be preserved by the API, seems "manageable." (The risk of this is at least in part ameliorated by the fact that author JS can opt out of the preservation for any part of any moved subtree, just by removing and re-inserting nodes)

from dom.

WebReflection avatar WebReflection commented on June 12, 2024 3

if something like node.moveBefore(...) that mimics insertBefore lands I think this would be huge for the entirety of the Web! thanks for the update, looking forward to see progress or even test, whenever possible, the implementation if you need any extra report/eyes around the diffing side-topic this could solve too.

from dom.

pkozlowski-opensource avatar pkozlowski-opensource commented on June 12, 2024 3

Catching up with the conversation and adding Angular's perspective here.

tl;dr;

  • very supportive of the effort, definitively see the issues of loosing state when moving DOM nodes around;
  • strongly prefer a new, imperative DOM API (node.moveBefore(...) or similar);
  • not requiring re-parenting in the core of the framework;
  • would like to see general "preserve DOM state" considered and not have it limited to IFrames only;
  • not too concerned with the breaking changes as we see the current non-preserving-state behaviour as problematic - some backward compatibly escape hatch might be useful but not required.

Given the above I think that Angular's position is very well aligned with the current direction of the proposal 🎉

Some more details below.

Background

We 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 proposal

declarative vs. imperative

We 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 shape

Technically speaking most of the proposed API signatures (parent, parent.insertBeforeAtomic, parent.insertBefore(..., {atomic: true}), node.moveBefore(...) ) would work for us but node.moveBefore(...) seems like a cleanest and preferred shape.

listing state to preserve

We would rather not explicitly list state to preserve as proposed with parent.insertBeforeAtomic(div, reference, {preserve: 'iframecontent,selection,foo'}) - this would require us to keep adding new capabilities as they come around, track those and consider breaking changes. Would prefer the approach where we decide, on the platform level on "state that makes sense to preserve".

re-parenting

Not 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 changes

We 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 comments

The 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)

from dom.

ydogandjiev avatar ydogandjiev commented on June 12, 2024 2

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

from dom.

rniwa avatar rniwa commented on June 12, 2024 2

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.

from dom.

noamr avatar noamr commented on June 12, 2024 2

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.

from dom.

gnoff avatar gnoff commented on June 12, 2024 2

@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

from dom.

1cg avatar 1cg commented on June 12, 2024 2

from an htmx perspective, we have an attribute, hx-preserve that preserves an element in the DOM:

https://htmx.org/attributes/hx-preserve/

It does this by looking for "preserved" elements in the new content (fragment) and finding the "old" version in the existing DOM by id:

https://github.com/bigskysoftware/htmx/blob/c247cae9bf04b5b274d3bd65937541e8224a359c/src/htmx.js#L887

        function handlePreservedElements(fragment) {
            forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function (preservedElt) {
                var id = getAttributeValue(preservedElt, "id");
                var oldElt = getDocument().getElementById(id);
                if (oldElt != null) {
                    preservedElt.parentNode.replaceChild(oldElt, preservedElt);
                }
            });
        }

With a new state preserving API we'd update this to something like:

        function handlePreservedElements(fragment) {
            forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function (preservedElt) {
                var id = getAttributeValue(preservedElt, "id");
                var oldElt = getDocument().getElementById(id);
                if (oldElt != null) {
                    preservedElt.parentNode.replaceChildSPAM(oldElt, preservedElt);
                }
            });
        }

And then all people would need to do to keep, for example, that video playing between pages would be to mark the element as hx-preserved, regardless of what the new content looks like.

(Apologies for the forEach() and findAll() methods, they do what you think they do, htmx 1.x is IE compatible so we had to roll our own versions of some stuff.)

from dom.

annevk avatar annevk commented on June 12, 2024 2

I would be much more in favor of tackling this once. I.e., go through "remove" and "insert" and collect all the impacted pieces of state and then determine how to handle them for "move". Write expansive test coverage for all the scenarios and then implement, and then write some more tests.

from dom.

Zlatkovsky avatar Zlatkovsky commented on June 12, 2024 2

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:

  • Primary use-case: load an iframe invisibly in the background (based on a heuristic of the user eventually needing it) so that when the user clicks on a button to view the iframe, we can swap in the already-loaded iframe much quicker than if we were to load from scratch.
  • Secondary use-case: allow moving an already user-visible iframe from one spot in the DOM into another.

Looking forward to seeing this proposal come to life!

from dom.

sorvell avatar sorvell commented on June 12, 2024 2

I didn't see this explicitly stated so asking directly: is it intended that atomic moves can be observed via APIs like MutationObserver and custom element reactions connected/disconnectedCallback? It seems like this is required to ensure compat.

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 MutationRecord than a custom element reaction, but both should be considered.

from dom.

annevk avatar annevk commented on June 12, 2024 2

Yes, it needs to be. See #1255 (comment).

from dom.

noamr avatar noamr commented on June 12, 2024 1

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 ?

from dom.

noamr avatar noamr commented on June 12, 2024 1

Yes, it needs to be. See #1255 (comment).

See also #1270 where we track the individual side effects of atmoc moves.

from dom.

weizman avatar weizman commented on June 12, 2024 1

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:

"Not allowing atomic moves across documents, which should greatly simplify the security story of this work"

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.

from dom.

noamr avatar noamr commented on June 12, 2024 1

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:

"Not allowing atomic moves across documents, which should greatly simplify the security story of this work"

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.

Thanks Gal, yes the scope of this is same-document moves. Magic iframes are a separate can of worms 🐛.

from dom.

domfarolino avatar domfarolino commented on June 12, 2024 1

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 insertBefore(), for example) that had setPointerCapture() called on it before the move, results in pointer capturing being reset & the lostpointercapture event being fired. And it sounds like you'd want it to be preserved, and thus the lostpointercapture event would not be fired. Is that right?

I would like to be able to move a document from an iframe to a separate window open

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.

from dom.

domfarolino avatar domfarolino commented on June 12, 2024 1

This is because when swapping two children one has to make a choice which one to remove from the DOM and then reinsert. So if we always remove the one on the right (as React does), pointer capture will be lost only when moving to the left.

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 (insertBefore(prev, next.nextSibling) instead of insertBefore(next, prev)), but this is not something React does.

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!

from dom.

jarhar avatar jarhar commented on June 12, 2024

from dom.

smaug---- avatar smaug---- commented on June 12, 2024

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?

from dom.

infogulch avatar infogulch commented on June 12, 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).

from dom.

iteriani avatar iteriani commented on June 12, 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?

from dom.

noamr avatar noamr commented on June 12, 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.

from dom.

sebmarkbage avatar sebmarkbage commented on June 12, 2024

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.

from dom.

rniwa avatar rniwa commented on June 12, 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.

from dom.

domfarolino avatar domfarolino commented on June 12, 2024

(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!

from dom.

rniwa avatar rniwa commented on June 12, 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.

from dom.

domfarolino avatar domfarolino commented on June 12, 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.

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).

from dom.

domfarolino avatar domfarolino commented on June 12, 2024

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!

from dom.

noamr avatar noamr commented on June 12, 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.

from dom.

annevk avatar annevk commented on June 12, 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.)

from dom.

noamr avatar noamr commented on June 12, 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.

from dom.

sebmarkbage avatar sebmarkbage commented on June 12, 2024

Although, if it was web-compatible, making it just a breaking change so it's always atomic is even better.

from dom.

infogulch avatar infogulch commented on June 12, 2024

I think there are two things here:

  1. The DOM needs a new fundamental capability that it never had before, i.e. insertBeforeAtomic
  2. If an app is composed from multiple separate components they will need to coordinate around how to use insertBeforeAtomic.

1 must be solved by adding a new DOM api to the spec. IMO, 2 should be the developer's responsibility to manage for now, and adding attributes to allow elements to control how they are swapped should be a separate feature request and discussion.

If a page is designed around components that expect some convention wrt how it coordinates use of the new API that's fine, but that doesn't need to be baked into the spec right now, for two reasons:

  • It dramatically increases the scope of this feature. Just the debate of exactly what the semantics of the attribute should be, how it applies to different node types, etc could burn a lot of time.
  • We don't even know how components will need to coordinate. I don't particularly mind the preserve attribute approach, but I'm not fully convinced that this is the correct API to permanently bake into the DOM either. We've never had insertBeforeAtomic before, so we could just guess what the best API is to coordinate use of it and bake it into the spec and cross our fingers that we got it right, or we could wait until the new feature has some mileage in real apps and consider adding something based on what authors have learned from experience.

from dom.

domfarolino avatar domfarolino commented on June 12, 2024

Great, thanks for the input @gnoff @infogulch @1cg etc! Here are some of the main points I'm gathering, and some extra thoughts on compat and future extensibility:

  1. The code performing the move should decide whether the move is state-preserving atomic or not (not the preservable elements themselves). This tilts things towards an imperative API, which is straight to the point of introducing the new capability, and mirrors existing DOM APIs 🎉
  2. For compatibility, whatever API we land on probably needs a way to express the exact breadth of the "preservation" of a SPAM move. This is important when we consider "preserving" things beyond just iframe documents in the future, like input selection/focus, CSS transitions, and for elements that can have several of these preserved/reset during any given move.
    • I think this is important compat-wise, because if you start using parent.insertBeforeAtomic(div, reference) to preserve iframe documents in div, and then later we introduce the ability to preserve <input> selection/focus, we probably don't want insertBeforeAtomic() to all of the sudden start preserving <input>s inside div without your consent.
    • Imperatively, this could look like parent.insertBeforeAtomic(div, reference, {preserve: 'iframecontent,selection,foo'}). This ensures the behavior of insertBeforeAtomic() doesn't change out from under you as we make more elements/behavior preservable over time.
  3. Even with an imperative API, which gives the mover the ability to choose whether to preserve state, there could still be some legacy content deep inside the moved subtree that might not expect state to be preserved during a move. There are a few ways to handle this:
    • [Seems most popular] Do nothing! We don't need to design the API around this; insertBeforeAtomic() should always preserve the specified state in the entire moved tree. If you really want to opt out part of the tree from preservation, you can just manually remove & re-insert those things right after insertBeforeAtomic().
    • Add an option to the API that lets you specify portions of a subtree to not preserve: parent.insertBeforeAtomic(div, reference, {notPreserve: [div.querySelector('.legacy-content-1', …)]}).
    • Introduce an HTML attribute that can apply to portions of a subtree, which makes insertBeforeAtomic() behave like insertBefore() for that subtree bearing the opt-out attribute. It sounds like this isn't popular, but I figured I'd just list it for completeness.

Regarding (2) above, if we start by introducing the new API to preserve iframe document state (before we consider other state like selection/focus), we might not want to force users to pass in the clunky options dictionary insertBeforeAtomic(div, reference, {preserve: iframecontent}). We could instead just make insertBeforeAtomic(div, reference) always preserve iframe content state, and then progressively introduce an options dictionary if we make more state in the future preservable. That way right off the bat, the insertBeforeAtomic() API is simple and easy to use, in case we never extend it further for other kinds of state.

Thoughts?

from dom.

jarhar avatar jarhar commented on June 12, 2024

from dom.

aralroca avatar aralroca commented on June 12, 2024

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.

from dom.

guillaumebrunerie avatar guillaumebrunerie commented on June 12, 2024

Great initiative!
As a React user, I've encountered two specific situations where the lack of such an API has been an issue, which I hope will be covered by this proposal:

  • Unexpectedly losing pointer capture when an element in a list gets reordered. Pointer capture is a feature allowing an element to keeps receiving move events even when the pointer moves outside the element, but it is automatically canceled if the element is removed from the DOM. It's natural to use it for elements that can be dragged around, which are also most likely going to get reordered in the HTML.

  • I would like to be able to move a document from an iframe to a separate window open with window.open, and conversely, while preserving all JavaScript state. I'm not sure if this is covered by the "reparenting iframes" use case? This should then ideally be handled automatically by React by simply changing the target of a portal.

from dom.

guillaumebrunerie avatar guillaumebrunerie commented on June 12, 2024

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 insertBefore(), for example) that had setPointerCapture() called on it before the move, results in pointer capturing being reset & the lostpointercapture event being fired. And it sounds like you'd want it to be preserved, and thus the lostpointercapture event would not be fired. Is that right?

Yes, pointer capture itself should stay active and the lostpointercapture event should not be fired.
Here is a codepen closer to the issue I was facing: https://codepen.io/Latcarf/pen/NWXjYNb. Try to drag around the "Drag me" rectangle and you'll notice that while it’s easy to drag it to the right, it’s pretty hard to drag it to the left (it keeps getting "stuck"). This is because when swapping two children one has to make a choice which one to remove from the DOM and then reinsert. So if we always remove the one on the right (as React does), pointer capture will be lost only when moving to the left.

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 (insertBefore(prev, next.nextSibling) instead of insertBefore(next, prev)), but this is not something React does.

from dom.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.