关于解决layui官方Dropdown插件对table渲染不太友好的的问题
前言
在最开始感谢您查看我的笔记,但我首先要声明文中所有观点仅为个人观点,同时本人不是专业前端,所以下文有些地方处理方式可能很非常粗暴,解决的问题也只是个人认为是问题的问题,同时layui版本为2.9.7,最后希望大家可以友好交流,谢谢。
不想看我的废话请直接跳转到最后
起因
最近工作涉及一些后台管理系统的重构,感觉也刚好是可以拥抱新版本新技术的机会便将layui升级到的目前官网的最新版本v2.9.7,业务功能中会经常有使用 layui.table 数据表格组件,同时因为版本较低所以没有内置 dropdown 方法,为了解决实际需求所以引入了 Microanswer 大佬制作的( dropdown )三方插件,当时也为了渲染更方便同时解决表格传参和列浮动等一系列出现的问题做了一下封装。到了新版本中看到 dropdown 组件已经内置了好多个版本了欣喜若狂,但是经过体验后发现在表格中和之前三方插件相比使用过于麻烦,但是本着能用原生就不用三方和自己动手丰衣足食的原则,开始了对官方插件渲染的封装。
解决的问题
- 解决渲染时行数据读取过于粗暴(原封装个人遗留问题)
- 解决官方Dropdown插件不太符合个人使用习惯
解决过程
0. 历史封装方法原因
在老版本使用三方插件的时候个人思路是在表格done事件中针对所有下拉按钮进行渲染,然后菜单内调用 layui.on('tool(tableId)')
中监听和定义的事件。前者在下拉菜单位于非浮动列时可以很好的运作,但是在表头配置 fixed 固定后会出现浮动行未被渲染所以未生效的问题,同时还有将行参数带入使用模板引擎进行条件渲染时非常的麻烦的问题;而后者可以非常方便的触发行工具条事件,所以没有问题。
1. 历史解决思路
所以上面那堆废话说完之后有两个问题摆在面前要解决
- 解决下拉按钮位于 fixed 固定列时渲染不到的问题
- 解决下拉按钮位于 fixed 固定列时数据无法传递的问题(问题1的衍生问题)
在最初的使用时我直接在 table.done
回调函数中进行渲染
table.render({
//..layuiConfig
done:(res, curr, count)=>{
dropdown.suite(".moreOper", {
templateMenu: "#moreOperMenu"
});
}
})
这样会带来的一个问题就是我无法使用模板引擎进行有条件的渲染,为了解决这个问题我决定加上循环逐条数据渲染行的,也就是循环一遍回调中的 res
数组(不完善的代码所以样例早没了),但是这时候我发现数据是能传进去了,但是对浮动列又渲染不上了,于是我决定干脆就把 layui 渲染好的 table tr
遍历一遍好了,于是就出现了下面的代码
function Render(limit) {
//下拉列表渲染
//获取所有行
var $tr = $("tr");
var rows;
//判断当前表格是否完全显示
if ($tr.length == limit + 1) {
rows = limit + 1
}
//未完全显示
else {
rows = $tr.length
}
for (var i = 1; i < rows; i++) {
//构造参数
var itemData = {
//获取行中AnyData列的数据
AnyData: $($tr[i]).find("td[data-field='AnyData']").find('div').text()
}
//给操作列的按钮添加Class标识
$($tr[i]).find("a.moreOper").addClass("dropMore" + i)
//渲染
dropdown.suite("a.moreOper" + i, {
templateMenu: "#moreOperMenu",
templateMenuSptor: "()",
data: itemData
});
}
}
这时所有需要的数据直接从所在行的 tr
中取,这时候针对无浮动列的渲染已经没问题了,但是对于固定列的渲染会有无法取值的问题,原因是 layui 在渲染表格时浮动列时作为一个单独的表格,也就是如果直接获取 tr
的话比如有15 条数据就会是 15*2+1 个tr 元素,不过这时候取消浮动列也能满足需要,所以就凑合用用没继续优化。
后来在一个项目系统改版的时候又想到之前还有遗留这个问题,寻思干脆看一看再优化一下好了,于是就有了第二部分历史封装代码。
2. 历史封装(三方Dropdown组件)
在重新封装的时候又检查了一遍 layui 表格渲染后的 html 结构,发现实际渲染后的表格针对不同的元素的 class 区分还是很明确的,各个层级也没有很乱,完全可以通过 class 区分浮动和主表格元素。浮动列的元素都在 .layui-table-fixed-r .layui-table-fixed-l
下,通过获取这两个元素就可以在渲染的时候对浮动列中的按钮进行渲染了。
因为没有调用 layui 的官方API,所以在渲染数据取值时使用的时比较笨的方法,直接通过 data-content
获取原始数据或者通过 .layui-table-cell
直接获取选然后的数据。
于是便有了现有的这一版封装方法
/**
* 渲染下拉菜单
* @param {string} tableName 目标表名称
* @param {string} [fixedPath=""] 浮动渲染 参数: r l r-l
* @param {string} [operClass="moreOpr"] 渲染按钮Class
* @param {string} [tempId="dropOper"] 渲染模板元素Id
* @param {[{colName:string,isOri:boolean}]} paramArr 渲染传参 colName: 数据列名称 isOri: true原始数据 false渲染呈现数据
* @param {string} [tempSptor="{}"] 间隔元素 默认 {}
*/
function RenderDropdown(tableName, fixedPath = "", operClass = "moreOpr", tempId = "dropOper", paramArr = [], tempSptor = "{}") {
let dropdown = layui.dropdown
//目标表格
let $table = $("div.layui-table-view[lay-id=" + tableName + "]")
//取到目标表格的所有行
let $mainTrs = $table.find("div.layui-table-main tr")
let fixed = fixedPath.split("-");
let fixedTrs = [];
//获取所有浮动
fixed.forEach((val, index, arr) => {
try {
let fixedTr = $table.find("div.layui-table-fixed-" + val + " div.layui-table-body tr")
fixedTrs.push(fixedTr)
} catch (e) {
console.log(e)
}
})
if ($mainTrs.length > 0) {
$mainTrs.each((index, trv) => {
let renderParam = [];
paramArr.forEach((pmv, pindex, parr) => {
let itemParam = [];
let prmArr = Object.entries(pmv)[0];
itemParam.push(prmArr[0])
let target = $(trv).find('td[data-field=' + prmArr[0] + ']')
let v = ''
try {
if (prmArr[1]) {
v = target.attr("data-content")
} else {
v = target.find(".layui-table-cell").text()
}
} catch (e) {
console.log(e)
}
itemParam.push(v)
renderParam.push(itemParam);
})
let outParam = Object.fromEntries(renderParam);
fixedTrs.forEach((fxv, fin dex, farr) => {
if (fxv.length > 0) {
$(fxv[index]).find('.' + operClass).addClass("oper-" + tableName + index)
}
})
$(trv).find('.' + operClass).addClass("oper-" + tableName + index)
//渲染
dropdown.suite(".oper-" + tableName + index, {
templateMenu: "#" + tempId,
templateMenuSptor: tempSptor,
data: outParam
});
})
}
}
在实现时现根据传入的浮动参数获取到所有的要渲染的 tr
,再通过循环母表格中的 tr
去到需要传递的数据并且将所有要渲染的元素增加 class 进行打标,然后调用一次下拉框渲染。
实际调用
<table class="layui-table" id="test1" lay-filter="testEvent"></table>
<script type="text/html" id="opertool">
<a href='javascript:;' class="layui-btn layui-btn-xs layui-bg-gray-s moreOpr">下拉</a>
</script>
<script type="text/plain" id="dropQcOper">
[
{{# if (d.TestNum == 1){ }}
[{layIcon: "layui-icon-github", txt: " Github", event: "github"}]
{{# } }}
[{layIcon: "layui-icon-moon", txt: " Moon", event: "moon"}]
]
</script>
<script>
layui.use(['table','dropdown'], function() {
let table = layui.table,
dropdown = layui.dropdown
table.render({
...layuiConfig,
cols: [[
{ field: 'TestNum', title: '测试数字' },
{ field: '', title: '操作', fixed: 'right', toolbar: '#opertool' }
]],
id: 'test1',
done: function (res, curr, count) {
let para = [
{ "TestNum": true }
]
RenderDropDown("test1", "r", "moreOpr", "dropQcOper", para, "[]")
}
})
//监听工具条
table.on('tool(testEvent)', function (obj) {
var data = obj.data;
if (obj.event == 'github') {
//do something...
} else if (obj.event == 'moon') {
//do something...
}
});
})
</script>
实际使用中可以很好的解决个人遇到的问题,配置时也很方便,按需插件自身也可以使用模板引擎进行配置,同时调用事件也是行内事件,可以在 table.on('tool(filter)', func)
API中和其他操作按钮配置一起一次性配置,代码能更加清晰。
3. 新版本封装(官方Dropdown组件)
在 layui 的新版本中内置了 dropdown 插件,但是文档里面也有说的比较清楚。
下拉菜单 dropdown
是基于基础菜单结构衍生的多功能通用下拉菜单组件,它将原本静态呈现的菜单,通过各种事件的形式触发,从而以独立面板的形式弹出。不仅可用作常见的下拉菜单,更可用于右键菜单来实现更多的交互可能。
大概是因为需要照顾的地方很多,所以整个组件的实际使用下来感觉是比较独立的,所有的API都需要单独进行配置,有些类似 table 组件。在官方demo中也有关于表格中下拉菜单的使用,但是 demo 中下拉菜单是在监听的 table.on('tool(filter)', func)
中进行渲染后弹出,至于下拉菜单中的事件则需要在渲染时再次配置事件,个人觉得十分的麻烦,同时会让 table.on('tool(filter)', func)
中过于杂乱,同时列表操作列有时候有些按钮的功能和下拉菜单中一样,只是在下拉菜单中又出现了一次,但是这样需要重复监听事件,最后指向的都是相同方法个人觉得十分的陈冗繁杂,最后总结下来在 table 中个人使用有四点觉得不太好:
- 使用模板引擎比较麻烦
- 使用条件渲染比较麻烦(问题2的衍生问题)
- 使用时需要在
table.on('tool(filter)', func)
中监听很麻烦 - 事件需要重复监听让
table.on('tool(filter)', func)
中很乱(问题3的衍生问题)
在开始封装时我想直接把原来的代码拿过来改改用用看,但是发现有个问题一直解决不了,我不知道该怎么调起 table 监听的事件,在layui官方找了到了底层方法中的 layui.event(modName, events, params) API,但是在介绍很少,在网上搜了半天也没有相关的信息,自己试了一下也没试出来。
然后我就想了一下那我为什么不直接去看看 table 模块怎么实现的呢,笨蛋了,然后就去 GitHub 上找了源码翻了翻,最后在 moduls/table.js 的源码里面找到了这个。
layui.event.call(this, MOD_NAME, 'colTool('+ filter +')', $.extend({
event: events,
config: options,
col: col
},{}));
是通过这个方法触发表头自定义元素事件的,我调整了一下入参,发现也可以调起行操作列事件,这下问题可以解决了。
同时既然已经调官方API了那干脆数据也也从官方API里拿好了,翻了翻发现页面渲染数据都是在 layui.table.cache
下保存的,通过表 id 即可拿到数据。
最终代码
/**
* 渲染下拉菜单 使用layui官方组件
* @param {layuiTable} layTable 渲染实例
* @param {[{title:string,id:string,event:string,pridecate:Function}]} templet 渲染元素
* @param {string} [flexPath=""] 浮动渲染 参数: r l r-l
* @param {string} [operClass="moreOpr"] 渲染按钮Class 默认 moreOpr
*/
function RenderDropDown(layTable, templet, flexPath = "", operClass = "moreOpr") {
let dropdown = layui.dropdown
//目标表格
let tableName = layTable.config.id;
let table = layTable;
let $table = $("div.layui-table-view[lay-id=" + tableName + "]")
//取到目标表格的所有行
let $mainTrs = $table.find("div.layui-table-main tr")
let flex = flexPath.split("-");
let flexTrs = [];
//获取所有浮动
flex.forEach((val, index, arr) => {
try {
let flexTr = $table.find("div.layui-table-fixed-" + val + " div.layui-table-body tr")
flexTrs.push(flexTr)
} catch (e) {
console.log(e)
}
})
if ($mainTrs.length > 0) {
$mainTrs.each((index, trv) => {
//循环对需要渲染的按钮打标
flexTrs.forEach((fxv, findex, farr) => {
if (fxv.length > 0) {
$(fxv[index]).find('.' + operClass).addClass("oper-" + tableName + index)
}
})
$(trv).find('.' + operClass).addClass("oper-" + tableName + index)
//获取回调参数和行数据
let layDataIndex = $(".oper-" + tableName + index).parents('tr').first().attr('data-index')
let callbackData = layui.table.cache[tableName][layDataIndex];
let renderTemp = [];
let checkParam = [callbackData];
//条件过滤
templet.forEach((val, index, arr) => {
if (val.pridecate != undefined && val.pridecate != null) {
try {
if (checkParam.filter(val.pridecate).length == 1) {
renderTemp.push(val);
}
} catch (ex) {
console.error(ex);
}
} else {
renderTemp.push(val);
}
})
//渲染
dropdown.render({
elem: ".oper-" + tableName + index,
data: renderTemp,
click: function (data, othis) {
layui.event.call($(".oper-" + tableName + index).parents('td')[0]
, 'table'
, 'tool(' + table.config.id + ')'
, $.extend({
event: data.event,
config: table.config,
data: layui.table.clearCacheKey(callbackData),
dataCache: callbackData,
index: layDataIndex
}, {}));
}
})
})
}
}
至此,大功告成,渲染方法内尽可能都使用了 layui 的官方内置API,条件渲染也进行了优化,由于官方API中能直接拿到整行数据所以可以直接使用 lambda 表达式,在实际实现时我会先将行数据放入 Array
中然后使用 Array.filter(func)
进行匹配。
封装时有个小插曲,开始的时候触发的行事件时不带参数的,应该时因为有的参数没传导致的不知道实际触发行,后来翻了翻源码把参数补全了,还有data中传入的参数需要用 layui.table.clearCacheKey()
方法处理一下,具体原理不太清楚,没有再深入研究。
实际调用
<table class="layui-table" id="test1" lay-filter="testEvent"></table>
<script type="text/html" id="opertool">
<a href='javascript:;' class="layui-btn layui-btn-xs layui-bg-gray-s moreOpr">下拉</a>
</script>
<script>
layui.use(['table','dropdown'], function() {
let table = layui.table,
dropdown = layui.dropdown
let dataTable = table.render({
...layuiConfig,
cols: [[
{ field: 'TestNum', title: '测试数字' },
{ field: '', title: '操作', fixed: 'right', toolbar: '#opertool' }
]],
id: 'test1',
done: function (res, curr, count) {
RenderDropDown(dataTable , [{
title: 'GitHub',
event: 'github'
}, {
title: 'Moon',
event: 'moon',
pridecate: s => s.TestNum == 1
}], 'r', 'moreOpr')
}
})
//监听工具条
table.on('tool(testEvent)', function (obj) {
var data = obj.data;
if (obj.event == 'github') {
//do something...
} else if (obj.event == 'moon') {
//do something...
}
});
})
</script>
结尾
功能大致测试之后暂时没发现什么问题,研究和解决的过程还是挺有意思的。也不知道官方以后会不会优化下插件,打算去提个 issues 碰碰运气,翻来翻去没见过相关的东西难道是只有我遇到了这个问题嘛。😂
最后叠个甲,本人非专业前端,遇到的问题也只是个人觉得是问题的问题,解决方案中的代码处理可能非常粗暴,因为是个人笔记所以碎碎念可能比较多,见谅。
如果有其他的思路或者我这里有哪里处理的不够好欢迎大佬指点。