CdkDragDrop and ngTemplateOutlet

3.7k views Asked by At

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-templates 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?

1

There are 1 answers

1
tom On BEST ANSWER

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 from cdkDrag. cdkDrag must be in a tag nested inside the one with cdkDropList, otherwise the dragged element won't detect the drop zone.

The solution in your case would be an additional <div cdkDrag> below cdkDropList, and only under that you would call the template with ngTemplateOutlet.