javascriptangularanimation

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


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.


Solution

  • Here is simple implementation such functionality Plunker Example

    1) Build directives

    @Directive({
      selector: '[transition-group-item]'
    })
    export class TransitionGroupItemDirective {
      prevPos: any;
    
      newPos: any;
    
      el: HTMLElement;
    
      moved: boolean;
    
      moveCallback: any;
    
      constructor(elRef: ElementRef) {
        this.el = elRef.nativeElement;
      }
    }
    
    
    @Component({
      selector: '[transition-group]',
      template: '<ng-content></ng-content>'
    })
    export class TransitionGroupComponent {
      @Input('transition-group') class;
    
      @ContentChildren(TransitionGroupItemDirective) items: QueryList<TransitionGroupItemDirective>;
    
      ngAfterContentInit() {
        this.refreshPosition('prevPos');
        this.items.changes.subscribe(items => {
          items.forEach(item => {
            item.prevPos = item.newPos || item.prevPos;
          });
    
          items.forEach(this.runCallback);
          this.refreshPosition('newPos');
          items.forEach(this.applyTranslation);
    
          // force reflow to put everything in position
          const offSet = document.body.offsetHeight;
          this.items.forEach(this.runTransition.bind(this));
        })
      }
    
      runCallback(item: TransitionGroupItemDirective) {
        if(item.moveCallback) {
          item.moveCallback();
        }
      }
    
      runTransition(item: TransitionGroupItemDirective) {
        if (!item.moved) {
          return;
        }
        const cssClass = this.class + '-move';
        let el = item.el;
        let style: any = el.style;
        el.classList.add(cssClass);
        style.transform = style.WebkitTransform = style.transitionDuration = '';
        el.addEventListener('transitionend', item.moveCallback = (e: any) => {
          if (!e || /transform$/.test(e.propertyName)) {
            el.removeEventListener('transitionend', item.moveCallback);
            item.moveCallback = null;
            el.classList.remove(cssClass);
          }
        });
      }
    
      refreshPosition(prop: string) {
        this.items.forEach(item => {
          item[prop] = item.el.getBoundingClientRect();
        });
      }
    
      applyTranslation(item: TransitionGroupItemDirective) {
        item.moved = false;
        const dx = item.prevPos.left - item.newPos.left;
        const dy = item.prevPos.top - item.newPos.top;
        if (dx || dy) {
          item.moved = true;
          let style: any = item.el.style;
          style.transform = style.WebkitTransform = 'translate(' + dx + 'px,' + dy + 'px)';
          style.transitionDuration = '0s';
        }
      }
    }
    

    2) Use it as follows

    <ul [transition-group]="'flip-list'">
      <li *ngFor="let item of items" transition-group-item>
        {{ item }}
      </li>
    </ul>