Skip to content
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

State-preserving atomic move integration #10657

Merged
merged 2 commits into from
Mar 7, 2025
Merged

State-preserving atomic move integration #10657

merged 2 commits into from
Mar 7, 2025

Conversation

domfarolino
Copy link
Member

@domfarolino domfarolino commented Sep 29, 2024

This PR is a supplementary PR to whatwg/dom#1307. It provides the HTML-overrides for the DOM Standard's "moving steps" extension, to allow pieces of the HTML Standard to react to atomic DOM moves without the use of the "insertion steps" or "removing steps", which by default, intentionally fail to preserve most state. The changes in this PR are inspired by the audit carried out in https://docs.google.com/document/d/1qfYyvdK4zhzloABKeh0K1lHPm-SpnEcsWEE9UdDuoMk/edit.

(See WHATWG Working Mode: Changes for more details.)


/custom-elements.html ( diff )
/embedded-content.html ( diff )
/form-control-infrastructure.html ( diff )
/form-elements.html ( diff )
/images.html ( diff )
/index.html ( diff )
/infrastructure.html ( diff )
/media.html ( diff )

@domfarolino domfarolino requested a review from annevk November 20, 2024 21:52
@domfarolino domfarolino marked this pull request as ready for review November 20, 2024 21:52
@domfarolino domfarolino added impacts documentation Used by documentation communities, such as MDN, to track changes that impact documentation addition/proposal New features or enhancements labels Nov 20, 2024
Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you mentioned connectedMoveCallback everywhere connectedCallback is mentioned (and it's relevant).

@noamr
Copy link
Collaborator

noamr commented Dec 3, 2024

I don't think you mentioned connectedMoveCallback everywhere connectedCallback is mentioned (and it's relevant).

This was there already, see https://whatpr.org/html/10657/custom-elements.html#custom-element-reactions ("When it is moved"). Not sure what's missing?

Done

  • It could do with some examples as well.

Added non-normative note and an example

@domenic
Copy link
Member

domenic commented Dec 6, 2024

Seems like #10828 should be addressed here too?

@domfarolino
Copy link
Member Author

Seems like #10828 should be addressed here too?

I think it needs to be done separately; I've uploaded #10840.

source Outdated
<var>oldParent</var>, are:</p>

<ol>
<li><p>If <var>movedNode</var>'s next sibling was an <code>img</code> element and

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"was"? So somehow this algorithm needs to get the old sibling as argument?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yes I guess, but the same goes for the removing steps which pre-date this PR and this API. I think fixing that is best left to a follow-up where we evaluate and perhaps overhaul the inputs to these steps.

For example, we could hack together a single parameter hot fix for this hook, but that's not ideal. Rather we need to do something consistent with what's desired in whatwg/dom#1288, where we teach the various hooks to take in a bundle of contextual information (ChildrenChange and ContainerNode in Chromium).

So I think I'd prefer to fix this and removing steps in one swoop after this, since it's easy, ensures nothing diverges, and this doesn't make the problem any worse. How does that sound?

Furthermore, I am actually skeptical that the spec has been doing the right thing in this case anyways. For example, Chromium doesn't track the "old next sibling" of the removed source, and trigger a relevant mutation on it. It triggers relevant mutations on all image children of the picture (https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/html_picture_element.cc;l=26-29;drc=a7096322ab1e5c9e70ae7b773faed492ed8461cb) so maybe implementations are compatible with that, and we can simplify all of this anyways. Regardless, I'd prefer it not block this.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing move and removing steps as a followup should be ok.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please file an issue to track this. And ideally leave a source comment pointing to it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aarongable pushed a commit to chromium/chromium that referenced this pull request Dec 27, 2024
This CL ships the `ParentNode#moveBefore` API to Chrome stable,
targeting M133. See:

 - https://chromestatus.com/feature/5135990159835136
 - https://groups.google.com/a/chromium.org/g/blink-dev/c/YE_xLH6MkRs
 - https://issues.chromium.org/issues/40150299
 - whatwg/dom#1255
 - whatwg/dom#1307
 - whatwg/html#10657

R=chrishtr, masonf, nrosenthal

Bug: 40150299
Change-Id: I005f1db250d731d6845b5238048bbd0b90b20c81
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6112499
Reviewed-by: Noam Rosenthal <[email protected]>
Reviewed-by: Mason Freed <[email protected]>
Reviewed-by: Chris Harrelson <[email protected]>
Commit-Queue: Chris Harrelson <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1400559}
@mfreed7
Copy link
Contributor

mfreed7 commented Feb 25, 2025

Just noting #10954 (comment), which will require a small change to this PR if/when that one lands.

@theengineear
Copy link

First off, I want to say that moveBefore is going to be a major win in terms of functionality — so, thanks so much for working on it!

That said, I wanted to raise a concern about the following part of the specification:

<p>To opt in to a state-preserving behavior while <span
  data-x="concept-node-move-ext">moving</span>, the author can implement a "<code
  data-x="">connectedMoveCallback</code>". The existence of this callback, even if empty, would
  supercede the default behavior of calling "<code data-x="">disconnectedCallback</code>" and "<code
  data-x="">connectedCallback</code>". "<code data-x="">connectedMoveCallback</code>" can also be an
  appropriate place to execute logic that depends on the element's ancestors. For example:</p>

I find it confusing that the existence of a connectedMoveCallback method on a custom element changes the behavior of moveBefore. I think it will lead to custom element authors needing to write empty connectedMoveCallback() {/* do nothing, just here to prevent disconnectedCallback */} methods — and that feels like an anti-pattern. It will also lead to integration uncertainty for folks trying to leverage moveBefore.

By way of counter point, I don’t think the presence of an adoptedCallback lifecycle method changes how adoptNode works, just like the presence of connectedMoveCallback won’t change how the current insertBefore works.

Is there any flexibility in the specification at this point to change that? Or, would it be possible to understand the rationale on this decision in a little more detail?

@noamr
Copy link
Collaborator

noamr commented Mar 7, 2025

First off, I want to say that moveBefore is going to be a major win in terms of functionality — so, thanks so much for working on it!

That said, I wanted to raise a concern about the following part of the specification:

<p>To opt in to a state-preserving behavior while <span
  data-x="concept-node-move-ext">moving</span>, the author can implement a "<code
  data-x="">connectedMoveCallback</code>". The existence of this callback, even if empty, would
  supercede the default behavior of calling "<code data-x="">disconnectedCallback</code>" and "<code
  data-x="">connectedCallback</code>". "<code data-x="">connectedMoveCallback</code>" can also be an
  appropriate place to execute logic that depends on the element's ancestors. For example:</p>

I find it confusing that the existence of a connectedMoveCallback method on a custom element changes the behavior of moveBefore. I think it will lead to custom element authors needing to write empty connectedMoveCallback() {/* do nothing, just here to prevent disconnectedCallback */} methods — and that feels like an anti-pattern. It will also lead to integration uncertainty for folks trying to leverage moveBefore.

This is by design. By adding this empty callback you signal that your component is capable of handling moves.

By way of counter point, I don’t think the presence of an adoptedCallback lifecycle method changes how adoptNode works, just like the presence of connectedMoveCallback won’t change how the current insertBefore works.

I don't think this analogy is valid. The presence of the callback doesn't change how ’moveBefore’ works, only which custom element callbacks are called.

Is there any flexibility in the specification at this point to change that? Or, would it be possible to understand the rationale on this decision in a little more detail?

It's late at this point.

To explain the rationale:
This is to avoid breaking existing components that might assume that an element cannot be moved, as custom elements predate moving. For example, a custom element might check the tag name of Its parent in its ’connectedCallback’. Before moving this was guaranteed to not change until the next ’disconnectedCallback’, so by calling both callbacks by default we simulate the old behavior to ensure components that rely on it, and by providing the new callback the component can open out from that.

@annevk annevk force-pushed the atomic-move-patches branch from 7c0a2fc to 39b3664 Compare March 7, 2025 14:06
annevk added a commit to whatwg/dom that referenced this pull request Mar 7, 2025
This introduces a new API on the ParentNode mixin: moveBefore(). It mirrors insertBefore() in shape, but uses a new manipulation primitive that is also added here: "move". The move primitive contains some of the node tree bookkeeping steps from the remove primitive, as well as the insert primitive, and does three more interesting things:

1. Calls the moving steps hook with the moved node and the old parent (possibly null, just like the removing steps).
2. Queues a custom element callback reaction for the connectedMoveCallback().
3. Queues two back-to-back mutation record tasks: one for removal from the old parent; one for insertion into the new one.

The power of the move primitive comes from the fact that the algorithm does not defer to the traditional insert and remove 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.

Corresponding HTML PR: whatwg/html#10657.

Tests: https://github.com/web-platform-tests/wpt/tree/master/dom/nodes/moveBefore (the tentative directory will be flattened).

Closes #1255. Closes #1335.

Co-authored-by: Anne van Kesteren <[email protected]>
@annevk annevk merged commit 432e8fb into main Mar 7, 2025
2 checks passed
@annevk annevk deleted the atomic-move-patches branch March 7, 2025 14:24
@theengineear
Copy link

The presence of the callback doesn't change how ’moveBefore’ works, only which custom element callbacks are called.

@noamr — Thanks for the thoughtful response. I get your point from a “don’t break the web perspective”, but I am struggling to understand the mental model related to disconnectedCallback and moveBefore.

I had previously thought that part of the point of using moveBefore was to prevent disconnection. I.e., I use moveBefore on some set of elements to ensure that they don’t disconnect — why is it the case that using moveBefore doesn’t guarantee that disconnectedCallback is skipped? Perhaps I just need to fix my mental model to think about these lifecycle methods as maybeDisconnectedCallback and maybeConnectedCallback.

Either way, I’ll just bake connectedMoveCallback into a base element and I’m excited to see this stuff landing ❤️

@noamr
Copy link
Collaborator

noamr commented Mar 7, 2025

The presence of the callback doesn't change how ’moveBefore’ works, only which custom element callbacks are called.

@noamr — Thanks for the thoughtful response. I get your point from a “don’t break the web perspective”, but I am struggling to understand the mental model related to disconnectedCallback and moveBefore.

It also fires the connectedCallback straight after. The mental model is that if your existing component is not prepared for a move, it should fall back to disconnecting and reconnecting, like a "restart" to make sure it didn't go into some funky state.

I had previously thought that part of the point of using moveBefore was to prevent disconnection. I.e., I use moveBefore on some set of elements to ensure that they don’t disconnect — why is it the case that using moveBefore doesn’t guarantee that disconnectedCallback is skipped? Perhaps I just need to fix my mental model to think about these lifecycle methods as maybeDisconnectedCallback and maybeConnectedCallback.

Either way, I’ll just bake connectedMoveCallback into a base element and I’m excited to see this stuff landing ❤️

@domenic
Copy link
Member

domenic commented Mar 8, 2025

In case it helps, a possible mental model might be that it disconnects and reconnects the vast majority of elements (e.g. <p>, <div>, <strong>, custom elements by default, ...). But for certain special elements it performs a move (e.g. <img>, <iframe>, <select>, <audio>, <video>, certain custom elements, ...).

@theengineear
Copy link

@domenic — That is actually super helpful! Makes much more sense to me that this works the way it does 🤘

@noamr
Copy link
Collaborator

noamr commented Mar 8, 2025

@domenic — That is actually super helpful! Makes much more sense to me that this works the way it does 🤘

Thanks for asking for clarifications, I will make sure this is reflected in documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements impacts documentation Used by documentation communities, such as MDN, to track changes that impact documentation
Development

Successfully merging this pull request may close these issues.

8 participants