Arrastre de filas en Angular cuadrícula

    In Ignite UI for Angular Grid, RowDrag is initialized on the root igx-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 Grid Row Drag Example

    Configuración

    In order to enable row-dragging for your igx-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-grid [rowDraggable]="true">
     ...
    </igx-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-grid to support row dragging and how to properly handle the drop event.

    En este ejemplo, nos encargaremos de arrastrar una fila de una cuadrícula a otra, eliminarla de la primera fuente de datos y agregarla a la segunda.

    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/directives';
    // import { ..., IgxDragDropModule } from '@infragistics/igniteui-angular'; for licensed package
    ...
    @NgModule({
        imports: [..., IgxDragDropModule]
    })
    

    Luego, en nuestra plantilla, definimos un área de colocación usando el selector de directivas:

    En este caso, nuestra área de colocación será una segunda cuadrícula completa donde colocaremos las filas.

    <igx-grid #targetGrid igxDrop [data]="data2" [autoGenerate]="false" [emptyGridTemplate]="dragHereTemplate"
        (enter)="onEnterAllowed($event)" (leave)="onLeaveAllowed($event)" (dropped)="onDropAllowed($event)" [primaryKey]="'ID'">
        ...
    </igx-grid>
    

    Dado que la cuadrícula inicialmente estará vacía, también definimos una plantilla que será más significativa para el usuario:

    <ng-template #dragHereTemplate>
        Drop a row to add it to the grid
    </ng-template>
    

    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.

    Puede habilitar animaciones como esta:

    export class IgxGridRowDragComponent {
    
        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 IgxGridRowDragComponent {
        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 = 'add'
    }
    

    A continuación, tenemos que definir qué debería suceder cuando el usuario realmente coloca la fila dentro del área de colocación.

    export class IgxGridRowDragComponent {
        @ViewChild('sourceGrid', { read: IgxGridComponent }) public sourceGrid: IgxGridComponent;
        @ViewChild('targetGrid', { read: IgxGridComponent }) public targetGrid: IgxGridComponent;
    
        public onDropAllowed(args) {
            this.targetGrid.addRow(args.dragData.data);
            this.sourceGrid.deleteRow(args.dragData.key);
        }
    }
    

    We define a reference to each of our grids via the ViewChild decorator and the handle the drop as follows:

    • add a row to the targetGrid that contains the data of the row being dropped
    • remove the dragged row from the sourceGrid
    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-grid's body:

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

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

    Demostración de ejemplo

    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-grid's body:

    <igx-grid>
    ...
        <ng-template igxDragIndicatorIcon>
            <igx-icon>drag_handle</igx-icon>
        </ng-template>
    ...
    </igx-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",
        ...
    }
    

    Una vez que nuestros controladores de entrega estén configurados correctamente, ¡estamos listos para comenzar! El resultado de la configuración se puede ver a continuación:

    Demostración de ejemplo

    Application Demo

    Using Row Drag Events

    La siguiente demostración demuestra cómo utilizar la información del evento de arrastre de filas para cambiar ambos estados de un componente personalizado, donde se suelta la fila, y la propia cuadrícula de origen. Intenta arrastrar lunas desde la cuadrícula y suéltalas en sus planetas correspondientes. El fondo del fantasma de arrastre de fila cambia dinámicamente, dependiendo del planeta sobre el que se encuentre. Si tiene éxito, se seleccionará la fila de la cuadrícula y se desactivará el arrastre. Al hacer clic en los planetas obtendrás información útil.

    Note

    Las clases aplicadas al fantasma de arrastre de filas, utilizadas en la demostración anterior, utilizan el modificador ::ng-deep, porque el arrastre de filas es una característica de la cuadrícula interna y no se puede acceder a ella en el nivel de la aplicación, debido a la encapsulación de CSS.

    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-grid #grid [data]="data" [rowDraggable]="true" [primaryKey]="'ID'" igxDrop (dropped)="onDropAllowed($event)">
        ...
    </igx-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:

    • ¿Se dejó caer la fila dentro de la cuadrícula?
    • Si es así, ¿en qué otra fila se soltó la fila arrastrada?
    • Once you've found the target row, swap the records' places in the data array

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

    export class GridRowReorderComponent {
        public onDropAllowed(args) {
            const event = args.originalEvent;
            const currRowIndex = this.getCurrentRowIndex(this.grid.rowList.toArray(),
                { x: event.clientX, y: event.clientY });
            if (currRowIndex === -1) { return; }
            this.grid.deleteRow(args.dragData.key);
            this.data.splice(currRowIndex, 0, args.dragData.data);
        }
    
        private getCurrentRowIndex(rowList, cursorPosition) {
            for (const row of rowList) {
                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 this.data.indexOf(this.data.find((r) => r.rowID === row.rowID));
                }
            }
    
            return -1;
        }
    }
    

    ¡Con estos sencillos pasos, ha configurado una cuadrícula que permite reordenar filas mediante arrastrar y soltar! Puede ver el código anterior en acción en la siguiente demostración.

    Mantener presionado el ícono de arrastrar le permitirá mover una fila a cualquier lugar de la cuadrícula:

    Improving UX in row drag scenarios

    Ser capaz de obtener el índice de fila que se encuentra actualmente debajo del cursor le brinda la oportunidad de crear ricas funcionalidades personalizadas y mejorar la UX de su aplicación. Por ejemplo, puede cambiar el fantasma de arrastre o mostrar un indicador de colocación, según la posición de la fila arrastrada sobre la cuadrícula. Otro comportamiento útil que puedes lograr de esa manera es desplazar la cuadrícula hacia arriba o hacia abajo mientras arrastras una fila, al llegar al borde de la cuadrícula.

    A continuación puede encontrar fragmentos de ejemplo de un par de implementaciones personalizadas que puede lograr conociendo la posición de la fila.

    Cambiar el fantasma de arrastre según la posición del cursor

    En los fragmentos a continuación, verá cómo puede cambiar el texto dentro del fantasma de arrastre para mostrar el nombre de la fila sobre la que se encuentra.

    First, you specify a template which you'd like to use for the drag ghost. The dropName property will dynamically change, getting the name of the row over which the cursor is hovering:

    <ng-template igxRowDragGhost>
        <div class="customGhost">
            <div>{{ dropName }}</div>
        </div>
    </ng-template>
    

    Luego, define un método que devuelva la instancia de la fila que has terminado (similar al usado en la demostración de reordenamiento de filas):

    class MyRowGhostComponent {
        private getRowDataAtPoint(rowList: IgxGridRowComponent[], cursorPosition: Point): any {
            for (const row of rowList) {
                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 this.data.find((r) => r.rowID === row.rowID);
                }
            }
            return null;
        }
    }
    

    Finally, we create a method that will be used to handle the IgxDragDirective.dragMove event (emitted for the dragged row). The method will change the value of the property used in the igxRowDragGhost template and force a rerender. We want to subscribe to the dragMove event only of the specific row we're dragging and unsubscribe from it (to prevent memory leaks) each time a row is dropped.

    class MyRowGhostComponent {
        public ngAfterViewInit(): void {
            this.grid.rowDragStart.pipe(takeUntil(this.destroy$)).subscribe(this.onRowDragStart.bind(this));
        }
    
        private onRowDragStart(e: IRowDragStartEventArgs) {
            if (e !== null) {
                this._draggedRow = e.dragData.rowData;
            }
            const directive = e.dragDirective;
            directive.dragMove
                .pipe(takeUntil(this.grid.rowDragEnd))
                .subscribe(this.onDragMove.bind(this));
        }
    
        private onDragMove(args: IDragMoveEventArgs) {
            const cursorPosition = this.getCursorPosition(args.originalEvent);
            const hoveredRowData = this.getRowDataAtPoint(
                this.grid.rowList.toArray(),
                cursorPosition
            );
            if (!hoveredRowData) {
                args.cancel = true;
                return;
            }
            const rowID = hoveredRowData.ID;
            if (rowID !== null) {
                let newName = this.dropName;
                if (rowID !== -1) {
                    const targetRow = this.grid.rowList.find((e) => {
                        return e.rowData.ID === rowID;
                    });
                    newName = targetRow?.rowData.Name;
                }
                if (newName !== this.dropName) {
                    this.dropName = newName;
                    args.owner.cdr.detectChanges();
                }
            }
        }
    }
    
    

    Mostrar un indicador de caída basado en la posición del cursor

    En la demostración de la siguiente sección verá cómo puede mostrar un indicador de dónde se soltaría la fila arrastrada. Puede personalizar este indicador como desee: puede ser una fila de marcador de posición, colocada en la posición donde se soltaría la fila arrastrada, un estilo de borde que indica si la fila arrastrada se soltaría encima o debajo de la fila actualmente suspendida, etc.

    In order to track the position of the cursor, we bind to the dragMove event of the IgxDragDirective when we start dragging a row.

    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

    public ngAfterViewInit() {
      this.grid.rowDragStart
        .pipe(takeUntil(this.destroy$))
        .subscribe(this.handleRowStart.bind(this));
    }
    
    private handleRowStart(e: IRowDragStartEventArgs): void {
      if (e !== null) {
        this._draggedRow = e.dragData.data;
      }
      const directive = e.dragDirective;
      directive.dragMove
        .pipe(takeUntil(this.grid.rowDragEnd))
        .subscribe(this.handleDragMove.bind(this));
    }
    
    private handleDragMove(event: IDragMoveEventArgs): void {
      this.handleOver(event);
    }
    
    private handleOver(event: IDragMoveEventArgs) {
      const ghostRect = event.owner.ghostElement.getBoundingClientRect();
      const rowIndex = this.getRowIndexAtPoint(this.grid.rowList.toArray(), {
        x: ghostRect.x,
        y: ghostRect.y
      });
      if (rowIndex === -1) {
        return;
      }
      const rowElement = this.grid.rowList.find(
        e => e.rowData.ID === this.grid.data[rowIndex].ID
      );
      if (rowElement) {
        this.changeHighlightedElement(rowElement.element.nativeElement);
      }
    }
    
    private clearHighlightElement(): void {
      if (this.highlightedRow !== undefined) {
        this.renderer.removeClass(this.highlightedRow, 'underlined-class');
      }
    }
    
    private setHightlightElement(newElement: HTMLElement) {
      this.renderer.addClass(newElement, 'underlined-class');
      this.highlightedRow = newElement;
    }
    
    private changeHighlightedElement(newElement: HTMLElement) {
      if (newElement !== undefined) {
        if (newElement !== this.highlightedRow) {
          this.clearHighlightElement();
          this.setHightlightElement(newElement);
        } else {
          return;
        }
      }
    }
    

    Desplazarse por la cuadrícula al arrastrar filas

    Un escenario muy útil es poder desplazar la cuadrícula cuando la fila arrastrada alcanza su borde superior o inferior. Esto permite reordenar filas fuera de la ventana gráfica actual cuando el número de filas en la cuadrícula requiere una barra de desplazamiento.

    Below you see an example of the two methods we use to check if we have reached the edge of the viewport and to scroll it if needed. The isGridScrolledToEdge accepts one parameter - the direction we'd like to scroll the grid (1 for "Down", -1 for "Up") and returns true if we've reach the final row in that direction. The scrollGrid method will attempt to scroll the grid in a direction (1 or -1), doing nothing if the grid is already at that edge.

    class MyGridScrollComponent {
        private isGridScrolledToEdge(dir: 1 | -1): boolean {
            if (this.grid.data[0] === this.grid.rowList.first.data && dir === -1) {
                return true;
            }
            if (
                this.grid.data[this.grid.data.length - 1] === this.grid.rowList.last.data &&
                dir === 1
            ) {
                return true;
            }
            return false;
        }
    
        private scrollGrid(dir: 1 | -1): void {
            if (!this.isGridScrolledToEdge(dir)) {
                if (dir === 1) {
                    this.grid.verticalScrollContainer.scrollNext();
                } else {
                    this.grid.verticalScrollContainer.scrollPrev();
                }
            }
        }
    }
    
    

    We'll still be subscribing to the dragMove event of the specific row in the way we did in the previous example. Since dragMove is fired only when the cursor actually moves, we want to have a nice and simple way to auto-scroll the grid when the row is at one of the edges, but the user does not move the mouse. We'll an additional method which will setup an interval, auto-scrolling the grid every 500ms.

    We create and subscribe to the interval when the pointer reaches the grid's edge and we unsubscribe from that interval every time the mouse moves or the row is dropped (regardless of cursor position).

    class MyGridScrollComponent {
        public ngAfterViewInit() {
            this.grid.rowDragStart
                .pipe(takeUntil(this.destroy$))
                .subscribe(this.onDragStart.bind(this));
            this.grid.rowDragEnd
                .pipe(takeUntil(this.destroy$))
                .subscribe(() => this.unsubInterval());
        }
    
        private onDragMove(event: IDragMoveEventArgs): void {
            this.unsubInterval();
            const dir = this.isPointOnGridEdge(event.pageY);
            if (!dir) {
                return;
            }
            this.scrollGrid(dir);
            if (!this.intervalSub) {
                this.interval$ = interval(500);
                this.intervalSub = this.interval$.subscribe(() => this.scrollGrid(dir));
            }
        }
    
        private unsubInterval(): void {
            if (this.intervalSub) {
                this.intervalSub.unsubscribe();
                this.intervalSub = null;
            }
        }
    }
    

    A continuación se muestra el ejemplo de ambos escenarios descritos anteriormente: se muestra un indicador de caída y se desplaza la ventana gráfica cuando se alcanza el borde del borde.

    Limitations

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

    API References

    Additional Resources

    Nuestra comunidad es activa y siempre da la bienvenida a nuevas ideas.