
È possibile in un’applicazione web visualizzare dati in forma tabellare dando la possibilità all’utente di spostare le colonne mediante drag and drop? In quest’articolo vi racconterò la mia esperienza al riguardo, in un progetto Angular.

Partiamo dalle basi usando il classico tag HTML <table>:
<table class="table" >
<thead class="thead-dark" >
<tr >
<th *ngFor="let column of columns" >
{{column}}
</th >
</tr >
</thead >
<tbody >
<tr *ngFor="let row of rows" >
<td *ngFor="let column of columns" >
{{row[column]}}
</td >
</tr >
</tbody >
</table >
columns è un array di stringhe per impostare gli header delle colonne mentre rows è un array di oggetti.
I dati ci sono, ma ovviamente manca ancora la possibilità di poter spostare le colonne.
Dragula
Una libreria che permette di aggiungere questa funzionalità con poche modifiche al codice è ng2-dragula che è il wrapper ufficiale della libreria open source dragula.
Di seguito il link alla pagina su github:
https://github.com/valor-software/ng2-dragula
Occorre innanzitutto aggiungerla mediante il comando:
npm install ng2-dragula
ed in seguito aggiungere qualche riga di configurazione per poterla utilizzare.
- Aggiungere (window as any).global = window; al polyfills.ts, file che permette ad Angular di mantenere la compatibilità con i differenti browser;
- Aggiungere DragulaModule.forRoot() nelle imports del app.module.ts;
- Aggiungere nel proprio file .css (es : style.css ) le seguenti regole di stile:
/* in-flight clone */
.gu-mirror {
position: fixed !important;
margin: 0 !important;
z-index: 9999 !important;
opacity: 0.8;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
filter: alpha(opacity=80);
pointer-events: none;
}
/* high-performance display:none; helper */
.gu-hide {
left: -9999px !important;
}
/* added to mirrorContainer (default = body) while dragging */
.gu-unselectable {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
/* added to the source element while its mirror is dragged */
.gu-transit {
opacity: 0.2;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
filter: alpha(opacity=20);
}
La libreria offre la direttiva dragula che, applicata a un contenitore, permette di abilitare il trascinamento per tutti i suoi figli. A tale direttiva è possibile passare una stringa che ne determina il nome del gruppo di appartenenza. In questo modo, è possibile assegnare lo stesso gruppo a più contenitori abilitando di fatto il trascinamento di un elemento tra i contenitori appartenenti allo stesso gruppo.
Per salvare i cambiamenti effettuati dal trascinamento è possibile utilizzare il two-way binding con [(dragulaModel)]=”…” oppure sottoscriversi a DragulaService dropModel.
Ho scelto di utilizzare il primo metodo non dovendo effettuare ulteriori operazioni sui dati.
<table class="table" >
<thead class="thead-dark" >
<tr dragula="table_columns" [(dragulaModel)]="columns" >
<th *ngFor="let column of columns" >
{{column}}
</th >
</tr >
</thead >
<tbody >
<tr *ngFor="let row of rows" >
<td *ngFor="let column of columns" >
{{row[column]}}
</td >
</tr >
</tbody >
</table >
Ora è possibile trascinare una colonna e posizionarla prima o dopo un’altra.
Quando la tabella ha un numero di colonne tali da richiedere l’inserimento in pagina di una scrollbar, il funzionamento della libreria non è quello sperato. Ad esempio, se vogliamo spostare una colonna dalle prime posizioni a una delle ultime, occorre eseguire dei trascinamenti parziali.

Dom-autoscroller
È possibile in questo caso farci aiutare da una libreria open source che si occupi di effettuare uno scroll in automatico in base alla posizione del mouse rispetto all’elemento scrollabile.
Di seguito il link alla repository pubblica: https://github.com/hollowdoor/dom_autoscroller
Installiamo la libreria nel nostro progetto:
npm install dom-autoscroller
Per poter abilitare uno o più elementi allo scroll è necessario aggiungere le seguenti righe di codice
import * as autoScroll from 'dom-autoscroller';
autoScroll([elem], {
margin: 30,
maxSpeed: 10,
scrollWhenOutside: true,
autoScroll() {
return this.down;
}
});
dove elem è l’elemento sulla quale vogliamo applicare il nostro scroll automatico.
Potremmo, quindi, aggiungere nel ngAfterViewInit del nostro component le seguenti righe di codice:
ngAfterViewInit(): void {
const elem = Array.from(this.elemRef.nativeElement
.querySelectorAll('.table-container'));
autoScroll(elem, {
margin: 30,
maxSpeed: 10,
scrollWhenOutside: true,
autoScroll() {
return this.down;
}
});
}
non prima di aver aggiunto tra i parametri del costruttore
private elemRef: ElementRef
che ci permette di ottenere un riferimento al nostro elemento scrollabile.
Adesso il funzionamento è corretto ma possiamo aumentare la riutilizzabilità del nostro codice creando una direttiva.
import { Directive, OnInit, ElementRef } from '@angular/core';
import * as autoScroll from 'dom-autoscroller';
@Directive({ selector: '[appAutoScroll]' })
export class AutoScrollDirective implements OnInit {
constructor(private elemRef: ElementRef) { }
ngOnInit(): void {
autoScroll([this.elemRef.nativeElement], {
margin: 30,
maxSpeed: 10,
scrollWhenOutside: true,
autoScroll() {
return this.down;
}
});
}
}
E poi applicarla al contenitore della tabella:
<div class="text-center mt-5">
<h1>
Welcome to {{ title }}!
</h1>
</div>
<div class="p-5 m-5">
<div class="table-responsive table-container" appAutoScroll>
<table class="table">
<thead class="thead-dark">
<tr dragula="table_columns" [(dragulaModel)]="columns">
<th *ngFor="let column of columns">
{{column}}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of rows">
<td *ngFor="let column of columns">
{{row[column]}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
Tutto il codice, infine, può essere spostato in un componente riutilizzabile ovunque nel progetto. Ad esso passeremo i dati da visualizzare come input.
Il codice della demo è disponibile su github al seguente indirizzo:
https://github.com/AARNOLD87/AngularDraggableTableWithScrollbar
Alla prossima!