I'm trying to use Drag&Drop features relased with Angular Material 7.
I separated my template in reusable pieces using ngTemplateOutlet
and every option can be either a Thing™ or a nested Thing™ which have some more sub-Things™.
Nested Things™ are displayed as an expansion panel. I want all the first-level Things™ to be re-orderable as if thery were a list.
(Ok, ok, it's obvious that's a reorderable sidenav with normal and nested options, just pretend it's not so obvious)
This is the code I initially wrote.
<div cdkDropList (cdkDropListDropped)="dropItem($event)" lockAxis="y">
<ng-container *ngFor="let thing of things">
<ng-container
*ngTemplateOutlet="!thing.children ? singleThing : multipleThing; context: { $implicit: thing }"
></ng-container>
</ng-container>
</div>
<ng-template #singleThing let-thing>
<div cdkDrag>
<ng-container *ngTemplateOutlet="thingTemplate; context: { $implicit: thing }"></ng-container>
</div>
</ng-template>
<ng-template #multipleOption let-thing>
<mat-expansion-panel cdkDrag (cdkDropListDropped)="dropItem($event)">
<mat-expansion-panel-header>
<mat-panel-title>
<p>Nested thing title</p>
<span cdkDragHandle></span>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-container *ngFor="let childThing of thing.children">
<div class="childThing">
<ng-container *ngTemplateOutlet="thingTemplate; context: { $implicit: childThing }"></ng-container>
</div>
</ng-container>
</mat-expansion-panel>
</ng-template>
<ng-template #thingTemplate let-thing>
<p>I'm a thing!</p>
<span cdkDragHandle></span>
</ng-template>
Problem: single Things™ are draggable, but they are not enforced as a list like cdkDropList should do, I can just drag them around everywhere.
I had a similar problem some time ago when trying to use template outlets and putting ng-template
s back into the 'HTML flow' worked to solve that, so I tried the same.
<div cdkDropList (cdkDropListDropped)="dropItem($event)" lockAxis="y">
<ng-container *ngFor="let thing of things">
<ng-container
*ngIf="!thing.children; then singleThing; else multipleThing"
></ng-container>
<ng-template #singleThing>
<div cdkDrag>
<ng-container *ngTemplateOutlet="thingTemplate; context: { $implicit: thing }"></ng-container>
</div>
</ng-template>
<ng-template #multipleOption>
<mat-expansion-panel cdkDrag (cdkDropListDropped)="dropItem($event)">
<mat-expansion-panel-header>
<mat-panel-title>
<p>Nested thing title</p>
<span cdkDragHandle></span>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-container *ngFor="let childThing of thing.children">
<div class="childThing">
<ng-container *ngTemplateOutlet="thingTemplate; context: { $implicit: childThing }"></ng-container>
</div>
</ng-container>
</mat-expansion-panel>
</ng-template>
</ng-container>
</div>
<ng-template #thingTemplate let-thing>
<p>I'm a thing!</p>
<span cdkDragHandle></span>
</ng-template>
And, of course why not, it works! Yeah, fine, but why?
Not much changed, we used a ngIf
instead of the first ngTemplateOutlet
and removed context bindings for the Thing™ because now both templates have its local variable reference thanks to the shared scope.
So, why exactly does it work in the second way and not in the first one?
Bonus points: is it possible to make it work keeping the first code structure which, to me, seems obviously more readable and clean?
I had the same problem, I even reported this as an issue on GitHub.
It turns out to be caused by the separateness of
cdkDropList
fromcdkDrag
.cdkDrag
must be in a tag nested inside the one withcdkDropList
, otherwise the dragged element won't detect the drop zone.The solution in your case would be an additional
<div cdkDrag>
belowcdkDropList
, and only under that you would call the template withngTemplateOutlet
.