diff --git a/utils/transition-group/transition-group-item.directive.ts b/utils/transition-group/transition-group-item.directive.ts
new file mode 100644
index 00000000..a9b08f97
--- /dev/null
+++ b/utils/transition-group/transition-group-item.directive.ts
@@ -0,0 +1,16 @@
+import {Directive, ElementRef} 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;
+ }
+}
diff --git a/utils/transition-group/transition-group.component.less b/utils/transition-group/transition-group.component.less
new file mode 100644
index 00000000..8eddb9c9
--- /dev/null
+++ b/utils/transition-group/transition-group.component.less
@@ -0,0 +1,3 @@
+.list-move {
+ transition: transform 1s;
+}
diff --git a/utils/transition-group/transition-group.component.ts b/utils/transition-group/transition-group.component.ts
new file mode 100644
index 00000000..14fc2648
--- /dev/null
+++ b/utils/transition-group/transition-group.component.ts
@@ -0,0 +1,98 @@
+import {TransitionGroupItemDirective} from "./transition-group-item.directive";
+import {AfterViewInit, Component, ContentChildren, OnDestroy, QueryList, ViewEncapsulation} from "@angular/core";
+import {Subscription} from "rxjs";
+
+@Component({
+ selector: '[transition-group]',
+ template: '',
+ styleUrls: ['transition-group.component.less'],
+ encapsulation: ViewEncapsulation.None
+})
+export class TransitionGroupComponent implements AfterViewInit, OnDestroy {
+ @ContentChildren(TransitionGroupItemDirective) items: QueryList;
+ private subscription: Subscription;
+
+ ngAfterViewInit() {
+ this.init();
+ this.subscription = 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);
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ if (this.subscription) {
+ this.subscription.unsubscribe();
+ }
+ }
+
+ init() {
+ this.refreshPosition('prevPos');
+ this.refreshPosition('newPos');
+ }
+
+ runCallback(item: TransitionGroupItemDirective) {
+ if (item.moveCallback) {
+ item.moveCallback();
+ }
+ }
+
+ runTransition(item: TransitionGroupItemDirective) {
+ if (!item.moved) {
+ return;
+ }
+ const cssClass = 'list-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';
+ }
+ }
+}
diff --git a/utils/transition-group/transition-group.module.ts b/utils/transition-group/transition-group.module.ts
new file mode 100644
index 00000000..a1d3d0cd
--- /dev/null
+++ b/utils/transition-group/transition-group.module.ts
@@ -0,0 +1,13 @@
+import {NgModule} from "@angular/core";
+import {CommonModule} from "@angular/common";
+import {TransitionGroupItemDirective} from "./transition-group-item.directive";
+import {TransitionGroupComponent} from "./transition-group.component";
+
+@NgModule({
+ declarations: [TransitionGroupItemDirective, TransitionGroupComponent],
+ imports: [CommonModule],
+ exports: [TransitionGroupItemDirective, TransitionGroupComponent]
+})
+export class TransitionGroupModule {
+
+}