最近遇到一个需求:页面上的数据可能会有很多条,需要将数据分页展示在表格中。项目用的是 jQuery 和 Bootstrap,本来想直接用 bootstrapTable 插件,但是需要额外引入 js 文件、语言包等等,样式、语言翻译啥的都不太好做,索性还是决定参照这个,基于 jQuery 和 Bootstrap,自己实现一个简单版的。因为项目提供的后端接口直接将查询到的所有数据返回,所以这个分页实际上只是个假分页罢了,抓住关键点——当前页展示的数据与当前页码以及每页展示数量有关
,其它的就好办了。先明确要实现的效果。
所以整个分页组件可以分为三大部分:选择每页展示条数的选择框、展示数据的表格(可以多选、可以操作)、底部显示当前页相关信息以及切换页码的按钮。html 以及 css 部分的代码如下,注意要引入 jQuery 以及 Bootstrap 相关的 js 和 css 文件。
<div class="table-box" id="tableList">
<div class="per-page row">
<div class="col-xs-4">
<span>每页展示span>
<div class="dropdown selectPageSize">
<input type="hidden" class="pageSize" value="10">
<button class="form-control dropdown-toggle" data-toggle="dropdown">10button>
<ul class="dropdown-menu" role="menu">
<li data-value="3"><label>3label>li>
<li data-value="5"><label>5label>li>
<li data-value="10"><label>10label>li>
ul>
div>
div>
div>
<table class="table"
data-multi-select="true"
data-has-id="true"
data-has-operate="true"
data-operate-items="onlyDelete">
<thead>thead>
<tbody>tbody>
table>
<div class="table-footer row">
<div class="col-xs-1">
<input type="checkbox" class="selectAll">
<span>全选span>
div>
<div class="col-xs-11">
<span class="dataInfo">span>
<ul class="pagination">
<li><span class="startPage" aria-label="Start" aria-hidden="true">«span>li>
<li><span class="prevPage" aria-label="Previous" aria-hidden="true"><span>li>
<li class="active"><span class="nowPage">1span>li>
<li><span class="nextPage" aria-label="Next" aria-hidden="true">>span>li>
<li><span class="endPage" aria-label="End" aria-hidden="true">»span>li>
ul>
div>
div>
div>
/* 整体的样式 */
.table-box {
width: 80%;
border-radius: 5px;
margin: 20px auto;
border: 1px solid #f2f2f2;
}
.per-page {
margin: 15px -10px;
}
/* 选择框 */
.selectPageSize {
display: inline-block;
width: 50px;
margin-left: 5px;
}
.dropdown-toggle {
padding: 5px;
height: 25px;
line-height: 15px;
}
/* 选择框中的小三角 */
.dropdown-toggle::after {
display: block;
content: "";
float: right;
width: 0;
height: 0;
border-color: #FFFFFF transparent;
border-style: solid;
border-width: 4px 4px 0 4px;
margin-top: 6px;
border-top-color: #FFFFFF;
}
/* 下拉列表 */
.dropdown-menu {
width: 100%;
min-width: unset;
padding: 0;
overflow-y: auto;
}
.dropdown-menu > li > label {
width: 100%;
padding: 2px 10px;
margin: 0;
font-weight: 400;
text-align: center;
}
/* 鼠标悬停在选择框某一项 */
.dropdown-menu > li > label:hover {
color: #FFFFFF;
background-color: #0066cc;
}
/* 表头 */
.table > thead > tr > th {
text-align: center;
font-weight: normal;
line-height: 34px;
border: none;
background-color: #f2f2f2;
}
/* 可多选表格的多选框样式 */
.table[data-multi-select="true"] > thead > tr > th:first-child {
width: 30px;
line-height: 30px;
}
/* 单元格样式 */
.table > tbody > tr > td {
text-align: center;
border: none;
line-height: 24px;
}
.table > tbody > tr:last-child {
border-bottom: 1px solid #f2f2f2;
}
/* 可操作的表格 */
.table[data-has-operate="true"] > tbody .deleteThis {
color: orange;
cursor: pointer;
}
.table[data-has-operate="true"] > tbody .editThis {
color: #0066cc;
cursor: pointer;
}
.table-footer {
margin: 15px -15px;
}
/* 可多选的表格要在表头以及表格下方提供多选框 */
.table[data-multi-select="true"] + .row > div:first-child {
display: inline-flex;
align-items: center;
justify-content: flex-start;
padding-left: 25px;
}
.table[data-multi-select="true"] + .row > div:first-child > input {
margin: 0 5px 0 0;
}
.table[data-multi-select="true"] + .row > div:first-child > span {
text-align: center;
flex: 1;
}
.table-footer > div:last-child {
display: inline-flex;
align-items: center;
justify-content: flex-end;
}
/* 分页部分的样式 */
.pagination {
margin: 0 10px;
}
.pagination > li > span {
padding: 0 10px;
margin: 0 3px;
border-radius: 4px;
cursor: pointer;
}
.pagination > li > span:hover,
.pagination > li > span:focus {
background-color: #FFFFFF;
}
.dropdown-toggle,
.dropdown-menu > li > label:hover,
.pagination > li.active > span,
.pagination > li.active > span:hover,
.pagination > li.active > span:focus {
color: #FFFFFF;
border-color: #0066cc;
background-color: #0066cc;
}
组件基本的结构和样式已经完成,接下来是最关键的 js 部分。针对上面列出的5个效果,来一步步分析如何通过 js 实现。首先肯定要获取我们后续要操作的对象,基于 jQuery 封装一个方法,代码如下。
(function($){
$.fn.initTable = function(data, isFirst, thName) {
// data: 表格需要展示的所有数据 isFirst: 需要添加表头、绑定事件等 thName: 表头的名称
isFirst = isFirst || 0;
// nPanel: 当前表格组件对象
var nPanel = this;
// pageSize: 每页展示条数
var pageSize = nPanel.find(".pageSize").val();
// total: 数据总条数
var total = data.length;
// tPage: 页面总数
var tPage = Math.ceil(total/perPage);
// nPage: 当前页数
var nPage = parseInt(nPanel.find(".nowPage").text());
var table = nPanel.find(".table");
// multi: 表格数据是否可多选
var multi = table.attr("data-multi-select") === "true" ? 1 : 0;
// hasID: 表是否有序号列
var hasID = table.attr("data-has-id") === "true" ? 1 : 0;
// hasOp: 表是否有操作列
var hasOp = table.attr("data-has-operate") === "true" ? 1 : 0;
// operate: 表的操作列有哪些操作
var operate = table.attr("data-operate-items") || "all";
}
})(jQuery);
先考虑如何生成表头,需要传入一个数组 thName,存放表头的名称,并非每次调用 initTable 方法时都需要重新生成表头以及给元素绑定事件,通过参数 isFirst 来判断,如果是第一次调用 initTable 方法则需要生成表头、给选择每页条数、多选框等绑定事件。在给每行的元素(比如多选框)绑定事件时要注意,因为表格每一行都是动态生成的,现在还获取不到目标元素,无法直接绑定事件,所以采用事件委托的方式,通过 jQuery 实现起来还是很简单的。对于页码切换的事件绑定则需要在每次调用 initTable 方法的时候重新绑定,否则还是基于最初的数据进行更改。代码如下。
// 首次调用该方法,添加表头、绑定事件、应用样式等
if (isFirst === 1) {
if (multi) {
nPanel.find(".table-footer").find("div:first-child").show();
nPanel.find(".table-footer").find("div:last-child").removeClass("col-xs-offset-1");
// 选中所有条目
nPanel.on("click", ".selectAll", function(){
nPanel.find(":checkbox:not(:disabled)").prop("checked", $(this).is(":checked"));
});
// 当全选按钮被选中时,取消选中表格任意一条的同时取消选中全选
table.on("click", ":checkbox:not(:disabled)", function(){
if (nPanel.find(".selectAll").is(":checked") && !$(this).is(":checked")) {
nPanel.find(".selectAll").prop("checked", false);
}
});
} else {
nPanel.find(".table-footer").find("div:first-child").hide();
nPanel.find(".table-footer").find("div:last-child").addClass("col-xs-offset-1");
}
// 根据传入的表头名称,动态生成表头
// 因为表头只有一行,就可以直接生成一个 tr 元素,不需要文档碎片
var thEle = $(" ");
thName.forEach(function(v, i){
if (i === 0) {
// 表格是否可多选
if (multi) {
thEle.append(" ");
}
// 表格是否有序号
if (hasID) {
thEle.append("序号 ");
}
}
thEle.append(""+v+" ");
// 表格是否可操作
if (i === thName.length-1 && hasOp) {
thEle.append("操作 ");
}
});
table.find("thead").append(thEle);
// 选择每页展示数量
$(".dropdown-menu").on("click", "li", function(){
var inputBox = $(this).parent().siblings("input");
var nowValue = $(this).attr("data-value");
var nowText = $(this).find("label").text();
if (nowValue !== inputBox.val()) {
// 新值与旧值对比,发生了变化才更改
$(this).parent().prev().text(nowText);
inputBox.val(nowValue);
// 由于直接修改 input 值不会触发 change 事件,因此使用 trigger 方法触发自定义事件 changed,并将当前值传给它
inputBox.trigger("changed", nowValue);
}
});
} else {
// 不是第一次调用则将需要先移除之前的事件,重新绑定事件
$(".pageSize").off("changed");
$(".startPage, .prevPage, .endPage, .nextPage").off("click");
}
// 监听自定义事件 changed,更新表格当前页数据
$(".pageSize").on("changed", function(e, v){
pageSize = v;
tPage = Math.ceil(total/pageSize);
nPage = nPage > tPage ? tPage : nPage;
changeRange();
});
// 跳转到第一页或上一页
$(".startPage, .prevPage").on("click", function(){
if (nPage > 1) {
nPage = $(this).hasClass("startPage") ? 1 : nPage-1;
changeRange();
} else {
alert("当前已经在第一页");
}
});
// 跳转到最后一页或下一页
$(".endPage, .nextPage").on("click", function(){
if (nPage < tPage) {
nPage = $(this).hasClass("endPage") ? tPage : nPage+1;
changeRange();
} else {
alert("当前已经在最后一页");
}
});
现在表头有了,数据也有了,接下来要将它们放到表格。因为在修改页码以及每页展示数量时,都需要重新渲染表格主体内容,所以将这部分代码放到 changeRange 函数中方便直接调用。思路大概是这样:已知当前页码和每页展示数量,就可以知道当前页需要展示的数据是哪些(数组中第几条到第几条的数据)。接下来就是dom操作了,遍历当前页的数据,每一条数据对应表格的一行(tr),具体的数据项放到对应单元格(td),遍历完之后利用文档碎片一次性插入到 tbody 中完成显示。代码如下。
// 修改表格主体内容
function changeRange(){
// 清空表格主体部分
table.find("tbody").empty();
// 更新当前页码
nPanel.find(".nowPage").text(nPage);
// 当前页第一条数据在数组中的索引号
var pageStart = (nPage-1)*pageSize;
// 当前页最后一条数据的序号
var pageEnd = nPage*pageSize > total ? total : nPage*pageSize;
// 取消选定所有多选框
if (multi) {
nPanel.find(":checkbox").prop("checked", false);
}
// 更新当前页的信息显示
nPanel.find(".dataInfo").text("第"+((total === 0) ? 0 : (pageStart+1))+"到第"+pageEnd+"条,总共"+total+"条");
// 利用碎片化文档,避免频繁操作 DOM
var dom = document.createDocumentFragment();
if (total > 0) {
// 获取当前页的数据,利用数组的 slice 方法截取数据片段
var nowPageData = data.slice(pageStart, pageEnd);
// 遍历数组,将数据放到对应表格 td 中,得到表格主体 tbody 的内容
nowPageData.forEach(function(v, i){
var trEle = $(" ");
Object.keys(v).forEach(function(k, j){
if (j == 0 && hasID) {
trEle.append(""+(pageStart+1+i)+" ");
}
trEle.append(""+v[k]+" ");
});
// 表格可以多选,添加多选框
if (multi) {
trEle.prepend(" ");
}
// 表格可以操作,添加操作项
if (hasOp) {
if (operate === "onlyDelete") {
// 操作项只有删除
trEle.append("删除 ");
} else if (operate === "all") {
// 默认操作项有编辑和删除
trEle.append("编辑 | 删除 ");
}
}
dom.appendChild(trEle[0]);
});
} else if (total === 0) {
var thNum = table.find("thead").find("th").length;
dom.appendChild($("+thNum+">没有数据 ")[0]);
}
// 将存放了表格主体内容的碎片化文档放到 DOM 中,完成一次性更新表格
table.find("tbody").append(dom);
}
整体 js 代码如下。
(function($){
$.fn.initTable = function(data, isFirst, thName) {
// data: 表格需要展示的所有数据 isFirst: 需要添加表头、绑定事件等 thName: 表头的名称
isFirst = isFirst || 0;
// nPanel: 当前表格组件对象
var nPanel = this;
// pageSize: 每页展示条数
var pageSize = nPanel.find(".pageSize").val();
// total: 数据总条数
var total = data.length;
// tPage: 页面总数
var tPage = Math.ceil(total/pageSize);
// nPage: 当前页数
var nPage = parseInt(nPanel.find(".nowPage").text());
// 表格对象
var table = nPanel.find(".table");
// multi: 表格数据是否可多选
var multi = table.attr("data-multi-select") === "true" ? 1 : 0;
// hasID: 表是否有序号列
var hasID = table.attr("data-has-id") === "true" ? 1 : 0;
// hasOp: 表是否有操作列
var hasOp = table.attr("data-has-operate") === "true" ? 1 : 0;
// operate: 表的操作列有哪些操作
var operate = table.attr("data-operate-items") || "all";
// 首次调用该方法,添加表头、绑定事件、应用样式等
if (isFirst === 1) {
if (multi) {
nPanel.find(".table-footer").find("div:first-child").show();
nPanel.find(".table-footer").find("div:last-child").removeClass("col-xs-offset-1");
// 选中所有条目
nPanel.on("click", ".selectAll", function(){
nPanel.find(":checkbox:not(:disabled)").prop("checked", $(this).is(":checked"));
});
// 当全选按钮被选中时,取消选中任意一条规则的同时取消选中全选
table.on("click", ":checkbox:not(:disabled)", function(){
if (nPanel.find(".selectAll").is(":checked") && !$(this).is(":checked")) {
nPanel.find(".selectAll").prop("checked", false);
}
});
} else {
nPanel.find(".table-footer").find("div:first-child").hide();
nPanel.find(".table-footer").find("div:last-child").addClass("col-xs-offset-1");
}
// 生成表头,获取对象的属性名
var thEle = $(" ");
thName.forEach(function(v, i){
if (i === 0) {
if (multi) {
thEle.append(" ");
}
if (hasID) {
thEle.append("序号 ");
}
}
thEle.append(""+v+" ");
if (i === thName.length-1 && hasOp) {
thEle.append("操作 ");
}
});
table.find("thead").append(thEle);
table.find("thead").attr("data-number", thName.length+multi+hasID+hasOp);
// 选择每页展示数量
$(".dropdown-menu").on("click", "li", function(){
var inputBox = $(this).parent().siblings("input");
var nowValue = $(this).attr("data-value");
var nowText = $(this).find("label").text();
if (nowValue !== inputBox.val()) {
// 新值与旧值对比,发生了变化才更改
$(this).parent().prev().text(nowText);
inputBox.val(nowValue);
// 由于val方法直接修改input值不会触发change事件,因此使用 trigger 方法触发自定义事件 changed,并将当前值传给它
inputBox.trigger("changed", nowValue);
}
});
}
// 监听自定义事件 changed,更新表格当前页数据
nPanel.on("changed", ".pageSize", function(e, v){
pageSize = v;
tPage = Math.ceil(total/pageSize);
if (nPage > tPage) {
nPage = tPage;
}
changeRange();
});
// 跳转到第一页或上一页
nPanel.on("click", ".startPage, .prevPage", function(){
if (nPage > 1) {
nPage = $(this).hasClass("startPage") ? 1 : nPage-1;
changeRange();
} else {
alert("当前已经在第一页");
}
});
// 跳转到最后一页或下一页
nPanel.on("click", ".endPage, .nextPage", function(){
if (nPage < tPage) {
nPage = $(this).hasClass("endPage") ? tPage : nPage+1;
changeRange();
} else {
alert("当前已经在最后一页");
}
});
changeRange();
// 获取指定页的数据
function changeRange(){
// 清空表格主体部分
table.find("tbody").empty();
// 更新当前页码
nPanel.find(".nowPage").text(nPage);
// 当前页第一条数据在数组中的索引号
var pageStart = (nPage-1)*pageSize;
// 当前页最后一条数据的序号
var pageEnd = nPage*pageSize > total ? total : nPage*pageSize;
// 取消任意多选框的选择
if (multi) {
nPanel.find(":checkbox").prop("checked", false);
}
// 更新当前页的信息显示
nPanel.find(".dataInfo").text("第"+((total === 0) ? 0 : (pageStart+1))+"到第"+pageEnd+"条,总共"+total+"条");
// 利用碎片化文档,避免频繁操作 DOM
var dom = document.createDocumentFragment();
if (total > 0) {
// 获取当前页的数据,利用数组的 slice 方法截取数据片段
var nowPageData = data.slice(pageStart, pageEnd);
// 遍历数组,将数据放到对应表格 td 中,得到表格主体 tbody 的内容
nowPageData.forEach(function(v, i){
var trEle = $("+(pageStart+i)+"> ");
Object.keys(v).forEach(function(k, j){
if (j == 0 && hasID) {
trEle.append(""+(pageStart+1+i)+" ");
}
trEle.append(""+v[k]+" ");
});
// 表格可以多选,添加多选框
if (multi) {
trEle.prepend(" ");
}
// 表格可以操作,添加操作项
if (hasOp) {
if (operate === "onlyDelete") {
// 操作项只有删除
trEle.append("删除 ");
} else if (operate === "all") {
// 默认操作项有编辑和删除
trEle.append("编辑 | 删除 ");
}
}
dom.appendChild(trEle[0]);
});
} else if (total === 0) {
var thNum = table.find("thead").find("th").length;
dom.appendChild($("+thNum+">没有数据 ")[0]);
}
// 将存放了表格主体内容的碎片化文档放到 DOM 中,完成一次性更新表格
table.find("tbody").append(dom);
}
}
})(jQuery);
现在放一些数据,添加一个按钮删除第一行数据,展示一下效果吧
var tableData = [
{
name: 'Alice',
mark1: 90,
mark2: 89,
mark3: 100
},
{
name: 'Bob',
mark1: 80,
mark2: 90,
mark3: 90
},
{
name: 'Cindy',
mark1: 82,
mark2: 86,
mark3: 84
},
{
name: 'Daisy',
mark1: 88,
mark2: 79,
mark3: 80
},
{
name: 'Frack',
mark1: 72,
mark2: 60,
mark3: 70
},
{
name: 'Daniel',
mark1: 62,
mark2: 67,
mark3: 60
}
]
$("#tableList").initTable(tableData, 1, ["姓名", "语文", "数学", "英语"]);
$("#deleteFirst").on("click", function(){
tableData.splice(0, 1);
$("#tableList").initTable(tableData);
});