ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

layui表格单元格点击事件:获取表头与行数据的完整方案

layui表格单元格点击事件:获取表头与行数据的完整方案 做后台管理系统这些年layui的表格table模块是躲不开的主力。最近做一个库存预警面板时产品提了个看似简单的需求点击表格里任意一个单元格要弹出该单元格的具体内容并且得告诉用户这个格子属于哪一列——也就是表头叫什么。听起来不就是个点击事件吗真上手才发现layui表格并没有官方提供一个现成的单元格点击API。row事件点击整行就触发edit事件又局限于开启了编辑的格子。被逼着研究了一下午表格渲染后的DOM结构之后我找到了一个稳定、通用的实现方案这篇文章就把完整的思路和代码写出来希望能帮到同样被这个需求卡住的人。1. 在动手之前先看layui表格现有的交互事件够不够用1.1 row、edit、tool事件各自的限制很多人听到单元格点击第一反应就是翻layui官方文档里的table.on。官方确实提供了几个事件回调但逐个试过之后就发现各有各的别扭。先说row事件这是大家最熟的。绑定方式如下table.on(row(testTableFilter), function(obj) { var data obj.data; // 当前行数据 var tr obj.tr; // 当前行DOM对象 console.log(data, tr); });这个事件点击行内任意位置都会触发能拿到整行数据但问题就在于它太粗了。你只知道点了一行不知道点的是名字列、数量列还是操作列。想在回调里再通过event.target去反推单元格会因为表格内部结构复杂而变得很麻烦。然后是edit事件这个只对开启了edit属性的单元格有效table.on(edit(testTableFilter), function(obj) { var value obj.value; // 单元格输入框的值 var data obj.data; // 行数据 var field obj.field; // 字段名 console.log(value, data, field); });用这个事件有硬伤不是所有单元格都能触发必须列配置里加edit: text才行而且拿到的值是输入框提交后的值不是单元格展示的HTML文本最关键的一点obj里同样没有表头标题信息。给全表所有列都加edit属性来曲线救国显然是不现实的会改变表格的交互样式。最后是tool事件它只在点击设置了lay-event属性的按钮或链接时触发table.on(tool(testTableFilter), function(obj) { var data obj.data; var layEvent obj.event; // 自定义事件名如 del/edit });这个局限性就更明显了它压根不是给单元格点击准备的。如果产品要求的是点点状态列就切换状态点点金额列就弹详情tool事件一个都满足不了。1.2 真正需要的行为边界是什么在研究方案之前我先把需求掰开揉碎明确了要实现的交互边界点击表格正文区域的任意单元格不包括表头、不包括分页栏要触发事件事件里能拿到三个关键信息当前单元格的内容文本、当前单元格对应字段名、当前所在列的标题也就是表头文字如果可能最好还能拿到当前行的完整数据因为很多业务逻辑需要根据行ID去请求详情表格通过table.reload()重新加载数据后事件不能被冲掉同时还要尽量避免两类误触点击操作列里的按钮时不应该和单元格点击逻辑混在一起点击固定列左侧冻结列时如果业务上不需要要有办法区分。带着这份需求清单我开始扒layui表格渲染后的真实DOM结构。2. 从渲染结果中找到突破点td和th的结构规律2.1 layui表格渲染完毕后的DOM长什么样layui的table在渲染完成后会在容器元素上生成一套固定的HTML结构。假设我们在页面里放了一个divdiv iddemoTable/div渲染后这个div内部会被填充成类似下面的结构我简化了无关属性div iddemoTable classlayui-table-view div classlayui-table-box !-- 表头区域 -- div classlayui-table-header table classlayui-table thead tr th>$(#demoTable .layui-table-body tbody td).on(click, function(e) { var td $(this); console.log(字段名:, td.data(field)); console.log(单元格内容:, td.text()); console.log(所在行index:, td.closest(tr).data(index)); console.log(DOM结构:, td[0].outerHTML); });这个时候你会发现点任何单元格都能拿到字段名和文本内容思路完全走得通。但先别高兴太早直接用$(#demoTable .layui-table-body tbody td).on(click, ...)这种方式有个致命问题表格一旦reloadDOM被重建这个绑定就失效了。所以正式方案必须用事件委托。2.3 为什么必须用事件委托而不是直接绑定事件委托的核心思想是不把监听器绑在目标元素td上而是绑在它们稳定的父容器上。因为td会被动态创建和销毁但容器#demoTable一直在那里。事件冒泡到容器时通过event.target判断实际点到的是不是td。这样做的好处有三个方面table.reload()重新渲染表格后只要容器div没变监听就依然有效绑定的容器要比表格内部结构层级更稳定不受固定列、行展开等额外DOM影响不用在done回调里反复off()再on()代码写一次就不用管了这个思路是整个方案的地基。把地基打牢后面所有坑都好填。3. 核心实现事件委托方案完整代码与原理3.1 最简版本拿到单元格内容和表头标题先用最直接的方式实现第一版。在layui.use的回调里给表格容器做一次事件委托layui.use([table, jquery], function() { var table layui.table; var $ layui.jquery; // 渲染表格 table.render({ elem: #demoTable, id: demoTableId, url: /api/list, cols: [[ { field: id, title: ID, width: 80 }, { field: name, title: 名称, width: 160 }, { field: stock, title: 库存, width: 120 } ]], page: true }); // 事件委托监听容器内 tbody td 的点击 $(#demoTable).on(click, .layui-table-body tbody td, function() { var td $(this); var field td.data(field); var content td.text().trim(); if (!field) { // 没有>$(#demoTable).on(click, .layui-table-body tbody td, function() { var td $(this); var field td.data(field); if (!field) { return; } var tr td.closest(tr); var rowIndex tr.data(index); var tableId demoTableId; // 对应 table.render 里的 id // 从表格缓存中取当前行数据 var rowData table.cache[tableId][rowIndex]; console.log(单元格内容:, td.text().trim()); console.log(表头标题:, getThTitle(field)); console.log(当前字段值:, rowData ? rowData[field] : ); console.log(整行数据:, rowData); }); // 提取表头标题逻辑方便复用 function getThTitle(field) { var th $(#demoTable .layui-table-header th[data-field field ]); return th.length ? th.text() : field; }table.cache[tableId]是layui内部维护的当前页数据数组tr上的>$(#demoTable).on(click, .layui-table-body tbody td, function(e) { var td $(this); // 方案A完全忽略固定列 if (td.closest(.layui-table-fixed).length 0) { return; } // 方案B固定列同样处理但标注来源 var isFixed td.closest(.layui-table-fixed).length 0; // ... });我个人习惯用方案A因为固定列通常是ID列或操作列点击它们的含义和普通数据列不一样分开处理逻辑更清晰。3.4 封装成通用函数随处复用调研完一个页面后我发现其他模块的表格也会有同样需求于是把整个逻辑封装成了一个独立函数。这样每次只需要一行代码就能接入单元格点击事件/** * 给layui表格绑定单元格点击事件 * param {string} containerSelector 表格容器选择器如 #demoTable * param {string} tableId table.render 里配置的 id * param {function} callback 回调函数入参为 { field, content, title, rowData, tr, td } */ function bindLayuiCellClick(containerSelector, tableId, callback) { var $container $(containerSelector); $container.off(click.cellClick).on(click.cellClick, .layui-table-body tbody td, function() { var td $(this); var field td.data(field); if (!field) { return; } var tr td.closest(tr); var rowIndex tr.data(index); var rowData (layui.table.cache[tableId] || [])[rowIndex] || {}; var th $container.find(.layui-table-header th[data-field field ]); callback({ field: field, content: td.text().trim(), title: th.length ? th.text() : field, rowData: rowData, fieldValue: rowData[field], tr: tr, td: td }); }); }使用时在渲染表格后调用一次bindLayuiCellClick(#demoTable, demoTableId, function(info) { layer.msg(你点击了【 info.title 】列内容是 info.content); });这段封装里有几个细节值得说明。我用了off(click.cellClick)先解绑再绑定是为了防止页面里同一个表格被重复调用时叠加绑定算是防御性编程。回调参数里同时提供tr和td是为了方便在某些场景下给特定格子加高亮或插入图标。整体逻辑不复杂但把容易踩坑的点都处理掉了。4. 复杂表头、固定列、动态表头下的兼容方案4.1 多级表头为什么会导致索引错位前面提到的方法都基于一个前提表头th和表体td通过>cols: [[ { title: 基本信息, align: center, colspan: 2 }, { title: 库存信息, align: center, colspan: 2 } ], [ { field: id, title: ID, width: 80 }, { field: name, title: 名称, width: 160 }, { field: stock, title: 库存, width: 120 }, { field: warning, title: 预警值, width: 120 } ]]这种情况下thead里会有两行tr第一行是分组标题第二行才是具体字段。如果你天真地通过td.index()去thead里找对应th一定会错位因为colspan导致th的数量和td的数量对不上。解决办法很简单不要依赖列索引始终用td.data(field)去匹配th[data-field]。这样无论表头分几级都能准确定位到第二行里真正对应该字段的th。如果还需要分组标题可以再用th.closest(tr).prev()向上找父级thvar th $container.find(.layui-table-header th[data-field field ]); var groupTh th.closest(tr).prev(tr).find(th).eq(th.index()); var groupTitle groupTh.length ? groupTh.text() : ;这里groupTitle拿到的就是基本信息或库存信息这样的分组名。有些需求会要求在弹窗里显示库存信息 库存把两级标题拼起来这样就齐活了。4.2 动态表头场景下怎么保证取到最新表头有些模块的表格列不是写死的而是根据接口返回动态生成的。比如不同角色看到的字段不同或者用户能自定义显示列。这时候table.render里的cols是动态拼的不能假设表头的结构固定。好消息是我们的事件委托方案天然不受影响。因为每次table.reload()后DOM里的th[data-field]和td[data-field]都会被重新生成而我们每次点击时都是实时去DOM里查找的所以拿到的一定是当前最新表头。这就是用选择器匹配而不是缓存的优势。如果你在初始化时把表头标题存到一个变量里动态重载后就容易取到旧值。所以记住一个原则表头标题这种动态信息每次点击时都去DOM里现查不要缓存。4.3 特殊单元格内容模板列、按钮、输入框的处理layui支持在templet中返回自定义内容常见的包括状态标签、按钮组、图片缩略图等。这就导致单元格的text()内容和实际值可能不一致。比如cols: [[ { field: status, title: 状态, templet: function(d) { return d.status 1 ? span stylecolor:green正常/span : span stylecolor:red停用/span; }}, { field: name, title: 名称 } ]]如果直接用td.text()拿到的可能是正常或停用而不是原始数据里的1或0。两者的用途完全不同显示文本用于给用户看原始值用于业务判断。所以在回调里我同时提供content显示文本和fieldValue原始数据值就是给这种场景留的口子。实际开发中如果你需要根据值做判断优先用info.fieldValue而不是info.content。但这种td.text()方式对模板列中带有图标的单元格不太友好因为text()会把图标元素的文字内容也带进来。遇到这种情况更稳妥的做法是用td.find(.layui-table-cell).text()或者直接依赖fieldValue。我自己通常默认用fieldValue作为程序逻辑依据用content只做展示。5. 实测中踩过的坑和排查链路5.1 问题一点击事件触发了两次现象点一次单元格控制台打印了两遍日志。排查过程先看是不是绑定重复执行了。如果页面里同一段初始化代码被调用了多次比如引入了两次公共JS文件或者在ready里和done回调里各绑了一次就会造成事件叠加。排查方式是在绑定前把绑定的命名空间解掉或者用变量控制只初始化一次。再看是不是DOM结构嵌套导致的。某些templet里写了带子元素的HTML点击子元素时事件冒泡到td再冒泡到容器。如果容器里还有其他监听也会出现多次触发。这时候可以通过e.target判断实际的点击来源必要时用e.stopPropagation()阻止继续冒泡。我建议的最终写法是$(#demoTable).on(click, .layui-table-body tbody td, function(e) { e.stopPropagation(); // 处理逻辑 });注意stopPropagation会阻止事件继续向上冒泡如果父容器没有其他需要监听的事件用了它不会有什么副作用。5.2 问题二点击操作列的按钮被当成单元格点击操作列有toolbar列的td虽然没有>$(#demoTable).on(click, .layui-table-body tbody td, function(e) { // 如果点击的目标是按钮或链接不处理 if ($(e.target).closest(button, a, input, select, .layui-btn).length 0) { return; } // ... });这样可以保证用户点击编辑删除按钮时按钮事件正常触发又不和单元格点击逻辑打架。5.3 问题三报错 layui.table.cache 是 undefined排查思路这个报错通常是因为tableId没写对。table.render里的id参数和table.cache里的键必须完全一致。另外确认layui.table是否正常use进来了。有时候是因为页面里同时存在多个layui实例比如同时引入了layui.all.js和模块化layui.js导致layui对象被覆盖。检查一下控制台打印的layui.table.cache结构就能定位问题。5.4 问题四表格reload之后事件完全失效这是我最早踩的坑。当时我把点击事件绑定在done回调里done: function(res) { $(#demoTable .layui-table-body tbody td).on(click, function() { // ... }); }表格第一次渲染没问题但一翻页或reload点击就失灵了。原因就是DOM被重建旧绑定全部丢失。这也是为什么我坚持用事件委托的根本原因。如果你现在还在用done回调绑定点击看到这里可以改成委托写法一劳永逸。5.5 一套标准的排查链路遇到问题别急着改代码先在控制台按这个顺序查确认渲染后的容器里有没有.layui-table-body tbody td结构执行$(#demoTable .layui-table-body tbody td).length看数量是否和预期一致确认点击时td上有没有>$(#demoTable).on(mouseenter, .layui-table-body tbody td, function() { $(this).css(background-color, #f8f8f8); }).on(mouseleave, .layui-table-body tbody td, function() { $(this).css(background-color, ); });第二个是给特定列设置点击白名单。有些时候产品只希望某一列比如库存预警列可点击其他列保持普通状态。与其点击后判断表头文字不如在委托回调里直接对字段名做过滤var clickableFields [stock, status]; $(#demoTable).on(click, .layui-table-body tbody td, function() { var field $(this).data(field); if (clickableFields.indexOf(field) -1) { return; } // 只有白名单内的列才会走到这里 });这样做的好处是过滤逻辑集中、直观也方便后续把白名单配置从接口下发。回到这个需求本身它的核心难点不是怎么绑定点击而是点击之后怎么精准定位到列和值。只要理解了layui表格渲染后td携带>
返回列表