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

后端 未结 4 1241
悲&欢浪女
悲&欢浪女 2020-12-28 12:02

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

\"shuffle

相关标签:
4条回答
  • 2020-12-28 12:16

    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'.

    0 讨论(0)
  • 2020-12-28 12:30

    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>
    
    0 讨论(0)
  • 2020-12-28 12:34

    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;
    }
    
    0 讨论(0)
  • 2020-12-28 12:40

    Here is my version of @yurzui code. Changes:

    • supports insert and deletion of items
    • a forced reflow survives webpack optimizations


    import { Component, ContentChildren, Directive, ElementRef, Input, QueryList } from '@angular/core';
    
    @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>;
    
        ngAfterViewInit() {
            setTimeout(() => this.refreshPosition('prevPos'), 0); // save init positions on next 'tick'
    
            this.items.changes.subscribe(items => {
                items.forEach(item => item.prevPos = item.newPos || item.prevPos);
                items.forEach(this.runCallback);
                this.refreshPosition('newPos');
                items.forEach(item => item.prevPos = item.prevPos || item.newPos); // for new items
    
                const animate = () => {
                    items.forEach(this.applyTranslation);
                    this['_forceReflow'] = document.body.offsetHeight; // force reflow to put everything in position
                    this.items.forEach(this.runTransition.bind(this));
                }
    
                const willMoveSome = items.some((item) => {
                    const dx = item.prevPos.left - item.newPos.left;
                    const dy = item.prevPos.top - item.newPos.top;
                    return dx || dy;
                });
    
                if (willMoveSome) {
                    animate();
                } else {
                    setTimeout(() => { // for removed items
                        this.refreshPosition('newPos');
                        animate();
                    }, 0);
                }
            })
        }
    
        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';
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题