Angular CDK drag and drop issue inside CSS flexbox
Asked Answered
I

2

18

I ran into an issue using drag and drop module from the Angular CDK. I use it inside a container div which has (among others) the following CSS properties :

display: flex;
flex-wrap: wrap;

The flex_wrap property is here so that if the contained draggable elements don't fit in the container, they wrap into a second line and so on.

As the dragging is horizontal (cdkDropListOrientation="horizontal"), this works fine when all elements fit in a single line, but as soon as they wrap to a second line, drag and drop becomes buggy. I made the following stackblitz to reproduce the error : https://stackblitz.com/edit/angular-fytgp6 .

If anyone know how to fix this issue or thinks about a workaround for this, it would be of great help !

Ichthyosaur answered 24/1, 2019 at 14:46 Comment(1)
Did you find a solution?Corvin
C
18

This is a known issue with CDK Drag and Drop: https://github.com/angular/material2/issues/13372

Essentially, you need to have a parent div that is defined as a "cdkDropListGroup", then you need to treat each draggable item as a "cdkDropList" in addition to having the "cdkDrag" property on it. This should make it so that each item is its own container, and the "cdkDropListGroup" directive connects them all together.

Then, you can have a *ngFor on the cdkDropList container to spawn one for each of your array items. Put a [cdkDropListData]="index" with the cdkDropList so you can transfer the currently dragging index to the cdkDrag. With the child cdkDrag element, you can get this index with [cdkDragData]="index". Then, have an event binding (cdkDragEntered)="entered($event)" on the cdkDrag child which will fire every time you try to drag the element to one of the new containers. Inside the entered function, use the moveItemInArray method from the CDK to transfer the items around.

entered(event: CdkDragEnter) {
  moveItemInArray(this.items, event.item.data, event.container.data);
}
<div style="display:flex;flex-wrap:wrap" cdkDropListGroup>
  <div cdkDropList [cdkDropListData]="i" *ngFor="let item of items; let i = index;"  [style.width]="item.width || '100%'">
    <div cdkDrag [cdkDragData]="i" (cdkDragEntered)="entered($event)">
      {{item}}
    </div>
  </div>
</div>

If this doesn't work for you, then you can try using mat-grid instead to control your layout.

<mat-grid-list cdkDropListGroup>
  <mat-grid-tile cdkDropList [cdkDropListData]="i" *ngFor="let item of items; let i = index;" [colspan]="item.cols" [rowspan]="item.rows">
    <div cdkDrag [cdkDragData]="i" (cdkDragEntered)="entered($event)"> 
      {{item}}
    </div> 
  </mat-grid-tile> 
</mat-grid-list>
Cot answered 30/1, 2019 at 7:51 Comment(5)
Hi Jeff. Do you have any tutorial/material that could solve this by using mat-grid?Furnishings
Hi legin, it should be as simple as getting rid of the display:flex; and divs and instead using the <mat-grid-list> and <mat-grid-tile> tags. I've added an example to the answer. This assumes that each of your items will have a "cols" and "rows" property to determine the number of columns and rows each item should span.Cot
I tried to implement this and the cdkDropList divs are switching places like intended, however, Im running into a problem where the placeholder remains inside the cdkDropList it was dragged to, even when it already switched places, and both the cdkDrag inside and the placeholder from the new cdkDrag that was dragged inside occupy the same cdkDropList until it's dropped.Funke
Hi Willy, sounds like you need to add some CSS to the various containers to control what it looks like for the drop placeholder and what the boxes look like while dragging. The Angular Material documentation has some example CSS for this at material.angular.io/cdk/drag-drop/overview . You can see it under " Customizing the drag preview / placeholder" .Cot
Thanks @JeffGilliland , this works fine for a single list. I want to drag boxes from one list to another which was accomplished using transferArrayItem how can I do the same when we use flex-wrap:wrapRedwine
S
6

I have implemented a simple solution using Angular's toolkit (cdkDropListGroup, moveItemInArray, and transferArrayItem): https://stackblitz.com/edit/angular-drag-n-drop-mixed-orientation-example

All I do is creating an items table matrix using as a view model for template and the component syncronizes between the items list (input model) with the table (view model). I have posted a detailed explanation here: https://taitruong.github.io/software-developer.org/post/2019/10/26/Angular-drag'n'drop-mixed-orientation-in-flex-row-wrap/

Template:

<div #tableElement cdkDropListGroup>
  <!-- Based on the width of template reference #tableElement' and item box width,
       columns per row can be calculated and a items table matrix is initialized-->
  <div
    fxLayout="row"
    *ngFor="let itemsRow of getItemsTable(tableElement)"
    cdkDropList
    cdkDropListOrientation="horizontal"
    [cdkDropListData]="itemsRow"
    (cdkDropListDropped)="reorderDroppedItem($event)"
  >
    <!-- Component.reorderDroppedItem():
         reorders table/view model, update input model, and resize table matrix-->
    <div *ngFor="let item of itemsRow" cdkDrag>
      <div class="drag-placeholder" *cdkDragPlaceholder></div>
      <div fxLayoutAlign="center center" class="item-box">{{ item }}</div>
    </div>
  </div>
</div>

CSS:

.item-box {
  width: 150px;
  height: 150px;
  border: solid 3px #ccc;
  background: #fff;
  font-size: 30pt;
  font-weight: bold;
  border-radius: 5px;
  margin: 0px 0px 5px 5px;
}

 .drag-placeholder {
   background: #ccc;
   border: dotted 3px #999;
   height: 150px;
   width: 50px;
   transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
 }

Component:

export class AppComponent {
  // one dimensional input model
  items: Array<number> = Array.from({ length: 21 }, (v, k) => k + 1);
  // two dimensional table matrix representing view model
  itemsTable: Array<number[]>;

  // fix column width as defined in CSS (150px + 5px margin)
  boxWidth = 155;
  // calculated based on dynamic row width
  columnSize: number;

  getItemsTable(tableElement: Element): number[][] {
    // calculate column size per row
    const { width } = tableElement.getBoundingClientRect();
    const columnSize = Math.round(width / this.boxWidth);
    // view has been resized? => update table with new column size
    if (columnSize != this.columnSize) {
      this.columnSize = columnSize;
      this.initTable();
    }
    return this.itemsTable;
  }

  initTable() {
    // create table rows based on input list
    // example: [1,2,3,4,5,6] => [ [1,2,3], [4,5,6] ]
    this.itemsTable = this.items
      .filter((_, outerIndex) => outerIndex % this.columnSize == 0) // create outter list of rows
      .map((
        _,
        rowIndex // fill each row from...
      ) =>
        this.items.slice(
          rowIndex * this.columnSize, // ... row start and
          rowIndex * this.columnSize + this.columnSize // ...row end
        )
      );
  }

  reorderDroppedItem(event: CdkDragDrop<number[]>) {
    // same row/container? => move item in same row
    if (event.previousContainer === event.container) {
      moveItemInArray(
        event.container.data,
        event.previousIndex,
        event.currentIndex
      );
    } else {
      // different rows? => transfer item from one to another list
      transferArrayItem(
        event.previousContainer.data,
        event.container.data,
        event.previousIndex,
        event.currentIndex
      );
    }

    // update items after drop: flatten matrix into list
    // example: [ [1,2,3], [4,5,6] ] => [1,2,3,4,5,6]
    this.items = this.itemsTable.reduce(
      (previous, current) => previous.concat(current),
      []
    );

    // re-initialize table - makes sure each row has same numbers of entries
    // example: [ [1,2], [3,4,5,6] ] => [ [1,2,3], [4,5,6] ]
    this.initTable();
  }
}
Subsidence answered 26/10, 2019 at 0:31 Comment(2)
Can you fix the link to the article?Headwind
Article seems to have been deleted. It worked correctly (in Angular 13) when I removed <div class="drag-placeholder" *cdkDragPlaceholder></div>Bertie

© 2022 - 2024 — McMap. All rights reserved.