如何使用Angular的ngFor实现项目重新排序/随机播放动画?

时间:2017-05-12 02:51:16

标签: javascript angular animation

Vue用户很容易实现这样的项目随机播放动画,请参阅他们的官方文档:

shuffle animation

我经常搜索但无法为Angular用户找到解决方案。 ngFor似乎在改组项目内容时而不是移动项目。

这是我的演示:http://embed.plnkr.co/3IcKcC/

当您点击shift时,您会看到项目因li {transform: all 1s;}而移动动画。但是当你洗牌时,没有动画。所以我在这里要求解决方案。

5 个答案:

答案 0 :(得分:13)

以下是简单的实施功能 Plunker Example

1)构建指令

@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)使用如下

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

答案 1 :(得分:2)

这是我的@yurzui代码版本。变化:

  • 支持插入和删除项目
  • 强制重排在webpack优化中幸存下来


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';
        }
    }
}

答案 2 :(得分:1)

更正确(且符合TSLint规范)的是使用其他指令名称,例如:

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

并使用组件作为元素,并且不会重载输入名称:

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

哪种代码可以提供更好的Angular结构,我的标准,更好阅读(YMMV)代码是:

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

此外,如果您想知道过渡动画为何不起作用,请不要忘记所需的CSS:

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

答案 3 :(得分:0)

一旦动画元素不在视图中,动画就会中断。 我通过编辑refreshPosition函数来修复它:

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

@yurzui最初使用el.getBoundingClientRect()获取位置,但是此方法返回相对于视口的位置。

我更改了它,以便使用el.offsetTop和el.offsetLeft获得相对于未定位为“静态”的第一个祖先的位置。

答案 4 :(得分:0)

通过将 CSS transforms 与 Angular trackBy 结合使用,您可以获得非常接近所需的效果。诀窍是使用 trackBy 函数在列表位置更改之间保持 HTML 元素。

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;
}