Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular CDK drag and drop issue inside CSS flexbox

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 !

like image 405
Paul Selle Avatar asked Jan 24 '19 14:01

Paul Selle


2 Answers

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>
like image 94
Jeff Gilliland Avatar answered Nov 12 '22 05:11

Jeff Gilliland


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();
  }
}
like image 5
Tai Truong Avatar answered Nov 12 '22 05:11

Tai Truong