14

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?

Lazar Ljubenović
  • 18,976
  • 10
  • 56
  • 91
Nicu
  • 3,476
  • 4
  • 23
  • 43

2 Answers2

14

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 Demo on stackblitz

Shashank Vivek
  • 16,888
  • 8
  • 62
  • 104
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • How would you do this without mutating the array for example how would you animate insertions or updates with an Observable> – Max Alexander Jul 09 '16 at 00:21
  • Not sure, but I guess it should work the same. I assumw `ngFor` checks for changes and updates only what changed. Not verified though. – Günter Zöchbauer Jul 09 '16 at 07:12
  • @GünterZöchbauer any idea how to get rid of the redundent add / remove? I tried to add a trackby ([plunker](http://plnkr.co/edit/OxNC4Nh3kCHKw185masi?p=preview)) with a unique id, but it does not work. Thanks anyway! – maxbellec Jan 11 '17 at 14:15
  • @maxou That should be fixed since a long time (about 2.0.0 final release) – Günter Zöchbauer Jan 11 '17 at 14:16
  • 1
    @maxou I created a new Plunker http://plnkr.co/edit/TMZKPbuzorVt9UsGJ2Nr?p=preview – Günter Zöchbauer Jan 11 '17 at 14:22
  • 1
    You can see the problem clearly here: https://plnkr.co/edit/Xz48XN?p=preview when you click 'swap' All the items who's index has changed are re-added to the dom, regardless of the 'trackBy' clause – Jamie Pate Oct 18 '17 at 21:15
  • 1
    @JamiePate there still seems to be a bug in the IterableDiffer when more than one item changes at a time. First removing, later adding doesn't cause all in between to get removed and re-added https://plnkr.co/edit/ky0sU4EV5NfXLTCkxC2d?p=preview (the later added is again animated though, rightfully in this setup) – Günter Zöchbauer Oct 19 '17 at 05:54
  • 1
    @GünterZöchbauer Yes, that's what i'm trying to show. It seems to work properly if you use `@angular/animations` instead, so that's probably the way to go. The animations web api is much easier than css for this kind of animation on complicated DOM interaction anyways. (even when using it directly) – Jamie Pate Oct 23 '17 at 16:45
  • `@angular/animations` didn't exist back then when I posted the answer. – Günter Zöchbauer Oct 23 '17 at 17:38
8

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 };
    }
}
Stephen
  • 1,603
  • 16
  • 19
  • Is it possible to achieve the same transition effect with custom transition `stateChangeExpression` - other than `void => *` (`:entry`) / `* => void` (`:leave`) **for `*ngFor`**? For example define `transition('in => out',...` and `transition('out => in',...` and in template use `[@growShrinkStaticStart]="transition"` with binding to `transition` variable in the component class?. I ask because `*ngFor` makes DOM addition and removal and I not successful to achive that with custom transitions. – Felix Apr 09 '18 at 13:38
  • 1
    Hi, i am using :leave and if i go back and refresh and assign an api call the result it always triggers the animation. Is it possible to only show animation when removing one item? – Ernesto Ulloa Apr 26 '18 at 16:28