Angular Nested Drag and Drop / CDK Material cdkDropListGroup cdkDropList nested
Asked Answered
C

1

16

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

Counterinsurgency answered 30/4, 2021 at 17:13 Comment(0)
C
27

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

Counterinsurgency answered 30/4, 2021 at 17:13 Comment(5)
hey can you please share the source code for this example. it would be really helpful solve my issueRacon
can you share the code? please!Jadajadd
As it was now requested multiple times, I'll try to provide the code.Counterinsurgency
hey @MarcusKaseder can you share example source code or Stackblitz demo url?Brucie
@HarshPatel you can find the sample code here: github.com/MarcusKaseder/cdk-drag-and-drop-formCounterinsurgency

© 2022 - 2024 — McMap. All rights reserved.