DEV Community

Cover image for GSAP Animations in Angular - Handle animateOut
Nicola
Nicola

Posted on

GSAP Animations in Angular - Handle animateOut

Handle the out animation of our components

Now that we have done the animationIn of our components, we want to handle also the animationOut.

For example, we want to hide our HTML element with a fancy fade out animation, but if we use the structural directive *ngIf of angular the animation will not work, because angular will remove physically the element from the view.

So how can we do it? Let's start with the classic *ngIf directive.

Toggle HTML elements

Let's proceed with the logic to toggle our HTML elements, in our app.component.ts we need to add this code:

export class AppComponent {
  title = 'my-app-title';
  showP = true;
  toggleP() {
    this.showP = !this.showP;
  }
}

The method toggleP will toggle the boolean showP, which will be used in our app.component.html to show/hide our elements:

<p
  fadeInAnimation
  [duration]="1"
  [delay]="1"
  *ngIf="showP"
>
  That's a cool effect, or not?
</p>
<p
  fadeInAnimation
  [duration]="1"
  [delay]="2"
  *ngIf="showP"
>
  This too, but a little bit delayed
</p>

Let's add a button to call toggleP method (animated too 😎):

<p
  fadeInAnimation
  [duration]="1"
  [delay]="1"
  *ngIf="showP"
>
  That's a cool effect, or not?
</p>
<p
  fadeInAnimation
  [duration]="1"
  [delay]="2"
  *ngIf="showP"
>
  This too, but a little bit delayed
</p>
<button
  fadeInAnimation
  [duration]="1"
  [delay]="3"
  (click)="toggleP()"
>{{showP ? "Hide P" : "Show P"}}
</button>

And run the application, as you will see the animationOut will not be triggered:

ngIf fail

As you can see the directive removes directly the HTML element, so how can we handle it without deleting the HTML element?

The ngIfAnimated directive

We can create a custom structural directive, create a directory inside directives/ folder, called structural, and a file named ngIf-animated.directive.ts:

import {Directive, ElementRef, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef} from '@angular/core';
import {CoreAnimationDirective} from '../gsap/core-animation.directive';

@Directive({
  selector: '[ngIfAnimated]'
})
export class NgIfAnimatedDirective {
  childViewRef: EmbeddedViewRef<CoreAnimationDirective> = null;

  constructor(
    private element: ElementRef,
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}

  @Input()
  set ngIfAnimated(show) {
    if(show) {
      this.childViewRef = this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      if(this.childViewRef) {
        const node = this.childViewRef.rootNodes[0];
        if(node) {
          node.dispatchEvent(new CustomEvent('animate-out', {detail: {parentViewRef: this.viewContainer}}));
        }
      }
    }
  }
}

This directive will show and hide an embedded viewRef using an @Input parameter called ngIfAnimated.

If passed show boolean is true, then it will create and embeddedView, else it will dispatch a CustomEvent on the first rootNode, passing the viewContainer reference. We will see why in a moment.

We need to handle the event trigger inside our CoreAnimationDirective, the component will receive the event, run the animation out and clear the parent view:

export class CoreAnimationDirective {
  @Input() duration = 1;
  @Input() delay = 0;

  @Output() complete: EventEmitter<null> = new EventEmitter();
  @Output() reverseComplete: EventEmitter<null> = new EventEmitter();
  protected timeline: TimelineMax;

  constructor(protected element: ElementRef) {
    // handle animate-out event
    this.element.nativeElement.addEventListener('animate-out', ({detail}) => {
      this.animateOut(detail.parentViewRef);
    })
    this.timeline = new TimelineMax({
      onComplete: _ => this.complete.emit(),
      onReverseComplete: _ => this.reverseComplete.emit(),
      paused:true,
      reversed:true
    });
  }

  protected animateIn() {
    if(this.timeline.isActive()) {
      this.timeline.kill();
    }
    this.timeline.play();
  }

  protected animateOut(parentViewRef: ViewContainerRef) {
    if(this.timeline.isActive()) {
      this.timeline.kill();
    }
    setTimeout(() => {
      this.timeline.timeScale(this.duration).delay(0).reverse();
      setTimeout(_ => {
        if (parentViewRef) {
          parentViewRef.clear();
        }
      }, this.duration * 1000);
    }, this.delay * 1000);
  }
}

As you can see we have added 2 new thing to our CoreAnimationDirective:

  1. animate-out event handler - handle the event on HTML element and call the method animateOut

  2. animateOut method - this method play the animation of the directive, but reversed. It kills the timeline if is running, and set a timeout to clear the parentViewRef, according to the animation duration and delay.

Now we have only to declare our ngIfAnimated inside the app.module and replace the previous *ngIf with *ngIfAnimated on our html elements:

@NgModule({
  declarations: [
    AppComponent,
    FadeInAnimationDirective,
    NgIfAnimatedDirective
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
<p
  fadeInAnimation
  [duration]="1"
  [delay]="1"
  *ngIfAnimated="showP"
>
  That's a cool effect, or not?
</p>
<p
  fadeInAnimation
  [duration]="1"
  [delay]="2"
  *ngIfAnimated="showP"
>
  This too, but a little bit delayed
</p>
<button
  fadeInAnimation
  [duration]="1"
  [delay]="3"
  (click)="toggleP()"
>{{showP ? "Hide P" : "Show P"}}
</button>

The result should look like this:

Animate out result

What will come next

In the next parts of this series we will take a look on how to:

  1. Handle animations with MouseEvent, for example to scale up a button.
  2. How to keep all stateless, using ngrx, to handle complex animations while routing.

Top comments (2)

Collapse
 
pointbre profile image
pointbre

This is great. Thanks for sharing this. This really helps me a lot.

Collapse
 
nicolalc profile image
Nicola

I'm happy to hear that! If you need some help just DM me ;)