Reorder Rows in a Grouped Grid
Environment
| Product | Progress® 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.
Follow these steps to achieve the full implementation:
-
Apply
kendoDragTargetContainerandkendoDropTargetContainerdirectly on the<kendo-grid>element. UsedragTargetFilteranddropTargetFilterto scope drag and drop to the Grid rows, and set thedragHandleto 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> -
Store references to both container directives using
@ViewChild. You will need these references to call thenotify()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; -
Use the
data-kendo-grid-item-indexattribute from thedragTargetelement to pass the flat drag index of the Grid row to thedragDatainput, so it is available in theonDropevent handler.TSpublic dragData = ({ dragTarget }: { dragTarget: HTMLElement }) => ({ fromIndex: Number(dragTarget.getAttribute('data-kendo-grid-item-index')), }); -
Implement a custom helper function that iterates over the Grid groups and returns the corresponding
groupIndexanditemIndexfor a given flat index:typescriptexport 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-indexattribute 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. -
Handle the
onDropevent and obtain the flat indexes from the drag data and the drop target.typescriptpublic 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.typescriptpublic onDrop(e: DropTargetEvent): void { // ... if (this.isGrouped) { this.reorderGrouped(flatDragIndex, flatDropIndex, inLowerHalf); } else { this.reorder(flatDragIndex, flatDropIndex, inLowerHalf); } this.dragTargetContainer.notify(); this.dropTargetContainer.notify(); } -
When groups are active, use the custom helper (
findDropLocation) to resolve the flat indexes to group-aware positions.typescriptprivate 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.
typescriptprivate 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); } -
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.
typescriptprivate 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.
-
To provide visual feedback during dragging, handle the
onDragOverandonDragLeaveevents. 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.typescriptpublic 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
findDropLocationhelper to recurse through the nestedGroupResulttree, and adjust theupdateGroupFieldmethod to set the grouped field value at every grouping level on the moved item.