Row Dragging in Angular Tree Grid

    In Ignite UI for Angular Tree Grid, RowDrag is initialized on the root igx-tree-grid component and is configurable via the rowDraggable input. Enabling row dragging provides users with a row drag-handle with which they can initiate dragging of a row.

    Angular Tree Grid Row Drag Example

    Configuration

    In order to enable row-dragging for your igx-tree-grid, all you need to do is set the grid's rowDraggable to true. Once this is enabled, a row-drag handle will be displayed on each row. This handle can be used to initiate row dragging.

    <igx-tree-grid [rowDraggable]="true">
     ...
    </igx-tree-grid>
    

    Clicking on the drag-handle and moving the cursor while holding down the button will cause the grid's rowDragStart event to fire. Releasing the click at any time will cause rowDragEnd event to fire.

    Below, you can find a walkthrough on how to configure an igx-tree-grid to support row dragging and how to properly handle the drop event.

    In this example, we'll handle dragging a row from a grid to a designated area and, when dropping it, removing it from the grid.

    Drop Areas

    Enabling row-dragging was pretty easy, but now we have to configure how we'll handle row-dropping. We can define where we want our rows to be dropped using the igxDrop directive.

    First we need to import the IgxDragDropModule in our app module:

    import { ..., IgxDragDropModule } from 'igniteui-angular';
    // import { ..., IgxDragDropModule } from '@infragistics/igniteui-angular'; for licensed package
    ...
    @NgModule({
        imports: [..., IgxDragDropModule]
    })
    

    Then, in our template, we define a drop-area using the directive's selector:

    <div class="drop-area" igxDrop (enter)="onEnterAllowed($event)" (leave)="onLeaveAllowed($event)"
    (dropped)="onDropAllowed($event)">
        <igx-icon>delete</igx-icon>
        <div>Drag a row here to delete it</div>
    </div>
    

    You may enable animation when a row is dropped on a non-droppable area using the animation parameter of the rowDragEnd event. If set to true, the dragged row will animate back to its' original position when dropped over a non-droppable area.

    You may enable animation like this:

    export class IgxTreeGridRowDragComponent {
    
        public onRowDragEnd(args) {
            args.animation = true;
        }
    
    }
    

    Drop Area Event Handlers

    Once we've defined our drop-area in the template, we have to declare our handlers for the igxDrop's enter, leave and dropped events in our component's .ts file.

    First, let's take a look at our enter and leave handlers. In those methods, we just want to change the icon of the drag's ghost so we can indicate to the user that they are above an area that allows them to drop the row:

    export class IgxTreeGridRowDragComponent {
        public onEnterAllowed(args) {
            this.changeGhostIcon(args.drag.ghostElement, DragIcon.ALLOW);
        }
    
        public onLeaveAllowed(args) {
            this.changeGhostIcon(args.drag.ghostElement, DragIcon.DEFAULT);
        }
    
        private changeGhostIcon(ghost, icon: string) {
            if (ghost) {
                const currentIcon = ghost.querySelector('.igx-grid__drag-indicator > igx-icon');
                if (currentIcon) {
                    currentIcon.innerText = icon;
                }
            }
        }
    }
    

    The changeGhostIcon private method just changes the icon inside of the drag ghost. The logic in the method finds the element that contains the icon (using the igx-grid__drag-indicator class that is applied to the drag-indicator container), changing the element's inner text to the passed one. The icons themselves are from the material font set and are defined in a separate enum:

    enum DragIcon {
        DEFAULT = 'drag_indicator',
        ALLOW = 'remove'
    }
    

    Next, we have to define what should happen when the user actually drops the row inside of the drop-area.

    export class IgxTreeGridRowDragComponent {
    
        public onDropAllowed(args: IDropDroppedEventArgs) {
            const draggedRow: RowType = args.dragData;
            draggedRow.delete();
        }
    
    }
    

    Once the row is dropped, we just call the row's delete() method

    Note

    When using row data from the event arguments (args.dragData.data) or any other row property, note that the entire row is passed in the arguments as a reference, which means that you must clone the data you need, if you want to distinguish it from the one in the source grid.

    Templating the drag ghost

    The drag ghost can be templated using the IgxRowDragGhost directive, applied to a <ng-template> inside of the igx-tree-grid's body:

    <igx-tree-grid>
    ...
       <ng-template igxRowDragGhost>
            <div>
                <igx-icon fontSet="material">arrow_right_alt</igx-icon>
            </div>
        </ng-template>
    ...
    </igx-tree-grid>
    

    The result of the configuration can be seem below in a igx-tree-grid with row dragging and multiple selection enabled. The demo shows the count of the currently dragged rows:

    Example Demo

    Templating the drag icon

    The drag handle icon can be templated using the grid's dragIndicatorIconTemplate. In the example we're building, let's change the icon from the default one (drag_indicator) to drag_handle. To do so, we can use the igxDragIndicatorIcon to pass a template inside of the igx-tree-grid's body:

    <igx-tree-grid>
    ...
        <ng-template igxDragIndicatorIcon>
            <igx-icon>drag_handle</igx-icon>
        </ng-template>
    ...
    </igx-tree-grid>
    

    Once we've set the new icon template, we also need to adjust the DEFAULT icon in our DragIcon enum, so it's properly change by the changeIcon method:

    enum DragIcon {
        DEFAULT = "drag_handle",
        ...
    }
    

    Styling the drop area

    Once our drop handlers are properly configured, all that's left is to style our drop area a bit:

    .drop-area {
        width: 160px;
        height: 160px;
        background-color: #d3d3d3;
        border: 1px dashed #131313;
        display: flex;
        justify-content: center;
        align-items: center;
        flex-flow: column;
        text-align: center;
        margin: 8px;
    }
    
    :host {
        display: flex;
        justify-content: center;
        align-items: center;
        flex-flow: column;
        width: 100%;
    }
    

    The result can be seen in the demo below:

    Example Demo

    Application Demo

    Row Reordering Demo

    With the help of the grid's row drag events and the igxDrop directive, you can create a grid that allows you to reorder rows by dragging them.

    Since all of the actions will be happening inside of the grid's body, that's where you have to attach the igxDrop directive:

    <igx-tree-grid igxPreventDocumentScroll  #treeGrid [data]="localData" [rowDraggable]="true" foreignKey="ParentID"
        [primaryKey]="'ID'" (rowDragStart)="rowDragStart($event)" igxDrop (dropped)="dropInGrid($event)">
        ...
    </igx-tree-grid>
    
    Note

    Make sure that there is a primaryKey specified for the grid! The logic needs an unique identifier for the rows so they can be properly reordered

    Once rowDraggable is enabled and a drop zone has been defined, you need to implement a simple handler for the drop event. When a row is dragged, check the following:

    • Is the row expanded? If so, collapse it.
    • Was the row dropped inside of the grid?
    • If so, on which other row was the dragged row dropped?
    • Once you've found the target row, swap the records' places in the data array
    • Was the row initially selected? If so, mark it as selected.

    Below, you can see this implemented in the component's .ts file:

    export class TreeGridRowReorderComponent {
        public rowDragStart(args: any): void {
            const targetRow = args.dragData;
            if (targetRow.expanded) {
                this.treeGrid.collapseRow(targetrow.key);
            }
        }
    
        public dropInGrid(args: IDropDroppedEventArgs): void {
            const draggedRow = args.dragData;
            const event = args.originalEvent;
            const cursorPosition: Point = { x: event.clientX, y: event.clientY };
            this.moveRow(draggedRow, cursorPosition);
        }
    
        private moveRow(draggedRow: RowType, cursorPosition: Point): void {
            const row = this.catchCursorPosOnElem(this.treeGrid.rowList.toArray(), cursorPosition);
            if (!row) { return; }
            if (row.data.ParentID === -1) {
                this.performDrop(draggedRow, row).ParentID = -1;
            } else {
                if (row.data.ParentID === draggedrow.data.ParentID) {
                    this.performDrop(draggedRow, row);
                } else {
                    const rowIndex = this.getRowIndex(draggedrow.data);
                    this.localData[rowIndex].ParentID = row.data.ParentID;
                }
            }
            if (draggedRow.selected) {
                this.treeGrid.selectRows([this.treeGrid.rowList.toArray()
                    .find((r) => r.rowID === draggedrow.key).rowID], false);
            }
    
            this.localData = [...this.localData];
        }
    
        private performDrop(
            draggedRow: IgxTreeGridRowComponent, targetRow: IgxTreeGridRowComponent) {
            const draggedRowIndex = this.getRowIndex(draggedrow.data);
            const targetRowIndex: number = this.getRowIndex(targetrow.data);
            if (draggedRowIndex === -1 || targetRowIndex === -1) { return; }
            this.localData.splice(draggedRowIndex, 1);
            this.localData.splice(targetRowIndex, 0, draggedrow.data);
            return this.localData[targetRowIndex];
        }
    
        private getRowIndex(rowData: any): number {
            return this.localData.indexOf(rowData);
        }
    
        private catchCursorPosOnElem(rowListArr: IgxTreeGridRowComponent[], cursorPosition: Point)
            : IgxTreeGridRowComponent {
            for (const row of rowListArr) {
                const rowRect = row.nativeElement.getBoundingClientRect();
                if (cursorPosition.y > rowRect.top + window.scrollY && cursorPosition.y < rowRect.bottom + window.scrollY &&
                    cursorPosition.x > rowRect.left + window.scrollX && cursorPosition.x < rowRect.right + window.scrollX) {
                    return row;
                }
            }
    
            return null;
        }
    }
    

    With these few easy steps, you've configured a grid that allows reordering rows via drag/drop! You can see the above code in action in the following demo.

    Notice that we also have row selection enabled and we preserve the selection when dropping the dragged row.

    Limitations

    Currently, there are no known limitations for the rowDraggable directive.

    API References

    Additional Resources

    Our community is active and always welcoming to new ideas.