4
votes

With reference to Angular CDK Drag & Drop, I'm trying to create a simple dashboard with left sidebar and main content area. Both of these areas will contain unique custom components which need to be draggable and can be reordered within their contained area and transferred to another area.

For eg. Sidebar contains Comp and Comp1, then I can reorder them within that area and transfer them to the Main Content area.

From what I understand Angular Material CDK Drag & Drop only works with lists. Additionally, the items in the lists have to be of a similar type for them to be reordered/transferred.

Is there a way to utilize CDKDrag and CDKDropList for static items rather than items in an array? I'm not able to reorder or transfer custom components to different drop lists.

I've created a sample project: https://stackblitz.com/edit/ng-mat-dnd Demo: https://ng-mat-dnd.stackblitz.io/

app.component.html

<div class="example-container">
    <h2>Sidebar</h2>

    <div cdkDropList #sidebarList="cdkDropList" [cdkDropListData]="sidebar" cdkDropListConnectedTo="[mainList]"
        class="example-list" (cdkDropListDropped)="drop($event)">
        <div class="example-box" cdkDrag>
            <app-demo-comp-2 [btn]=2></app-demo-comp-2>
        </div>
        <div class="example-box" cdkDrag>
            <app-demo-comp [ddn]=2></app-demo-comp>
        </div>
        <div class="example-box" cdkDrag>
            <app-demo-comp-3 [txt]=3></app-demo-comp-3>
        </div>
    </div>
</div>

<div class="example-container">
    <h2>Main</h2>

    <div cdkDropList #mainList="cdkDropList" [cdkDropListData]="main" cdkDropListConnectedTo="[sidebarList]"
        class="example-list" (cdkDropListDropped)="drop($event)">
        <div class="example-box" cdkDrag>
            <app-demo-comp [ddn]=1></app-demo-comp>
        </div>
        <div class="example-box" cdkDrag>
            <app-demo-comp-2 [btn]=3></app-demo-comp-2>
        </div>
    </div>
</div>

app.component.ts

import { Component, OnInit, ViewChildren, QueryList } from '@angular/core';
import { CdkDragDrop, moveItemInArray, transferArrayItem, CdkDrag } from '@angular/cdk/drag-drop';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {  
  sidebar;
  main;

  @ViewChildren(CdkDrag) draggables: QueryList<CdkDrag>;

  constructor() { }

  ngOnInit() { }

  ngAfterViewInit() {
    console.log(this);
    this.sidebar = [this.draggables.toArray()[0], this.draggables.toArray()[1], this.draggables.toArray()[2]];
    console.log(this.sidebar);
    
    this.main = [this.draggables.toArray()[4], this.draggables.toArray()[3]];
    console.log(this.main);
  }

  drop(event: CdkDragDrop<any[]>) {
    console.log(event);
    if (event.previousContainer === event.container) {
      console.log('Same container');
      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
    } else {
      console.log('Different containers');
      transferArrayItem(event.previousContainer.data,
        event.container.data,
        event.previousIndex,
        event.currentIndex);
    }
  }
}
1

1 Answers

4
votes

Angular Material cdk drag and drop functionality works the way when it restores the element's visibility and inserts it at its old position in the DOM.

It's important that they maintain the position, because they relay on ngFor directive and moving the element around in the DOM can throw off NgFor which does smart diffing and re-creates elements only when necessary.

That means if you want to make it working without ngFor then you have to move html elements around by yourself.

Here's an example of how it can be done:

Stackblitz with manual dom manipulations

drop(event: CdkDragDrop<any[]>) {
  const nodeToMove = event.item.element.nativeElement;
  const { previousContainer, container, previousIndex, currentIndex } = event;

  console.log(this.sidebar, this.main);

  if (previousContainer === container) {
    moveItemInArray(container.data, previousIndex, currentIndex);

    moveWithinContainer(
      container.element.nativeElement,
      previousIndex,
      currentIndex
    );
  } else {
    transferArrayItem(
      previousContainer.data,
      container.data,
      previousIndex,
      currentIndex
    );
    transferNodeToContainer(
      nodeToMove,
      container.element.nativeElement,
      currentIndex
    );

    Promise.resolve().then(() => {
      previousContainer.removeItem(event.item);
      event.item.dropContainer = container;
      event.item._dragRef._withDropContainer(container._dropListRef);
      container.addItem(event.item);
    });
  }
}

...

function moveWithinContainer(container, fromIndex, toIndex) {
  if (fromIndex === toIndex) {
    return;
  }

  const nodeToMove = container.children[fromIndex];
  const targetNode = container.children[toIndex];

  if (fromIndex < toIndex) {
    targetNode.parentNode.insertBefore(nodeToMove, targetNode.nextSibling);
  } else {
    targetNode.parentNode.insertBefore(nodeToMove, targetNode);
  }
}

function transferNodeToContainer(node, container, toIndex) {
  if (toIndex === container.children.length) {
    container.appendChild(node);
  } else {
    const targetItem = container.children[toIndex];
    targetItem.parentNode.insertBefore(node, targetItem);
  }
}

NgFor

As you may have noticed the solution above is a bit hacky and can be broken depending on library version.

What you can do is to use ngFor directive instead:

ts

sidebar = [0, 1, 2];
main = [0, 1];

html

 <div *ngFor="let item of sidebar" class="example-box" cdkDrag>
    <app-demo-comp-2 *ngIf="item === 0" [btn]=2></app-demo-comp-2>

    <app-demo-comp *ngIf="item === 1" [ddn]=2></app-demo-comp>

    <app-demo-comp-3 *ngIf="item === 2" [txt]=3></app-demo-comp-3>
 </div> 

 ...
 <div *ngFor="let item of main" class="example-box" cdkDrag>
    <app-demo-comp *ngIf="item === 0" [ddn]=1></app-demo-comp>

    <app-demo-comp-2 *ngIf="item === 1" [btn]=3></app-demo-comp-2>
 </div>

Stackblitz with NgFor implementation