Search code examples
angularangular-materialdrag-and-dropangular-cdk

Angular Nested Drag and Drop / CDK Material cdkDropListGroup cdkDropList nested


I use CDK Material Drag and Drop utilities to create a form editor with drag and drop enabled.

It works fine, but nesting a cdkDropList within a cdkDropListGroup does not work. I'm not able to drag anything into the nested drop list container.

<div class="container">
  <div class="row" cdkDropListGroup>
    <div class="col-2">
      <div id="toolbox" cdkDropList>
        ...
      </div>
    </div>
    <div class="col-10">
      <div id="formContainer" cdkDropList>
        ...
        <div class="row">
          <div class="col-md-6" cdkDropList>
            ... column 1 content
          </div>
          <div class="col-md-6" cdkDropList>
            ... column 1 content
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Drag and drop not working


Solution

  • It took me some time but I finally found a solution thanks to the hints from that posts:

    The problem is that the cdkDropListGroup does not support nested drop lists. You need to connect the drop lists with the [cdkDropListConnectedTo] binding.

    But if you only connect the lists to an array for the [cdkDropListConnectedTo] binding the list order has a affect to the drop behavior. In addition, sorting within a nested drop list won't work.

    To avoid those problems, you need to create a service that looks for the correct cdkDropList while dragging.

    export class DragDropService {
      dropLists: CdkDropList[] = [];
      currentHoverDropListId?: string;
    
      constructor(@Inject(DOCUMENT) private document: Document) {}
    
      public register(dropList: CdkDropList) {
        this.dropLists.push(dropList);
      }
    
      dragMoved(event: CdkDragMove<IFormControl>) {
        let elementFromPoint = this.document.elementFromPoint(
          event.pointerPosition.x,
          event.pointerPosition.y
        );
    
        if (!elementFromPoint) {
          this.currentHoverDropListId = undefined;
          return;
        }
    
        let dropList = elementFromPoint.classList.contains('cdk-drop-list')
          ? elementFromPoint
          : elementFromPoint.closest('.cdk-drop-list');
    
        if (!dropList) {
          this.currentHoverDropListId = undefined;
          return;
        }
    
        this.currentHoverDropListId = dropList.id;
      }
    
      dragReleased(event: CdkDragRelease) {
        this.currentHoverDropListId = undefined;
      }
    }
    
    • register adds a new drop list to the dropList array that is used by each cdkDropList.

    • dragMoved determines the correct cdkDropList beneath the mouse pointer.

    The best thing is to create a own component that holds a cdkDropList.

    The following component is just for simplicity and demonstration purposes. You should not use service properties directly.

    <div
      *ngIf="container"
      cdkDropList
      [cdkDropListData]="container.controls"
      [cdkDropListConnectedTo]="dragDropService.dropLists"
      [cdkDropListEnterPredicate]="allowDropPredicate"
      (cdkDropListDropped)="dropped($event)"
    >
      <div
        *ngFor="let item of container.controls"
        cdkDrag
        [cdkDragData]="item"
        (cdkDragMoved)="dragMoved($event)"
        (cdkDragReleased)="dragReleased($event)"
      >
        Drag Content
      </div>
    </div>
    
    export class FormContainerComponent implements OnInit, AfterViewInit {
      @ViewChild(CdkDropList) dropList?: CdkDropList;
      @Input() container: IFormContainer | undefined;
    
      allowDropPredicate = (drag: CdkDrag, drop: CdkDropList) => {
        return this.isDropAllowed(drag, drop);
      };
    
      constructor(
        public dragDropService: DragDropService
      ) {}
      ngOnInit(): void {}
    
      ngAfterViewInit(): void {
        if (this.dropList) {
          this.dragDropService.register(this.dropList);
        }
      }
      dropped(event: CdkDragDrop<IFormControl[]>) {
        // Your drop logic
      }
    
      isDropAllowed(drag: CdkDrag, drop: CdkDropList) {
        if (this.dragDropService.currentHoverDropListId == null) {
          return true;
        }
    
        return drop.id === this.dragDropService.currentHoverDropListId;
      }
    
      dragMoved(event: CdkDragMove<IFormControl>) {
        this.dragDropService.dragMoved(event);
      }
    
      dragReleased(event: CdkDragRelease) {
        this.dragDropService.dragReleased(event);
      }
    }
    
    
    • Whenever a cdkDrag is moved, dragMoved determines the correct cdkDropList
    • Whenever a cdkDrag is released, reset the determined cdkDropList
    • The most important method is the isDropAllowed method that is set as [cdkDropListEnterPredicate]="allowDropPredicate" to the cdkDropList
      • As mentioned before, cdk material is not able to determine the correct drop list.
      • If there is a wrong drop list selected by cdk, we just disallow the drop by returning false. In that case, cdk automatically selects the next possible cdkDropList which is the correct one :)

    Drag and Drop nested working

    Code

    You can find the sample code here: https://github.com/MarcusKaseder/cdk-drag-and-drop-form