Add hzSelect directive to Angular table
This patch adds a hzSelect directive to Angular table. The directive should be set as an attribute on each row's select checkbox. If `hzSelectAll` is defined, if all checkboxes in the table is selected, the select all checkbox will be selected. Change-Id: Id1db39e617c7bcd57ef8a33f3f9c8ca71950491a
This commit is contained in:
144
horizon/static/angular/table/table.js
vendored
144
horizon/static/angular/table/table.js
vendored
@@ -18,6 +18,7 @@
|
||||
* | Directives |
|
||||
* |-------------------------------------------------------------------|
|
||||
* | {@link hz.widget.table.directive:hzTable `hzTable`} |
|
||||
* | {@link hz.widget.table.directive:hzSelect `hzSelect`} |
|
||||
* | {@link hz.widget.table.directive:hzSelectAll `hzSelectAll`} |
|
||||
* | {@link hz.widget.table.directive:hzExpandDetail `hzExpandDetail`} |
|
||||
*
|
||||
@@ -42,29 +43,34 @@
|
||||
* @description
|
||||
* The `hzTable` directive extends the Smart-Table module to provide
|
||||
* support for saving the checkbox selection state of each row in the
|
||||
* table. Also included is the `updateSelectCount` function which
|
||||
* updates the checkbox selection count of the table. A default sort
|
||||
* key can be specified to sort the table initially by this key. To
|
||||
* reverse the sort, add default-sort-reverse='true' as well.
|
||||
* table. A default sort key can be specified to sort the table
|
||||
* initially by this key. To reverse, add default-sort-reverse='true'
|
||||
* as well.
|
||||
*
|
||||
* Required: Use `st-table` attribute to pass in the displayed
|
||||
* row collection and `st-safe-src` attribute to pass in the
|
||||
* safe row collection.
|
||||
*
|
||||
* @restrict A
|
||||
* @scope true
|
||||
* @example
|
||||
*
|
||||
* ```
|
||||
* <table st-table='rowCollection' hz-table default-sort="email">
|
||||
* <table st-table='displayedCollection' st-safe-src='rowCollection'
|
||||
* hz-table default-sort="email">
|
||||
* <thead>
|
||||
* <tr>
|
||||
* <th><input type='checkbox' hz-select-all='rowCollection'/></th>
|
||||
* <th>
|
||||
* <input type='checkbox' hz-select-all='displayedCollection'/>
|
||||
* </th>
|
||||
* <th>Name</th>
|
||||
* </tr>
|
||||
* </thead>
|
||||
* <tbody>
|
||||
* <tr ng-repeat="row in rowCollection">
|
||||
* <tr ng-repeat="row in displayedCollection">
|
||||
* <td>
|
||||
* <input type='checkbox'
|
||||
* ng-model='selected[row.id].checked'
|
||||
* ng-change='updateSelectCount(row)'/>
|
||||
* <input type='checkbox' hz-select='row'
|
||||
* ng-model='selected[row.id].checked'/>
|
||||
* </td>
|
||||
* <td>Foo</td>
|
||||
* </tr>
|
||||
@@ -82,37 +88,31 @@
|
||||
$scope.selected = {};
|
||||
$scope.numSelected = 0;
|
||||
|
||||
$scope.updateSelectCount = function(row) {
|
||||
if ($scope.selected.hasOwnProperty(row.id)) {
|
||||
if ($scope.selected[row.id].checked){
|
||||
$scope.numSelected++;
|
||||
}
|
||||
else {
|
||||
$scope.numSelected--;
|
||||
}
|
||||
}
|
||||
// return true if the row is selected
|
||||
this.isSelected = function(row) {
|
||||
var rowState = $scope.selected[row.id];
|
||||
return angular.isDefined(rowState) && rowState.checked;
|
||||
};
|
||||
|
||||
this.select = function(row, checkedState) {
|
||||
var oldCheckedState = $scope.selected.hasOwnProperty(row.id) ?
|
||||
$scope.selected[row.id].checked :
|
||||
false;
|
||||
|
||||
// set the row selection state
|
||||
this.select = function(row, checkedState, broadcast) {
|
||||
$scope.selected[row.id] = {
|
||||
checked: checkedState,
|
||||
item: row
|
||||
};
|
||||
|
||||
if (checkedState && !oldCheckedState) {
|
||||
if (checkedState) {
|
||||
$scope.numSelected++;
|
||||
} else if (!checkedState && oldCheckedState) {
|
||||
} else {
|
||||
$scope.numSelected--;
|
||||
}
|
||||
};
|
||||
|
||||
this.isSelected = function(row) {
|
||||
var rowState = $scope.selected[row.id];
|
||||
return rowState && rowState.checked;
|
||||
if (broadcast) {
|
||||
// should only walk down scope tree that has
|
||||
// matching event bindings
|
||||
var rowObj = { row: row, checkedState: checkedState };
|
||||
$scope.$broadcast('hzTable:rowSelected', rowObj);
|
||||
}
|
||||
};
|
||||
},
|
||||
link: function(scope, element, attrs, stTableCtrl) {
|
||||
@@ -124,6 +124,51 @@
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name hz.widget.table.directive:hzSelect
|
||||
* @element input type='checkbox'
|
||||
* @description
|
||||
* The `hzSelect` directive updates the checkbox selection state of
|
||||
* the specified row in the table. Assign this as an attribute to a
|
||||
* checkbox input element, passing in the row.
|
||||
*
|
||||
* @restrict A
|
||||
* @scope
|
||||
* @example
|
||||
*
|
||||
* ```
|
||||
* <tr ng-repeat="row in displayedCollection">
|
||||
* <td>
|
||||
* <input type='checkbox' hz-select='row'/>
|
||||
* </td>
|
||||
* </tr>
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
app.directive('hzSelect', [ function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: '^hzTable',
|
||||
scope: {
|
||||
row: '=hzSelect'
|
||||
},
|
||||
link: function (scope, element, attrs, hzTableCtrl) {
|
||||
// select or unselect row
|
||||
function clickHandler() {
|
||||
scope.$apply(function() {
|
||||
scope.$evalAsync(function() {
|
||||
var checkedState = element.prop('checked');
|
||||
hzTableCtrl.select(scope.row, checkedState, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
element.click(clickHandler);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name hz.widget.table.directive:hzSelectAll
|
||||
@@ -137,16 +182,33 @@
|
||||
* row collection and `st-safe-src` attribute to pass in the
|
||||
* safe row collection.
|
||||
*
|
||||
* Define a `ng-model` attribute on the individual row checkboxes
|
||||
* so that they will be updated when the select all checkbox is
|
||||
* clicked. The `hzTable` controller provides a `selected` object
|
||||
* which stores the checked state of the row.
|
||||
*
|
||||
* @restrict A
|
||||
* @scope rows: '=hzSelectAll'
|
||||
* @scope
|
||||
* @example
|
||||
*
|
||||
* ```
|
||||
* <input type='checkbox' hz-select-all='displayedCollection'/>
|
||||
* <thead>
|
||||
* <th>
|
||||
* <input type='checkbox' hz-select-all='displayedCollection'/>
|
||||
* </th>
|
||||
* </thead>
|
||||
* <tbody>
|
||||
* <tr ng-repeat="row in displayedCollection">
|
||||
* <td>
|
||||
* <input type='checkbox' hz-select='row'
|
||||
* ng-model='selected[row.id].checked'/>
|
||||
* </td>
|
||||
* </tr>
|
||||
* </tbody>
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
app.directive('hzSelectAll', [ '$timeout', function($timeout) {
|
||||
app.directive('hzSelectAll', [ function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: [ '^hzTable', '^stTable' ],
|
||||
@@ -159,10 +221,15 @@
|
||||
|
||||
// select or unselect all
|
||||
function clickHandler() {
|
||||
$timeout(function() {
|
||||
var checkedState = element.prop('checked');
|
||||
angular.forEach(scope.rows, function(row) {
|
||||
hzTableCtrl.select(row, checkedState);
|
||||
scope.$apply(function() {
|
||||
scope.$evalAsync(function() {
|
||||
var checkedState = element.prop('checked');
|
||||
angular.forEach(scope.rows, function(row) {
|
||||
var selected = hzTableCtrl.isSelected(row);
|
||||
if (selected !== checkedState) {
|
||||
hzTableCtrl.select(row, checkedState);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -188,6 +255,9 @@
|
||||
|
||||
// watch the row length for add/removed rows
|
||||
scope.$watch('rows.length', updateSelectAll);
|
||||
|
||||
// watch for row selection
|
||||
scope.$on('hzTable:rowSelected', updateSelectAll);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
describe('table directives', function () {
|
||||
|
||||
var $scope, $timeout, $element;
|
||||
var $scope, $element;
|
||||
|
||||
beforeEach(module('smart-table'));
|
||||
beforeEach(module('hz'));
|
||||
@@ -20,7 +20,6 @@
|
||||
beforeEach(inject(function($injector) {
|
||||
var $compile = $injector.get('$compile');
|
||||
$scope = $injector.get('$rootScope').$new();
|
||||
$timeout = $injector.get('$timeout');
|
||||
|
||||
$scope.safeFakeData = [
|
||||
{ id: '1', animal: 'cat' },
|
||||
@@ -31,27 +30,26 @@
|
||||
$scope.fakeData = [];
|
||||
|
||||
var markup =
|
||||
'<table st-table="fakeData" st-safe-src="safeFakeData" hz-table>' +
|
||||
'<thead>' +
|
||||
'<tr>' +
|
||||
'<th><input type="checkbox" hz-select-all="fakeData" ' +
|
||||
'ng-checked="numSelected === fakeData.length"/></th>' +
|
||||
'<th></th>' +
|
||||
'<th>Animal</th>' +
|
||||
'</tr>' +
|
||||
'</thead>' +
|
||||
'<tbody>' +
|
||||
'<tr ng-repeat-start="row in fakeData">' +
|
||||
'<td><input type="checkbox" ng-model="selected[row.id].checked" ' +
|
||||
'ng-change="updateSelectCount(row)"/></td>' +
|
||||
'<td><i class="fa fa-chevron-right" hz-expand-detail></i></td>' +
|
||||
'<td>{{ row.animal }}</td>' +
|
||||
'</tr>' +
|
||||
'<tr class="detail-row" ng-repeat-end>' +
|
||||
'<td class="detail" colspan="3"></td>' +
|
||||
'</tr>' +
|
||||
'</tbody>' +
|
||||
'</table>';
|
||||
'<table st-table="fakeData" st-safe-src="safeFakeData" hz-table>' +
|
||||
'<thead>' +
|
||||
'<tr>' +
|
||||
'<th><input type="checkbox" hz-select-all="fakeData"/></th>' +
|
||||
'<th></th>' +
|
||||
'<th>Animal</th>' +
|
||||
'</tr>' +
|
||||
'</thead>' +
|
||||
'<tbody>' +
|
||||
'<tr ng-repeat-start="row in fakeData">' +
|
||||
'<td><input type="checkbox" hz-select="row" ' +
|
||||
'ng-model="selected[row.id].checked"></td>' +
|
||||
'<td><i class="fa fa-chevron-right" hz-expand-detail></i></td>' +
|
||||
'<td>{{ row.animal }}</td>' +
|
||||
'</tr>' +
|
||||
'<tr class="detail-row" ng-repeat-end>' +
|
||||
'<td class="detail" colspan="3"></td>' +
|
||||
'</tr>' +
|
||||
'</tbody>' +
|
||||
'</table>';
|
||||
|
||||
$element = angular.element(markup);
|
||||
$compile($element)($scope);
|
||||
@@ -70,49 +68,136 @@
|
||||
});
|
||||
|
||||
it('should have each checkbox initially unchecked', function() {
|
||||
var checkboxes = $element.find('tbody tr[ng-repeat-start] input[type="checkbox"]');
|
||||
var checkboxes = $element.find('input[hz-select]');
|
||||
angular.forEach(checkboxes, function(checkbox) {
|
||||
expect(checkbox.checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have numSelected === 1 when first checkbox is clicked', function() {
|
||||
var firstInput = $element.find('tbody input[type="checkbox"]').first();
|
||||
var ngModelCtrl = firstInput.controller('ngModel');
|
||||
ngModelCtrl.$setViewValue(true);
|
||||
it('should return false when calling isSelected for each row', function() {
|
||||
var hzTableCtrl = $element.controller('hzTable');
|
||||
angular.forEach($scope.safeFakeData, function(row) {
|
||||
expect(hzTableCtrl.isSelected(row)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$digest();
|
||||
it('should update selected and numSelected when select called', function() {
|
||||
var hzTableCtrl = $element.controller('hzTable');
|
||||
var firstRow = $scope.safeFakeData[0];
|
||||
hzTableCtrl.select(firstRow, true);
|
||||
|
||||
var hzTableScope = $element.scope();
|
||||
expect(hzTableScope.selected[firstRow.id]).toBeDefined();
|
||||
expect(hzTableScope.numSelected).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('hzSelect directive', function() {
|
||||
|
||||
it('should have numSelected === 1 when first checkbox is clicked', function() {
|
||||
var checkbox = $element.find('input[hz-select]').first();
|
||||
checkbox[0].checked = true;
|
||||
checkbox.triggerHandler('click');
|
||||
|
||||
expect($element.scope().numSelected).toBe(1);
|
||||
});
|
||||
|
||||
it('should have numSelected === 3 and select-all checkbox checked when all rows selected', function() {
|
||||
var checkboxes = $element.find('tbody input[type="checkbox"]');
|
||||
it('should have numSelected === 0 when first checkbox is clicked, then unclicked', function() {
|
||||
var checkbox = $element.find('input[hz-select]').first();
|
||||
checkbox[0].checked = true;
|
||||
checkbox.triggerHandler('click');
|
||||
|
||||
expect($element.scope().numSelected).toBe(1);
|
||||
|
||||
checkbox[0].checked = false;
|
||||
checkbox.triggerHandler('click');
|
||||
|
||||
expect($element.scope().numSelected).toBe(0);
|
||||
});
|
||||
|
||||
it('should have numSelected === 3 and select-all checked when all rows selected', function() {
|
||||
var checkboxes = $element.find('input[hz-select]');
|
||||
angular.forEach(checkboxes, function(checkbox) {
|
||||
checkbox.checked = true;
|
||||
var ngModelCtrl = angular.element(checkbox).controller('ngModel');
|
||||
ngModelCtrl.$setViewValue(true);
|
||||
angular.element(checkbox).triggerHandler('click');
|
||||
});
|
||||
|
||||
$scope.$digest();
|
||||
|
||||
expect($element.scope().numSelected).toBe(3);
|
||||
expect($element.find('thead input[hz-select-all]')[0].checked).toBe(true);
|
||||
expect($element.find('input[hz-select-all]')[0].checked).toBe(true);
|
||||
});
|
||||
|
||||
it('should have select-all unchecked when all rows selected, then one deselected', function() {
|
||||
var checkboxes = $element.find('input[hz-select]');
|
||||
angular.forEach(checkboxes, function(checkbox) {
|
||||
checkbox.checked = true;
|
||||
angular.element(checkbox).triggerHandler('click');
|
||||
});
|
||||
|
||||
// all checkboxes selected so check-all should be checked
|
||||
expect($element.scope().numSelected).toBe(3);
|
||||
expect($element.find('input[hz-select-all]')[0].checked).toBe(true);
|
||||
|
||||
// deselect one checkbox
|
||||
var firstCheckbox = checkboxes.first();
|
||||
firstCheckbox[0].checked = false;
|
||||
firstCheckbox.triggerHandler('click');
|
||||
|
||||
// check-all should be unchecked
|
||||
expect($element.scope().numSelected).toBe(2);
|
||||
expect($element.find('input[hz-select-all]')[0].checked).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('hzSelectAll directive', function() {
|
||||
|
||||
it('should select all checkboxes if select-all checkbox checked', function() {
|
||||
it('should select all checkboxes if select-all checked', function() {
|
||||
var selectAll = $element.find('input[hz-select-all]').first();
|
||||
selectAll[0].checked = true;
|
||||
selectAll.triggerHandler('click');
|
||||
|
||||
$timeout.flush();
|
||||
expect($element.scope().numSelected).toBe(3);
|
||||
var checkboxes = $element.find('tbody input[hz-select]');
|
||||
angular.forEach(checkboxes, function(checkbox) {
|
||||
expect(checkbox.checked).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should deselect all checkboxes if select-all checked, then unchecked', function() {
|
||||
var selectAll = $element.find('input[hz-select-all]').first();
|
||||
selectAll[0].checked = true;
|
||||
selectAll.triggerHandler('click');
|
||||
|
||||
var checkboxes = $element.find('tbody input[hz-select]');
|
||||
|
||||
expect($element.scope().numSelected).toBe(3);
|
||||
var checkboxes = $element.find('tbody input[type="checkbox"]');
|
||||
angular.forEach(checkboxes, function(checkbox) {
|
||||
expect(checkbox.checked).toBe(true);
|
||||
});
|
||||
|
||||
selectAll[0].checked = false;
|
||||
selectAll.triggerHandler('click');
|
||||
|
||||
expect($element.scope().numSelected).toBe(0);
|
||||
angular.forEach(checkboxes, function(checkbox) {
|
||||
expect(checkbox.checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should select all checkboxes if select-all checked with one row selected', function() {
|
||||
// select the first checkbox
|
||||
var checkbox = $element.find('input[hz-select]').first();
|
||||
checkbox[0].checked = true;
|
||||
checkbox.triggerHandler('click');
|
||||
|
||||
// now click select-all checkbox
|
||||
var selectAll = $element.find('input[hz-select-all]').first();
|
||||
selectAll[0].checked = true;
|
||||
selectAll.triggerHandler('click');
|
||||
|
||||
expect($element.scope().numSelected).toBe(3);
|
||||
var checkboxes = $element.find('tbody input[hz-select]');
|
||||
angular.forEach(checkboxes, function(checkbox) {
|
||||
expect(checkbox.checked).toBe(true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user