Tutte le soluzioni proposte fino ad oggi hanno un unico problema comune. Le direttive non sono riutilizzabili, richiedono la conoscenza delle variabili create nell'oggetto $ parent fornito dal controller. Ciò significa che se si volesse utilizzare la stessa direttiva in una vista diversa, sarebbe necessario implementare nuovamente tutto ciò che si è fatto con il controller precedente e assicurarsi di utilizzare gli stessi nomi di variabili per le cose, poiché le direttive hanno fondamentalmente nomi di variabili $ scope con hard coding in loro. Sicuramente non sarebbe possibile utilizzare la stessa direttiva due volte all'interno dello stesso ambito genitore.
Il modo per aggirare questo è utilizzare l'ambito isolato nella direttiva. In questo modo è possibile rendere riutilizzabile la direttiva indipendentemente dall'ambito $ padre mediante la parametrizzazione generica degli elementi richiesti dall'ambito principale.
Nella mia soluzione l'unica cosa che il controller deve fare è fornire una variabileIndice selezionata che la direttiva usa per tracciare quale riga della tabella è attualmente selezionata. Avrei potuto isolare la responsabilità di questa variabile alla direttiva ma facendo in modo che il controllore fornisse la variabile che consente di manipolare la riga attualmente selezionata nella tabella all'esterno della direttiva. Ad esempio, è possibile implementare "sul clic selezionare la riga" nel controller mentre si utilizzano ancora i tasti freccia per la navigazione nella direttiva.
La direttiva:
angular
.module('myApp')
.directive('cdArrowTable', cdArrowTable);
.directive('cdArrowRow', cdArrowRow);
function cdArrowTable() {
return {
restrict:'A',
scope: {
collection: '=cdArrowTable',
selectedIndex: '=selectedIndex',
onEnter: '&onEnter'
},
link: function(scope, element, attrs, ctrl) {
// Ensure the selectedIndex doesn't fall outside the collection
scope.$watch('collection.length', function(newValue, oldValue) {
if (scope.selectedIndex > newValue - 1) {
scope.selectedIndex = newValue - 1;
} else if (oldValue <= 0) {
scope.selectedIndex = 0;
}
});
element.bind('keydown', function(e) {
if (e.keyCode == 38) { // Up Arrow
if (scope.selectedIndex == 0) {
return;
}
scope.selectedIndex--;
e.preventDefault();
} else if (e.keyCode == 40) { // Down Arrow
if (scope.selectedIndex == scope.collection.length - 1) {
return;
}
scope.selectedIndex++;
e.preventDefault();
} else if (e.keyCode == 13) { // Enter
if (scope.selectedIndex >= 0) {
scope.collection[scope.selectedIndex].wasHit = true;
scope.onEnter({row: scope.collection[scope.selectedIndex]});
}
e.preventDefault();
}
scope.$apply();
});
}
};
}
function cdArrowRow($timeout) {
return {
restrict: 'A',
scope: {
row: '=cdArrowRow',
selectedIndex: '=selectedIndex',
rowIndex: '=rowIndex',
selectedClass: '=selectedClass',
enterClass: '=enterClass',
enterDuration: '=enterDuration' // milliseconds
},
link: function(scope, element, attrs, ctr) {
// Apply provided CSS class to row for provided duration
scope.$watch('row.wasHit', function(newValue) {
if (newValue === true) {
element.addClass(scope.enterClass);
$timeout(function() { scope.row.wasHit = false;}, scope.enterDuration);
} else {
element.removeClass(scope.enterClass);
}
});
// Apply/remove provided CSS class to the row if it is the selected row.
scope.$watch('selectedIndex', function(newValue, oldValue) {
if (newValue === scope.rowIndex) {
element.addClass(scope.selectedClass);
} else if (oldValue === scope.rowIndex) {
element.removeClass(scope.selectedClass);
}
});
// Handles applying/removing selected CSS class when the collection data is filtered.
scope.$watch('rowIndex', function(newValue, oldValue) {
if (newValue === scope.selectedIndex) {
element.addClass(scope.selectedClass);
} else if (oldValue === scope.selectedIndex) {
element.removeClass(scope.selectedClass);
}
});
}
}
}
La presente direttiva non solo permette di navigare una tabella utilizzando i tasti freccia, ma consente di associare un metodo di callback per il tasto Invio. In questo modo, quando viene premuto il tasto Invio, la riga attualmente selezionata verrà inclusa come argomento per il metodo di callback registrato con la direttiva (onEnter).
Come un piccolo vantaggio aggiuntivo è anche possibile passare una classe CSS e la durata alla direttiva cdArrowRow in modo che quando la chiave di invio viene colpita su una riga selezionata la classe CSS passata verrà applicata all'elemento riga quindi rimosso dopo la durata passata (in millisecondi). In pratica, ciò ti consente di fare qualcosa come fare in modo che la riga lampeggi di un colore diverso quando viene premuto il tasto Invio.
View Usage:
<table cd-arrow-table="displayedCollection"
selected-index="selectedIndex"
on-enter="addToDB(row)">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in displayedCollection"
cd-arrow-row="row"
selected-index="selectedIndex"
row-index="$index"
selected-class="'mySelcetedClass'"
enter-class="'myEnterClass'"
enter-duration="150"
>
<td>{{row.firstName}}</td>
<td>{{row.lastName}}</td>
</tr>
</tbody>
</table>
Controller:
angular
.module('myApp')
.controller('MyController', myController);
function myController($scope) {
$scope.selectedIndex = 0;
$scope.displayedCollection = [
{firstName:"John", lastName: "Smith"},
{firstName:"Jane", lastName: "Doe"}
];
$scope.addToDB;
function addToDB(item) {
// Do stuff with the row data
}
}
È un buon approccio, ma non funziona quando le voci sono ordinate ('registra nei record | orderBy: '-name''). Hai una soluzione anche per questo? (non solo per questo caso ma anche più generico) – akirk
Grazie per il feedback. È sempre divertente e piacevole essere sfidati con casi d'uso più difficili. Aggiungo il codice aggiuntivo che supporta l'elenco ordinato/filtrato. – Tosh
Grazie! La tua soluzione è stata davvero d'ispirazione. – akirk