Drag and Drop image icons using Angular

Solution 1:

I know I'm late to the party, but...

To manage cdk-drag and drop in a "grid" it's very complex, because material angular cdkDropList is think about uni-dimension

The idea is always the same, instead using an unique cdkDropList, we can create so many cdkDropList as element we has -in out case, the grid. All enclosed in a div with cdkDropListGroup-

<div class="content" cdkDropListGroup>
  <div #board class="board">
    <mat-grid-list cols="5">
      <mat-grid-tile *ngFor="let cell of cells; let i = index">
        <div
          class="cell"
          cdkDropList
          [cdkDropListData]="cell"
          (cdkDropListDropped)="drop($event)"
          [style.background]="i == indexOver ? 'red' : 'yellowgreen'"
          (mouseover)="this.indexOver = i"
          (mouseout)="indexOver = -1"
        >
          <div cdkDrag>
            <img
              *ngIf="cell.src"
              [src]="cell.src"
              width="100%"
            />
            <span *ngIf="!cell.src"></span>
            <div *cdkDragPlaceholder></div>
          </div>
        </div>
      </mat-grid-tile>
    </mat-grid-list>
  </div>
  <div
    class="side"
    cdkDropList
    sortingDisabled="true"
    [cdkDropListData]="icons"
    (cdkDropListDropped)="drop($event)"
  >
    <div *ngFor="let icon of icons">
      <div cdkDrag (mousedown)="calculeMargin($event)">
        <img [src]="icon" />
        <img
          *cdkDragPreview
          [src]="icon"
          [ngStyle]="previewStyle ? previewStyle : null"
        />
        <div *cdkDragPlaceholder></div>
      </div>
    </div>
  </div>
</div>

Our mat-grid-tile will be the "cdkDrag", this show a img or a span acording the value of cell.src. See that we create a "empty" *cdkDragPlaceholder

In the other side, we has a typical list. We transform the cdkDragPreview to make that when we drag the element was in the same size of our "tiles" (it's the aim of the (mousedown)="calculeMargin($event")

I use the same function "drop" to control all drops. To check if we are dragging from the "board" or from the "side" I use the own "data". If has a property "src" is a item in the board, else is an item of the side. See that we not use "transfer" nor others "strange functions", else simply push or change the element of the array

  indexOver:number=-1;
  previewStyle: any = null;
  @ViewChild(MatGridTile, { static: false, read: ElementRef }) model;
  @ViewChild('board', { static: false,read:ElementRef })board:ElementRef;

  icons = [
    'https://picsum.photos/100/100?random=1',
    'https://picsum.photos/100/100?random=2',
    'https://picsum.photos/100/100?random=3',
  ];

  cells = Array(25).fill(' ').map((_,index)=>({src:null,id:index}));
  constructor() {}

  ngOnInit() {
    setTimeout(() => {
      const rect = this.model.nativeElement.getBoundingClientRect();
      this.previewStyle = {
        width: rect.width + 'px',
        height: rect.height + 'px',
      };
    });
  }
  calculeMargin(event: MouseEvent) {
    const rect = this.model.nativeElement.getBoundingClientRect();
    this.previewStyle = {
      width: rect.width + 'px',
        height: rect.height + 'px',
      'margin-top': -event.offsetY + 'px',
      'margin-left': -event.offsetX + 'px',
    };
  }
  drop(event: CdkDragDrop<any>) {
    if (event.previousContainer != event.container) {
      if (event.container.data.src!==undefined)
      {
        if (event.previousContainer.data.src!=undefined)
        {
          event.container.data.src=event.previousContainer.data.src
          event.previousContainer.data.src=null;
        }
        else
        {
          event.container.data.src=event.previousContainer.data[event.previousIndex]
        }
      }
      else
      {
        if (event.container.data.src===undefined && event.previousContainer.data.src!==undefined) 
        event.previousContainer.data.src=null;
      }
    }
  }

The stackblitz

Update really we can improve the code if we use two mat-grid-list. One for "board" and another one for "side"

<div class="content" cdkDropListGroup>
  <div #board class="board">
    <mat-grid-list cols="5">
        ...
    </mat-grid-list>
  </div>
  <div class="side">
    <mat-grid-list cols="1">
      <mat-grid-tile *ngFor="let cell of icons; let i = index">
        <div
          class="cell"
          cdkDropList
          [cdkDropListData]="cell"
          (cdkDropListDropped)="drop($event)"
        >
          <div cdkDrag>
            <img [src]="cell" width="100%" />
            <div *cdkDragPlaceholder></div>
          </div>
        </div>
      </mat-grid-tile>
    </mat-grid-list>
  </div>
</div>

See that the idea is similar, instead using an unique cdkDropList, for the side we can use the so many cdkDropList as icos we has. This allow us that, when "drag", it's not reorder the list of icons. futhermore, we can use a .css -see that it's thinking about a grid of 5x5- to make in the same size to the "icons" that the tiles of our board. This allow us not use the cdkPreview because all the "icons" has the same size.

.content
{
  display:flex;
  justify-content: space-between;
  position:relative;

}
.board{
  flex-basis:80%;
  position:relative;
}
.board::affter
{
  content:' '
}
.side{
  flex-basis:16%;
}

.cell{
  width:100%;
  height:100%;
}

The only change in the code is, when we make the drop use and remove the function "CalculeMargin"

    if (event.previousContainer.data.src!=undefined)
    {
        ....
    }
    else
    {
       //see that the data is the "src" of the icon
      event.container.data.src=event.previousContainer.data
    }

the new stackblitz