New to Kendo UI for AngularStart a free 30-day trial

Reorder Rows in a Grouped Grid

Updated on Mar 4, 2026

Environment

ProductProgress® Kendo UI® for Angular Grid

Description

The built-in row reordering feature does not work when you group the Grid data. Row reordering relies on flat data indexes that become incorrect when grouping rearranges the data.

I want to reorder rows within the same group, move rows between different groups, and also support reordering rows when no grouping is active. How can I achieve this?

This Knowledge Base article also answers the following questions:

  • How can I drag and drop rows in a grouped Grid?
  • Is it possible to move a row to a different group by dragging it?
  • Can I combine Grid grouping with custom row reordering?
  • How to implement row reordering in grouped Grid while keeping the default row reordering?

Solution

To reorder rows in a grouped Grid, use the Kendo UI for Angular Drag and Drop utility instead of the built-in row reordering. Apply the kendoDragTargetContainer and kendoDropTargetContainer directives directly on the <kendo-grid> element and handle the onDrop event to reorder the data.

The following example demonstrates the suggested approach in action. Try dragging any row by the handle icon and drop it within the same group or into a different group.

Change Theme
Theme
Loading ...

Follow these steps to achieve the full implementation:

  1. Apply kendoDragTargetContainer and kendoDropTargetContainer directly on the <kendo-grid> element. Use dragTargetFilter and dropTargetFilter to scope drag and drop to the Grid rows, and set the dragHandle to use a dedicated handle icon.

    html
    <kendo-grid
        ...
        kendoDragTargetContainer
        kendoDropTargetContainer
        dragTargetFilter=".k-master-row"
        dropTargetFilter=".k-master-row"
        dragHandle=".k-drag-handle"
    >
        <kendo-grid-column [width]="40">
            <ng-template kendoGridCellTemplate>
                <kendo-svg-icon class="k-drag-handle" [icon]="reorderIcon"></kendo-svg-icon>
            </ng-template>
        </kendo-grid-column>
    </kendo-grid>
  2. Store references to both container directives using @ViewChild. You will need these references to call the notify() method after each drop, so the reordered rows can be reflected in the view.

    typescript
    @ViewChild('grid', { read: DragTargetContainerDirective })
    private dragTargetContainer: DragTargetContainerDirective;
    
    @ViewChild('grid', { read: DropTargetContainerDirective })
    private dropTargetContainer: DropTargetContainerDirective;
  3. Use the data-kendo-grid-item-index attribute from the dragTarget element to pass the flat drag index of the Grid row to the dragData input, so it is available in the onDrop event handler.

    TS
    public dragData = ({ dragTarget }: { dragTarget: HTMLElement }) => ({
        fromIndex: Number(dragTarget.getAttribute('data-kendo-grid-item-index')),
    });
  4. Implement a custom helper function that iterates over the Grid groups and returns the corresponding groupIndex and itemIndex for a given flat index:

    typescript
    export const findDropLocation = (flatIndex: number, groupedData: GroupResult[]): DropLocation => {
        let offset = 0;
        for (let g = 0; g < groupedData.length; g++) {
            const items = (groupedData[g] as GroupResult).items;
            if (flatIndex < offset + items.length) {
                return { groupIndex: g, itemIndex: flatIndex - offset };
            }
            offset += items.length;
        }
        const lastItems = (groupedData[groupedData.length - 1] as GroupResult).items;
        return { groupIndex: groupedData.length - 1, itemIndex: lastItems.length };
    };

    Each Grid row element carries a data-kendo-grid-item-index attribute that reflects its position across all groups combined, not within its individual group. This helper resolves the flat indexes into group-aware positions before splicing the data arrays to move the rows.

  5. Handle the onDrop event and obtain the flat indexes from the drag data and the drop target.

    typescript
    public onDrop(e: DropTargetEvent): void {
        if (!e.dropTarget) { return; }
    
        const flatDragIndex = e.dragData.fromIndex;
        const flatDropIndex = Number(e.dropTarget.getAttribute('data-kendo-grid-item-index'));
    
        if (flatDragIndex === flatDropIndex) { return; }
    }

    Implement different drop logic based on whether the Grid is grouped. Call notify() on both directives after each drop to update the rows.

    typescript
    public onDrop(e: DropTargetEvent): void {
        // ...
    
        if (this.isGrouped) {
            this.reorderGrouped(flatDragIndex, flatDropIndex, inLowerHalf);
        } else {
            this.reorder(flatDragIndex, flatDropIndex, inLowerHalf);
        }
    
        this.dragTargetContainer.notify();
        this.dropTargetContainer.notify();
    }
    
  6. When groups are active, use the custom helper (findDropLocation) to resolve the flat indexes to group-aware positions.

    typescript
    private reorderGrouped(flatDragIndex: number, flatDropIndex: number, inLowerHalf: boolean): void {
        const groupedView = this.gridView as GroupResult[];
        const dragLocation = findDropLocation(flatDragIndex, groupedView);
        const dropLocation = findDropLocation(flatDropIndex, groupedView);
    
        const dragGroup = groupedView[dragLocation.groupIndex];
        const dropGroup = groupedView[dropLocation.groupIndex];
    
        const dragItems = dragGroup.items as Product[];
        const dropItems = dropGroup.items as Product[];
    
        const draggedItem = dragItems[dragLocation.itemIndex];
    }

    Then, splice the item between the source and target group arrays, and remove any groups that become empty.

    typescript
    private reorderGrouped(flatDragIndex: number, flatDropIndex: number, inLowerHalf: boolean): void {
        // ...
    
        // Remove the item from its source group.
        dragItems.splice(dragLocation.itemIndex, 1);
    
        // When moving to a different group, update the grouped field so the item belongs to the new group.
        if (dragLocation.groupIndex !== dropLocation.groupIndex) {
            this.updateGroupField(draggedItem, dropGroup);
        }
    
        // Recalculate the drop index after splicing the source group when dragging item within the same group downwards.
        const isSameGroup = dragLocation.groupIndex === dropLocation.groupIndex;
        let targetIndex = dropLocation.itemIndex;
    
        if (isSameGroup && dragLocation.itemIndex < targetIndex) {
            targetIndex -= 1;
        }
    
        dropItems.splice(inLowerHalf ? targetIndex + 1 : targetIndex, 0, draggedItem);
    
        // Remove empty groups when moving the last item out.
        this.gridView = groupedView.filter((g) => g.items.length > 0);
    }
  7. When the drop crosses a group boundary, the item's grouped field value must be updated to match the target group. For this purpose, implement another custom helper that sets the target group field on the moved item.

    typescript
    private updateGroupField(item: Product, targetGroup: GroupResult): void {
        const field = this.groups[0]?.field;
        if (!field) { return; }
        const parts = field.split('.');
        let target: any = item;
        for (let i = 0; i < parts.length - 1; i++) { target = target[parts[i]]; }
        target[parts[parts.length - 1]] = targetGroup.value;
    }

    The helper supports single-level grouping. To support multi-level grouping, extend the method to set the grouped field value at every grouping level on the moved item.

  8. To provide visual feedback during dragging, handle the onDragOver and onDragLeave events. Use a helper method to determine if the row is dropped in the lower half of the target. Based on this, toggle a CSS class on the targeted row and render a top or bottom border indicating the drop position.

    typescript
    public onDragOver(e: DropTargetEvent): void {
        const inLowerHalf = isDroppedInLowerHalf(e);
        e.dropTarget.classList.toggle('drop-above', !inLowerHalf);
        e.dropTarget.classList.toggle('drop-below', inLowerHalf);
    }
    
    public onDragLeave(e: DropTargetEvent): void {
        this.clearDropIndicator(e.dropTarget);
    }
    
    private clearDropIndicator(el: HTMLElement): void {
        el.classList.remove('drop-above', 'drop-below');
    }

    Apply the corresponding CSS rules to the defined classes:

    css
    .drop-above { box-shadow: inset 0 2px 0 0 var(--kendo-color-primary); }
    .drop-below { box-shadow: inset 0 -2px 0 0 var(--kendo-color-primary); }

The approach described in this article supports single-level grouping. To support multi-level grouping, extend the findDropLocation helper to recurse through the nested GroupResult tree, and adjust the updateGroupField method to set the grouped field value at every grouping level on the moved item.

See Also

In this article
EnvironmentDescriptionSolutionSee Also
Not finding the help you need?
Contact Support