Search code examples
angularangular-materialcarouselprimengangular-cdk-drag-drop

Drag and Drop Functionality in Angular Material Not Working Inside PrimeNG Carousel


I'm currently working on an Angular project where I need to use PrimeNG's Carousel component along with Angular Material's Drag and Drop functionality. Individually, both components work as expected. However, when I try to combine them, the drag and drop functionality stops working within the Carousel. There are no errors in the console when I attempt to drop an item.

The error occurs, when you drop Task 1 from "To Do" into the "Done" Container. Task 1 is still in "To Do" visible.

What am I doing wrong?

Demo@StackBlitz

task-status.component.html

<div class="task-status-container">
  <div class="title-container">
    <div class="title">{{ taskStatus.value }}</div>
  </div>

  <div
    cdkDropList
    [id]="taskStatus.key"
    [cdkDropListData]="assignTasksByStatus(taskStatus)"
    (cdkDropListDropped)="drop($event)"
    class="task-status-drop-list"
  >
    @if(getTaskCount(taskStatus) === 0) {
    <div class="no-task-container">
      <div>No Tasks</div>
    </div>
    }

    <p-carousel
      [value]="assignTasksByStatus(taskStatus)"
      [numVisible]="1"
      [numScroll]="1"
      [circular]="true"
    >
      <ng-template let-task pTemplate="item">
        <div class="border-1 surface-border border-round m-2 p-3">
          <div class="mb-3">
            <div class="relative mx-auto">
              <app-task cdkDrag [cdkDragData]="task" [task]="task"></app-task>
            </div>
          </div>
        </div>
      </ng-template>
    </p-carousel>
  </div>
</div>

Solution

  • Below are the list of changes:

    1. We have to provide the input [cdkDropListConnectedTo] to CDK drag drop, so that it knows what other drag drop modules it's connected to. Here we specify this by inputing an array of IDs prepared in the root component and passed as @Input

    2. Instead of a function, we can use a template reference of the dnd list #list="cdkDropList", then use the same to access the data property, which is the list, instead of using a separate function. Since the manipulation happen at the DND level, we should use the same reference in other locations.

    3. After updating the list using moveItemInArray or transferArrayItem we need to create a new reference, else the change is not detected by the carousel, for this we can use array-destructuring to refresh the carousel

    Full code:

    Task Status HTML:

    <div class="task-status-container">
      <div class="title-container">
        <div class="title">{{ taskStatus.value }}</div>
      </div>
    
      <div
        #list="cdkDropList"
        cdkDropList
        [id]="taskStatus.key"
        [cdkDropListData]="assignTasksByStatus(taskStatus)"
        (cdkDropListDropped)="drop($event)"
        [cdkDropListConnectedTo]="taskStatusKeys"
        class="task-status-drop-list"
      >
        <!-- 1-->
        @if(list.data.length=== 0) {
        <!-- 2 -->
        <div class="no-task-container">
          <div>No Tasks</div>
        </div>
        }
        <!-- 2 -->
        <p-carousel
          [value]="list.data"
          [numVisible]="1"
          [numScroll]="1"
          [circular]="true"
        >
          <ng-template let-task pTemplate="item">
            <div class="border-1 surface-border border-round m-2 p-3">
              <div class="mb-3">
                <div class="relative mx-auto">
                  <app-task cdkDrag [cdkDragData]="task" [task]="task"></app-task>
                </div>
              </div>
            </div>
          </ng-template>
        </p-carousel>
      </div>
    </div>
    

    Task Status TS:

    import {
      CdkDragDrop,
      moveItemInArray,
      transferArrayItem,
      CdkDropList,
      CdkDrag,
    } from '@angular/cdk/drag-drop';
    import { Component, Input, ViewChild } from '@angular/core';
    import { Subscription } from 'rxjs';
    import { TaskComponent } from './task/task.component';
    import { TASK_STATUSES } from '../../../task-constants';
    import { TaskStatus } from '../../../status';
    import { Task } from '../../../task';
    import { CarouselModule } from 'primeng/carousel';
    
    @Component({
      selector: 'app-task-status',
      standalone: true,
      imports: [TaskComponent, CdkDropList, CdkDrag, CarouselModule],
      templateUrl: './task-status.component.html',
      styleUrl: './task-status.component.scss',
    })
    export class TaskStatusComponent {
      @ViewChild(CdkDropList) list!: CdkDropList;
      protected readonly TASK_STATUSES = TASK_STATUSES;
      @Input() taskStatus!: TaskStatus;
      @Input() taskStatusKeys!: Array<string>; //<-1
      tasksSubscription!: Subscription;
      tasks: Task[] = [
        { id: 1, title: 'Task 1', status: TASK_STATUSES['TO_DO'] },
        { id: 2, title: 'Task 2', status: TASK_STATUSES['DONE'] },
        { id: 3, title: 'Task 3', status: TASK_STATUSES['DONE'] },
      ];
      tasksByStatus!: { [key: string]: Task[] };
      ngOnInit() {
        this.tasksByStatus = Object.keys(TASK_STATUSES).reduce((acc, cur) => {
          acc[cur] = this.tasks.filter((x) => x.status.key === cur);
    
          return acc;
        }, {} as { [key: string]: Task[] });
      }
    
      drop(event: CdkDragDrop<Task[]>) {
        if (event.previousContainer === event.container) {
          moveItemInArray(
            event.container.data,
            event.previousIndex,
            event.currentIndex
          );
        } else {
          transferArrayItem(
            event.previousContainer.data,
            event.container.data,
            event.previousIndex,
            event.currentIndex
          );
        }
        event.previousContainer.data = [...event.previousContainer.data]; // <= 3
        event.container.data = [...event.container.data]; // <= 3
      }
    
      public assignTasksByStatus(status: TaskStatus): Task[] {
        return this.tasksByStatus[status.key];
      }
    
      public getTaskCount(status: TaskStatus): number {
        return this.tasksByStatus[status.key]?.length || 0;
      }
    }
    

    Task list HMTL

    <div cdkDropListGroup class="task-status-list-container">
      @for (taskStatus of Object.values(TASK_STATUSES); track taskStatus.key) {
      <app-task-status
        [taskStatus]="taskStatus"
        [taskStatusKeys]="taskStatusKeys"
      ></app-task-status>
      <!-- 1-->
      }
    </div>
    

    Task Status TS:

    import { Component } from '@angular/core';
    import { TaskStatusComponent } from './task-status/task-status.component';
    import { TASK_STATUSES } from '../../task-constants';
    import { CdkDropListGroup } from '@angular/cdk/drag-drop';
    
    @Component({
      selector: 'app-task-status-list',
      standalone: true,
      imports: [TaskStatusComponent, CdkDropListGroup],
      templateUrl: './task-status-list.component.html',
      styleUrl: './task-status-list.component.scss',
    })
    export class TaskStatusListComponent {
      protected readonly TASK_STATUSES = TASK_STATUSES;
      public readonly taskStatusKeys = Object.values(TASK_STATUSES).map(
        //<-1
        (x: any) => x.key //<-1
      ); //<-1
      protected readonly Object = Object;
    }
    

    Stackblitz Demo