较深入地探讨AngularJS实现checkbox全选功能

image

全选功能在网页开发中算是一个极为常见的功能了。尤其在开发系统的管理后台时,维护人员可能要做重复的机械动作,手动更改每一条数据的状态,这时候如果有全选功能,应该会让我们程序同学和策划同学被人背地里吐槽的几率小很多。

一、要实现的需求

一图胜千言,先来看图。

image

再来细化一下功能:

  1. 需要有一个checkbox负责全选和取消全选
  2. 全选后,取消其中一项,全选取消
  3. 取消全选后,再手动选上所有项,全选checkbox自动勾上

二、思路1 给数组中的每一项绑定checked属性

通过checkedArray这个数组,存储已经选中的数组元素,其作用主要是实现上文中的第3项功能,代码如下。

  • HTML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <div ng-controller="myCtrl">
    <label for="select-all">全选
    <input
    type="checkbox"
    id="select-all"
    ng-change="selectAll()"
    ng-model="all" >
    </label>
    <ul>
    <li ng-repeat="item in list">
    <input type="checkbox" ng-model="item.checked" ng-change="selectOne()">
    <span ng-bind="item.id"></span>
    </li>
    </ul>
    </div>
  • JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
var app = angular.module('myApp', []);
app.controller('myCtrl', ['$scope', function($scope){
$scope.list = [
{
id: '001',
},
{
id: '002',
},
{
id: '003',
},
{
id: '004',
},
{
id: '005',
},
{
id: '006',
},
{
id: '007',
}
];
$scope.checkedArray = [];
$scope.selectAll = function() {
if ($scope.all) {
$scope.checkedArray = [];
angular.forEach($scope.list, function(item) {
item.checked = true;
$scope.checkedArray.push(item.id);
});
} else { // 清空全选
$scope.checkedArray = [];
angular.forEach($scope.list, function(item) {
item.checked = false;
});
}
};
$scope.selectOne = function() {
angular.forEach($scope.list, function(item) {
var localIndex = $scope.checkedArray.indexOf(item.id);
// 选中
if (localIndex === -1 && item.checked) {
$scope.checkedArray.push(item.id);
} else if (localIndex !== -1 && !item.checked) { // 取消选中
$scope.checkedArray.splice(localIndex, 1);
}

$scope.all = $scope.list.length === $scope.checkedArray.length;
});
}
}]);

这种思路比较完整地实现了我们需要的功能,但是在实际项目中,数组list中的元素引入了一个可能不需要的属性checked,这种写法引入了脏属性,在表单提交的时候,可能需要额外处理checked属性,构造合法的请求数据格式,引起不必要的麻烦。

三、思路2 ng-true-value等指令的运用

思路阐述:

  1. 根据现有的list数组初始化一个初始化一个数组checkedArray,并在ng-repeat的时候,将其每一项初始化为false
  2. 考虑到要给服务端返回选中的id数组,引入ng-true-value指令,ng-true-value="item.id"
  3. 使用$watchCollection方法监控数组checkedArray变化,并使用数组的every 方法判断是否都不是false,决定全选checkbox的状态。

代码如下。

  • HTML
1
2
3
4
5
6
7
8
9
10
11
12
<div ng-controller="myCtrl">
<label for="select-all">全选
<input type="checkbox" id="select-all" ng-click="selectAll()" ng-model="isAll">
</label>
<ul>
<li ng-repeat="item in list track by $index" ng-init="checkedArray[$index]=false">
<input type="checkbox" ng-model="checkedArray[$index]" ng-true-value="{{item.id}}" ng-false-value="false">
<span ng-bind="item.id"></span>
</li>
</ul>
<input type="button" value="获取所有选中的元素" ng-click="getCheckedList()">
</div>
  • JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var app = angular.module('myApp', []);
app.controller('myCtrl', ['$scope', function($scope) {
$scope.list = [{
'id': 101
}, {
'id': 102
}, {
'id': 103
}, {
'id': 104
}, {
'id': 105
}, {
'id': 106
}, {
'id': 107
}];

$scope.checkedArray = new Array($scope.list.length);

$scope.selectAll = function() {
if ($scope.isAll) {
$scope.checkedArray = $scope.list.map(function(item) {
return item.id;
});
} else {
$scope.checkedArray = $scope.list.map(function(item) {
return false;
});
}
};

$scope.$watchCollection('checkedArray', function(newCollection) {
if (newCollection.every(function(item) {
return item !== false;
})) {
$scope.isAll = true;
} else {
$scope.isAll = false;
}
});

$scope.getCheckedList = function() {
console.log($scope.checkedArray.filter(function(item) {
return item
}));
}

}]);

四、思路3 组合指令

全选是一个很常见的功能,如果能做成指令复用,将会大大提高我们的开发效率。我们依照要实现的功能,拆分成四个指令:commonOperatecheckSinglecheckAlloperateBatch

将公共的方法和属性写在指令commonOperate中,在其他三个指令中require这个指令,完成单选、全选、导出选中项的功能。这种写法的好处是可以嵌入到现有的模板中,使用及其方便快捷。

  • HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<div ng-controller="myCtrl">
<div common-operate>
<table border="1">
<thead>
<tr>
<th><input type="checkbox" check-all> NO.</th>
<th>name</th>
<th>gender</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" check-single="1"> 1</td>
<td>David</td>
<td>male</td>
</tr>
<tr>
<td><input type="checkbox" check-single="2"> 2</td>
<td>Bob</td>
<td>male</td>
</tr>
<tr>
<td><input type="checkbox" check-single="3"> 3</td>
<td>Kate</td>
<td>female</td>
</tr>
<tr>
<td><input type="checkbox" check-single="4"> 4</td>
<td>Xiaoming</td>
<td>male</td>
</tr>
</tbody>
</table>
<button type="button" operate-batch="getBatch">导出选中项目</button>
</div>
</div>
  • JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
var app = angular.module('myApp', []);

app.controller('myCtrl', ['$scope', function($scope) {
$scope.getBatch = function(list) {
console.log(list)
}

}]);

app.directive('commonOperate', function() {
return {
restrict: 'EA',
controller: ['$scope', '$element', '$attrs', '$timeout',
function($scope, $element, $attrs, $timeout) {
var checkedClassName = $attrs['commonOperate'],
checkList = [];
this.scope = $scope;
$scope.checkCommonOperateCheckList = checkList;

// 设置选中状态
this.setCheckedStatus = function(element, needStatus) {
if (checkedClassName) {
if (needStatus) {
element.removeClass(checkedClassName)
.addClass(checkedClassName);
} else {
element.removeClass(checkedClassName);
}
} else {
element.prop('checked', needStatus);
}
};

// 获取点击之前的状态
this.getCheckedStatus = function(element) {
return checkedClassName ? element.hasClass(checkedClassName)
: !element.prop('checked');
};

// 清除选中列表
this.clearCheckList = function() {
checkList = [];
$timeout(function() {
$scope.checkCommonOperateCheckList = checkList;
});
this.setCheckedStatus($element.find('[check-single]'), false);
};

// 获取选中列表
this.getCheckList = function() {
return checkList;
}

// 增加选中列表
this.setCheckList = function(checkId, needStatus) {
var selectAllDOM = $element.find('[check-all]');
if (needStatus) {
checkList.push(checkId);
if (checkList.length === $element.find('[check-single]').length) {
this.setCheckedStatus(selectAllDOM, true);
}
} else {
this.setCheckedStatus(selectAllDOM, false);
var localIndex = checkList.indexOf(checkId);
if (localIndex !== -1) {
checkList.splice(localIndex, 1);
}
}
$timeout(function () {
$scope.espCommonOperateCheckList = checkList;
}, 0);
return checkList;
};

// 全选
this.selectAll = function(needStatus) {
var singleList = $element.find('[check-single]');
this.setCheckedStatus(singleList, needStatus);
if (needStatus) {
checkList = singleList.map(function (item) {
return $(item).attr('check-single');
});
$timeout(function () {
$scope.espCommonOperateCheckList = checkList;
}, 0);
} else {
this.clearCheckList();
}
}

}]
}
});

app.directive('checkSingle', function() {
return {
restrict: 'A',
require: '^commonOperate',
link: function($scope, $element, $attrs, commonOperate) {
var itemId = $attrs['checkSingle'];
var mapValue = $attrs['mapValue'];
$element.click(function(e) {
var needStatus = !commonOperate.getCheckedStatus($element);
commonOperate.setCheckList(itemId, needStatus);
commonOperate.setCheckedStatus($element, needStatus);
});
}
}
});

app.directive('checkAll', function() {
return {
restrict: 'A',
scope: {
reloadPage: '=?',
page: '=?'
},
require: '^commonOperate',
link: function($scope, $element, $attrs, commonOperate) {
$element.click(function (e) {
var needStatus = !commonOperate.getCheckedStatus($element);
commonOperate.selectAll(needStatus);
commonOperate.setCheckedStatus($element, needStatus);
});
$scope.$watch('reloadPage', function () {
commonOperate.setCheckedStatus($element, false);
commonOperate.clearCheckList();
});
$scope.$watch('page', function () {
commonOperate.setCheckedStatus($element, false);
commonOperate.clearCheckList();
});
}
}
});

app.directive('operateBatch', function() {
return {
restrict: 'A',
require: '^commonOperate',
link: function ($scope, $element, $attrs, commonOperate) {
$element.click(function (e) {
var funcName = $attrs['operateBatch'];
if (funcName && commonOperate.scope[funcName]) {
commonOperate.scope[funcName](commonOperate.getCheckList());
}
});
}
}
});

从以上的代码中可以看出,当需要为现有的页面加入全选功能时,只需要将这几个指令嵌入到相关的DOM上即可,极大地提高了其复用性。

最近几天好不容易闲下来,有时间思考一些东西,这篇博客花了两天时间构思,一天时间写内容和demo,第三个思路是参考同事写的东西,整理得来。写之前总觉得自己有千言万语,到了真正动笔的时候,又不知道从哪里下手,真心羡慕那些能写出赏心悦目文字的作者。

加油2017。