Search code examples
javascriptangularangular-cdk-drag-drop

Angular cdkDragDrop behavior changed with update of Angular, UI no longer updates when dragging starts


We have a component which has items that can be dragged and dropped, we use the CDK drag/drop module for managing this.

Part of the functionality is to change a property of the underlying object when the user starts dragging. To get the UI to update we call ChangeDetectorRef.detectChanges().

When on version 9 of Angular this worked fine, however since updating to version 12.1 the UI no longer updates until the dragged item is dropped and I cannot find a way to get it working

Here is a simplified version of the component:

HTML:

<div cdkDropList>
<div cdkDrag 
 (cdkDragStarted)="onCdkDragStarted($event)"
 *ngFor="let step of steps">Step: {{step.id}}  {{step.text}}</div>
</div>

Code:

import { ChangeDetectorRef, Component} from '@angular/core';

@Component({
  selector: 'drag-poc',
  templateUrl: 'drag-poc.component.html',
  styles: []
})
export class DragPocComponent { 
  steps: any[] = [
    { id: 1, text: '' },
    { id: 2, text: '' },
    { id: 3, text: '' },
    { id: 4, text: '' },
    { id: 5, text: '' },
  ];

  constructor(private ref: ChangeDetectorRef){

  }

  onCdkDragStarted($event){
    // this is just a hack to get a reference to the array object that is being dragged
    const draggedItem = $event.source.__ngContext__[8].ngForOf[$event.source.__ngContext__[8].index];

    draggedItem.text = "MOVED";
    this.ref.detectChanges();
  }
}

With Angular 9 the UI updates once the dragging starts:

Dragging with updated UI

With Angular 12 (or 13), the code is the exact same, but the UI doesn't update until the dragged item is dropped:

Dragging with non-updated UI

Is there a way to get the UI to update once the dragging starts?

Stackblitz Angular 9: https://stackblitz.com/edit/angular-ivy-s4pmmm

StackBlitz Latest Angular: https://stackblitz.com/edit/angular-ivy-ztdufb

EDIT: the Latest Angular StackBlitz is updated to show the problem after the answer from @Eliseo

updated problem


Solution

  • I'm afraid that you need create a *cdkDragPreview. And a cdkDrag it's a bit ?@X##??!! because you need take acount several thing: the size of the drag, the point when you drag the element...

    To take account all of this I usually create two variables

      style: any = null;
      offset:any=null
    

    And the cdkDrag use (cdkDragMoved) and (mousedown) -you also need use (cdkDragStarted) and possible (cdkDragEnded) so your .html becomes like

    <div cdkDropList class="example-list" (cdkDropListDropped)="drop($event)">
        <div class="example-box" *ngFor="let movie of movies;let i=index" cdkDrag 
        [cdkDragData]="movie"
        (cdkDragStarted)="onDragStarted($event,i)"
        (cdkDragMoved)="onDragMove($event)"
        (cdkDragEnded)="onDragEnded($event,i)"
        
         (mousedown)="setStyle($event)" >{{movie.title}}{{movie.text}}
          <div  *cdkDragPreview class="example-box" [ngStyle]="style">
            {{movie.title}}{{movie.text}}
          </div>
        </div>
      </div>
    

    And the .ts

      drop(event: CdkDragDrop<any[]>) {
        moveItemInArray(this.movies, event.previousIndex, event.currentIndex);
      }
      onDragStarted(event:CdkDragStart<any>,index:number)
      {
          //you can use
          this.movies[index].text="moved"
          //or use
          event.source.data.text="moving...."
    
      }
      onDragEnded(event:CdkDragEnd<any>,index:number)
      {
        this.movies[index].text=""
      }
      setStyle(event: MouseEvent) {
        const rect = (event.target as HTMLElement).getBoundingClientRect();
        this.style = { width: rect.width + 'px', height: rect.height + 'px'}
        this.offset={x:event.offsetX,y:event.offsetY };
      }
      public onDragMove(event: CdkDragMove<any>): void {
        const el=(document.getElementsByClassName('cdk-drag-preview')[0])as any
        const xPos = event.pointerPosition.x - this.offset.x;
        const yPos = event.pointerPosition.y - this.offset.y;
        el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
      }
    

    See in the stackblitz how I pass the "index" to the functions (onDragStarted) and (onDragEnded) to change the array of "object" (you needn't use some complex like your $event.source.__ngContext__[8].ngForOf[$eve....

    NOTE: You can also check the property event.source or event.source.element to get the HTML element, you can also add a property cdkDragData to your Drag