通过angularjs的directive以及service来实现的列表页加载排序分页
前两篇:(列表页的动态条件搜索,我是如何做列表页的)分别介绍了我们是如何做后端业务系统数据展示类的列表页以及动态搜索的,那么还剩下最重要的一项:数据展示。数据展示一般包含三部分:
- 数据列头
- 数据行
- 分页统计信息,分页导航
技术依赖项:基于angularjs的MVVM模式,后台是spring mvc。
数据表格需求:
- 需要支持列头的排序
- 需要支持单页操作,局部更新(angular model更新),比如更新某行数据成功后,自动更新当前行的数据,而不需要刷新页面或者另外请求后台数据。
- 需要支持数据逻辑运算以及复杂的html格式的数据,比如根据不同的数据值展示不同的按钮,文本等,展示的文本需要经过特定的逻辑运算等。
- 需要有统计信息以及分页展示
第三方的组件:
- jquery.datatable http://www.datatables.net/
最终展示的结果是html table,优点是支持嵌套表格等复杂功能,缺点是加载的文件大,且不能满足上面的需求angular model更新,对于复杂数据展示控制起来也比较复杂,需要额外的js编码工作。
- angular ui-grid https://github.com/angular-ui/ui-grid/wiki/Getting-started
最终展示的结果是div,与angularjs兼容性很好,能支持表格在线编辑,缺点同样也是不能满足上面的需求angular model更新,对于复杂数据展示控制起来也比较复杂,需要额外的js编码工作。
第三方组件的特点就是功能多,但有些看起来很高级的功能我们基本上都不用,比如在线表格数据的编辑,嵌套表格等。我们必须的功能只有这些:排序,数据展示,分页,如果能支持angular model更新更好。所以我决定结合html table,angularjs来完成上面的需求。
数据加载:
由于我们的数据动态查询方案的存在,决定了大部分页面前后台交互的模式是相同的,所以采用angular service来提供一个listService供列表页使用。主要是分页大小的选择框,以及定义了一些可配置的参数,比如执行查询的请求地址,由于我们的动态查询以表单提交,所以是将整个表单参数序列化之后再加上分页相关信息然后提交到后端查询。
angular.module('app.service', ['app.constant']) .service('$listService', function () { var $scopeLocal = {}; var pageSizeList = [{ "text": "10", "value": "10" }, { "text": "20", "value": "20" }]; var defaultOptions = { beforeSend: function () { }, callback: function ($scope, data) { }, error: function () { }, pageSize: pageSizeList[0].value, searchFormId: "searchForm", }; this.init = function ($scope, option) { var options = $.extend({}, defaultOptions, option); $scopeLocal = $scope; $scopeLocal.pageSizeList = pageSizeList; $scopeLocal.pageRequest = { "pageNum": 1, "pageSize": options.pageSize }; $scopeLocal.pageRequest.getResponse = function (orderBy) { var requestData = $("#" + options.searchFormId + "").serialize(); var url = options.listUrl + "?" + requestData + "&pageNum=" + $scopeLocal.pageRequest.pageNum + "&pageSize=" + $scopeLocal.pageRequest.pageSize; if(angular.isDefined($scopeLocal.pageRequest.orderBy)&&$scopeLocal.pageRequest.orderBy!=""){ url+="&orderBy="+$scopeLocal.pageRequest.orderBy; } $.ajax({ type: "POST", url: url, dataType: 'json', async: false, beforeSend: options.beforeSend, error: options.error, success: function (data) { $scopeLocal.pageResponse = data; $scopeLocal.content = data.list; options.callback($scopeLocal, data); } }); }; this.get = function () { $scopeLocal.pageRequest.getResponse(); }; }; })
使用时,只需要注入listService,然后配置上参数即可:
mainApp.controller('manageCtrl', function ($scope, $http, $listService) { var options = { listUrl:"<c:url value="/theme/getAllByPage"/>" }; $listService.init($scope, options); $listService.get(); });
数据排序:
列头排序的方案是在列头上增加排序字段,通过angular directive来实现。排序的图片以及变换的样式是从jquery.datatable借鉴过来,逻辑并不复杂,无非就是显示排序标签以及根据用户的点击变换排序图标。
angular.module('app.directives', []).directive("sortName", [ function() { return { restict : "A", link : function(scope, element, attrs) { var sortName = attrs["sortName"]; var sortType = attrs["sortType"]; if (!angular.isString(sortName) || sortName == "") return; if (!angular.isString(sortType) || sortType == "") { element.removeClass("sorting").removeClass("sorting_asc").removeClass("sorting_desc").addClass("sorting").attr("sort-type","asc"); } $(element).bind("click", function(){ var thisObj=$(this); var sortType = thisObj.attr("sort-type"); if (!angular.isString(sortName) || sortName == "") return; if (!angular.isString(sortType) || sortType == "") return; var orderBy = sortName + " " + sortType; scope.pageRequest.orderBy=orderBy; scope.pageRequest.getResponse(); if (sortType == "asc") { thisObj.removeClass("sorting").removeClass("sorting_asc").removeClass("sorting_desc").addClass("sorting_asc").attr("sort-type","desc"); } else if (sortType == "desc") { thisObj.removeClass("sorting").removeClass("sorting_asc").removeClass("sorting_desc").addClass("sorting_desc").attr("sort-type","asc"); } thisObj.siblings().each(function (){ var item=$(this); if(typeof(item.attr("sort-name"))!="undefined"){ item.removeClass("sorting").removeClass("sorting_asc").removeClass("sorting_desc").addClass("sorting"); } }); scope.$apply(); }); } } } ]);
一个scope作用域的问题,在directive中获得的scope比较特殊,它的值变更不会影响外层页面上的$scope。因为我们需要在用户点击排序按钮后进行数据更新,所以我们需要调用$apply方法将scope的变化传播出去。
html中只需要在列头指定排序字段即可实现排序功能:sort-name,值是需要排序的字段:
<th class="col-md-1" sort-name ="id">编号</th> <th class="col-md-4" sort-name ="name">名称</th>
数据展示:
直接在页面中采用table来布局,数据行采用angularjs来做加载。table布局的优点在于:直观上很清晰,处理某些特殊数据行时也比较容易,重要的是能够很容易的支持angular model 更新。
<table id="datatableTheme" cellpadding="0" cellspacing="0" border="0" class="datatable table table-striped table-bordered table-hover"> <thead> <tr> <th>编号</th> <th>名称</th> <th>状态</th> <th>描述</th> <th>操作</th> </tr> </thead> <tbody> <tr ng-repeat="item in content"> <td ng-bind="item.id"></td> <td ng-bind="item.name"></td> <td> <div ng-show="item.status=='1'"> <span class="label label-success">启用</span> </div> <div ng-show="item.status =='0'"> <span class="label label-danger">禁用</span> </div> </td> <td ng-bind="item.description"></td> <td> <a href="javascript:void(0)" data-toggle="modal" ng-click="edit(item.id)" data-original-title="编辑"> <span class="label label-primary">编辑</span> </a> <a href="javascript:void(0)" ng-show="item.status=='0'" ng-click="enabled(item)"> <span class="label label-primary">启用</span> </a> <a href="javascript:void(0)" ng-show="item.status=='1'" ng-click="enabled(item)"> <span class="label label-primary">禁用</span> </a> </td> </tr> </tbody> </table>
分页信息:
采用angularjs ui自带的uib-pagination。由于需要支持当前页记录大小的选择,如果每个页面都需要包含分页相关内容,这样代码会比较冗余,于时很容易的我们可以借助angular directive来解决:
angular.module('app.directives', []).directive("pagerFooter", [ function() { return { restrict : "A", link : function(scope, element) { return null; }, templateUrl : "../app/template/pagerFooter.html" } } ])
<meta charset="UTF-8"> <div class="row form-inline"> <div class="col-md-6"> <span> 每页 <ui-select ng-model="pageRequest.pageSize" ng-change="pageRequest.getResponse()" theme='select2' style="min-width:35px;" > <ui-select-match>{{$select.selected.text}}</ui-select-match> <ui-select-choices repeat="item.value as item in (pageSizeList | filter: $select.search)"> <div ng-bind="item.text"></div> </ui-select-choices> </ui-select> 条记录 总共<span ng-bind="pageResponse.total"></span>条记录 </span> </div> <div class="col-md-6 text-right"> <uib-pagination total-items="pageResponse.total" ng-model="pageRequest.pageNum" max-size="4" class="pagination-sm" boundary-links="true" force-ellipses="false" first-text="首页" last-text="末页" previous-text="上一页" next-text="下一页" num-pages="pageResponse.pages" ng-change="pageRequest.getResponse()" items-per-page="pageRequest.pageSize" > </uib-pagination> </div> </div>
使用时,我们只需要这样指定:加一个pager-footer的属性。
<div class="box-body" pager-footer> </div>
写这个指令时遇到一个编码问题,模板页中出现的中文,在spring mvc环境下调用中乱码,最终在web.xml中增加配置得以解决:
<mime-mapping> <extension>html</extension> <mime-type>text/html;charset=utf-8</mime-type> </mime-mapping>
目前还有一个疑问没有得到解决,就是模板页中必须还要指定<meta charset="UTF-8">,否则也会显示成乱码,回头找时间整体研究下spring mvc下的编码。
数据行数据的model更新
以避免通过二次请求或者刷新页面来重新加载数据。比如行数据中有状态一栏,操作列会根据状态值动态显示启用或者停用按钮,当用户点击启用按钮操作成功后,当前数据行的状态栏数据需要动态更新,且不需要请求后台也不需要刷新页面,我们可以非常容易的通用ng-bind来让其自动更新:
操作列绑定事件:
<td> <a href="javascript:void(0)" data-toggle="modal" ng-click="edit(item.id)" data-original-title="编辑"> <span class="label label-primary">编辑</span> </a> <a href="javascript:void(0)" ng-show="item.status=='0'" ng-click="enabled(item)"> <span class="label label-primary">启用</span> </a> <a href="javascript:void(0)" ng-show="item.status=='1'" ng-click="enabled(item)"> <span class="label label-primary">禁用</span> </a> </td>
操作成功后更新model,页面数据自动更新。
$scope.enabled=function(theme) { bootbox.confirm("确认操作吗?", function (flag) { if (flag) { var status=theme.status==1?0:1; var model={id:theme.id,status:status}; $http.post("<c:url value="/theme/enabled"/>",model).success(function(ret){ if (ret.err) { bootbox.alert(ret.err); } else { theme.status=status; bootbox.alert("操作成功!"); } }); } }); };
列表页最终效果
上述的功能虽然不能解决所有场景的问题(嵌套表格,在线编辑表格,换肤等),但常见的业务操作均能满足,足够简单,不需要依赖第三方组件,重要的是能够完成其它js组件所不擅长的model更新场景以及复杂列的运算以及控制。我目前还在寻找其它的组件,如果有即能满足上述的需求又使用简单那么也是可以替换的,但做为学习总结总结倒也不错。