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:
Kelly Domico
2015-03-31 13:39:17 -07:00
parent fc963b9647
commit c52a68cc85
2 changed files with 231 additions and 76 deletions

View File

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

View File

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