-
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
Introduce moveBefore()
state-preserving atomic move API
#1307
base: main
Are you sure you want to change the base?
Conversation
moveBefore()\
state-preserving atomic move APImoveBefore()
state-preserving atomic move API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the mutation record needs some more design work. I would expect it to capture the information of a remove and an insert at the same time. Perhaps it needs to be a new object, though we could further overload the existing MutationRecord
as well I guess. At least I think you need:
- old target
- target
- moved node (I'm not sure you can ever move multiple at this point, but maybe we should allow for it in the mutation record design?)
- old previous sibling
- old next sibling
- previous sibling
- next sibling
Would be good to know what @smaug---- thinks and maybe @ajklein even wants to chime in.
Shouldn't the target node be all the time the same, it is just the siblings which change. If this is really just remove and add back elsewhere, we could just reuse the existing childList MutationRecords, one for remove, one for adding node back, and possibly just add a flag to MutationRecord that it was about move. (movedNodes is a bit confusing, since it seems to depend on the connectedness of the relevant nodes and it is apparently empty for the removal part. And it is unclear to me why we need the connectedness check. This is about basic DOM tree operations, and I'd assume those to work the same way whether or not the node is connected) |
Creating two separate mutation records that a consumer would have to merge to (fully) understand it's a move seems suboptimal? I agree that it should probably work for disconnected nodes as well, but I don't think we want to support a case where the shadow-including root changes. |
It's been a long time since I've thought about this stuff, but I'm inclined to agree with @smaug---- that creating a new type of |
Per discussion in whatwg/dom#1307, we've decided to not ship range and selection preservation initially. Since this was mostly implemented in Chromium, this CL flag-gaurds our implementation of that preservation (so it does not ship along with the rest of the API's side-effects) and updates the tests. [email protected] Bug: 40150299 Change-Id: Ia4412d95859497593ac2e4d9e9b87dfc36240ef4 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6022661 Reviewed-by: Noam Rosenthal <[email protected]> Commit-Queue: Dominic Farolino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1385070}
Per discussion in whatwg/dom#1307, we've decided to not ship range and selection preservation initially. Since this was mostly implemented in Chromium, this CL flag-gaurds our implementation of that preservation (so it does not ship along with the rest of the API's side-effects) and updates the tests. [email protected] Bug: 40150299 Change-Id: Ia4412d95859497593ac2e4d9e9b87dfc36240ef4 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6022661 Reviewed-by: Noam Rosenthal <[email protected]> Commit-Queue: Dominic Farolino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1385070}
Per discussion in whatwg/dom#1307, we've decided to support `moveBefore()` in the disconnected->disconnected scenario. This CL enables and tests that. [email protected] Bug: 40150299 Change-Id: Ia0cf64a1a623c53ed5d9ae01b50b0e04f7028da2 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6037586 Reviewed-by: Noam Rosenthal <[email protected]> Commit-Queue: Dominic Farolino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1386266}
Per discussion in whatwg/dom#1307, we've decided to support `moveBefore()` in the disconnected->disconnected scenario. This CL enables and tests that. [email protected] Bug: 40150299 Change-Id: Ia0cf64a1a623c53ed5d9ae01b50b0e04f7028da2 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6037586 Reviewed-by: Noam Rosenthal <[email protected]> Commit-Queue: Dominic Farolino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1386266}
Per discussion in whatwg/dom#1307, we've decided to support `moveBefore()` in the disconnected->disconnected scenario. This CL enables and tests that. [email protected] Bug: 40150299 Change-Id: Ia0cf64a1a623c53ed5d9ae01b50b0e04f7028da2 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6037586 Reviewed-by: Noam Rosenthal <[email protected]> Commit-Queue: Dominic Farolino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1386266}
Would it be possible to have this method fallback to Without this, in practice it seems likely that users will need to write a boilerplate function that tests the 2 nodes for const moveOrInsertNode = (container, node, ref_node = null) => {
const canMove = (container.isConnected && node.isConnected && (!ref_node || ref_node.parentNode === container);
return canMove ? container.moveBefore(node, ref_node) : container.insertBefore(node, ref_node);
} |
FWIWI I fully agree with @sorvell ... the whole point of this proposal was to simplify DOM manipulation, not to complicate it even further with tons of repeated checks or |
I'm personally pretty sympathetic to the arguments above. It was the original direction we went in, and I do think it is simpler to use, however the discussion we had at TPAC resulted in editors pushing back, on account of "move" being a fundamentally different primitive from "insert". @annevk are you open to revisiting this? |
"Move" would still be a fundamentally different primitive if it tried to preserve state where possible since the "insert" guarantees a default state. I understand that from a spec perspective this makes "move" a superset of "insert" given that it may need to run initialization hooks but I think that is a more usable primitive for developers. |
I definitely understand that. That said, if 99% of I guess it comes down to how often we think people actually care to know whether a truly "atomic" move can be performed, and might act differently if they find out it can't. That's where the error-throwing behavior becomes really useful; without it, if you want to know whether |
I think that the following scenario is plausible: try {
element.moveBefore(newNode, refNode);
} catch {
element.insertBefore(newNode, refNode);
} finally {
// check whether we need to do something extra - e.g. the focused element reparented
// and some library relies on the side effects
} It's likely that the |
I wonder if that choice of error tolerance could be moved to a boolean argument. Like, Also, how does this handle the case of |
@noamr There is some precedent for light ergonomics in the DOM already: |
Yes, and we can certainly consider adding more ergonomic variants in the future on top of the I think it would help if we think of |
This particular API shape is a boolean trap, but I think we can can consider variants in the future that have an 'move if you can insert if you can't' behavior. But this is a slightly higher level behavior than moving. As I said, there is value in having at least some DOM APIs that are as primitive as possible.
It throws when moving across documents. |
if everyone will inevitably end up using a When a developer does not want to have side-effects it can perform those checks manually, the rest 99% of the world will just Please note I am not suggesting In short, if try/catch is the workaround, everyone will use it and all the reasons it couldn't be done behind the scene in the name of "it's better for developers" will be futile, I hope we can agree on that. edit
exactly, without forgetting Here I feel like everyone expects developers to wrap |
The reason it throws is because in the future we might become more ambitious and also tackle those scenarios. The reason we don't have various accompanying APIs such as |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The aspects shared between insert and move, and remove and move, need to be shared. We don't want to have to maintain identical range mutation in multiple places.
<li><p>If <var>parent</var> is not an {{Element}} or {{DocumentFragment}} <a for=/>node</a>, then | ||
<a>throw</a> a "{{HierarchyRequestError!!exception}}" {{DOMException}}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason this restriction is okay is because documents cannot have multiple element children and you can only use insertBefore()
at the root when the current element is removed. So it really relies on the connected checks above. That's very subtle.
However, when looking at non-elements it's quite a bit more restrictive than what it probably should be. I don't see why we'd forbid moving a comment to be a child of a document. So I would suggest we do this differently, even though in practice there's no benefit to moving comments and the like.
Ideally we'd align this with "pre-insertion validity" or share as much as possible.
This PR introduces a new DOM API on the
Node
interface:moveBefore()
. It mirrorsinsertBefore()
in shape, but defers to a new DOM manipulation primitive that this PR adds in service of this new API: the "move" primitive. The move primitive contains some of the DOM tree bookkeeping steps from the remove primitive, as well as the insert primitive, and does three more interesting things:connectedMoveCallback()
The power of the move primitive comes from the fact that the algorithm does not defer to the traditional insert and removal primitives, and therefore does not invoke the removing steps and insertion steps. This allows most state to be preserved by default (i.e., we don't tear down iframes, or close dialogs). Sometimes, the insertion/removing step overrides in other specifications have steps that do need to be performed during a move anyways. These specifications are expected to override the moving steps hook and perform the necessary work accordingly. See whatwg/html#10657 for HTML.
Remaining tasks (some will be PRs in other standards):
focusin
event semantics[ ] Preserve text-selection. See set the selection range. Edit: Nothing needs to be done here. Selection metadata (i.e.,selectionStart
and kin) is preserved by default in browsers, consistent with HTML (no action is taken on removal). The UI behavior of the selection not being highlighted is a side-effect of the element losing focusselectionchange
event: We've decided to allowselectionchange
event to still fire, since it is queued in a task. No changes for this part are required.Node.prototype.moveBefore
) WebKit/standards-positions#375Node.prototype.moveBefore
) mozilla/standards-positions#1053(See WHATWG Working Mode: Changes for more details.)
Preview | Diff