Skip to content

animate.leave is as buggy as a nursery log in a rain forest #64730

@phillipwildhirt

Description

@phillipwildhirt

Which @angular/* package(s) are the source of the bug?

animations

Is this a regression?

No

Description

animate.leave event is firing in an @for block where the element should not be removed. From the error, it would appear even angular thinks it should be there.

Here's the old code: NO PROBLEMS

@for (thread of messageThreadsFullHalfService.threads; track trackByThreadProperties($index, thread)) {
  <button #threadRef
          id="{{ 'id' + thread.threadId }}"
          class="list-group-item list-group-item-action p-0 min-height-animate"
          (mouseup)="threadRef.blur()"
          [@animateOnRemove]="thread.removeAnimate"
          (@animateOnRemove.done)="animateOnRemoveDone$.next()">
...

and the new: CAUSES ERROR

@for (thread of messageThreadsFullHalfService.threads; track trackByThreadProperties($index, thread)) {
  <button #threadRef
          id="{{ 'id' + thread.threadId }}"
          class="list-group-item list-group-item-action p-0 min-height-animate"
          (mouseup)="threadRef.blur()"
          [animate.leave]="'remove-x-easy-animation time-300 distance-n100'"
          [class.instant-animation]="!thread.removeAnimate"
          (animate.leave)="AnimationUtilities.onAnimationDone($event, onThreadRemoveAnimationDoneFunc, unsub$)">
...

AnimationUtilities.onAnimationDone:

  static onAnimationDone($event: AnimationCallbackEvent, callback: Function, unsub$: Subject<void>): void {
    const animations = $event.target.getAnimations();
    if (animations.length) {
      merge(...animations.map(animation => from(animation.finished))).pipe(
        takeUntil(unsub$)
      ).subscribe({
        complete: () => {
          callback();
          $event.animationComplete();
        }
      });
    } else {
      callback();
      $event.animationComplete();
      // currently angular will not remove the element if there is no animations and the (animate.leave) event fired.
      $event.target.remove();
    }
  }

(I have confirmed beyond any doubt that there IS an animation firing and the if (animations.length) is true and we start the subscription to the animations.finished promise and the else with the $event.target.remove(); is not run. —Of course it's also easy to just comment that out, but for the sake of my issue title, I'm including that I have seen this (animate.leave) event firing with NO animations, and this is a necessary line. Another bug.)

.instant-animation is animation-duration: 1ms, which I have found is more consistent than the animation: none recommendation in the docs. .min-height-animate is a transition of min-height which does not fire when the element is removed.

Full Description:

To the point of the problem though, a change is made in the array messageThreadsFullHalfService.threads. Some elements should be leaving (and do) but the element that causes the error should NOT be removed—confirmed in the trackby function—, yet it is being removed by the animate.leave effects—confirmed if you remove the animate.leave's completely. I then change the messageThreadsFullHalfService.threads back to its original state and the error pops up because the element should be there but it's not.

Three things fix this: 1) If we remove both the animate.leave's there is no error and the element exists after the change. 2) If we change the trackby to essentially rebuild all elements there is no error and the element exists after the change. 3) If I use the old code's animation system:

    trigger('animateOnRemove', [
      transition('true => void', [
        animate('300ms ease-in',
          style({transform: 'translateX(-100%)'})
        )
      ])
    ])

there is no error and the element exists after the change.

I spent nearly 6 hours trying to reproduce in a new repo, but was unsuccessful. It's a rather intense array of many complex elements, but despite adding many rows of complexity to the repo, I couldn't reproduce.

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw

root_effect_scheduler.mjs:3624 ERROR NotFoundError: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
    at EmulatedEncapsulationDomRenderer2.insertBefore (dom_renderer.mjs:645:20)
    at AnimationRenderer.insertBefore (browser.mjs:3852:19)
    at nativeInsertBefore (debug_node.mjs:6790:14)
    at applyToElementOrContainer (debug_node.mjs:8289:13)
    at applyNodes (debug_node.mjs:8884:17)
    at applyView (debug_node.mjs:8891:5)
    at addViewToDOM (debug_node.mjs:8341:5)
    at addLViewToLContainer (debug_node.mjs:10446:13)
    at LiveCollectionLContainerImpl.attach (debug_node.mjs:23150:9)
    at attachPreviouslyDetached (debug_node.mjs:22802:24)
handleError @ root_effect_scheduler.mjs:3624
(anonymous) @ debug_node.mjs:30771
invoke @ zone.js:398
run @ zone.js:113
runOutsideAngular @ debug_node.mjs:7741
(anonymous) @ debug_node.mjs:30763
(anonymous) @ debug_node.mjs:30699
invoke @ zone.js:398
onInvoke @ debug_node.mjs:7850
invoke @ zone.js:397
run @ zone.js:113
run @ debug_node.mjs:7696
next @ debug_node.mjs:30693
ConsumerObserver2.next @ Subscriber.js:96
Subscriber2._next @ Subscriber.js:63
Subscriber2.next @ Subscriber.js:34
(anonymous) @ Subject.js:41
errorContext @ errorContext.js:19
Subject2.next @ Subject.js:31
emit @ debug_node.mjs:7384
checkStable @ debug_node.mjs:7764
onLeave @ debug_node.mjs:7911
onInvokeTask @ debug_node.mjs:7844
invokeTask @ zone.js:430
runTask @ zone.js:161
invokeTask @ zone.js:515
invokeTask @ zone.js:1141
globalCallback @ zone.js:1172
globalZoneAwareCallback @ zone.js:1205

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 20.3.7
Node: 22.19.0
Package Manager: npm 10.9.3
OS: darwin arm64
    

Angular: 20.3.7
... animations, cli, common, compiler, compiler-cli, core
... elements, forms, localize, platform-browser
... platform-browser-dynamic, router

Package                         Version
---------------------------------------
@angular-devkit/architect       0.2003.7
@angular-devkit/build-angular   20.3.7
@angular-devkit/core            20.3.7
@angular-devkit/schematics      20.3.7
@angular/cdk                    20.2.9
@angular/material               20.2.9
@schematics/angular             20.3.7
rxjs                            7.8.2
typescript                      5.9.2
zone.js                         0.15.1

Anything else?

Honestly, this might be more of a feedback post rather than a true hope of fixing an issue. After attempting to remove the deprecated animation module in a 14MB, 2500 file large application, I'm posting this issue with a general sentiment of frustration for the planned deprecation of a working system in favor of this new limited replacement. In just one file I replaced 178 lines of animation code (yes it was complex menu component) into 427 lines of code because of the removal of the UNREPLACED functionality of query, stagger, sequence, animateChild, and group and the complexity of switching from (@trigger.start) and (@trigger.done) events to the (animate.leave) event. I also sit here with heavily eroded trust in the safety of upgrading angular version as this was released rather buggy and not production ready.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimecore: animationsIssues related to `animate.enter` and `animate.leave`needs reproductionThis issue needs a reproduction in order for the team to investigate further

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions