How can I animate *ngFor in Angular?

2019-01-15 00:30发布

问题:

I need to animate an ngFor list as it is populated and shown. Each element should have a transition, let's say something like this.

How can I do that?

回答1:

It has a few issues because ngFor does a few redundant add/removes which cause items to be animated which shouldn't:

import {Component} from 'angular2/core';
import { Component, Directive, OnDestroy, Input } from 'angular2/core';

@Component({
    selector: 'my-app',
    template: `<div (click)="$event.preventDefault()">
        <button type="button" (click)="pushItem()">Push</button>
        <button type="button" (click)="removeItemLast()">Remove Last</button><br/>
        <button type="button" (click)="unshiftItem()">Unshift</button>
        <button type="button" (click)="removeItemFirst()">Remove First</button><br/>
        <ul>
          <li class="my-animation" *ngFor="#item of items">
            {{item.title}}
          </li>
        </ul>
      </div>`
})
export class AppComponent {
  private count:number = 1;
  public items: Array<any>;
  constructor() { 
    console.clear(); 
    this.items = [];
    this.items.push(this.newItem());
    this.items.push(this.newItem());
    }
    pushItem() {
        this.items.push(this.newItem());
    },
    removeItemLast() {
      if(this.items.length > 0) this.items.splice(this.items.length - 1, 1);
    },
    unshiftItem() {
        this.items.unshift(this.newItem());
    },
    removeItemFirst() {
      if(this.items.length > 0) this.items.splice(0, 1);
    },
    newItem() {
      return {title: 'Item' + this.count++};
    }

}
@keyframes MyAnimation {
  0% {
    padding-left: 100px;
  }
  100% {
    padding-left: 0px;
  } 
}

.my-animation {
  animation: MyAnimation 1s;
}

Plunker example (RC.x) from https://github.com/angular/angular/issues/7239 demonstrates the issue.

Update

This was fixed a long time ago

working Plunker example (2.4.x)



回答2:

There is now the guide to Angular's animation system. This helps if we want to do fancy things, like only do the animation for elements added after the component has initialized, not the ones that are present already. I've modified the previous answer to do it the Angular 2 way.

Plunker: http://plnkr.co/edit/NAs05FiAVTlUjDOZfEsF?p=preview

import {
    Component,
    trigger, transition, animate, style, state
} from '@angular/core';

@Component({
    selector : 'my-app',
    animations: [
        trigger('growShrinkStaticStart', [
            state('in', style({ height: '*', 'padding-top': '*', 'padding-bottom': '*', 'margin-top': '*', 'margin-bottom': '*' })),
            transition('* => void', [
                style({ height: '*', 'padding-top': '*', 'padding-bottom': '*', 'margin-top': '*', 'margin-bottom': '*' }),
                animate("0.5s ease", style({ height: '0', 'padding-top': '0', 'padding-bottom': '0', 'margin-top': '0', 'margin-bottom': '0' }))
            ]),
            transition('void => false', [
                /*no transition on first load*/
            ]),
            transition('void => *', [
                style({ height: '0', 'padding-top': '0', 'padding-bottom': '0', 'margin-top': '0', 'margin-bottom': '0' }),
                animate("0.5s ease", style({ height: '*', 'padding-top': '*', 'padding-bottom': '*', 'margin-top': '*', 'margin-bottom': '*' }))
            ])
        ])
    ],
    template : `<div (click)="$event.preventDefault()">
        <button type="button" (click)="pushItem()">Push</button>
        <button type="button" (click)="removeItemLast()">Remove Last</button><br/>
        <button type="button" (click)="unshiftItem()">Unshift</button>
        <button type="button" (click)="removeItemFirst()">Remove First</button><br/>
        <ul style="background: light-blue">
          <li *ngFor="let item of items" 
          [@growShrinkStaticStart]="animationInitialized.toString()" 
          (@growShrinkStaticStart.done)="animationInitialized = true"
          style="background-color:pink; border:1px dashed gray; overflow:hidden">
            <h3>{{item.title}}</h3><p>{{item.description}}</p>
          </li>
        </ul>
        <div>Footer</div>
      </div>`
})
export class AppComponent
{
    private count: number = 1;
    public items: Array <{ title: string, description: string }> ;
    private animationInitialized: boolean = false;

    constructor() {
        this.items = [];
        this.items.push(this.newItem());
        this.items.push(this.newItem());
    }

    pushItem() {
        this.items.push(this.newItem());
    }

    removeItemLast() {
        if (this.items.length > 0)
            this.items.splice(this.items.length - 1, 1);
    }

    unshiftItem() {
        this.items.unshift(this.newItem());
    }

    removeItemFirst() {
        if (this.items.length > 0)
            this.items.splice(0, 1);
    }

    newItem() {
        let d: string = "";
        let possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZ    abcdefghijklmnopqrstuvwxyz0123456789 . ! ? ";

        for (let i = 0; i < Math.floor(Math.random() * 50000); i++)
            d += possible.charAt(Math.floor(Math.random() * possible.length));

        return { title : 'Item' + this.count++, description: d };
    }
}