Angular: Sort and Drag table

Start off with the app component and add

<h1 class="text-center">Table sortable demo</h1>

<div class="container">
  <div class="row">
    <div class="col-sm">
    </div>
    <div class="col-sm">
      <app-demo></app-demo>
    </div>
    <div class="col-sm">
    </div>
  </div>
</div>

In the middle column, reference the demo component where the table will live.

The first part just has the title and two buttons put into a button group.

<h3 class="text-center">The Table
    <div class="btn-group">
        <button type="button"
                class="btn btn-success"
                (click)="add()">A
        </button>
        &nbsp;
        <button type="button"
                class="btn btn-success"
                (click)="reset()">R
        </button>
    </div>
</h3>

<br>

The head of the table has sorting and the body has the dragging

<table class="table">
    <thead>
        <tr>
            <th *ngFor="let v of vars; index as i"     
                scope="col"
                (sort)="onSort($event)"               <== sort event
                [sortable]="v"                        <== sortable directive
                class="pointer">                      <== styling
                {{ v }}
            </th>
        </tr>
    </thead>
    <tbody cdkDropList                                <== drop list
           (cdkDropListDropped)="onDrop($event)">     <== drop event
        <tr *ngFor="let row of data; index as i"
            cdkDrag                                   <== dragging
            cdkDragLockAxis="y">                      <== drag axis
            <td *ngFor="let val of vars"
                class="table-cell-width">             <== width of anim
                {{ row[val] }}
            </td>
        </tr>
    </tbody>
</table>

Here are the styles:

For the sorting:

.pointer {
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
}

.pointer.desc:before, .pointer.asc:before {
  content: '';
  display: block;
  background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAmxJREFUeAHtmksrRVEUx72fH8CIGQNJkpGUUmakDEiZSJRIZsRQmCkTJRmZmJgQE0kpX0D5DJKJgff7v+ru2u3O3vvc67TOvsdatdrnnP1Y///v7HvvubdbUiIhBISAEBACQkAICAEhIAQ4CXSh2DnyDfmCPEG2Iv9F9MPlM/LHyAecdyMzHYNwR3fdNK/OH9HXl1UCozD24TCvILxizEDWIEzA0FcM8woCgRrJCoS5PIwrANQSMAJX1LEI9bqpQo4JYNFFKRSvIgsxHDVnqZgIkPnNBM0rIGtYk9YOOsqgbgepRCfdbmFtqhFkVEDVPjJp0+Z6e6hRHhqBKgg6ZDCvYBygVmUoEGoh5JTRvIJwhJo1aUOoh4CLPMyvxxi7EWOMgnCGsXXI1GIXlZUYX7ucU+kbR8NW8lh3O7cue0Pk32MKndfUxQFAwxdirk3fHappAnc0oqDPzDfGTBrCfHP04dM4oTV8cxr0SVzH9FF07xD3ib6xCDE+M+aUcVygtWzzbtGX2rPBrEUYfecfQkaFzYi6HjVnGBdtL7epqAlc1+jRdAap74RrnPc4BCijttY2tRcdN0g17w7HqZrXhdJTYAuS3hd8z+vKgK3V1zWPae0mZDMykadBn1hTQBLnZNwVrJpSe/NwEeDsEwCctEOsJTsgxLvCqUl2ACftEGvJDgjxrnBqkh3ASTvEWrIDQrwrnJpkB3DSDrGW7IAQ7wqnJtkBnLRztejXXVu4+mxz/nQ9jR1w5VB86ejLTFcnnDwhzV+F6T+CHZlx6THSjn76eyyBIOPHyDakhBAQAkJACAgBISAEhIAQYCLwC8JxpAmsEGt6AAAAAElFTkSuQmCC') no-repeat;
  background-size: 22px;
  width: 22px;
  height: 22px;
  float: left;
  margin-left: -22px;
}

.pointer.desc:before {
  transform: rotate(180deg);
  -ms-transform: rotate(180deg);
}

And for the dragging:

.cdk-drop-list-dragging .cdk-drag {
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
  
.cdk-drag-preview {  
  box-sizing: border-box;
  border-radius: 4px;
  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
}

.cdk-drag-placeholder   {
  opacity: 0;
}

.table-cell-width {
    width: 80px;
}

The component:

import { Component, OnInit, Directive, EventEmitter, Input, Output, ViewChildren, QueryList } from '@angular/core';
import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';


@Component({
  selector: 'app-demo',
  templateUrl: './demo.component.html',
  styleUrls: ['./demo.component.scss']
})
export class DemoComponent implements OnInit {

  vars = ['a', 'b', 'c', 'd'];
  data = [];

  constructor() { }

  ngOnInit(): void {
  }

  add() {
    this.data.unshift(randomRow(this.vars));
  }

  reset() {
    this.data = [];
  }
}

function randomNum(minimum: number, maximum: number): number {
  var randomnumber = Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
  return randomnumber;
}

function randomRow(vars: string[]) {
  let res = {}

  vars.forEach(v => {
    res[v] = randomNum(0, 9)
  });

  return res;
}

For dragging, we just need to add this as a method:

onDrop(event: CdkDragDrop<string[]>) {
  moveItemInArray(this.data, event.previousIndex, event.currentIndex);
}

For sorting, we need to add the following data definitions:

export type SortDirection = 'asc' | 'desc' | '';
const rotate: { [key: string]: SortDirection } = { 'asc': 'desc', 'desc': '', '': 'asc' };
const compare = (v1: string, v2: string) => v1 < v2 ? -1 : v1 > v2 ? 1 : 0;

export interface SortEvent {
  column;
  direction: SortDirection;
}

Directive:

@Directive({
  selector: 'th[sortable]',
  host: {
    '[class.asc]': 'direction === "asc"',
    '[class.desc]': 'direction === "desc"',
    '(click)': 'rotate()'
  }
})
export class NgbdSortableHeader {

  @Input() sortable = '';
  @Input() direction: SortDirection = '';
  @Output() sort = new EventEmitter<SortEvent>();

  rotate() {
    this.direction = rotate[this.direction];
    this.sort.emit({ column: this.sortable, direction: this.direction });
  }
}

And class:

@ViewChildren(NgbdSortableHeader) headers: QueryList<NgbdSortableHeader>;

onSort({ column, direction }: SortEvent) {

  // resetting other headers
  this.headers.forEach(header => {
    if (header.sortable !== column) {
      header.direction = '';
    }
  });

  if (direction === '' || column === '') {
    this.data = this.data;
  } else {
    this.data = [...this.data].sort((a, b) => {
      const res = compare(`${a[column]}`, `${b[column]}`);
      return direction === 'asc' ? res : -res;
    });
  }
}