Search code examples
angularangular-materialcss-animations

Angular Material Drag and Drop: Synchronize Animations Across Separate


I'm working with Angular Material and facing a challenge with the drag-and-drop feature using cdkDropList and cdkDrag. My goal is to synchronize drag-and-drop animations across two separate containers. Specifically, when an item in the first list is dragged, I want a corresponding item in the second list to mirror the drag animation.

Here's the structure of my current implementation:

<div
  cdkDropList
  cdkDropListOrientation="horizontal"
  class="parent"
  (cdkDropListDropped)="drop($event)"
>
  @for(data of data1; track data){
  <span
    class="child"
    cdkDragLockAxis="x"
    cdkDragPreviewContainer="parent"
    cdkDrag
  >
    {{ data }}
  </span>
  }
</div>

<div class="parent">
  @for(data of data2; track data){
    <span class="child"> {{ data }} </span>
  }
</div>
export class DummyComponent {
  protected readonly data1 = [1, 2, 3, 4];
  protected readonly data2 = ['A', 'B', 'C', 'D'];

    
    drop($event: CdkDragDrop<unknown[]>): void {
        moveItemInArray(this.data1, $event.previousIndex, $event.currentIndex);
        moveItemInArray(this.data2, $event.previousIndex, $event.currentIndex);
    }
}

I also have a working example on StackBlitz that you can refer to: stackblitz.

Any ideas how to approach this (elegant)?


Solution

  • Yes, cdk-drag works adding style transform3d. So we can get two ViewChildrens, the "original" and the "copy". We can store also the "index" of the element dragging and an array of "transformStyles"

      dragIndex=-1;
      transformStyle:any[]=[]
      @ViewChildren('origin') origin!:QueryList<ElementRef>
      @ViewChildren('copy') copy!:QueryList<ElementRef>
    

    The idea is that our "copys" use this "transfromStyles", so our .html becomes like

    <div cdkDropList ...>
      @for(data of data1; track data;let i=$index){
       <!--see the template reference variable "origin" -->
      <span #origin class="child"
        cdkDragLockAxis="x"
        cdkDragPreviewContainer="parent"
        cdkDrag
        (cdkDragReleased)="release()" //<--we use cdkDragRelease
                                      //when mouse up
        (cdkDragStarted)="dragIndex=i"  //<--simply equal dragIndex  
        (cdkDragMoved)="move($event.distance,i)" //<--here "recalculate" the transformStyle
                                                 //pass $event.distance
        
      >
        {{ data }}
      </span>
      }
    </div>
    
    <div class="parent fool" >
      @for(data of data2; track data;let i=$index){
      //we add the class.copy when is dragging
      //and the class cdk-shadow to the element is dragging
      <span #copy class="child" [class.copy]="dragIndex!=-1" 
                                [class.cdk-shadow]="i==dragIndex"  
                                //here the "style.transform"
                                [style.transform]="transformStyle[i]"> 
            {{ data }} 
      </span>
      }
    </div>
    

    Well

      move(distance:any,index:number)
      {
        //see that we "equal" the styles to the "copys"
        //else the element is dragging, that use "distance"
        this.transformStyle=this.origin.toArray()
                   .map((x,i)=>i!=index? x.nativeElement.style.transform|| null:
                                         'translate3d('+distance.x+'px,0px,0px)')
        
      }
      release()
      {
          //we get the position of the "place-holder"
          const placeHolder=(document.getElementsByClassName('cdk-drag-placeholder')[0] as any).style
    
          //and 
          
          this.transformStyle[this.dragIndex]=placeHolder.transform
      }
    

    Just only in dropped equal the dragIndex to -1 and remove the transform

      drop($event: CdkDragDrop<unknown[]>): void {
        moveItemInArray(this.data1, $event.previousIndex, $event.currentIndex);
        moveItemInArray(this.data2, $event.previousIndex, $event.currentIndex);
    
        this.transformStyle = this.transformStyle.map((_) => null);
        this.dragIndex = -1;
      }
    

    Well, this NOT work propertly. The reason is than when we make the release, first the element is dragged position to the place and the executted the dropped. We need enclosed the release function in a ngZone. So we inject in constructor

      constructor(private ngZone: NgZone) {}
    

    And our function release becomes like

      release() {
        this.ngZone.run(() => {
          const placeHolder = (
            document.getElementsByClassName('cdk-drag-placeholder')[0] as any
          ).style;
    
          this.transformStyle[this.dragIndex] = placeHolder.transform;
        });
      }
    

    The stackblitz