angulartypescriptangular-materialdrag-and-dropangular-cdk

Drag/drop an expanded row into another set of expanded rows - Angular Material


I want to have the ability to drag and drop one of the rows that expand in the table to another set of rows that expand in the table. I have added drag and drop properties in the HTML for the expanded rows (expandedDetail) by using cdkDrag, but that isn't working. I can drag an expanded row, but it just kind of sticks once I drop it instead of moving a row down or up like a normal drag and drop, if that makes sense. I am asking for assistance from anyone who is able to help me accomplish this. I am not sure what I'm missing from my code to be able to accomplish this.

HTML for the expanded rows that I want to drag and drop into other sets of expanded rows.

HTML

 <!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->

<ng-container matColumnDef="expandedDetail">
  <td
  mat-cell
  *matCellDef="let element"
  [attr.colspan]="columnsToDisplayWithExpand.length"
  >
   <div
    class="example-element-detail"
    [@detailExpand]="isElementExpanded(element) ? 'expanded' : 'collapsed'"
   >
    <div></div>
    <!-- <div class="example-element-diagram">
      <div class="example-element-position">{{element.position}}</div>
      <div class="example-element-symbol">{{element.symbol}}</div>
      <div class="example-element-name">{{element.name}}</div>
      <div class="example-element-weight">{{element.weight}}</div>
    </div>
    <div class="example-element-description">
      {{element.description}}
      <span class="example-element-description-attribution">
        -- Wikipedia
      </span>
    </div> -->
    <div class="example-element-name expanded-column-spacing">
      {{element.Name}}
    </div>
    <div class="example-element-weight expanded-column-spacing">
      {{element.weight}}
    </div>
    <div class="example-element-weight expanded-column-spacing">
      {{element.weight}}
    </div>
  </div>
  </td>
 </ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
 <tr
  mat-row
  *matRowDef="let element; columns: displayedColumns;"
  class="example-element-row"
  [class.example-expanded-row]="isElementExpanded(element)"
 ></tr>
 <tr
  mat-row
 *matRowDef="let row; columns: ['expandedDetail']"
  (click)="toggle(row)"
  cdkDrag
  cdkDragLockAxis="y"
  [cdkDragData]="row" ></tr>
  <tr
  mat-row
  *matRowDef="let row; columns: ['expandedDetail']"
  class="example-detail-row"
  ></tr>
  <tr
   mat-row
   *matRowDef="let row; columns: ['expandedDetail']"
   class="example-detail-row"
  ></tr>
  <tr
   mat-row
   *matRowDef="let row; columns: ['expandedDetail']"
   class="example-detail-row"
    ></tr>
    </tr>
    <tr mat-row *matRowDef="let row; columns: getDisplayedColumns()"></tr>  
   </table>

The code above is part of the StackBlitz link below, along with other files like the TypeScript and CSS files. I appreciate any assistance or guidance anyone has to get this to work. If there are other alternatives to use besides drag and drop, that would be helpful also.

Stackblitz Demo


Solution

  • In your StackBlitz, I see you hardcode the children by declaring multiple expandable details row.

    <tr
       mat-row
       *matRowDef="let row; columns: ['expandedDetail']"
       class="example-detail-row">
    </tr>
    

    Instead, you should remove those, and declare a children property in the PeriodicElement interface.

    export interface PeriodicElement {
      Name: string;
      position: number;
      weight: number;
      symbol: string;
      description: string;
      children?: PeriodicElement[];
    }
    

    To preset the children with default item based on parent, you can achieve like this:

    dataSource = new MatTableDataSource<PeriodicElement>(
      ELEMENT_DATA.map((x) => ({ ...x, children: [x] }))
    );
    

    In your expandedDetail, you should have:

    1. *ngFor directive to render each child row.
    2. A cdkDropList directive together with the cdkDropListData and cdkDropListDropped event to allow dropping the element in the container.
    <ng-container matColumnDef="expandedDetail">
      <td
        mat-cell
        *matCellDef="let element"
        [attr.colspan]="columnsToDisplayWithExpand.length"
        cdkDropList
        [cdkDropListData]="{ parent: element, children: element.children }"
        (cdkDropListDropped)="dropElement($event, element)"
      >
        <div
          class="example-element-detail"
          [@detailExpand]="isElementExpanded(element) ? 'expanded' : 'collapsed'"
          *ngFor="let child of element.children; trackBy child"
          cdkDrag
        >
          <div></div>
          <div class="example-element-diagram">
            <div class="example-element-position">{{child.position}}</div>
            <div class="example-element-symbol">{{child.symbol}}</div>
            <div class="example-element-name">{{child.name}}</div>
            <div class="example-element-weight">{{child.weight}}</div>
          </div>
          <div class="example-element-description">
            {{child.description}}
            <span class="example-element-description-attribution">
              -- Wikipedia
            </span>
          </div>
    
          <div class="example-element-name expanded-column-spacing">
            {{child.Name}}
          </div>
          <div class="example-element-weight expanded-column-spacing">
            {{child.weight}}
          </div>
          <div class="example-element-weight expanded-column-spacing">
            {{child.weight}}
          </div>
        </div>
      </td>
    </ng-container>
    

    If you want the feature that allows drop the element without expand the parent, you can add the cdkDropList directive as previous setup in <tr mat-row> element.

    <tr
      mat-row
      *matRowDef="let element; columns: displayedColumns;"
      class="example-element-row"
      [class.example-expanded-row]="isElementExpanded(element)"
      cdkDropList
      [cdkDropListData]="{ parent: element, children: element.children }"
      (cdkDropListDropped)="dropElement($event, element)"
    ></tr>
    

    In the dropElement function, you have two sets of events:

    You need to update the children reference and call the this.dataSource._updateChangeSubscription() so that the table will be updated.

    dropElement(
      event: CdkDragDrop<{
        parent: PeriodicElement;
        children: PeriodicElement[];
      }>,
      targetParent: PeriodicElement
    ) {
      if (event.previousContainer === event.container) {
        moveItemInArray(
          targetParent.children,
          event.previousIndex,
          event.currentIndex
        );
      } else {
        transferArrayItem(
          event.previousContainer.data.children,
          targetParent.children,
          event.previousIndex,
          event.currentIndex
        );
      }
    
      event.previousContainer.data.parent.children = [
        ...event.previousContainer.data.children,
      ];
    
      targetParent.children = [...targetParent.children];
      this.dataSource._updateChangeSubscription();
    }
    

    Demo @ StackBlitz