--- Update (Angular v20.2.0): ---
First of all, use of @angular/animations is discouraged by angular team, thankfully we now have the tools required to do it without the animation package.
TL;DR
Angular animation api allows for dynamically assigning classes to an element on enter on leave - no imports required.
HTML:
@if(isFooOpen()){
<p
class="foo-element"
animate.enter="enter-transition"
animate.leave="leave-transition"
>
Quo natus ab veritatis!
</p>
}
CSS:
.foo-element {
transition: transform 0.5s ease-in-out;
}
.enter-transition {
transform: translateX(0);
@starting-style {
transform: translateX(100%);
}
}
.leave-transition {
transform: translateX(100%);
}
If you want to play around here is stack blitz example.
Since its a normal attribute binding, you can even do this from inside the component using host binding:
@Component({
...
host: {
'animate.enter': 'enter-transition',
'animate.leave': 'leave-transition',
},
})
...
Stack blitz example.
--- Original answer (Angular v17.0.0): ---
Here are my two go to solutions:
Case 1: Light elements (e.g., tooltips)
Solution for elements I don´t mind just hiding instead of destroying.
CSS now supports transitioning and animating display: none. For this to work you have to indicate that one of properties you want to apply transition to is display and set transition-behavior: allow-discrete
<button (click)="toggleOpenFoo()">Toggle Component Display</button>
<app-foo class="app-foo" [ngClass]="{'app-foo--open': isFooOpen}" />
.app-foo {
display: none;
position: relative;
opacity: 0;
left: 5rem;
transition-property: display opacity;
transition-duration: 0.2s;
transition-behavior: allow-discrete;
}
.app-foo--open {
display: inline;
opacity: 1;
left: 0;
@starting-style {
opacity: 0;
left: 5rem;
}
}
Here is a stack blitz example. Here is a great explanation video.
Case 2: Heavy elements (e.g., maps) or lifecycle-bound components
Solution for elements that should not exist while not in use.
For this one we will have to somehow animate *ngIf / @if. Animating an element inside template conditional on appearance is no problem, the issue comes when it’s destroyed. CSS has no way of knowing the element is about to be removed, so element ends up just popping out of existence. To fix this, we have to give CSS time to react. For that we can use a helper variable and some flavour of timeout, like setTimeout or rxjs timer.
To solve this case we will build upon case 1.
The trick is to have one normal state, like isOpen, and another debounced state that updates only when the animation should finish. First variable you pass to css so it knows when animation should start. And the other one you pass to @if so it reacts with delay.
Please note that delay is only required during destruction, appearance animation should work fine as is.
Here is a simplified example:
<button (click)="toggleOpenFoo()">Toggle Component Display</button>
@if(isFooOpenDebounced){
<app-foo class="app-foo" [ngClass]="{'app-foo--open': isFooOpen}" />
}
export class App {
isFooOpen = true;
isFooOpenDebounced = this.isFooOpen;
toggleOpenFoo() {
this.isFooOpen = !this.isFooOpen;
setTimeout(
() => (this.isFooOpenDebounced = this.isFooOpen),
// Animation on appearance works just fine as it is, we need to add delay only on destruction
this.isFooOpen ? 0 : 1000
);
}
}
Here are two stack blitz examples, a simple one and a more advanced one.
A logical next step would be to create a custom pipe that would debounce a value for you regardless of where it comes from. This way you can hide the helper variable and other implementation logic.
In case you are interested in animating router transition, you can check this response.