2

With the latest angular version, @angular/animations is being deprecated. I am struggling with migrating an animation in my application. This minimal example shows the animated height.

Minimal example: https://stackblitz.com/edit/angular-mqjxfhfz?file=src%2Fmain.ts

What would be the best approach?

Minimal Reproducible Code:

import 'zone.js';
import { Component, signal } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import {
  animate,
  state,
  style,
  transition,
  trigger,
} from '@angular/animations';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';

const MY_ANIMATION = trigger('anim', [
  state(
    'open',
    style({
      height: '*',
      padding: '16px',
      margin: '16px 0',
    })
  ),
  state(
    'closed',
    style({
      height: 0,
      padding: '0 16px',
      margin: 0,
      border: 0,
    })
  ),
  transition('open <=> closed', animate('300ms ease-in-out'), {
    params: { height: '*' },
  }),
]);

@Component({
  selector: 'app-root',
  standalone: true,
  providers: [],
  styles: `
    p {
      overflow: hidden;
    }
  `,
  template: `
    <button type="button" (click)="toggleOpen()">Toggle</button>
    <p [@anim]="isOpen() ? 'open' : 'closed'">
    Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
    </p>
  `,
  animations: [MY_ANIMATION],
})
export class App {
  readonly isOpen = signal(false);

  toggleOpen() {
    this.isOpen.set(!this.isOpen());
  }
}

bootstrapApplication(App, {
  providers: [provideAnimationsAsync()],
});
1

6 Answers 6

1

it's ridiculous simple

Directive:

@Directive({
  selector: '[collapse]',
  standalone: true,
})
export class CollapseDirective {
   collapse=input(true);

 
  @HostBinding('style.height') private height=''

  constructor(private elementRef: ElementRef<HTMLElement>) {
    effect(() => this.height= this.collapse()?`${this.elementRef.nativeElement.scrollHeight}px`:'0px');
  }
}

Use:

@Component({
  selector: 'app-root',
  imports: [CommonModule, CollapseDirective], 
  template: `
      <button (click)="show = !show">
        Toggle Content
      </button>
      <div class="collapse" [collapse]="show">
           ....
      </div>

  `,
  style: [`
   .collapse
   {
     transition:height 0.35s ease-in-out;
     overflow:hidden
   }
  `]
})
export class AppComponent {
  show=false;
}

stackblitz

NOTE: I put the .css in component, but you can put in a global .css

NOTE2: You can avoid the .css if you add in your directive

 @HostBinding('style.overflow') private overflow='hidden'
 @HostBinding('style.transition') private transition='height 0.35s ease-in-out'
Sign up to request clarification or add additional context in comments.

Comments

0

This should work, although it looks weird


<div class="parent">
  <p [class.open]="isOpen()">
    .....
  </p>
</div>
.parent {
  display: grid;
  grid-template-rows: min-content 0fr;
  transition: grid-template-rows 300ms;
  overflow: hidden;
}

.open {
  grid-template-rows: min-content 1fr;
}

https://stackblitz.com/edit/angular-scn759gk?file=src%2Fmain.ts

Comments

0

We can use .css and the event transitionend like bootstrap makes.

Imagine a .css like

.show.on {
  display: block;
}
.show.off {
  display: none;
}
.show.running {
  height: 0;
  overflow: hidden;
  transition: height 0.35s ease;
}

we want that a div pass throught this states:

<!--initial state-->
<div class="show on" style="height:300px">

<!--start the animation-->
<div class="show running" style="height:300px">

<!--running the animation-->
<div class="show running">

<!--end the animation-->
<div class="show off">

So we can create a directive, I choose the selector "style-on" so our html become like

    <div>
      <button (click)="an.toggle()">Toogle vertical</button>
    </div>
    <div style-on="height:inherit" #an="Animate" class="show off" >
           ....
    </div>

See that to toggle we use a template reference (the #an="Animate") and the button simply call to an.toogle. It's necessary especify the template reference is our directive, else Angular think that it's only the div.

@Directive({
  selector: '[style-on]',
  exportAs: 'Animate',
})
export class AnimateDirective implements AfterViewInit {
  style = input<string>('', { alias: 'style-on' });

  //we can use also two inputs "initClass" and "finalClass"
  initClass = input<string>('on');
  finalClass = input<string>('off');
  ...
}

The first is transform the "style-on" in an object -we use the ngAfterViewInit-, we store in the variable "StyleOn" and use another variable "isOn" and inject in constructor the ElementRef to can reach the "div"

  styleOn: any = null;
  isOn: boolean = true;
  constructor(private el: ElementRef) {}
  ngAfterViewInit() {
      //Calculate the "styleOn"
      if (this.style()) {
        this.styleOn = {};
        this.style().split(';').forEach((x: string) => {
          const pair = x.split(':');
          if (pair[0] && pair[1]) this.styleOn[pair[0].trim()] = pair[1].trim();
        });
      } else this.styleOn = null;

      //
      this.isOn = this.el.nativeElement.classList.contains(this.initClass());
      ...
  }

We want that if the class is "on", also add the "style". We are going to use a function, so

  ngAfterViewInit() {
    ...
    if (this.isOn)
       this.calculateStyle();
    ...
  }

And, case this.styleOn is not null add a listener over "transitionend". we can use plain javascript in the way

this.el.nativeElement.addEventListener('transitionend', () => {..})

But I love rxjs so, I use fromEvent Rxjs operator. So we can unsubscribe in ngAfterViewInit. Well, we can only subscribe if we have any value in this.styleOn, so

  ngAfterViewInit() {
    ...
      if (this.styleOn) {
        this.sub=fromEvent(this.el.nativeElement,'transitionend').subscribe(() => {
          this.el.nativeElement.classList.remove('running');
          if (this.isOn) this.el.nativeElement.classList.add(this.initClass());
          else this.el.nativeElement.classList.add(this.finalClass());
        });
      }
   }

Well we have yet the "cycle" complete.

We use an auxiliar functions to animate

  toggle() {
    if (this.el.nativeElement.classList.contains(this.initClass())) this.off();
    else this.on();
  }
  off() {
    console.log('on---->off');
    if (this.el.nativeElement.classList.contains(this.initClass())) {
      this.el.nativeElement.classList.remove(this.initClass());
      if (this.styleOn) {
        this.el.nativeElement.classList.add('running');
        this.el.nativeElement.style = null;
        this.isOn = false;
      } else this.el.nativeElement.classList.add(this.finalClass());
    }
  }
  on() {
    if (this.el.nativeElement.classList.contains(this.finalClass())) {
      this.el.nativeElement.classList.remove(this.finalClass());
      if (this.styleOn) {
        this.el.nativeElement.classList.add('running');
        this.isOn = true;
        setTimeout(() => {
          this.calculateStyle();
        });
      } else this.el.nativeElement.classList.add(this.initClass());
    }
  }

And the last is a little "hard" code to calculate the "style". if the value of the style is not equal to "inherit" is easy, but if we use "inherit" (for e.g. when we can manage the width of the height) we need calculate.

In the case of width and heigth we can take the properties scrollWidth and scrollHeigth, in other case we use window.getComputedStyle to know the value.

  calculateStyle() {
    Object.keys(this.styleOn).forEach((key) => {
      if (this.styleOn[key] == 'inherit') {
        const capitalizedDimension =
          'scroll' + key[0].toUpperCase() + key.slice(1);
        if (this.el.nativeElement[capitalizedDimension])
          this.el.nativeElement.style[key] =
            this.el.nativeElement[capitalizedDimension] + 'px';
        else
          this.el.nativeElement.style[key] = window
            .getComputedStyle(this.el.nativeElement)
            .getPropertyValue(key);
      } else this.el.nativeElement.style[key] = this.styleOn[key];
    });
  }

the whole stackbliz

Update If we can emit an event when the animation change we need makes a few changes

export class AnimateDirective implements AfterViewInit, OnDestroy {
  ...
  animateEnd = output<'on'|'off'>(); //<--add an output
  ngAfterViewInit(){
     ...
    //Always subscribe to transitionend
    this.sub = fromEvent(this.el.nativeElement, 'transitionend').subscribe(
      () => {
        if (this.styleOn) { //but only use the class "running
                            //if this.styleOn
          this.el.nativeElement.classList.remove('running');
          if (this.isOn) this.el.nativeElement.classList.add(this.initClass());
          else this.el.nativeElement.classList.add(this.finalClass());
        }
        this.animateEnd.emit(this.isOn ? 'on' : 'off');
      }
    );
    ...
  }
  off() {
    if (this.el.nativeElement.classList.contains(this.initClass())) {
      this.el.nativeElement.classList.remove(this.initClass());
      this.isOn = false; //---this line executed always
      if (this.styleOn) {
        ...
      } else ...
    }
  }
  on() {
    if (this.el.nativeElement.classList.contains(this.finalClass())) {
      this.el.nativeElement.classList.remove(this.finalClass());
      this.isOn = true; //<-this line executed always
      if (this.styleOn) {
        ....
      } else ....
    }
  }

This allow us use some like

<div style-on #an5="Animate" class="color-red off" (animateEnd)="fool($event)">

where

  fool(event: 'on' | 'off') {
    console.log(event);
  }

Comments

0

TL;DR Here is solution not requiring angular animations stackblitz

@Component({
  selector: 'app-root',
  standalone: true,
  styles: `
    p {
      overflow: hidden;
      transition: all 300ms ease-in-out;
      padding: 16px;
    }

    .open {
      height: auto;
      margin: 16px 0;

      @starting-style {
        height: 0;
        margin: 0;
        padding-block: 0;
      }
    }
  `,
  template: `
    <button  type="button"(click)="toggleOpen()">
      Toggle
    </button>
    @if (isOpen()) {
      <p
        animate.enter="open"
        animate.leave="closed">
        Lorem ipsum dolor sit amet
      </p>
    }
  `,
})
export class App {
  readonly isOpen = signal(false);

  toggleOpen() {
    this.isOpen.set(!this.isOpen());
  }
}

animate.enter and animate.leave API is available since Angular 20.2, more info.

Comments

0

Animating height to "auto" is tricky. As an alternative I used grid-template-rows in the past, which you can animate.

Based on your stackBlitz: https://stackblitz.com/edit/angular-usprehck?file=src%2Fmain.ts

.container {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s ease-in-out;
}

.container.open {
  grid-template-rows: 1fr;
}

.content { 
  overflow: hidden;
}
<div class="container" [class.open]="isOpen()">
  <p class="content">
  Lorem ipsum dolor sit amet...
  </p>
</div>

Comments

0

From initial analysis we can see the animation framework is being deprecated to switch over to CSS based animations:, looking at the error.

@deprecated — 20.2 Use animate.enter or animate.leave instead. Intent to remove in v23


We can find this deprecation notice, which explains the reason for the deprecation.

The @angular/animations package is deprecated as of v20.2, which also introduced the new animate.enter and animate.leave feature to add animations to your application. Using these new features, you can replace all animations based on @angular/animations with plain CSS or JS animation libraries. Removing @angular/animations from your application can significantly reduce the size of your JavaScript bundle. Native CSS animations generally offer superior performance, as they can benefit from hardware acceleration. This guide walks through the process of refactoring your code from @angular/animations to native CSS animations.

Migrating away from Angular's Animations package


We can also find the deprecation notice on the animation providers:

Deprecation warning
Use animate.enter or animate.leave instead. Intent to remove in v23

provideAnimationsAsync provideAnimations provideNoopAnimations


We will no longer need to use provideAnimations or provideAnimationsAsync to the bootstrapApplication or environment providers.

bootstrapApplication(App, {
  providers: [
    // provideAnimations() // no longer needed!
    // provideAnimationsAsync() // no longer needed!
  ],
});

Instead there is tutorial for using animations going forward on the official site.

Animating your applications with animate.enter and animate.leave

Below is a description of the enter and leave:

Angular provides animate.enter and animate.leave to animate your application's elements. These two features apply enter and leave CSS classes at the appropriate times or call functions to apply animations from third party libraries. animate.enter and animate.leave are not directives. They are special API supported directly by the Angular compiler. They can be used on elements directly and can also be used as a host binding.


Conversion to new animation style:

I tried converting your example to the animation style. Below are my findings:

When creating the animate.open animation (Things to keep in mind):

We should declare the open (:enter) using keyframes (I was only able to get this working). In the below CSS we can see when open class is added we add animation: slide-fade 1s; to animate the slide-fade which contains the keyframes animation.

.open {
  animation: slide-fade 1s;
}
@keyframes slide-fade {
  from {
    opacity: 0;
    transform: translateY(-100%);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Same But with height transition instead of transform:

.open {
  animation: slide-fade 300ms;
}
@keyframes slide-fade {
  from {
    max-height:0px;
  }
  to {
    max-height:1000px;
  }
}

When creating the animate.leave animation (Things to keep in mind):

We should declare the leave (:leave) using CSS transitions. In the below CSS we can see when closed class is added we add the @starting-style to define the initial state of the transition. We can also emit this step and the animations should work fine, when I tried it (I may be wrong).

  @starting-style {
    opacity: 0;
    transform: translateY(-100%);
  }

Same but with height transition instead of transform:

  @starting-style {
    max-height:1000px;
  }

Then we define the CSS to first toggle opacity and then transform to the required position. So the final CSS will be:

.animate-item {
  margin: 0px;
  border: 1px solid #dddddd;
  margin-top: 1em;
  padding: 20px 20px 0px 20px;
  position: relative;
  @starting-style {
    opacity: 0;
    transform: translateY(-100%);
  }
}
.closed {
  opacity: 0;
  transform: translateY(-100%);
  transition: opacity 500ms ease-out, transform 500ms ease-out;
}

Same but with height transition instead of transform:

.closed {
  transition: max-height 300ms ease-out;
  max-height:0;
}

So Putting it all together the final code will be:

Full Code (Using Height):

import 'zone.js';
import { Component, signal } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
  selector: 'app-root',
  standalone: true,
  providers: [],
  styles: `
    p {
      overflow: hidden;
    }
    .animate-item {
      margin: 0px;
      border: 1px solid #dddddd;
      margin-top: 1em;
      padding: 20px 20px 0px 20px;
      position: relative;
      max-height:1000px;
      @starting-style {
        max-height:1000px;
      }
    }
    .closed {
      transition: max-height 300ms ease-out;
      max-height:0;
    }
    .open {
      animation: slide-fade 300ms;
    }
    @keyframes slide-fade {
      from {
        max-height:0px;
      }
      to {
        max-height:1000px;
      }
    }
  `,
  template: `
    <button type="button" (click)="toggleOpen()">Toggle</button>
    @if(isOpen()) {
      <p class="animate-item" animate.enter="open" animate.leave="closed">
      Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
      </p>
    }
  `,
  animations: [],
})
export class App {
  readonly isOpen = signal(false);

  toggleOpen() {
    this.isOpen.set(!this.isOpen());
  }
}

bootstrapApplication(App, {
  providers: [],
});

Full Code (Using Transform):

import 'zone.js';
import { Component, signal } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
  selector: 'app-root',
  standalone: true,
  providers: [],
  styles: `
    p {
      overflow: hidden;
    }
    .animate-item {
      margin: 0px;
      border: 1px solid #dddddd;
      margin-top: 1em;
      padding: 20px 20px 0px 20px;
      position: relative;
      @starting-style {
        opacity: 0;
        transform: translateY(-100%);
      }
    }
    .closed {
      opacity: 0;
      transform: translateY(-100%);
      transition: opacity 500ms ease-out, transform 500ms ease-out;
    }
    .open {
      animation: slide-fade 1s;
      animation-composition: add;
    }
    @keyframes slide-fade {
      from {
        opacity: 0;
        transform: translateY(-100%);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }
  `,
  template: `
    <button type="button" (click)="toggleOpen()">Toggle</button>
    @if(isOpen()) {
      <p class="animate-item" animate.enter="open" animate.leave="closed">
      Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
      </p>
    }
  `,
  animations: [],
})
export class App {
  readonly isOpen = signal(false);

  toggleOpen() {
    this.isOpen.set(!this.isOpen());
  }
}

bootstrapApplication(App, {
  providers: [],
});

Stackblitz Demo (Height)

Stackblitz Demo (Transform)

2 Comments

Hey, thanks for the effort. The animation works as you explained, but the animation I was looking for the height to be animated. It should look like the animation in my example.
updated my answer with height animation stackblitz Demo

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.