Search code examples
angularangular-animations

How to animate akin to :enter and :leave when object reference changes


Here is some working code:

Stackblitz: https://stackblitz.com/edit/angular-ivy-tt9vjd?file=src/app/app.component.ts

app.component.html

<button (click)='swap()'>Swap object</button>
<div @div *ngIf='object'>{{ object.data }}</div>

app.component.css

div {
  width: 100px;
  height: 100px;
  background: purple;
  display: flex;
  align-items: center;
  justify-content: center;
  color: antiquewhite;
}

app.component.ts

import { animate, style, transition, trigger } from "@angular/animations";
import { Component } from "@angular/core";
import { interval } from "rxjs";
import { first } from "rxjs/operators";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
  animations: [
    trigger("div", [
      transition(":enter", [
        style({ transform: "scale(0)" }),
        animate("250ms 0ms ease-out", style({ transform: "scale(1)" }))
      ]),
      transition(":leave", [
        style({ transform: "scale(1)" }),
        animate("250ms 0ms ease-in", style({ transform: "scale(0)" }))
      ])
    ])
  ]
})
export class AppComponent {
  object: any = { data: "DIV_1" };

  swap() {
    this.object = null;
    interval(0)
      .pipe(first())
      .subscribe(() => {
        this.object = { data: "DIV_2" };
      });

    // DESIRED CODE INSTEAD OF THE ABOVE
    // this.object = { data: "DIV_2" };
  }
}

The issue with this code is that an intermediary null state had to be introduced. Thus I am mixing presentation code with logic in order "to make it work". This is violating good encapsulation practices and adds unnecessary complexity to the code.

How can the same results be achieved using code exclusively in the animations property in the decorator?

requirements

  • Detect a change in the object reference.
  • React to the object reference change akin to how :enter and :leave work; First animate DIV_1 out, then animate DIV_2 in.
  • Encapsulate anything regarding the animation in the animations code. So the swap function should be: swap(){this.object = { data: "DIV_2" };}

Solution

  • So, I spent a bit to solve, but I found a possible solution that can help you. You should create a custom directive to use instead of the classic ngIf.

    import {
      Directive,
      Input,
      TemplateRef,
      ViewContainerRef
    } from '@angular/core';
    
    @Directive({
      selector: '[ngIfAnimation]'
    })
    export class NgIfAnimationDirective {
      private value: any;
      private hasView = false;
    
      constructor(
        private view: ViewContainerRef,
        private tmpl: TemplateRef<any>
      ) { }
    
      @Input() set ngIfAnimation(val: any) {
        if (!this.hasView) {
          this.view.createEmbeddedView(this.tmpl);
          this.hasView = true;
        } else if (val !== this.value) {
          this.view.clear();
          this.view.createEmbeddedView(this.tmpl);
          this.value = val;
        }
      }
    }
    

    Quickly: we clear the current view, instantiate an embedded view and inserts it into the div each time there's a change. About your component your animations will be like this (you can change them of course):

    animations: [
        trigger('div', [
          state('void', style({ transform: 'scale(0)' })),
          state('*', style({transform: 'scale(1)' })),
          transition('void => *', [animate('0.2s 0.2s ease-in')]),
          transition('* => void', [animate('0.2s ease-in')])
        ])
      ],
    

    And your template:

    <div [@div] *ngIfAnimation="object">{{ object.data }}</div>
    

    Remember to remove your *ngIf because you can't have multiple template bindings on one element.

    Stackblitz: https://stackblitz.com/edit/angular-ivy-hc8snt?file=src/app/app.component.html