一,背景介绍
对于指令,可以把它简单的理解成在特定DOM元素上运行的函数,指令可以扩展这个元素的功能。 例如,一些原生的指令如 ng-disabled , ng-if ,ng-repeat ,ng-click 等。ng-click可以让一个元素能够监听click事件,并在接收到事件的时候执行AngularJS表
达式。正是指令使得AngularJS这个框架变得强大,并且在AngularJs我们可以自己通过directive来创造新的指令。
二、知识剖析
当在我们的项目中需要实现一些功能,比如,时间筛选、分页的功能,我们最先想到的可能是先去网上找找看,有没有相应的插件可以给我们直接拿来使用。但是插件代码一般十分复杂,无法定位bug进行修改,也无法保证修改后不会出现别的bug,用起来可能不太顺手。像一些实现简单功能的插件,我们可以利用AngularJS中的directive自己写一个指令,进行封装,也可以方便以后重复使用。
三、常见问题
怎么将分页封装成指令?
四、解决方案
先了解一下directive自定义指令中都可以设置哪些选项?
app.directive('pagination', function() {
return {
restrict: String,
priority: Number,
terminal: Boolean,
template:string or Template Function
templateUrl: String,
replace: Boolean,
scope: Boolean or Object,
transclude: Boolean,
controller: String or function() { ... },
controllerAs: String,
require: String,
link: function() { ... },
compile: function() { ... }
});
下面来详细讲下各个配置
⑴ restrict:string
可选字符串参数,可以设置这个指令在DOM中以何种形式被声明,默认为A(作为属性)
-
设置为E,为标签
<my-directive></my-directive>
-
设置为C,为类名
<div class="my-directive:expression;"></div>
-
设置为M,为注释
<--directive:my-directive expression-->
-
设置为A,为属性
<div my-directive></div>
这些选项可以单独使用,也可以混合使用
⑵ priority:number
代表指令的优先级,可忽略,默认为0。
⑶ terminal:Boolean
默认为false,如果为true。则停止运行当前元素上比本指令优先级低的指令,优先级相同的指令还是会被执行
⑷ template:string or Template Funciton
指令中重要的属性,必须设置如下一种
-
一段html文本
-
可以接受两个参数的函数
模板中必须只有一个根标签,可以使用ES6模板字符串插入,如下:
template: `
<div class="pagination">
<ul class="pager">
</ul>
<ul class="pager">
</ul>
</div>`,
⑸ temlateUrl:string
当html过长时,可以设置templateUrl引用
接受一个路径形式的字符串,还可以在script标签中写模板
templateUrl: "html/imgload.html",
⑹ replace:Boolean
默认为false,模板内容会加载到标签内部
设置为true的话,模板内容会替换标签内容
⑺ transclude:Boolean
是否使用ng-transclude来包含html中原有的内容,默认为false。
var app = angular.module("app", [])
.directive("hello", function () {
var option = {
restrict: "AECM",
template: "<h3>Hello, Directive</h3><span ng-transclude></span>",
transculde: true //为true,所以原来的内容会被放在有ng-transclude属性的标签内
};
return option;
})
<html>
<head></head>
<body>
<hello>我是原来的内容</hello> ===> 变成如下
<hello><h3>Hello, Directive</h3><span ng-transclude>我是原来的内容</span></hello>
</body>
</html>
如果指令使用了transclude参数,那么在指令的控制器中就无法正常监听数据模型的变化了
⑻ scope:Boolean or Object
默认为false,直接使用父scope,比较容易产生问题。
设置为true,继承父scope,当它的内容发生变化时,不会修改父作用域中的内容。
设置为对象,创建一个新的隔离的scope,仍然可以与父scope通信。
对象中的绑定方式有三种:
-
@为单向绑定,外部scope能影响内部scope,但反过来不成立
-
=为双向绑定,外部scope和内部scope的model能互相改变
-
&为在独立的子作用域中直接调用父作用域的方法,此方法可以是个function
其中比较难理解的是&,如下举例说明
-
首先父scope中定义一个函数
$scope.click = function () { $scope.value = Math.random(); };
-
scope对象中添加一个变量
scope: { action: '&' },
-
模板中定义一个执行方法
template:` <input type="text" ng-click="action()">`
-
html文件中关联起来
<div>随机数{{value}}</div> <pagination action="click()"></pagination>
这样就可以做到,点击模板中的input框时,value值变为一个随机数。
⑼ controller:String or function(scope,element,attrs,transclude,otherInjectables)
自定义指令中的控制器,它是在编译之前执行,所以我们把呈现视图所需要的数据写在控制器中。它和下面的require多用于多个自定义指令的嵌套,即当一个子元素指令需要和父元素指令通信时。在父元素中,添加构造函数时,函数中的参数就是子元素自定义指令中的scope对象,如下
controller:function(){
this.a = function(childDirective){
}
}
在上述代码中,controller属性值对应一个构造函数,在函数中,this代表父元素指令本身,方法a是构造函数中的一个任意方法,在定义这个方法时,形参childDirective就是子元素指令中的scope对象,通过这种方式,父元素可以轻易访问到子元素指令中的scope对象。
⑽ require:String
通俗的说是用于指令之间的相互交流.字符串代表另外一个指令的名字
require会将控制器注入到其值所指定的指令中,并作为当前指令的链接函数的第四个参数。
如果不使用 ^ 前缀,指令只会在自身的元素上查找控制器
使用 ^ 如果添加了 ^ 前缀,指令会在外层寻找相应名称的指令
使用 ? 如果没找到则会将 null 作为传给 link 函数的第四个参数
使用 ^? 将前面两个选项的行为组合起来,我们可选择地加载需要的指令并在父指令链中进行查找举个例子来说:当在子元素指令中添加了require属性,并通过属性值找到父元素指令,那么就可以通过子元素指令中link函数的第4个参数来访问父元素指令中controller属性添加的方法,就因为这个参数是父元素指令的实例。
var app = angular.modeule('myapp',[]);
app.directive('common',function(){
return {
...
controller: function($scope){
this.method1 = function(){
};
this.method2 = function(){
};
},
...
}
});
app.directive('d1',function(){
return {
...
require: '?^common',
link: function(scope,elem,attrs,common){
scope.method1 = common.method1;
..
},
...
}
})
⑾ link: function(scope, iElement, iAttrs) { ... }
用 link 函数创建可以操作DOM的指令,比较常用link函数包含3个主要的参数,
-
其中scope参数表示指令所在的作用域。
-
iElement参数表示指令的元素,该元素可以通过Angular内部封装的jqLite框架进行调用,jqLite框架与jQuery框架在功能上虽然差别很大,但是它却包含了主要的元素操作API,是一个压缩版的jqLite
-
iAttrs参数表示指令元素的属性集合,通过这个参数可以获取元素中的各类属性
⑿compile
与link相比,compile属性的使用要少的多。该属性返回一个函数或对象
compile: function(tElement, tAttrs, transclude) {
return {
pre: function(scope, iElement, iAttrs, controller) { ... },
post: function(scope, iElement, iAttrs, controller) { ... }
}
// 或者
return function postLink(...) { ... }
}
};
设置compile函数的意义在于:在指令和实时数据被放到DOM中之前修改DOM
compile 和 link 选项是互斥的。如果同时设置了这两个选项,那么会把 compile所返回的函数当作链接函数,而 link 选项本身则会被忽略。
通常情况下,如果设置了 compile 函数,说明我们希望在指令和实时数据被放到DOM中之前进行DOM操作,在这个函数中进行诸如添加和删除节点等DOM操作是安全的
通常情况下,如果设置了 compile 函数,说明我们希望在指令和实时数据被放到DOM中之前进行DOM操作,在这个函数中进行诸如添加和删除节点等DOM操作是安全的
五、编码实战
将分页封装成指令
样式如下
自定义指令的选项配置还是很繁杂的,看的有点眼花缭乱,分页并不需要都用到。
下面讲一下我的封装分页过程
①首先写语义化标签,并把需要的变量暴露出来
<pagination page="page" max-page="maxPage" ng-click="pageTo()"></pagination>
依靠当前页和最大页,我们就能写出分页了。至于还需要更改每页数量,输入页数跳转等功能,都还可以再添加
其中pageTo是父控制器绑定的函数,用于执行其余代码,不属于分页指令内部函数
②设置声明方式
app.directive('pagination', function () {
return {
restrict: "E",
restrict可以设置这个指令再DOM中以何种形式被声明。设置为E,表示直接当作标签使用。还可设置为A(当作标签属性),为C(类名),为M(注释)。
③插入模板
template: `
<div class="pagination">
<ul class="pager">
<li ng-class={"disabled":firstPage}><a href="javascript:void(0)" ng-click="pageGo(1)">首页</a></li>
<li ng-class={"disabled":firstPage}><a href="javascript:void(0)" ng-click="pagePre()">上一页</a></li>
</ul>
<ul class="pagination">
<li ng-repeat="num in pageShowList" ng-class="{active:clickPage == num}">
<a href="javascript:void(0)" ng-click="pageGo(num)">{{num}}</a></li>
</ul>
<ul class="pager">
<li ng-class={"disabled":lastPage}><a href="javascript:void(0)" ng-click="pageNext()">下一页</a></li>
<li ng-class={"disabled":lastPage}><a href="javascript:void(0)" ng-click="pageGo(maxPage)">尾页:{{maxPage}}</a></li>
</ul>
</div>`,
//true为替换当前标签,默认false为添加内容到标签
replace: true,
解释一下模板
-
这里就使用了ES6的模板字符串,使用``反引号插入模板。避免写一大堆""和+。
-
class为pagination,引用BS的分页样式
-
因为样式插件为a标签。所以给href设置为javascript:void(0)。设置成点击后什么也不发生。但我发现其实设置为空字符串好像也是一样的""。
-
ng-class之disabled则是为了在第一页时禁用首页和上一页,在最后一页时禁用下一页和尾页。
-
ng-class之clickPage时为了选择页数时设置选中状态
-
最后的replace:true,替换标签,但会保留原本html标签里的属性
④监听当前页数
//link函数
link: function ($scope) {
var pageList = [];
$scope.page = 1; //初始默认为第一页
$scope.pageShowList = []; //最大显示
//监听当前页数,控制可选不可选
var watch = $scope.$watch('page',function(newValue){
console.log('newValue', newValue);
if(newValue == 1){
$scope.firstPage = true;
}else{
$scope.firstPage = false;
};
if(newValue == $scope.maxPage){
$scope.lastPage = true;
}else{
$scope.lastPage = false;
}
})
通过监听当前页数,控制首页上一页及尾页下一页的可选取样式状态。
⑤监听最大页数
//监听最大页数
var watch = $scope.$watch('maxPage', function (newValue, oldValue, scope) {
pageList = [];
for (let i = 1; i <= newValue; i++) {
pageList.push(i);
}
resetPageOrder($scope.page);
});
通过监听最大页数,初始化分页数组,并且能做到实时改变总页数时,分页数组也相应生成。
⑥设置基本功能
跳页和上一页下一页的功能。每次改变页数的时候,都重新设置一下页码数组。也可以在中间设···
//直接跳页
$scope.pageGo = function (num) {
$scope.page = num;
resetPageOrder($scope.page);
};
//上一页
$scope.pagePre = function () {
if ($scope.page > 1) {
$scope.page--;
console.log('$scope.pageA', $scope.page);
resetPageOrder($scope.page)
}
};
//下一页
$scope.pageNext = function () {
if ($scope.page < $scope.maxPage) {
$scope.page++;
console.log('$scope.pageA', $scope.page);
resetPageOrder($scope.page);
}
};
//重新设置页码
function resetPageOrder(num) {
$scope.clickPage = num;
//当前页小于4
if (num < 4) {
$scope.pageShowList = pageList.slice(0, 7);
}
//当前页大于等于4
else {
if (num > $scope.maxPage - 4) {
if(($scope.maxPage - 7) < 0){
var zero = 0;
}
$scope.pageShowList = pageList.slice(zero, $scope.maxPage);
} else {
$scope.pageShowList = [
num - 3,
num - 2,
num - 1,
num,
num + 1,
num + 2,
num + 3
重新设置页码的意思是,当选择的页面大于等于4的时候,并且后续页数大于4页时,就设置为页码数组为当前页及它的前后三页。
⑦获取数据
接下来就要在请求列表数据的回调函数中获取一些需要的数据,
这里我是从一个接口获取一个列表数据,返回数据为res
//以下为分页插件内容
$scope.size = res.data.data.size; //每页的tr数量
$scope.totalItems = res.data.data.total; //tr总数量
//设置当前页
if ($stateParams.page) {
$scope.page = parseInt($stateParams.page);
} else {
$scope.page = 1;
}
//计算总页数
$scope.maxPage = Math.ceil($scope.totalItems / $scope.size);
console.log('$scope.maxPage', $scope.maxPage);
//设置每页数或跳转页数
$scope.pageTo = function () {
$scope.showPage = $scope.page;
console.log('$scope.showPage', $scope.showPage);
$state.go("PageTab.article", { //state.go传参
page: $scope.showPage, //页数跳转至当前页
})
};
⑧增加功能
我这里增加的是跳转页数和设置每页数功能
html里添加
<p>每页数
<input type="text" ng-model="size"/>
</p>
<div>去第
<input type="text" ng-model="goPage">页
</div>
<button type="button" ng-click="pageSure(goPage,size)">确定</button>
js加上一个确定按钮的点击事件
//确定页数按钮的点击事件
$scope.pageSure = function (page = 1, size = 10) {
if (size == '') {
size = 10;
}
if (page > Math.ceil($scope.totalItems / size)) {
page = Math.ceil($scope.totalItems / size)
}
$state.go('home.article', {
page: page - 1,
size: size
}, {
reload: true
})
};
这样基本就完成了
六、拓展思考
分页功能还可以怎么做?
七、参考文献
八、更多讨论
Q1:分页时数字怎么实时变化?
A1:通过标签里的ng-class改变样式
<li ng-repeat="num in pageShowList" ng-class="{active:clickPage == num}">
然后在重设页面的函数中,把num赋给clickPage
function resetPageOrder(num) {
scope.clickPage = num;
Q2:设置transclude为什么在控制器中就不能正常监听数据模型的变化了?
A2:具体原因我也不清楚,没试过。但是link中反正是可以监听的。
Q3:为什么compile和link不能同时使用?
A3:如果同时设置了这两个选项,那么会把 compile所返回的函数当作链接函数,而 link 选项本身则会被忽略。