1

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([]);
  }
}
atwright147
  • 3,694
  • 4
  • 31
  • 57

1 Answers1

3

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

Moshezauros
  • 2,493
  • 1
  • 11
  • 15
  • Thank you, I was much closer than I thought (I'm new to Angular animations, so it just looks like a wall of text at the mo). In your StackBlitz, it animates on `:enter` but not on `:leave`, how would I fix that? – atwright147 Dec 21 '20 at 10:01
  • I'm not sure why, but if you move the leave animation to the main component, then it works, it probably has to do with *ngFor and animations, but I'm not really sure, check my stackblitz for the updated version – Moshezauros Dec 21 '20 at 11:03
  • @atwright147, if my answer helped, please mark it as the accepted answer, thanks – Moshezauros Dec 22 '20 at 11:39
  • Great solution, thank you for the Stackblitz example! I also like how you used `@animate` without brackets, reminding me that it didn't need to be a dynamic binding. – SpaceNinja Nov 27 '22 at 16:51