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>
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);
}
}
cdkDrag
is moved, dragMoved
determines the correct cdkDropList
cdkDrag
is released, reset the determined cdkDropList
isDropAllowed
method that is set as [cdkDropListEnterPredicate]="allowDropPredicate"
to the cdkDropList
cdkDropList
which is the correct one :)You can find the sample code here: https://github.com/MarcusKaseder/cdk-drag-and-drop-form