Search code examples
htmlangularobjectdrag-and-dropfrontend

Trouble with Angular Drag and Drop


I'm trying to make it so a User has a page where they view their company cards. I want to be able to create containers or just some sorting objects that they can use to organize them. I created an ng-expansion panel that I want it to be based on. However, I'm having trouble making a proper Drag and Drop to work between them. I've been using the cdkDragDrop tool and I only managed to make the company cards themselves be drag and dropped on screen but not on the container. I can't seem to be able to make the proper connection, the Angular website cdk page only seems to want to drag and drop sentences, I can't find a way to rework it to drag and drop my cards instead.

My expansion-panel is written like this:

    <mat-expansion-panel cdkDropList (opened)="panelOpenState = true" (closed)="panelOpenState = false" (cdkDropListDropped)="drop($event)">
        <mat-expansion-panel-header>
            <mat-panel-title>
                Group Name
            </mat-panel-title>
            <mat-panel-description>
                Currently I am {{panelOpenState ? 'open' : 'closed'}}
            </mat-panel-description>
        </mat-expansion-panel-header>
    </mat-expansion-panel>

and my cards are formatted to where I called the drag and drop here:

<div cdkDropList class="d-flex align-content-start flex-wrap mt-4" (cdkDropListDropped)="drop($event)" *ngIf="!isLoading">
            <ng-container *ngFor="let perspective of watchlist">
                <div class="watchlist-card" cdkDrag>
                    <div class="watchlist-card-content">
                        <div class="d-flex flex-column watchlist-card-body">
                            <!-- CARD HEADER AND REMOVE ICON -->
                            <div class="d-flex flex-row justify-content-between">
                                <p class="card-title">{{ perspective.company.title }}</p>
                                <button class="button-icon" (click)="removeCompanyFromWatchlist(perspective)">
                                    <mat-icon class="btn-close">close</mat-icon>
                                </button>
                            </div>

                            <!-- 3P-SCORE -->
                            <ng-container *ngIf="scoreExists(perspective.company.scores) === true">
                                <div class="d-flex flex-column">
                                    <!-- 3P SCORE HEADER -->
                                    <div class="d-flex flex-row justify-content-between">
                                        <p class="text-header">3P Score</p>
                                        <!-- TOTAL 3P SCORE -->
                                        <p class="text-header"> {{ calculateTotal3PScore(perspective.company.scores)}}/100</p>
                                    </div>
                                    <!-- SCORE BREAKDOWN: People, Product & Performance -->
                                    <ng-container *ngFor="let criteria of scoreCriteria">
                                        <div class="d-flex flex-column">
                                            <!-- SCORE CRITERIA & SCORE OUT OF 100 -->
                                            <div class="d-flex flex-row justify-content-between">
                                                <div class="d-flex flex-row jutify-content-start align-self-center">
                                                    <img src={{criteria.icon}} class="score-icon">
                                                    <p class="score-label align-self-center">{{criteria.label | titlecase}}</p>
                                                </div>
                                                <p class="score-label">{{ perspective.company.scores[0][criteria.label] }}/100
                                                </p>
                                            </div>
                                            <!-- PROGRESS BAR FOR CRITERIA -->
                                            <div class="d-flex flex-row jutify-content-start slider-bottom">
                                                <div class="slider-top"
                                                    [ngStyle]="{width: perspective.company.scores[0][criteria.label]+ '%'}">
                                                </div>
                                            </div>
                                        </div>
                                    </ng-container>
                                </div>
                            </ng-container>

                            <!-- NO 3P SCORE EXISTS -->
                            <ng-container *ngIf="scoreExists(perspective.company.scores) === false">
                                <div class="d-flex flex-column">
                                    <p class="text-header">3P Score</p>
                                    <p class="text-body">We don't have the 3P score of this input as of now. You can request the
                                        score and we will get back to you if it’s a
                                        company.</p>
                                    <button class="btn port-btn small-label" *ngIf="!perspective.company.score_requested"
                                        (click)="request3PScore(perspective)">Request 3P Score</button>
                                    <p class="text-body port-orange" *ngIf="perspective.company.score_requested"><b>Your request
                                            has been registered. We'll get back you to shortly.</b></p>
                                </div>
                            </ng-container>
    
                            <!-- FOOTER: KEWORDS AND ADD BUTTON -->
                            <div class="d-flex flex-column mt-auto">
                                <!-- KEYWORD CHIPS -->
                                <div class="d-flex flex-nowrap watchlist-keywords mt-2 mb-1">
                                    <mat-chip *ngFor="let keyword of perspective.keywords" class="keyword-chip mr-2" [removable]="removable"
                                        (removed)="removeKeywordFromWatchlist(perspective.keywords, keyword, perspective.id)">
                                        {{ keyword }}
                                        <mat-icon matChipRemove *ngIf="removable">close</mat-icon>
                                    </mat-chip>
                                </div>
                                <!-- ADD KEYWORD BUTTON -->
                                <div class="d-flex flex-row flex-fill mr-auto mt-auto">
                                    <!-- INPUT FIELD FOR ADDING KEYWORDS -->
                                    <div class="d-flex flex-row flex-fill justify-content-between align-items-center" *ngIf="perspective.activeAddKeyword">
                                        <div class="d-flex flex-row justify-content-between align-items-center search-bar p-3">
                                            <input [(ngModel)]="keywordInput" type="text" placeholder="Add keyword here" 
                                                (keyup.enter)="addKeywordToWatchlist(perspective.keywords, perspective.id)">
                                            <mat-icon matSuffix type="submit" (click)="addKeywordToWatchlist(perspective.keywords, perspective.id)">add</mat-icon>
                                        </div>
                                        <button class="button-curved ml-2 p-1" *ngIf="perspective.activeAddKeyword"
                                            (click)="closeAddKeywordInput(perspective)">
                                            Cancel
                                        </button>
                                    </div>

                                    <button class="button-icon button-raised mt-auto" *ngIf="!perspective.activeAddKeyword" (click)="showAddKeywordInput(perspective)">
                                        <mat-icon>add</mat-icon>
                                    </button>
                                </div>
                                <div class="card-handle" 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>
                        </div>
                    </div>
                </div>
            </ng-container>
        </div>

The rest of the card setups follows, this is just where I just the cdkDrag

*Edit, fixed up the cdkDropList as suggested by one of the answers. *Edit2, Showed the whole card code.


Solution

  • The angular material Drag&Drop works with the concept of lists. What you can do is connect lists (Drag and drop, or within the same list, or to another list).

    So, let's assume, that the expansion panel is a list (It is a list because if you can drag things there, for Angular Material Drag & Drop it is a list)

    To connect your expansion panel with cards that are not in there. You are going to have and keep two arrays. The first contain all your cards, and the second contain the cards that are inside the expansion panel

    In your expansion panel:

    <mat-expansion-panel
             cdkDropList
             #expansionPanelList="cdkDropList"
             [cdkDropListData]="arrayCardsInsideExpansion"
             [cdkDropListConnectedTo]="[listOfAllCards]"
             (cdkDropListDropped)="drop($event)"
             (opened)="panelOpenState = true" 
             (closed)="panelOpenState = false">
        <mat-expansion-panel-header>
            <mat-panel-title>
                Group Name
            </mat-panel-title>
            <mat-panel-description>
                Currently I am {{panelOpenState ? 'open' : 'closed'}}
            </mat-panel-description>
        </mat-expansion-panel-header>
    
        <ng-container *ngFor="let perspective of arrayCardsInsideExpansion">
            <div class="watchlist-card" cdkDrag>
                       .
                       .
                       .
            </div>
        </ng-container>
    </mat-expansion-panel>
    

    And your cards:

    <div cdkDropList 
         #listOfAllCards="cdkDropList"
         [cdkDropListData]="watchlist"
         [cdkDropListConnectedTo]="[expansionPanelList]"
         class="d-flex align-content-start flex-wrap mt-4" 
         (cdkDropListDropped)="drop($event)">
                <ng-container *ngFor="let perspective of watchlist">
                    <div class="watchlist-card" cdkDrag>
                       .
                       .
                       .
    

    Finally in your .ts Code, your drop Function:

    import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag-drop';
      .
      .
      .
      .
     drop(event: CdkDragDrop<string[]>) {
        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);
        }
      }
    }
    

    If you have a lot of expansion panel, check CdkDropListGroup