Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CdkDrag updating position

I have multiple CdkDrag element that are rendered from a ngFor loop on an array in my component. when i remove one element i splice the array. Then some of the elements will update there position. how do i avoid that?

I tried to get the freeDragPosition of all the draggable element before deleting one of them, then to reset their position, but it did not work.

this is my app.component.html

<div class="container" id="container">
  <div *ngFor="let f of fields; let i = index;" 
    class="textField"
    [attr.data-guid]="f.guid"  
    (mouseenter)="onFieldHover($event)" 
    cdkDrag 
    cdkDragBoundary="#container"
    (cdkDragEnded)="onDragEnded($event)"
    [cdkDragFreeDragPosition]="f.position">
    <div class="textField-inner">
      <span style="color: hotpink; font-weight: bold">{{f.guid}}</span>
    </div>
    <div class="textField-options">
      <div class="textField-move" cdkDragHandle>
        <svg width="24px" fill="currentColor" viewBox="0 0 24 24">
          <path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"></path>
          <path d="M0 0h24v24H0z" fill="none"></path>
        </svg>
      </div>
      <div class="textField-remove">
        <i class="fas fa-trash-alt" (click)="onRemoveTextField(i)"></i>
      </div>
    </div>
  </div>
</div>
<button type="button" (click)="onTextFieldAdded()">Add a field</button>

and this is my app.component.ts

import { Component } from '@angular/core';
import {CdkDrag, CdkDragEnd} from '@angular/cdk/drag-drop';


@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.scss' ]
})
export class AppComponent  {
  name = 'Angular';
  fields: Field[] = [];
    draggables: CdkDrag<any>[] = [];


  onDragEnded(event: CdkDragEnd){
        if(!this.draggables.find(f => f == event.source)){
            this.draggables.push(event.source);
        }
    }

  onFieldHover(event: any){
        if(event.target.classList.contains('initialPosition')){
            event.target.classList.remove('initialPosition');
            event.target.style.transform = "translate3d(250px, 180px, 0px)";
        }
    }

  onTextFieldAdded() {
        let field = new Field();
        field.index = this.fields.length;
        field.guid = this.newGuid();

        this.fields.push(field);
    }

  onRemoveTextField(index: number){
    let positions : any[] =[];

        this.draggables.forEach(drag => {
            let pos = { guid: drag.element.nativeElement.getAttribute("data-guid"), pos: drag.getFreeDragPosition()  };
            positions.push(pos);
            console.log(pos);
        });

        this.fields.splice(index, 1);

    positions.forEach(p => {
      let newPos = {x: p.pos.x, y: p.pos.y};
      console.log(newPos);
      this.fields.find(f => f.guid == p.guid).position = newPos;
        });
    }

  newGuid(): string{
     return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
            return v.toString(16);
        });
  }

}

export class Field{
    index: number;
    guid: string;
    position: {x: number, y: number}
}


I would like for my element to remain in their current position when one of the element was deleted.

here's a .gif representing the problem element moving has i delete one

here's a stackblitz https://stackblitz.com/edit/angular-p3yfe7

like image 954
Francis Groleau Avatar asked Oct 17 '19 13:10

Francis Groleau


2 Answers

After some testing in your stackblitz I was able to reproduce your intended behavior, turns out when a cdkDrag is inside a ng For it will bind to the same list and when you remove one cdkDrag from the list it renders the template but with new positions to match the ngFor list order, to avoid that you can override the css style position from cdkDrag.

In your example just add position: absolute; to .textField css class.

like image 190
Tiago Silva Avatar answered Oct 29 '22 13:10

Tiago Silva


The issue is that you did not specify a trackBy function in your *ngFor, this will cause the objects to re-render whenever the array changes:

You should create your *ngFor like this:

*ngFor="let f of fields; let i = index; trackBy: trackByField"

And update your component to have a trackByField method

 trackByField = (i: number, field: Field) => field.guid;

Then the items that have already been moved, remain in their positions, and non moved items will still re-order

stack

like image 44
Poul Kruijt Avatar answered Oct 29 '22 11:10

Poul Kruijt