fix(core): prevent animated element duplication with dynamic components in zoneless mode#67032
Conversation
thePunderWoman
left a comment
There was a problem hiding this comment.
This seems fine, but I think we should be more aggressive in potential error cases. Tests look green, which is good, but I'm just a bit worried about the cancelLeavingNodes changes needing a few more guards for safety. Can you (or claude) do an assessment?
Gemini is worried that the leavingNodes keying logic isn't safe and could lead to cross-instance interference, but I think that's outside of the scope of this PR.
| } | ||
|
|
||
| // Finish remaining animations (wait 100ms real time which is > 10ms animation) | ||
| // After all animations complete, 20 toggles (even) = closed = 0 |
There was a problem hiding this comment.
Important note: In this environment animation events are not fired by the browser. They have to be manually dispatched in the test, and I don't see that happening in your test here. You'll see examples of that in earlier tests. So this is likely something you'll want to verify is happening here, otherwise it may be a false positive or false negative.
There was a problem hiding this comment.
Addressed in 9b449d6 (also verified the test still fails with the patch not applied)
packages/core/src/animation/utils.ts
Outdated
| // renders in its own container, e.g. CDK Overlay). | ||
| // Leaving elements in the same parent that are NOT the previousSibling | ||
| // are left alone (e.g. @for items animating out at different positions). | ||
| if (leavingEl === prevSibling || (leavingParent && leavingParent !== newParent)) { |
There was a problem hiding this comment.
I am a little concerned about this line and the potential for null === null in the case that newParent, prevSibling, or leavingEl are null. It's probably worth adding null check guards for safety.
This was claudes response: Assessment: cancelLeavingNodes / leavingNodes safety How it works: leavingNodes is a WeakMap<TNode, HTMLElement[]>. TNodes are shared across all embedded views created from the same template (same TView). This means if two components use the same Is cross-instance interference possible? Theoretically, but only if:
The leavingParent !== newParent check would match the other instance's element and cancel it prematurely. This is not a new risk. The leavingNodes WeakMap keyed by TNode existed before this PR — it was already populated by trackLeavingNodes inside animateLeaveClassRunner and consumed by Practical impact: The scenario requires two menus from the same template to close simultaneously, then one to reopen before the other's CSS animation completes. With CDK Overlay, only one menu is Could we make it safer? The key would need something instance-specific (e.g. LView ID or VCR identity), but that's a more invasive refactor of the existing leavingNodes data structure and outside |
thePunderWoman
left a comment
There was a problem hiding this comment.
LGTM, and a huge thank you!
|
@mattlewis92 Looks like you have a minor lint issue to fix. |
…ts in zoneless mode When using ViewContainerRef to rapidly toggle animated elements in zoneless mode (e.g. CDK Overlay menus), multiple copies of the element could appear in the DOM. This happened because leave animations were queued but not yet executed, and the existing `cancelLeavingNodes` mechanism could not find the leaving element to cancel it — it ran during template execution before the new element was in the DOM, and used the declaration container's anchor which doesn't work for overlay/portal patterns where elements are moved to separate containers. This commit replaces the previous approach with a simpler mechanism: - Track leaving nodes immediately during the Detach tree walk (before the leave animation queue flushes) so they are discoverable - Cancel leaving nodes after DOM insertion in the Insert tree walk, checking both `previousSibling` (same-container toggle) and different DOM parents (overlay/portal case) - Revert the ineffective fix from 38263f6 (`removeAnimationsFromQueue`) This also removes the circular dependency between `animation/utils.ts` and `render3/node_manipulation.ts` (`getBeforeNodeForView` import).
…omponents in zoneless mode fixup! fix(core): prevent animated element duplication with dynamic components in zoneless mode Address review comments: - Add null guards to cancelLeavingNodes to prevent null === null matches - Rewrite test to use fakeAsync with tickAnimationFrames and manual animationend event dispatch instead of real async delays
9b449d6 to
b6f00f7
Compare
Odd, I didn't change the file it's complaining about. Rebasing against main and hoping that helps 🙏 |
|
@mattlewis92 Looks like the rebase took care of it. Thanks! |
|
@mattlewis92 Bummer...looks like this has conflicts with the patch branch. We can merge it into main targeting the next minor. You'd have to create a patch version of this PR to get it to safely land in patch as well. |
We are actually still on v20 right now so not a big deal for us, I've just been having claude patch these fixes into our angular version 😅 If other people report the same issue I'd be happy to make a patch version as well though! |
|
This PR was merged into the repository. The changes were merged into the following branches:
|
|
I don't know 100% sure, but what I see in 21.2.0 is that if I use: @HostBinding("animate.leave")
animateLeave = "state-leave";the element will not be removed from the DOM at all. |
Please could you make a minimal stackblitz that reproduces the problem you're experiencing, and file a new issue with a bug report? It's hard to reproduce the problem to fix it with just the code snippet you provided. |
|
Done: #67400 |
Important
This was a fully vibe coded solution by Claude. Maybe there is a simpler way to solve this? At the very least, the test case reproduces the bug (I patched this into our application and verified it does fix the issue)
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: #66794
What is the new behavior?
Fast clicking an animated menu to open and close it repeatedly in zoneless mode doesn't leave orphaned DOM nodes
Does this PR introduce a breaking change?
Other information