如何在对象引用更改时制作类似于 :enter 和 :leave 的动画

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

这是一些工作代码:

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" };
  }
}

此代码的问题在于必须引入中间 null 状态。因此,我将表示代码与逻辑混合在一起以“使其工作”。这违反了良好的封装做法,并给代码增加了不必要的复杂性。

如何使用装饰器中 animations 属性 中的代码实现相同的结果?

要求

所以,我花了一点时间来解决,但我找到了一个可以帮助你的可能的解决方案。您应该创建一个自定义指令来代替经典的 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;
    }
  }
}

快速:我们清除当前视图,实例化一个嵌入视图,并在每次发生变化时将其插入到 div 中。 关于您的组件,您的动画将是这样的(您当然可以更改它们):

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')])
    ])
  ],

你的模板:

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

记得删除您的 *ngIf,因为您不能在一个元素上绑定多个模板。

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

只是想分享我的结果。该解决方案的灵感主要来自 Francesco Lisandro 的回答。

  • 已创建结构指令
  • 该指令采用具有 2 个属性的对象
    • data: 引用改变的对象
    • leaveTransitionDuration: 延迟插入空状态的持续时间
  • 该指令在旧对象之后和新对象创建之前插入一个空状态。在 leaveTransitionDuration 之后插入 void 状态。
  • 当引用从虚值变为对象时,指令不应用 leaveTransitionDuration。所以正常的:enter动画没有延迟。

app.component.html

<div @div *ngIfAnimation="{data: object, leaveTransitionDuration: 2000}">{{ object.data }}</div>

app.component.ts

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

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

  swap() {
    this.object = { data: "DIV_2" };
  }
}

ngIfAnimation.diretive.ts

import { Directive, Input, TemplateRef, ViewContainerRef } from "@angular/core";
import { interval } from "rxjs";
import { first } from "rxjs/operators";

@Directive({
  selector: "[ngIfAnimation]"
})
export class NgIfAnimationDirective {
  constructor(private view: ViewContainerRef, private tmpl: TemplateRef<any>) {}
  private old_data = null;
  private leaveTransitionDuration: number;

  @Input() set ngIfAnimation(object: {
    data: any;
    leaveTransitionDuration: number;
  }) {
    this.leaveTransitionDuration = object.leaveTransitionDuration;
    this.view.clear();
    if (object.data) this.add_new_view();
    this.old_data = object.data;
  }

  private get delay(): number {
    return this.old_data ? this.leaveTransitionDuration : 0;
  }
  private add_new_view(): void {
    interval(this.delay) // timeout until `:leave` animation is ran
      .pipe(first())
      .subscribe(() => {
        this.view.createEmbeddedView(this.tmpl);
      });
  }
}

查看 Stackblitz 的演示:https://stackblitz.com/edit/angular-ivy-t7ea53?file=src/app/ngIfAnimation.directive.ts

注意:确保 leaveTransitionDuration 等于 :leave 过渡的持续时间。