Arrastar linha em Angular grade

    Em Ignite UI for Angular Grid, RowDrag é inicializado no componente raiz igx-grid e é configurável por meio da rowDraggable entrada. Habilitar o arrastar de linha fornece aos usuários uma alça de arrastar de linha com a qual eles podem iniciar o arrasto de uma linha.

    Angular Grid Row Drag Example

    Configuração

    Para habilitar o arrasto de linha para você igx-grid, tudo o que você precisa fazer é definir a grade rowDraggable para true. Depois que isso estiver ativado, uma alça de arrastar linha será exibida em cada linha. Essa alça pode ser usada para iniciar o arrasto de linha.

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

    Clicar na alça de arrastar e mover o cursor enquanto mantém pressionado o botão fará com que o evento da rowDragStart grade seja acionado. Liberar o clique a qualquer momento fará com que rowDragEnd o evento seja acionado.

    Abaixo, você pode encontrar um passo a passo sobre como configurar um igx-grid para dar suporte ao arrasto de linha e como lidar corretamente com o evento de soltar.

    Neste exemplo, vamos lidar com o arrasto de uma linha de uma grade para outra, removendo-a da primeira fonte de dados e adicionando-a à segunda.

    Drop Areas

    Habilitar o arrasto de linha foi muito fácil, mas agora temos que configurar como lidaremos com a queda de linha. Podemos definir onde queremos que nossas linhas sejam descartadas usando a igxDrop diretiva.

    Primeiro, precisamos importar o em nosso módulo de IgxDragDropModule aplicativo:

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

    Em seguida, em nosso modelo, definimos uma área de soltar usando o seletor da diretiva:

    Nesse caso, nossa área de queda será uma segunda grade inteira onde soltaremos as linhas.

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

    Como a grade estará inicialmente vazia, também definimos um modelo que será mais significativo para o usuário:

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

    Você pode ativar a animação quando uma linha é solta em uma área não soltável usando o animation​ ​rowDragEnd parâmetro do evento. Se definido como true, a linha arrastada será animada de volta à sua posição original quando solta sobre uma área que não pode ser solta.

    Você pode habilitar a animação assim:

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

    Drop Area Event Handlers

    Depois de definirmos nossa área de soltar no modelo, temos que declarar nossos manipuladores para os igxDrop​ ​enter eventos leave​ ​dropped e eventos no arquivo do.ts nosso componente.

    Primeiro, vamos dar uma olhada em nossos enter manipuladores leave. Nesses métodos, queremos apenas alterar o ícone do fantasma do drag para que possamos indicar ao usuário que ele está acima de uma área que permite soltar a linha:

    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;
                }
            }
        }
    }
    

    O changeGhostIcon método privado apenas altera o ícone dentro do fantasma de arrastar. A lógica no método localiza o elemento que contém o ícone (usando a igx-grid__drag-indicator classe aplicada ao contêiner indicador de arrastar), alterando o texto interno do elemento para o passado. Os ícones em si são do material fontesconjunto de e são definidos em separado enum:

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

    Em seguida, temos que definir o que deve acontecer quando o usuário realmente solta a linha dentro da área de soltar.

    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);
        }
    }
    

    Definimos uma referência a cada uma de nossas grades por meio do ViewChild decorador e lidamos com a queda da seguinte maneira:

    • Adicione uma linha ao targetGrid que contém os dados da linha que está sendo descartada
    • remova a linha arrastada do sourceGrid
    Note

    Ao usar dados de linha dos argumentos do evento (args.dragData.data) ou qualquer outra propriedade de linha, observe que a linha inteira é passada nos argumentos como referência, o que significa que você deve clonar os dados necessários, se quiser distingui-los dos da grade de origem.

    Templating the drag ghost

    O fantasma de arrastar pode ser modelado usando a IgxRowDragGhost diretiva, aplicada a um <ng-template> interior do igx-grid corpo:

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

    O resultado da configuração pode ser exibido abaixo em um igx-grid com arrastar linhas e seleção múltipla habilitada. A demonstração mostra a contagem das linhas arrastadas no momento:

    Exemplo de demonstração

    Templating the drag icon

    O ícone da alça de arrastar pode ser modelado usando o da grade dragIndicatorIconTemplate. No exemplo que estamos construindo, vamos alterar o ícone do padrão (drag_indicator) para drag_handle. Para fazer isso, podemos usar o igxDragIndicatorIcon para passar um modelo dentro do igx-grid corpo:

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

    Depois de definir o novo modelo de ícone, também precisamos ajustar o DEFAULT ícone em nosso DragIcon enum, para que ele seja alterado corretamente pelo changeIcon método:

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

    Assim que nossos gerenciadores de drop estiverem configurados corretamente, estamos prontos para começar! O resultado da configuração pode ser visto abaixo:

    Exemplo de demonstração

    Application Demo

    Using Row Drag Events

    A demonstração a seguir demonstra como usar informações de evento de arrastar linha para alterar os dois estados de um componente personalizado, onde a linha é solta, e a própria grade de origem. Tente arrastar luas da grade e soltá-las em seus planetas correspondentes. O plano de fundo fantasma de arrasto de linha é alterado dinamicamente, dependendo do planeta pairado. Se você tiver sucesso, a linha na grade será selecionada e o arrastar será desativado para ela. Clicar em planetas fornecerá informações úteis.

    Note

    As classes aplicadas ao fantasma de arrastar linha, usado na demonstração acima, estão usando o modificador ::ng-deep, porque o arrasto de linha é um recurso de grade interna e não pode ser acessado no nível do aplicativo, devido ao encapsulamento CSS.

    Row Reordering Demo

    Com a ajuda dos eventos de arrastar linha da grade e da igxDrop diretiva, você pode criar uma grade que permite reordenar as linhas arrastando-as.

    Como todas as ações acontecerão dentro do corpo da grade, é aí que você deve anexar a igxDrop diretiva:

    <igx-grid #grid [data]="data" [rowDraggable]="true" [primaryKey]="'ID'" igxDrop (dropped)="onDropAllowed($event)">
        ...
    </igx-grid>
    
    Note

    Certifique-se de que haja um primaryKey especificado para a grade! A lógica precisa de um identificador exclusivo para as linhas para que possam ser reordenadas corretamente

    Uma vez rowDraggable habilitado e uma zona de queda definida, você precisa implementar um manipulador simples para o evento de queda. Quando uma linha é arrastada, verifique o seguinte:

    • A linha foi lançada dentro da grade?
    • Em caso afirmativo, em qual outra linha a linha arrastada foi solta?
    • Depois de encontrar a linha de destino, troque os lugares dos registros na data matriz

    Abaixo, você pode ver isso implementado no arquivo do.ts componente:

    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;
        }
    }
    

    Com essas etapas fáceis, você configurou uma grade que permite reordenar linhas por meio de arrastar/soltar! Você pode ver o código acima em ação na demonstração a seguir.

    Segurar o ícone de arrastar permitirá que você mova uma linha para qualquer lugar na grade:

    Improving UX in row drag scenarios

    Ser capaz de obter o índice de linha que está atualmente abaixo do cursor oferece a oportunidade de criar funcionalidades personalizadas avançadas e melhorar a experiência do usuário do seu aplicativo. Por exemplo, você pode alterar o fantasma de arrastar ou exibir um indicador de soltar, com base na posição da linha arrastada sobre a grade. Outro comportamento útil que você pode obter dessa maneira é rolar a grade para cima ou para baixo enquanto arrasta uma linha, ao atingir a borda da grade.

    Abaixo, você pode encontrar trechos de exemplo de algumas implementações personalizadas que você pode obter conhecendo a posição da linha.

    Alterando o fantasma de arrastar com base na posição do cursor

    Nos trechos abaixo, você vê como alterar o texto dentro do fantasma de arrastar para exibir o nome da linha pairada.

    Primeiro, você especifica um modelo que gostaria de usar para o fantasma de arrastar. A dropName propriedade mudará dinamicamente, obtendo o nome da linha sobre a qual o cursor está passando:

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

    Em seguida, defina um método que retorne a instância da linha que você superou (semelhante ao usado na demonstração de reordenação de linha):

    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;
        }
    }
    

    Por fim, criamos um método que será usado para manipular o IgxDragDirective.dragMove evento (emitido para a linha arrastada). O método alterará o valor da propriedade usada no igxRowDragGhost modelo e forçará uma nova renderização. Queremos assinar o evento apenas da linha específica que estamos arrastando e cancelar a dragMove assinatura dela (para evitar vazamentos de memória) sempre que uma linha for descartada.

    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();
                }
            }
        }
    }
    
    

    Exibindo um indicador de queda com base na posição do cursor

    Na demonstração da próxima seção, você verá como exibir um indicador de onde a linha arrastada seria solta. Você pode personalizar este indicador como quiser - pode ser uma linha de espaço reservado, colocada na posição em que a linha arrastada seria solta, um estilo de borda indicando se a linha arrastada seria solta acima ou abaixo da linha atualmente pairada, etc.

    Para rastrear a posição do cursor, nos ligamos ao dragMove evento do IgxDragDirective quando começamos a arrastar uma linha.

    Note

    Certifique-se de que haja um primaryKey especificado para a grade! A lógica precisa de um identificador exclusivo para as linhas para que possam ser reordenadas corretamente

    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;
        }
      }
    }
    

    Rolando a grade ao arrastar linha

    Um cenário muito útil é poder rolar a grade quando a linha arrastada atinge sua borda superior ou inferior. Isso permite reordenar linhas fora da janela de visualização atual quando o número de linhas na grade requer uma barra de rolagem.

    Abaixo você vê um exemplo dos dois métodos que usamos para verificar se chegamos à borda da janela de visualização e rolá-la, se necessário. O isGridScrolledToEdge aceita um parâmetro - a direção em que gostaríamos de rolar a grade (1 para "Para baixo", -1 para "Para cima") e retorna true se chegarmos à linha final nessa direção. O scrollGrid método tentará rolar a grade em uma direção (1 ou -1), sem fazer nada se a grade já estiver nessa borda.

    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();
                }
            }
        }
    }
    
    

    Ainda estaremos assinando o dragMove evento da linha específica da maneira que fizemos no exemplo anterior. Como dragMove o é disparado apenas quando o cursor realmente se move, queremos ter uma maneira agradável e simples de rolar automaticamente a grade quando a linha está em uma das bordas, mas o usuário não move o mouse. Vamos usar um método adicional que configurará uma interval rolagem automática da grade a 500ms cada.

    Criamos e assinamos o interval quando o ponteiro atinge a borda da grade e a unsubscribe partir disso toda vez que interval o mouse se move ou a linha é solta (independentemente da posição do cursor).

    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 seguir está o exemplo de ambos os cenários descritos acima - mostrando um indicador de queda e rolando a janela de visualização quando a borda da borda é atingida.

    Limitations

    Atualmente, não há limitações conhecidas para a rowDraggable diretiva.

    API References

    Additional Resources

    Nossa comunidade é ativa e sempre acolhedora para novas ideias.