Marcus Kaseder
Marcus Kaseder

Reputation: 1178

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

Upvotes: 19

Views: 15036

Answers (1)

Marcus Kaseder
Marcus Kaseder

Reputation: 1178

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

Upvotes: 29

Related Questions