-
Notifications
You must be signed in to change notification settings - Fork 295
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
Proposal: New method to reorder child nodes #891
Comments
I think without fully understanding (and specifying) #808 it's hard to reason about how moving-within-a-parent should work. I.e., what side effects we need and do not need. I think the most logical API that follows existing conventions would be I would also like to see some more rationale as to why an arbitrary move (with both parents sharing a common root) is less feasible. What are the particular implementation challenges that we would only expose a more restricted move? |
I'm not convinced of spec complexity of modifying existing operations being that much higher (though it would technically be somewhat higher): #880 (comment) |
The disconnectedCallback and connectedCallback reactions fire on custom elements when nodes are shuffled like this. It seems very unlikely to me that this would not break sites (including ours). |
This is due to @rniwa's comments in whatwg/html#5484 against reparenting iframes without reloading them. |
Moving within a parent should:
Are there other types of side effects I'm not aware of? |
@josepharhar Why shouldn't it fire a mutation event of a new type? If a new method is added specifically for moving elements around, they can't possibly fire that mutation without invoking that previously-unknown method anyways, so in theory, a page would have to have its contents updated to experience breakage. Also, I would expect it to invoke a new hook within custom elements to notify it of nodes being reordered. (This is very useful info for them, BTW, as libraries like A-Frame don't use shadow roots to render their data but otherwise need to know layout order at all times.) As for the rest, I agree. |
Yeah I presume that we would add new mutation signaling stuff for MutationObserver and whatever the equivalent is for custom elements (I'm not super familiar with either yet). I don't think that we should make new mutation events since they seem to be deprecated and people are interested in removing them from the web - I don't have historical context on this though. Right now I'm just trying to work through the relationship between this new proposed method and #808, which is interested in mutation events. |
I would really like to better understand what the use cases are. Could someone provide a concrete scenario (not just framework X does Y so they need it) in which this capability is desirable? |
@sebmarkbage @isiahmeadows @marvinhagemeister @developit could any/all of you elaborate on scenarios where state is lost when reordering child nodes? Links to issues would be greatly appreciated, as well as live examples if possible. @isiahmeadows already made some live examples with four different frameworks listed in the description of #880. It does sound like appendChild has the desired behavior in that particular case though...? |
@josepharhar The transitions issue is that of this:
Though now that I'm taking a closer look, I'm not sure movement semantics would help with transitions, as it reproduces even with simple add/remove. So my issue is independent of this. Edit: Further confirmed that the transitions issue is independent of this. |
I don't think that discussion was really settled though. Those arguments would apply to moving within a parent as well as you have to contend with subtrees and multiple elements there too. |
@rniwa said that "Since a node can't have multiple parents, it needs to be disconnected at some point in some internal engine state. That's precisely what caused the problem." |
Well, it does address the previous concern but re-ordering nodes isn't something we've ever done so we'd likely encounter new list of issues with it. I'd still like to learn more about what specific use cases would require this new capability, and why careful manipulations of nodes in the user land won't suffice. |
@rniwa what problem would be caused by disconnecting an |
Well, that's a big "if". Something like that is not possible to implement in WebKit today. |
if DOM had pass by reference equivalent this would not be an issue. Need pointers in DOM. |
Now that I'm thinking about this problem more, one serious challenge is correctly invalidating and updating the computed style of each node when this happens due to things like sibling selector, |
I'm particularly interested in Some concerns/reservations about this feature though:
(3) and (4) makes me think a method like |
regarding |
That won't work for anything like a UI library, where someone would expect to be able to use the |
I came across this issue while searching for existing solutions for part of a UI library I'm writing. In essence, the DOM needs a way to move a child node among its sibling nodes without going through the state reset issues caused by having to first take the node out. This is possible for special cases where we can just move well-behaved sibling nodes Fundamentally, we could solve this by adding in a primitive operation that moves a node without removing it from its parent first. It wouldn't actually "break" anything except for in a hypothetical pathological case with a third party Just consider how much more efficient it would be to add a I'm sure that we will have to also make something along the lines of |
We basically need a swap function that doesn't remove elements from DOM. I'm not intimately familiar with DOM innerworkings but if DOM is seen as a Tree then it's not clear why swapping is so hard. In a tree structure, if I wanted to swap I'd assign one child to a temporary variable and then temp to node being swapped.
But maybe overwriting children[1] causes element to be taken out of DOM and wiped clean, DOM is recalculated/rerendered, even though it's added back in next step at children[2]'s position. So I see two solutions:
Thoughts? |
Please go read the past discussion in this issue as well as whatwg/html#5484 before making any suggestions or asking why something is hard. A lot of use cases have been already mentioned, and many questions have already been raised as well as corresponding implementation challenges. It's really counterproductive to keep repeating the same discussion every 2-3 years. |
I have gone down the rabbit-hole around this issue and I think that an atomic DOM child node move operation would be extremely valuable. Completely ignoring any convenience functions on top - and even going as far as excepting certain tricky cases in order to get this ball rolling - it would still be worth pursuing. Given that we know this type of operation is highly sought after in libraries like React (to name one), and we know that it would greatly improve web performance in many resource-intensive cases, how do we move forward? |
Are you suggesting that the reordering idea proposed here isn't good enough, and that we need to be able to move things all around the document? Just clarifying.
@rniwa has pointed out several difficulties with adding this capability to the DOM, and there's clearly a lot more complications and corner cases to consider with full tree moving than there is with the reordering proposed here. I failed to gain traction in this proposal because I didn't get the feedback I wanted from React or Preact in this comment: #891 (comment) |
Sorry for the miscommunication, I meant atomic move among siblings, the primitive operation enabling the proposal. The animation problem that the Mithril maintainer mentioned would be orthogonal to this - if I understood it right - because it was due to operations on siblings. Regardless of any complications due to iframes or any other exceptions we can make, this proposal is extremely worthwhile in order to keep input state and prevent unnecessary (dis)connectedCallbacks. Even if you didn't get an enthusiastic comment from a react/preact member here, it's almost guaranteed that you would if this had their attention. This is extremely underrated and you should push for it as much as you can. |
Except to show that this is still very much a current, common, desired feature that shouldn't be cancelled or overlooked due to an apparent lack of participation and interest. |
I actually don't know any UI library/framework which would not benefit from efficient |
I personally wanted this because I needed to show the same iframe for different screens, however, (react) unmounts a component and then mounts a new one for client side routing. This meant that the same iframe would have to reload every time. I figured if I could swap loaded-iframe with hidden div and then swap it back on route change then I'd have be able to keep the iframe element. I tried keep a JS only reference but the act of appending and removing from the DOM would 'erase' the iframe. So if for whatever reason the replaceNode() func can't be implemented even just having programmatic way to keep a reference to DOM element in a manner that preserves its rendering or something would be an upgrade. At the time I was so frustrated I was open to learn C++ and try adding it to chromium. |
The state you're interested is a nested browsing context. These contexts correspond to each "connectedness lifetime" (insertion to removal) of the element, of which there might be more than one. They are not the element itself, though. Disconnecting the element is somewhat like closing a tab. Right now, if you have connected iframes [ A, B ] as siblings in that order, there is (to my knowledge) no way to get them in the order [ B, A ] without destroying one of their associated browsing contexts. This is a spot where a reordering primitive would potentially enable preserving nested browsing contexts that currently can't be. But if I understand correctly, what you've described (a) would not be helped by such a method and (b) wouldn't need such help anyway: this case is already addressed by hiding/showing the iframe without disconnecting it. Unfortunately, React's agnosticism to elements having state/lifecycles goes pretty deep, so even though it's already possible, it may not be possible via React? I think that's something React would have to solve - the platform can't really help if it's disconnecting it and it's not an ordering issue. Plus a lot of stuff indirectly hangs off of the current iframe disconnection behavior, so the value bar to clear for even opt-in modifications to how iframe browsing context lifecycles work is probably really high. I thought maybe Portals might provide an alternative lifecycle here, but it looks like their browsing contexts will also be destroyed on disconnection. I'm not sure if there was any discussion there about the possibility of contexts that stay "alive" after disconnection, but it seems maybe more plausible in that fresher territory than in the already-so-complicated world of iframes? |
These details do matter but, considering every library/framework out there is using diffing based on DOM primitives anyway, I would say it's neither that important nor relevant, performance wise, or at least my libraries never had performance issue swapping nodes (after diffing) around. That being said, the diffing as meant by libraries and frameworks should operate as such, to provide the best DX:
I've proposed and discussed this approach as batched DOM operations a while ago, where moving nodes within a transaction should've behaved as described in this very same comment of mine, but I can't remember what was the argument then to ignore my idea and not going forward. Once again, this would be just the ideal world, but I wouldn't mind having just a native DOM differ and let engines optimize the rest, although I really do believe my proposed flow might be the most desired, and least unexpected, for developers. edit about polyfills: it's relatively trivial to implement one but likely extremely hard to avoid iframes destroying their context ... although I don't think this is important because that happens already in the wild and nobody seems to care much. |
Highly related: #880 A fix for this would resolve that issue as well, as it's another case where the current semantics of removing then re-adding causes state issues (in that case, transition state). |
@dead-claudia thanks for linking that! After a quick read I'd happily change my "implementation details" with the moved mutation record, in case the node is before and after the diff, and its index changed ... although I don't feel like this is really missing or super useful for developers, as they can already map and check before, and after, the |
@WebReflection I'd be good with something that's just |
That could be the very initial building block, so I’d sign for it too. Actually, moveBefore would be nicer, imho, instead of insertBefore. |
I don't think anyone is intentionally being ignored, but there's only a finite number of problems that can be tackled at any given time and it seems that at the moment nobody is pursuing this problem actively. And I strongly suspect that is primarily due to the complexity of the problem and unresolved technical debt in this area, not because nobody is interested. whatwg/html#5484 (comment) has some starting points if someone wants to pick this up and I'd be happy to mentor anyone interested in tackling this. |
I am sure there's no ill intention there, yet my point is that developers that see no movement over an open issue filed years ago will keep "bothering" about the very same issue every 2 or 3 years or until it's either solved or closed, and I think that's normal. This particular issue has been discussed in various shapes and colors up to 10 years to my memory, and yet not much happened, but every single library is in need of such primitive and benefits, at least in terms of unnecessary bloat, would be huge, as well as perf more predictable and, hopefully, less surprise, error, bugs prone. Thanks for pointing at "a starter" though, I hope something will happen soon(ish) 👍 |
@annevk Is this summary of the “story so far” accurate?:
Naturally I think solving #808 would be great (I have run into those existing discrepancies many times), but I’m a little fuzzy on why it would need to be solved to address #891 given (4) above — though I’m not 100% sure (4) is accurate. Please feel free to correct anything/everything I got wrong here :) |
I think fundamentally, we're looking at a "move" operation. And while it does seem simpler if the parent stays identical, we still need to fully understand what happens with insertion and removal (and this might well have to include mutation events as they haven't disappeared) in order to decide what will not happen (and what might happen instead) with move. More concretely, I don't see why the parent staying identical makes #808 disappear. We need to define/design what happens when you move an |
@annevk we already all know what happens when you remove and insert one, right? ‘cause if we don’t to date, I don’t see why that’s relevant at all to decide what happens when these move instead 🤔 |
Having this proposal taken forward and still having iframes (and other elements) losing state when moving between different parents would feel like a big opportunity loss, because reordering within the same parent will only cover a portion of the use cases. For example, if iframes are part of a drag and drop interaction, they might need to change parents. Moreover, there are many complex interaction patterns that relly on reparenting. Is this more general version of "moving" being planned somewhere else? Should a new parallel proposal be started? |
Just want to note that we see abuse of the CSS |
I think that the closest practical solution to this is the shadow dom with the imperative slot api. // Initially <body>abc</body>
const a = document.createTextNode("a")
const b = document.createTextNode("b")
const c = document.createTextNode("c")
document.body.append(a, b, c)
// body is a shadow host with a manually assigned slot
const slot = document.createElement("slot")
document.body
.attachShadow({
mode: "open",
slotAssignment: "manual"
})
.append(slot)
// can put child nodes in any order, exclude them, &c.
// even iframes will behave as if nothing happened
slot.assign(c, b, a) Since it's just a slot, there's no fumbling with style sheets or anything either, and it works without custom elements. However, there are restrictions on what elements can be shadow hosts. |
I don't think this is viable as an alternative - but since this API is already spec'ed and has concensus, a spec for generic DOM reordering should probably reference this.
The proposal makes no mention of preserving state of iframes, input elements, etc. - it doesn't look like this was spec'ed? So if it happens to work that way, that could be just luck. A generic DOM API likely needs to carefully spec this behavior. |
There’s nothing unique about that proposal AFAIK — this is how slots themselves behave. Adding and removing slots dynamically is also possible but it isn’t reparenting the things that get non-manually assigned to them, just assigning em. let shadow = document.body.attachShadow({ mode: "closed" });
document.body.innerHTML = "<iframe name=iframe>";
iframe.document.bgColor = "slot assignment isn’t connection/disconnection";
shadow.innerHTML = "<slot>"; |
Thanks @wlib, @bathos for highlighting this slot assignment behaviour. This interests me in the context of a few attempts I've made in the past to get predictable behaviour when moving chunks of DOM diagonally (ie from one parent node to another) in the least destructive way possible. I tried playing with this API to see if it could be used as a generic solution for re-ordering. CSS positional selectors stick to DOM ordering and don't reflect assignment order, which is fair enough. More interesting was that CSS list markers and counter increments seemed to respect assignment order in Firefox, while in Chrome they first fell out of sync, and in subsequent reorderings stuck to their last update. Here's a sandbox demonstrating the inconsistent behaviour. This indicates some ambiguity in the 'moving without moving' behaviour – perhaps useful in future API considerations. |
@barneycarroll that’s a great find + demo. In CSS Lists 3, the “tree” whose tree order is used to determine counter inheritance is the flattened tree. It takes/updates any resolved counter values during that algorithm, and though it talks about parents and siblings, the note at the end specifically clarifies that the tree in question is the flattened tree. I think that would mean the Firefox behavior is consistent with what’s specified.
I’m not good at reading CSS specs, though and may be looking in the wrong place or misinterpreting. @tabatkins would you know whether Blink or Gecko (or neither) is rendering @barneycarroll’s demo correctly? (aside 4 barney: been off twitter for most of a year now but yr name is among the few I miss encountering. nice to see ya) |
Unless otherwise specified, Selectors/Cascade is based on the DOM tree, and then everything else in CSS is based on the flattened tree. When this isn't true, it's almost certainly a browser bug; we're still shaking out wrong assumptions that predate the introduction of the flat tree. Chrome's "out of sync" counters are absolutely a counter invalidation bug; there is no world in which the counter going from 5-9 is correct. |
Absolutely great to see this technique is being used; you should tell them about this in [this bug](https://bugs.chromium.org/p/chromium/issues/detail?id=1330383) report.
Maybe I'm totally off, but there are use cases for having both. Is there any way that selectors can apply within the context of a slot? Like |
Closing this as a duplicate of #586 as this is both newer and that issue appears to have more traction. |
This issue is not a duplicate of #586, since the use case desired in this issue was avoiding reload of iframes and other resource/animation state. |
I guess that's fair, but we really shouldn't offer both I think, especially if they end up looking identical to mutation observers. I'll reopen for now and instead consider it a blocker of sorts to come up with a reasonable story. |
Summary
The goal of this method is to allow for the reordering of child nodes without the need to remove them and re-append them. Currently, reordering child nodes (by re-appending) causes several undesired side effects:
This is a problem for several frameworks, including React, Preact, and Mithril.
Adding a new API vs changing existing APIs
Instead of adding a new API, we could change existing uses of DOM APIs to avoid reparenting. For example:
In this case, you can see that the script wants to move
firstchild
pastsecondchild
and doesn't necessarily want the browser to removefirstchild
from the DOM and reparent it, so we could try to changeappendChild
to keep the parent throughout the DOM modification and avoid the loss of state.However, I think a new API would be a better solution:
appendChild
and other DOM modification methods would get more complicated due to difficulties defining and standardizing the existing behavior.API shape
I have some ideas for what this method could look like:
parentNode.reorderChildren([childOne, childTwo, childThree])
childNode.movePastNextSibling()
childNode.moveBeforePreviousSibling()
childNode.moveBefore(otherChildNode)
childNode.moveAfter(otherChildNode)
I don't want to bikeshed too much on this until it really sounds like we will add a new method, but if any API shape seems particularly good please speak up so I can start prototyping.
Relationship to #808
@annevk is #808 blocking us from having a new way to reorder child nodes? That issue seems more concerned about insertions and mutation events which don't really seem to apply to reordering. Couldn't we just have new spec steps with no special functionality for iframes and scripts and no mutation events?
I made this issue based on feedback in this discussion: whatwg/html#5484
The text was updated successfully, but these errors were encountered: