skip to content

In Angular, how do you run animate.leave through a JavaScript function, and what happens if it never calls animationComplete()?

level: middleimportance: should knowfreq 32%

answer

  1. event binding, not a class
  2. AnimationCallbackEvent carries target
  3. Angular cannot see the end
  4. an injection token sets the fallback

basics

~10 s

Bind (animate.leave)="fn($event)"; the handler gets an AnimationCallbackEvent with target and animationComplete(). Call animationComplete() when your animation finishes. If you never do, Angular removes the element after MAX_ANIMATION_TIMEOUT, 4000 ms by default.

solid answer

~40 s

With event-binding syntax, `(animate.leave)="slideOut($event)"`, Angular calls your method instead of adding classes. The `$event` is an `AnimationCallbackEvent`: `target` is the leaving element and `animationComplete()` tells Angular the animation is done. Because the motion runs in your code or an animation library, Angular cannot detect the end itself, so you must call `animationComplete()`, typically from the library's completion callback or a Web Animations `finished` promise. If you forget, the element stays in the DOM until the `MAX_ANIMATION_TIMEOUT` token's value elapses, 4000 ms by default; you can provide a different number of milliseconds. `(animate.enter)` has the same event shape, but calling `animationComplete()` there does nothing, since an entering element has nothing to wait for. Under `TestBed`'s default disabled animations, the leave handler is skipped and the element is removed at once.

code

ts · 27 lines
ts
import {AnimationCallbackEvent, Component, signal} from '@angular/core';

@Component({
  selector: 'app-toast-stack',
  template: `
    @for (toast of toasts(); track toast.id) {
      <div class="toast" (animate.leave)="slideOut($event)" (click)="dismiss(toast.id)">
        {{ toast.text }}
      </div>
    }
  `,
})
export class ToastStack {
  protected readonly toasts = signal([{id: 1, text: 'Saved'}]);

  dismiss(id: number): void {
    this.toasts.update((list) => list.filter((t) => t.id !== id));
  }

  slideOut(event: AnimationCallbackEvent): void {
    const motion = event.target.animate(
      [{transform: 'translateX(0)'}, {transform: 'translateX(110%)', opacity: 0}],
      {duration: 250, easing: 'ease-in', fill: 'forwards'},
    );
    motion.finished.then(() => event.animationComplete());
  }
}

go deeper

for a junior

Recall the event-binding syntax (animate.leave)="fn($event)" and that the handler must call event.animationComplete() when the animation is done.

for a middle

Explain why the call is needed: Angular cannot observe code-driven motion. Know the MAX_ANIMATION_TIMEOUT token, its 4000 ms default, and that enter handlers get a no-op completion function.

for a senior

Diagnose lingering elements as a missing or misplaced animationComplete() call, cover cancellation paths, and set a sensible timeout and test configuration for the team.

for a principal

Decide when code-driven motion justifies the bookkeeping over class-based CSS, and set conventions so animation libraries are wrapped once rather than completed ad hoc.

## Two ways to drive a leave `animate.leave` has two binding forms, and they answer the question "who decides when the animation is over?" differently. | Form | Syntax | Who detects the end | |---|---|---| | Class-based | `animate.leave="toast-out"` or `[animate.leave]="cls()"` | Angular, by listening for CSS end events | | Function-based | `(animate.leave)="slideOut($event)"` | **you**, by calling `animationComplete()` | The function form uses **event-binding syntax** (parentheses). It exists so that motion can come from code: the Web Animations API (`element.animate()`), a JavaScript animation library, or anything else that CSS classes cannot express. ## The event object The handler receives an **`AnimationCallbackEvent`**, a type exported from `@angular/core`, with two members: - **`target`**: the element that is leaving. Animate this element. - **`animationComplete()`**: a function you call when the animation is finished. For a leave, it is the signal that lets Angular detach the element. The handler runs with the component as `this`, so it can read inputs or signals to decide how to animate. ## Why the call is mandatory With a class, Angular can measure the running animations and wait for `animationend` or `transitionend`. With a function, Angular has no idea what your code is doing: it might tween with a library, animate a canvas, or wait for a network response. So the contract is explicit: 1. Angular calls your handler with the event object. 2. Your code starts the animation on `event.target`. 3. When your animation ends, your code calls `event.animationComplete()`. 4. Angular removes the element from the DOM. Angular also treats an `animationend` event reaching the element as completion, so a CSS keyframe animation on it can end the leave early; relying on that is fragile. Call `animationComplete()` yourself. ## The timeout when you forget If `animationComplete()` is never called, the element would otherwise stay in the DOM forever. Angular guards against that with a timeout: it removes the element after the value of the **`MAX_ANIMATION_TIMEOUT`** injection token, **4000 milliseconds** by default. The source notes that the default mirrors a browser's timeout for cross-document view transitions. You can change it with a provider: ```ts { provide: MAX_ANIMATION_TIMEOUT, useValue: 6000 } ``` A four-second pause is long enough to be noticed: a toast that seems to linger, or a list where a deleted row is still clickable, is often a missing `animationComplete()` call rather than slow CSS. ## Enter functions are different `(animate.enter)="slideIn($event)"` receives the same `AnimationCallbackEvent` shape, but for an entering element there is no removal to delay, and Angular passes an `animationComplete` that does nothing. Calling it is harmless and keeps enter and leave handlers symmetrical. ## Tests and disabled animations `TestBed` disables enter and leave animations by default. In that state, a function-based leave does **not** call your handler; Angular removes the element straight away so tests are not slowed down. Set `animationsEnabled: true` in `TestBed.configureTestingModule` to run the handler in a test. On the server, the instructions do nothing at all. ## Class form or function form? Most toasts, dialogs and list rows are well served by the class form: the motion is a few lines of CSS, Angular measures it, and there is nothing to remember to call. The function form earns its bookkeeping when: - the motion depends on runtime values, such as swiping a toast out in the direction the user dragged it; - the team already uses a JavaScript animation library and wants one motion vocabulary; - the animation is a physics or spring effect that CSS timing functions cannot express; - several elements must be coordinated from code, for example staggering the exit of a group. In each case the handler owns both the start and the end of the motion, which is exactly why Angular hands it `animationComplete()` instead of guessing. ## Practical rules - Always call `event.animationComplete()` from the animation's **completion callback**, never synchronously at the start of the handler, or the element disappears before it has moved. - Call it on cancellation paths too; a library that can be interrupted should still complete the leave. - Keep the handler on the component that owns the element, not in a service, so `this` and the element's lifetime line up. - Reach for the function form only when CSS classes cannot express the motion; class-based leaves need no completion bookkeeping at all.

  • How would you shorten the fallback for an app whose longest exit animation is 400 ms?
    Provide the `MAX_ANIMATION_TIMEOUT` token from `@angular/core` with a smaller number of milliseconds, for example `{provide: MAX_ANIMATION_TIMEOUT, useValue: 1000}` in the application providers. A forgotten `animationComplete()` then costs one second instead of four, while every real animation still finishes well within the limit.
  • A unit test expects the leave handler to have been called, but the spy never fires. Why?
    `TestBed` disables enter and leave animations by default, and with animations disabled Angular removes a function-bound leaving element without calling the handler. Configure the test with `TestBed.configureTestingModule({animationsEnabled: true})` so the handler runs.

saying these in an interview costs you the question

  • Angular detects when a JavaScript animation library finishes on its own.
  • Calling animationComplete() at the start of the handler is fine.
  • A leave handler that never calls animationComplete() keeps the element forever.
  • animationComplete() in an animate.enter handler delays anything.
  • The function form is bound with square brackets like a class binding.