Search code examples
angularangular-animations

Angular animation: stagger animate child components


I have been trying to get Angular animations working for child components (Content Children) with a stagger for several days now but despite reading everything I can find I cannot get it tow work.

This question appears to be doing roughly what I am trying to do, but I cannot get it to work: Angular Stagger Animation for multiple new elements

StackBlitz: https://stackblitz.com/edit/angular-anim-children

My components are:

Parent Component

import { Component } from '@angular/core';
import { trigger, transition, style, animate, query, stagger, group, animateChild } from '@angular/animations';

import { ListService } from '../services/list.service';

@Component({
  selector: 'app-list-container',
  template: `
    <button (click)="add()">Add</button>
    <hr>

    <app-list-item
      *ngFor="let item of list$ | async; let idx = index;"
      (delete)="remove(idx)"
      [@list]="(list$ | async).length"
      [title]="item.title"
      [index]="idx"
    ></app-list-item>
  `,
  styles: [`
    :host {
      display: block;
    }
  `],
  animations: [
    trigger('list', [
      transition('* => *', [
        query(':enter', stagger(50, animateChild()), { optional: true })
      ])
    ])
  ]
})
export class ListContainerComponent {
  readonly list$ = this.listService.list;
  readonly index: number;

  constructor(
    private readonly listService: ListService,
  ) { }

  add(): void {
    const counter = this.listService.counter;
    this.listService.add({ title: `Item ${counter + 1}` });
  }

  remove(index: number): void {
    this.listService.remove(index);
  }
}

Child Component

import { Component, EventEmitter,  Input, Output } from '@angular/core';
import { trigger, transition, style, animate, query, stagger } from '@angular/animations';

@Component({
  selector: 'app-list-item',
  template: `
    <div class="list-item" [@animate]="true">
      {{title}}
      <button (click)="onDelete(index)">Delete</button>
    </div>
  `,
  styles: [
    `
      :host {
        display: block;
      }

      button {
        margin-left: 20px;
      }
    `
  ],
  animations: [
    trigger('animate', [
      transition(':enter', [
        style({ opacity: 0, transform: 'translateX(-10px)' }),
        animate('250ms', style({ opacity: 1, transform: 'translateX(0px)' }))
      ]),
      transition(':leave', [
        style({ opacity: 1 }),
        animate('250ms', style({ opacity: 0, transform: 'translateX(10px)' }))
      ])
    ])
  ],
})
export class ListItemComponent {
  @Input() title: string;
  @Input() index: number;

  @Output() delete = new EventEmitter<number>();

  onDelete(index: number): void {
    this.delete.emit(index);
  }
}

Service

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

export interface IItem {
  title: string,
}

@Injectable()
export class ListService {
  private readonly _list = new BehaviorSubject<IItem[]>([]);
  readonly list = this._list.asObservable();

  counter = 0;

  get length() {
    return this._list.value.length;
  }

  add(item: IItem): void {
    this._list.next([item, ...this._list.value]);
    this.counter += 1;
  }

  remove(index: number): void {
    this._list.value.splice(index, 1);
  }

  clear(): void {
    this._list.next([]);
  }
}

Solution

  • You have a few errors, first, the animate on the parent (list animation), should look like the following:

    trigger("list", [
      transition("* => *", [query("@animate", stagger(50, animateChild()))])
    ])
    

    notice that you need to query the child animation in order to stagger it.

    you should add it on a parent of elements with the animate animation, so something like the following in the template of list-container.component.ts:

    <div @list>
      <app-list-item
        *ngFor="let item of (list$ | async); let idx = index"
        (delete)="remove(idx)"
        [title]="item.title"
        [index]="idx"
      ></app-list-item>
    </div>
    

    [@animate]="true" can change into @animate (this is not error, but redundant)

    Here is a working stackblitz