whatwg / dom

DOM Standard
https://dom.spec.whatwg.org/
Other
1.58k stars 295 forks source link

Proposal: New method to reorder child nodes #891

Open josepharhar opened 4 years ago

josepharhar commented 4 years ago

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:

<div id=parent>
  <div id=firstchild></div>
  <div id=secondchild></div>
</div>
<script>
  parent.appendChild(firstchild);
</script>

In this case, you can see that the script wants to move firstchild past secondchild and doesn't necessarily want the browser to remove firstchild from the DOM and reparent it, so we could try to change appendChild 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:

API shape

I have some ideas for what this method could look like:

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 https://github.com/whatwg/dom/issues/808

@annevk is https://github.com/whatwg/dom/issues/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: https://github.com/whatwg/html/issues/5484

annevk commented 4 years ago

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 parent.moveChildBefore(Node child, Node? referenceChild). However, if sole use case is reordering multiple children parent.moveChildren(Node...) probably makes more sense. Either way this also requires designing new mutation records as this is a new mutation primitive. And we should probably forbid firing (legacy) mutation events.

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?

dead-claudia commented 4 years ago

I'm not convinced of spec complexity of modifying existing operations being that much higher (though it would technically be somewhat higher): https://github.com/whatwg/dom/issues/880#issuecomment-671033686

bathos commented 4 years ago

If there are any sites out there which rely on the full reparenting logic, they could become broken.

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

josepharhar commented 4 years ago

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?

This is due to @rniwa's comments in https://github.com/whatwg/html/issues/5484 against reparenting iframes without reloading them.

josepharhar commented 4 years ago

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.

Moving within a parent should:

Are there other types of side effects I'm not aware of?

dead-claudia commented 4 years ago

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

josepharhar commented 4 years ago

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

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. To clarify - when I say "mutation events," I am referring to this: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events

rniwa commented 4 years ago

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?

josepharhar commented 4 years ago

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

dead-claudia commented 4 years ago

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

annevk commented 4 years ago

This is due to @rniwa's comments in whatwg/html#5484 against reparenting iframes without reloading them.

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.

josepharhar commented 4 years ago

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." When reordering children instead of reparenting them across the DOM, we will never have to change the pointer to the parent node, only the node's sibling pointers and the parent's first/last node pointers. @rniwa this does address your concern, right?

rniwa commented 4 years ago

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." When reordering children instead of reparenting them across the DOM, we will never have to change the pointer to the parent node, only the node's sibling pointers and the parent's first/last node pointers. @rniwa this does address your concern, right?

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.

annevk commented 4 years ago

@rniwa what problem would be caused by disconnecting an <iframe> from its parent if we can also guarantee that no script would run until it is attached again?

rniwa commented 4 years ago

@rniwa what problem would be caused by disconnecting an <iframe> from its parent if we can also guarantee that no script would run until it is attached again?

Well, that's a big "if". Something like that is not possible to implement in WebKit today.

techsin commented 3 years ago

if DOM had pass by reference equivalent this would not be an issue. Need pointers in DOM.

rniwa commented 3 years ago

@rniwa what problem would be caused by disconnecting an <iframe> from its parent if we can also guarantee that no script would run until it is attached again?

Well, that's a big "if". Something like that is not possible to implement in WebKit today.

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, :first-of-type, :first-child, etc... that could put display: none on object and embed elements since right now, making those elements display: none would unload the "plugin". There might be other subtle challenges that I haven't thought of like what happens to things like focus, selection, etc... since selection end points may clip if the reorder were to happen.

mindplay-dk commented 3 years ago

I'm particularly interested in parentNode.reorderChildren, which could probably greatly simplify the reconciliation algorithms in some UI libraries, where reordering (in as few operations as possible) is often the only really "tricky" part. Reconciliation generally adds a lot of complexity - there are many different implementations with many pros and cons, and the code tends to be fairly difficult to understand.

Some concerns/reservations about this feature though:

  1. Adding this feature would most likely be a breaking change in terms of MutationObserver, which would likely require a new type of mutation? Existing code that currently captures all mutations would essentially be incompatible with any newer code that uses the feature - and so, most likely, must be regarded as a breaking change?

  2. It probably can't be polyfilled? If you implement a polyfill that uses existing DOM mutations to affect the same change, MutationObserver would broadcast these as individual changes.

  3. What happens if you pass nodes that aren't already child-nodes of the given parent? Throwing errors of course is one option. Alternatively, this could work like appendChild and remove the node from somewhere else, if already present in the document.

  4. What happens if you omit an existing child-node from the list? Again, throwing errors is an option. More likely, what someone meant to do though, was remove the missing nodes.

(3) and (4) makes me think a method like setChildren might be more meaningful - this would clear out nodes no longer in the given list of node, add or move nodes already present in the document, essentially forcing the list of children into a particular state, in a single operation, which is what most UI libraries are trying to do. This seems more consistent with e.g. appendChild and is probably more generally useful? For example, this would make it easy (and fast) to implement sortable table rows.

techsin commented 3 years ago

regarding parentNode.reorderChildren, there is handy hackish way that this can be sort of achieved without much reordering, by using css order attribute with flexbox.

mindplay-dk commented 3 years ago

regarding parentNode.reorderChildren, there is handy hackish way that this can be sort of achieved without much reordering, by using css order attribute with flexbox.

That won't work for anything like a UI library, where someone would expect to be able to use the order attribute for their own purposes - responsive design, etc. Also, CSS order does not affect tab-order, screen readers, etc. and therefore really has entirely different, totally unrelated uses. Let's stay on topic.

wlib commented 3 years ago

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 .before() and .after() nodes that are trickier to deal with, like document.activeElement, iframes, custom elements with connectedCallback(), &c. For the general case, we start needing to find out how to minimize the nodes that we move - which means we have to find the longest common subsequence between the current and desired childNodes. That translates to massive diff, patch, and state normalizing algorithms - all just to try to minimize the number of times we need to (attempt to correctly) reset reordered element state.

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 MutationObserver making bad assumptions, since this operation is equivalent to moving sibling nodes instead. For convenience, we can have analogs for before, prepend, append, after, replaceChildren, and replaceWith that simply don't start by removing their own child nodes.

Just consider how much more efficient it would be to add a .replaceChildren() analog that only had to internally switch its childNodes. Reconciliation (both very time and space-complex) would be massively simplified, leading to browsers decreasing resource consumption by massive amounts for certain classes of UI's. UI libraries get to shrink and browsers have to deal with less code across the board. It would be an absolute shame to block this on the grounds of maybe breaking a backwards third party's MutationObserver.

I'm sure that we will have to also make something along the lines of MutationRecord.movedNodes too. In any case, this issue is much more important than it seems to be getting attention for.

techsin commented 3 years ago

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.

t  = children[1]
children[1] = children[2]
children[2] = t

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:

1) Either have hidden carry over child, that is never visible, so when swapping 1st element it can be assigned to this ever present placeholder child element and therefore remains part of the DOM preventing it getting cut of from DOM tree and wiped clean.

2) Or have batch render/compute. Don't trigger render on element being removed, allow internal swap function to do step 1,2,3 and then recompute/rendering logic.. Preventing children[1] content from being wiped right when it's over written as it's still referred by DOM.

Thoughts?

rniwa commented 3 years ago

Please go read the past discussion in this issue as well as https://github.com/whatwg/html/issues/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.

wlib commented 3 years ago

Please go read the past discussion in this issue as well as whatwg/html#5484 before making any suggestions ... 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?

josepharhar commented 3 years ago

an atomic DOM child node move operation would be extremely valuable

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.

how do we move forward?

@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: https://github.com/whatwg/dom/issues/891#issuecomment-690853328 And the Mithril maintainer said that this isn't actually related to their problem here: https://github.com/whatwg/dom/issues/891#issuecomment-690868930

wlib commented 3 years ago

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.

shannonmoeller commented 3 years ago

counterproductive to keep repeating the same discussion every 2-3 years

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.

Bobris commented 2 years ago

I actually don't know any UI library/framework which would not benefit from efficient replaceChildren. Efficient and state preserving reordering/adding/removing of nodes is basically their most important and also most difficult to implement function.

WebReflection commented 2 years ago

DOM diffing is what everyone uses these days to make anything on the Web, because it's behind every single library, utility, framework, out there, because lists, baskets after buying, calendars, anything dynamic, needs to update their view without trashing their content all over all the time ... on my side, I've also created and investigated all possible algorithms, including:

I believe if we had a primitive for this case, we'll be innovating on other areas, instead of keep solving the same thing we needed to solve since AJAX existed 10+ years ago, but I am sure all these attempts used in the real-world aren't a good-enough evidence everyone would benefit from native diffing, right?

counterproductive to keep repeating the same discussion every 2-3 years

it's also very counterproductive to keep ignoring developers voice for all these years, imho.

mindplay-dk commented 2 years ago

DOM diffing is what everyone uses these days to make anything on the Web, because it's behind every single library, utility, framework, out there

For the record, this is not an exaggeration - even frameworks that do precise updates (Svelte, Solid, Sinuous, etc.) need to perform reordering operations one level deep, when dealing with lists.

It's also worth noting that this feature would support both types of frameworks equally well: frameworks that do precise updates can reorder a single range of child nodes - while frameworks that use a virtual DOM (or some other means of recursive updates) can call this function recursively. In the latter case, having a native reorder method would eliminate most of the complexity either way.

Is it too soon to start thinking about a prototype/proposal/polyfill?

Actually, can this functionality be polyfilled?

It probably can't be precisely polyfilled? If we're talking about details like triggering reflows and repaints etc.?

@WebReflection you have some experience in this area, I think? Any idea how much these details matter? I mean, they matter in terms of perf, which is one reason I think this proposal is relevant - but in terms of things like reflows, repaints, focus management, input and iframe states and so on, a native implementation probably could/should do certain things "better" than what we can do with existing DOM APIs in a polyfill, right? 🤔

techsin commented 2 years ago

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.

bathos commented 2 years ago

preserves its rendering or something

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?

WebReflection commented 2 years ago

Any idea how much these details matter?

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.

dead-claudia commented 2 years ago

Highly related: https://github.com/whatwg/dom/issues/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).

WebReflection commented 2 years ago

@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 container.diffNodes(before, after) operation, but it'd probably be a nice addiction, as long as it won't block progress for too long, imho.

dead-claudia commented 2 years ago

@WebReflection I'd be good with something that's just container.moveAfter(child, refNode). Not that key reconciliation wouldn't also be useful, mind you.

WebReflection commented 2 years ago

That could be the very initial building block, so I’d sign for it too.

Actually, moveBefore would be nicer, imho, instead of insertBefore.

annevk commented 2 years ago

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. https://github.com/whatwg/html/issues/5484#issuecomment-622826440 has some starting points if someone wants to pick this up and I'd be happy to mentor anyone interested in tackling this.

WebReflection commented 2 years ago

I don't think anyone is intentionally being ignored

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

bathos commented 2 years ago

@annevk Is this summary of the “story so far” accurate?:

  1. html#5484 concerns the problem of preserving “connection-associated state” of iframes across DOM mutation operations that currently would always entail disconnection and reconnection including reordering and reparenting. It does not concern preserving a browsing context while disconnected, though; i.e. the request is for a synchronous operation where no intermediate “unparented” state exists.
  2. html#5484’s discussion expanded to look at the problem of “moving connected elements” more generically as there are other kinds of “connection-associated state” beyond just the browsing contexts associated with iframes while connected. A generic solution would be ideal but specifying it implies a kind of hygienic and overdue (but daunting) yak shaving contest.
  3. @josepharhar created #891 (this issue) to isolate the specific case of reordering connected siblings. This scenario is interesting because it has high value to existing code bases (due to its relevance to pretty much any “DOM rendering” library) regardless of whether the more general problem is solved ... and seemingly it would also be simpler to solve than the more general version of the problem.
  4. Part of what makes it seem simpler is that it is already not possible to synchronously & generically observe when/whether a given element’s “sibling index” or position-relative-to-a-specific-sibling changes except via legacy DOM mutation events and for any set of siblings, it is already possible to reorder them to any other sequence with ~>= half of the siblings~ [edit: derp] at least one sibling never needing to be disconnected — and it never matters “which ~half~ one,” either. I think?
  5. Even if this scenario is much simpler, it’s still considered to depend on #808. That issue concerns existing web realities where (whether due to underspecification, implementation errors, or whatever else) implementations diverge in terms of the timing (or existence?) of specific insertion and removal effects. Given these divergences, “move” methods that do not imply disconnect→reconnect where the same operation would require disconnect→reconnect today would potentially make the existing discrepancies worse or the method’s steps might just end up coherent for one agent’s behavior yet incoherent for another, etc.

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

annevk commented 2 years ago

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 iframe or script element. And in order to do that we need to know what happens when you remove and insert one.

WebReflection commented 2 years ago

@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 🤔

samuelmtimbo commented 2 years ago

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?

fantasai commented 2 years ago

Just want to note that we see abuse of the CSS order property to perform semantic ordering because it's easier/faster than using the DOM. I think it would be helpful if we could redirect such people to an appropriate DOM API that made such reordering (for e.g. sorting lists, table rows, etc.) both easy and fast.

wlib commented 2 years ago

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)
Screen Shot 2022-09-25 at 2 42 51 PM

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.

mindplay-dk commented 2 years ago

I think that the closest practical solution to this is the shadow dom with the imperative slot api.

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.

// even iframes will behave as if nothing happened

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.

bathos commented 2 years ago

makes no mention of preserving state of iframes, input elements, etc.

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>";
barneycarroll commented 2 years ago

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.

bathos commented 2 years ago

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

An element inherits its initial set of counters from its parent and preceding sibling. It then takes the values for those counters from the values of the matching counters on its preceding element in tree order (which might be its parent, its preceding sibling, or a descendant of its previous sibling). [...] Note: Counter inheritance, like regular CSS inheritance, operates on the “flattened element tree” in the context of the [DOM].

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)

tabatkins commented 2 years ago

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.