How to implement item reorder/shuffle animations with Angular's ngFor?

Viewed 7649

Vue users are easy to implement such item shuffle animations, see their official docs:

shuffle animation

I search a lot but can't find a solution for Angular users. ngFor seems to switch item contents instead of moving items when shuffling them.

Here's my demo: http://embed.plnkr.co/3IcKcC/

When you click shift, you should see items move animation thanks to li {transform: all 1s;}. But when you shuffle them, there's no animation. So I'm here asking for a solution.

5 Answers

More correct (and TSLint-compliant) would be to use a different Directive name, as:

@Directive({
    selector: '[appTransitionGroupItem]'
})

and using a component as an element and not overloading the input name:

@Component({
    selector: 'app-transition-group',
    template: '<ng-content></ng-content>'
})
export class TransitionGroupComponent implements AfterViewInit {
    @Input() className;

Which gives the code better Angular structure, my compliant, better read (YMMV) code, being:

<app-transition-group [className]="'flip-list'">
  <div class="list-items" *ngFor="let item of items" appTransitionGroupItem>
  etc

Also, if you're wondering why the transition animation isn't working, don't forget the CSS required:

.flip-list-move {
  transition: transform 1s;
}

Once the animated elements are not in the view the animation breaks. I fixed it by editing refreshPosition function:

refreshPosition(prop: string) {
  this.items.forEach(item => {
    item[prop] = {
      top: item.el.offsetTop,
      left: item.el.offsetLeft
    }
  });
}

Originally @yurzui used el.getBoundingClientRect() to get the positions but this method returns positions relative to the viewport.

I changed it so it gets the positions using el.offsetTop and el.offsetLeft which are relative to the first ancestor that isn't positioned 'static'.

You can get pretty close to the desired effect by using CSS transforms combined with Angular trackBy. The trick is to use the trackBy function to persist the HTML element between list position changes.

Stackblitz demo

enter image description here

@Component()
class AppComponent {
  arr = [
    { id: 1 },
    { id: 2 },
    { id: 3 },
    { id: 4 },
    { id: 5 },
    { id: 6 },
    { id: 7 }
  ];

  shuffle() {
    this.arr = shuffle(this.arr);
  }

  transform(index: number) {
    return `translateY(${(index + 1) * 100}%)`;
  }

  trackBy(index, x) {
    return x.id;
  }
}
<button (click)="shuffle()">Shuffle</button>
<div class="container">
  <div
    class="list-item"
    *ngFor="let obj of arr; index as index; trackBy: trackBy"
    [style.transform]="transform(index)"
  >
    id: {{ obj.id }}
  </div>
</div>
.list-item {
  position: absolute;
  width: 100%;
  transition: all 1s;
  background-color: coral;
  border: 1px solid white;
  padding: 8px;
  color: white;
}
Related