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 !
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>
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();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With