I have heard several requests for the ability to reparent iframes without reloading, including:
Has this been brought up as a spec issue before? I imagine someone must have thought about this before...
Would it make sense to add a method to iframes to reparent them to another element without reloading?
Another potential use case I've heard from React is to reparent elements other than iframes without losing state. An example of this would be to reparent an input element without losing focus and selection. In this case, perhaps we could have a reparenting method for any element?
cc @mfreed7 @tkent-google @sebmarkbage
Some more clarification: right now the use case only involves moving iframes or other elements within the same document without ever actually detaching them.
@jeremyroman
Node trees don't offer move as a primitive operation.
Some people implemented this behavior in WebKit, found several critical issues, and removed it.
This issue contains some pointers.
Based on this patch which removed magic iframes, it sounds like they were for moving iframes between documents, which sounds more vulnerability prone than moving iframes inside of one document.
Ryosuke's comment at the end of the issue makes it sound like there are also security vulnerabilities with moving iframes inside the same document, but I wonder if the security vulnerabilities they encountered were really for the same document cases...
Based on this patch which removed magic iframes, it sounds like they were for moving iframes between documents, which sounds more vulnerability prone than moving iframes inside of one document.
Ryosuke's comment at the end of the issue makes it sound like there are also security vulnerabilities with moving iframes inside the same document, but I wonder if the security vulnerabilities they encountered were really for the same document cases...
The most of security issues were agnostic to whether an iframe was moved across a document or not. The main issue is the document inside a (temporarily) disconnected iframe being considered alive.
Thanks for the feedback Ryosuke!
Do you think that it would be possible to move the iframe atomically and therefore never have a document in a disconnected frame?
Do you think that it would be possible to move the iframe atomically and therefore never have a document in a disconnected frame?
That was the idea. But how does one move a node from one place to another? 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. In particular, if there are multiple iframe that could get disconnected in order, then they can access each other in weird ways in scripts, etc... I'm not gonna go into details but I don't think this is an idea we should re-pursue anytime soon.
Moving between parents would be very useful but the use case for React (and similar) is to be able to move position within the same parent. Eg swapping the place of two nodes in the same parent.
@rniwa did you introduce a new type of mutation for node trees? It seems to me you would have to start there. We only have remove/insert at the moment.
@rniwa did you introduce a new type of mutation for node trees? It seems to me you would have to start there. We only have remove/insert at the moment.
I don't recall but I don't think that really helps so long as such a replacement involves more than one node at once (e.g. subtree) because then a subtree can contain iframe which then need to be kept alive, etc...
I think it would help in that you can define now rules around it in terms of script execution, but it would be a pretty large undertaking and there's a lot of non-interoperable behavior around this for the current remove/insert operations that should be sorted first.
Rather than inventing a new kind of tree mutation, it feels like the simplest solution for developers would be to keep the iframe alive for a microtask after disconnection (although maybe need to think more about mutation events, as they already use microtasks).
I realise this doesn't make implementation easier.
How would that work specification-wise? Please note https://github.com/whatwg/dom/issues/808 and ideally everything linked from there to get an idea about the complexity with regards to script execution that we have with insert and remove today. (Mutation events also fire synchronously for insert/remove and are also not specified yet and indeed also cause problems here.)
I figured it'd be something like:
An iframe has a pending discard flag which is unset.
When an iframe element is removed from a document:
Although, if the iframe is inserted into another document, the current nested browsing context should also be discarded (if that was a particular source of security bugs).
For what it's worth, from a web dev perspective, there's already a way to ask for an element to be moved in a single step: just call .appendChild or .insertBefore for its new position without ever calling .removeChild. This is in fact what React already does when reordering children within a list.
I'd like to call out this part too:
Another potential use case I've heard from React is to reparent elements other than iframes without losing state. An example of this would be to reparent an input element without losing focus and selection.
The example of iframes is one particularly complicated case but it's one of several examples of state being lost.
Text selection and focus within an <input> when that input moves position within the same parent is another case.
CSS transitions playing in a subtree will be reset if any parent is removed and appended.
These are much more common and problematic than iframes.
In this case, perhaps we could have a reparenting method for any element?
It might be worth while listing out all these cases, because I could very well imagine that it's not web compatible to change this behavior for all of them using appendChild only. That might motivate why a new API for moves is needed.
This is a big deal in diffing based solutions (this include virtual DOMs like React and template based solutions based on diffing rather than change tracking). The reason it is such a big deal is that there's nothing that says which node to move. If you swap two nodes and diff them, there's nothing in the API to imply which one should be moved. The algorithm just picks one to move. Depending on the algorithm tradeoffs you could also end up moving four other nodes to move one since it's not worth computing the longest common subsequence.
Any browser built-in diffing based approach will also need to consider this problem.
In an imperative manual API you might have some intuition about which is safer and less disturbing to move. As such this was much less of a problem for the first decades of the web. However, even then there are cases where you just have to live with the worse experience.
In React we have some code to work around cases such as restoring selection and focus which adds to the code size of React, but we leave some unsolved.
@sophiebits unfortunately that's not an atomic move and will do a remove and then an insert (with all the associated script running bits).
@sebmarkbage an exhaustive list of problematic moving scenarios would help a lot.
An iframe has a pending discard flag
And what happens when JS runs in the iframe while the iframe isn't really connected. How does window.parent in the iframe's window work, what should history.go() do, or location.href = "newurl"? How should window.frames behave in the parent window? ...
Somewhat limited, but for what is worth, you can sorta get that effect with shadow DOM and slotting, it seems to work in all browsers:
<!doctype html>
<button>Toggle</button>
<div id="host">
<iframe id="frame" src="https://wikipedia.org" slot="a" style="border: 0"></iframe>
</div>
<script>
document.getElementById("host").attachShadow({ mode: "open" }).innerHTML = `
<style>
div { height: 150px; background: blue; }
#b { background: purple }
</style>
<div id="a">
<slot name="a"></slot>
</div>
<div id="b">
<slot name="b"></slot>
</div>
`;
document.querySelector("button").addEventListener("click", function() {
let frame = document.getElementById("frame");
frame.slot = frame.slot == "a" ? "b" : "a";
})
</script>
@smaug----
An iframe has a pending discard flag
And what happens when JS runs in the iframe while the iframe isn't really connected. How does window.parent in the iframe's window work
I looked at .parent yesterday, and it seems like that uses the owner document, so it'd still work, as in it'd return the same parent as if it were connected, which seems reasonable.
what should history.go() do, or location.href = "newurl"?
I think this should behave the same as if it were connected, but I'm sure there are tricky details.
How should window.frames behave in the parent window?
That one's less clear to me.
(I didn't mean to give the impression that this would be easy, btw)
Somewhat limited, but for what is worth, you can sorta get that effect with shadow DOM and slotting, it seems to work in all browsers:
Thanks for the workaround @emilio, I didn't see that get suggested in the stackoverflow post or anywhere else!
Rather than inventing a new kind of tree mutation, it feels like the simplest solution for developers would be to keep the iframe alive for a microtask after disconnection (although maybe need to think more about mutation events, as they already use microtasks).
@jakearchibald I agree this sounds like it would be awesome for developers because it would just make iframes stop reloading as is. However, @rniwa and @smaug----'s words make me feel afraid of going forward with this due to the non-atomic nature of doing it async.
It might be worth while listing out all these cases, because I could very well imagine that it's not web compatible to change this behavior for all of them using appendChild only.
@sebmarkbage I totally agree, I think the best step for us to do right now is collect information on all the different cases where we are losing state due to reparenting, and how common/painful each one is.
Does anyone have any thoughts on where it would be best for me to start compiling this list? Maybe a github gist or a google doc? And does anyone have any other cases to add or links to anything that would help assess how common/painful each case is?
I think we need two things to make progress here:
@annevk Thanks for suggesting this path forward!
I put the problematic use cases @sebmarkbage listed here: https://github.com/josepharhar/reparenting-loses-state
@sebmarkbage are there any other related problematic use cases you know of? Are there any react issues you could point us to which show interest in any of these use cases?
Happy to see that this is getting traction again!
Just wanted to re-iterate the weight of the scenarios @sebmarkbage shared. The main issue we run into with working on Preact are the same. Loosing transition state or input state (focus/cursor position/text selection) are easily at the top of the most frequent issues we have with working with the DOM.
So far we haven't found more issues than those already mentioned here.
By using the new animations api, it is possible to save and restore the state of an animation:
const savedOffset = element.getAnimations()[0].currentTime;
// reparent...
element.getAnimations()[0].currentTime = savedOffset;
I documented this with the other problematic use cases and workarounds in https://github.com/josepharhar/reparenting-loses-state
I'm not sure how easy it would be for vdom frameworks to use this though - checking and restoring the animation state of every element in a tree when you reparent sounds like it could take a long time compared to restoring focus and selection where there should only be one focused element and one selection in the entire document.
It still sounds like adding a new api to atomically move node trees without clobbering state would be the best solution.
Moving between parents would be very useful but the use case for React (and similar) is to be able to move position within the same parent. Eg swapping the place of two nodes in the same parent.
@sebmarkbage After rereading this thread yet again, I realized this could be a very helpful constraint. Does simply reordering nodes within the same parent without clobbering state rather than actually reparenting them solve everything for vdom frameworks? @marvinhagemeister @developit
@josepharhar Mithril maintainer here, and it would for us. Edit: I mean specifically reordering within the same parent without clobbering state.
Rather than inventing a new kind of tree mutation, it feels like the simplest solution for developers would be to keep the iframe alive for a microtask after disconnection (although maybe need to think more about mutation events, as they already use microtasks).
@jakearchibald I agree this sounds like it would be awesome for developers because it would just make iframes stop reloading as is. However, @rniwa and @smaug----'s words make me feel afraid of going forward with this due to the non-atomic nature of doing it async.
Indeed, that's just not going to help. The issue is having any frame that's even temporarily disconnected from top-level browsing context which can run any scripts at all. Given our past experience with magic iframe, we'd probably need to do a major rearchitecting of WebKit's DOM implementation if we were to implement anything remotely that exotic in the future.
I talked a bit with @sebmarkbage, and it sounds like the more narrowly scoped idea of reordering children without losing state, rather than actually reparenting children without losing state, is the use case which would help React as well.
Based @rniwa's comments it sounds like reparenting iframes in general around the dom would be quite challenging, so I'm inclined to drop this proposal of reparenting elements all around the dom without losing state and instead focus on the child reordering without losing state idea - still with the goals of iframe reloading, animations restarting, and input selection preservation.
Could I close this issue and discuss the child reordering idea in another issue?
Would it be better to discuss this in whatwg/html or whatwg/dom?
Oh I just realized another option for tracking this - I could just change the name of this issue instead of opening another whatwg/html issue.
@annevk what do you think?
Whichever way you prefer. Anything we do to address this would need changes in DOM and HTML (and probably CSS). https://github.com/whatwg/html/issues/5484#issuecomment-622826440 still seem like good next steps here.
Thanks! I made a new issue to propose a new method for reordering here: https://github.com/whatwg/dom/issues/891
I'll close this issue because I'm not interested in only the iframe case or reparenting without losing state to other parts of the DOM.
If we need another HTML issue about something else I'll open another one.