原生JS实现嵌套表格

一、需求描述

(1)表格嵌套展示

(2)父表格每一行有编辑操作,点击编辑后表格内容从展示状态变为可编辑状态;点击取消后能恢复初始值

(3)表格左侧、顶端、右侧实现粘性布局

二、实现效果

漂亮的表格 | 菜鸟工具 --基于这个表格的样式,也可粘贴下方的代码到此网站看效果

(1)展示状态

(2)编辑状态

(3)确定提交,传递的参数

(4)粘性布局

三、实现代码

(1)HTML

<table width="100%" border="0" cellspacing="1" cellpadding="4" class="tb-father">
    <caption>
        <p class="tb-caption">表格标题</p>
        <p class="tb-remark text-right">2017-01-02---2017-05-02</p>
    </caption>
    <thead>
        <tr>
            <th class="titfont width60" rowspan="2">表头1</th>
            <th class="titfont width100" rowspan="2">表头2</th>
            <th class="titfont width60" rowspan="2">表头3</th>
            <th class="titfont width80" rowspan="2">表头4</th>
            <th class="titfont width80" rowspan="2">表头5</th>
            <th class="titfont width60" rowspan="2">表头6</th>
            <th class="titfont width60" rowspan="2">表头7</th>
            <th class="titfont width80" rowspan="2">表头8</th>
            <th class="titfont width100" rowspan="2">表头9</th>
            <th class="titfont width100" rowspan="2">表头10</th>
            <th class="titfont width60" rowspan="2">操作</th>
        </tr>
    </thead>
    <tbody>
        <!-- for循环展示,设置每行id为row_返回数据唯一标识 -->
        <tr id="row_key">
            <td class="width60">
                <div class="showVal">合并内容1</div>
                <div class="changeVal">
                    <!-- 修改数据:input -->
                    <input type="text" data-submit-prefix="fatherObj" data-id="name1" placeholder="表头1" value="合并内容1" />
                    <!-- 记录原有数据,点击取消能复原;data-id用于区分原始值元素和需提交元素 -->
                    <input type="hidden" data-id="name1Val" value="合并内容1" />
                </div>
            </td>
            <td class="width100">
                <div class="showVal">合并内容2</div>
                <div class="changeVal">
                    <!-- 修改数据:textarea -->
                    <textarea type="text" data-submit-prefix="fatherObj" data-id="name2" placeholder="表头2">合并内容2</textarea>
                    <!-- 记录原有数据,点击取消能复原 -->
                    <input type="hidden" data-id="name2Val" value="合并内容2" />
                </div>
            </td>
            <td class="width60">
                <div class="showVal">
                    <label><input type="radio" value="1" checked disabled />是</label><br />
                    <label><input type="radio" value="0" disabled />否</label>
                </div>
                <div class="changeVal">
                    <!-- 修改数据:radio,由于同一name的radio只能选其一的特殊性,需要加上唯一标识符 -->
                    <label><input type="radio" data-submit-prefix="fatherObj" data-id="name3" name="row_key_fatherObj.name3" value="1" checked />是</label><br />
                    <label><input type="radio" data-submit-prefix="fatherObj" data-id="name3" name="row_key_fatherObj.name3" value="0" />否</label>
                    <!-- 记录原有数据,点击取消能复原 -->
                    <input type="hidden" data-id="name3Val" value="1" />
                </div>
            </td>
            <td class="width80">
                <div class="showVal">
                    <label><input type="checkbox" value="1" checked disabled />多选1</label><br />
                    <label><input type="checkbox" value="2" checked disabled />多选2</label><br />
                    <label><input type="checkbox" value="3" disabled />多选3</label>
                </div>
                <div class="changeVal">
                    <!-- 修改数据:checkbox -->
                    <label><input type="checkbox" data-submit-prefix="fatherObj" data-id="name4" value="1" checked />多选1</label><br />
                    <label><input type="checkbox" data-submit-prefix="fatherObj" data-id="name4" value="2" checked />多选2</label><br />
                    <label><input type="checkbox" data-submit-prefix="fatherObj" data-id="name4" value="3" />多选3</label>
                    <!-- 记录原有数据,点击取消能复原 -->
                    <input type="hidden" data-id="name4Val" value="1,2" />
                </div>
            </td>
            <td class="width80">
                <div class="showVal">合并内容5</div>
                <div class="changeVal">
                    <!-- 修改数据:select -->
                    <select data-submit-prefix="fatherObj" data-id="name5">
                        <option value="">请选择</option>
                        <option value="1" selected>合并内容5</option>
                        <option value="2">选择2</option>
                        <option value="3">选择3</option>
                    </select>
                    <!-- 记录原有数据,点击取消能复原 -->
                    <input type="hidden" data-id="name5Val" value="1" />
                </div>
            </td>
            <td colspan="5" class="tb-son-container">
                <table border="0" cellspacing="0" cellpadding="4" class="tb-son">
                    <tbody>
                        <!-- for循环展示tr -->
                        <tr class="tb-son-tr">
                            <td class="width60">
                                <div class="showVal">拆分内容11</div>
                                <div class="changeVal">
                                    <!-- 修改数据:input,便于提交在data-submit-prefix设入循环index -->
                                    <input type="text" data-submit-prefix="fatherObj.sonObj[0]" data-id="name11" placeholder="表头1" value="拆分内容11" />
                                    <!-- 记录原有数据,点击取消能复原;data-id用于区分原始值元素和需提交元素 -->
                                    <input type="hidden" data-id="name11Val" value="拆分内容11" />
                                </div>
                            </td>
                            <td class="width60">
                                <div class="showVal">拆分内容12</div>
                                <div class="changeVal">
                                    <!-- 修改数据:textarea,便于提交在data-submit-prefix设入循环index -->
                                    <textarea type="text" data-submit-prefix="fatherObj.sonObj[0]" data-id="name12" placeholder="表头2">拆分内容12</textarea>
                                    <!-- 记录原有数据,点击取消能复原 -->
                                    <input type="hidden" data-id="name12Val" value="拆分内容12" />
                                </div>
                            </td>
                            <td class="width80">
                                <div class="showVal">
                                    <label><input type="radio" value="1" checked disabled />是</label><br />
                                    <label><input type="radio" value="0" disabled />否</label>
                                </div>
                                <div class="changeVal">
                                    <!-- 修改数据:radio,便于提交在data-submit-prefix设入循环index;由于同一name的radio只能选其一的特殊性,需要加上唯一标识符和子表格for循环渲染的index -->
                                    <label><input type="radio" data-submit-prefix="fatherObj.sonObj[0]" data-id="name13" name="row_key_0_sonObj.name13" value="1" checked />是</label><br />
                                    <label><input type="radio" data-submit-prefix="fatherObj.sonObj[0]" data-id="name13" name="row_key_0_sonObj.name13" value="0" />否</label>
                                    <!-- 记录原有数据,点击取消能复原 -->
                                    <input type="hidden" data-id="name13Val" value="1" />
                                </div>
                            </td>
                            <td class="width100">
                                <div class="showVal">
                                    <label><input type="checkbox" value="1" checked disabled />多选1</label><br />
                                    <label><input type="checkbox" value="2" checked disabled />多选2</label><br />
                                    <label><input type="checkbox" value="3" disabled />多选3</label>
                                </div>
                                <div class="changeVal">
                                    <!-- 修改数据:checkbox,便于提交在data-submit-prefix设入循环index -->
                                    <label><input type="checkbox" data-submit-prefix="fatherObj.sonObj[0]" data-id="name14" value="1" checked />多选1</label><br />
                                    <label><input type="checkbox" data-submit-prefix="fatherObj.sonObj[0]" data-id="name14" value="2" checked />多选2</label><br />
                                    <label><input type="checkbox" data-submit-prefix="fatherObj.sonObj[0]" data-id="name14" value="3" />多选3</label>
                                    <!-- 记录原有数据,点击取消能复原 -->
                                    <input type="hidden" data-id="name14Val" value="1,2" />
                                </div>
                            </td>
                            <td class="width100">
                                <div class="showVal">拆分内容15</div>
                                <div class="changeVal">
                                    <!-- 修改数据:select,便于提交在data-submit-prefix设入循环index -->
                                    <select data-submit-prefix="fatherObj.sonObj[0]" data-id="name15">
                                        <option value="">请选择</option>
                                        <option value="1" selected>拆分内容15</option>
                                        <option value="2">选择2</option>
                                        <option value="3">选择3</option>
                                    </select>
                                    <!-- 记录原有数据,点击取消能复原 -->
                                    <input type="hidden" data-id="name15Val" value="1" />
                                </div>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </td>
            <td class="width60">
                <!-- 传入每行id唯一标识:row_key -->
                <div class="showVal">
                    <button onclick="edit('row_key')">编辑</button>
                </div>
                <div class="changeVal">
                    <button onclick="submit('row_key')">确定</button>
                    <button onclick="reset('row_key')">取消</button>
                </div>
            </td>
        </tr>
    </tbody>
</table>

(2)CSS

@charset "utf-8";
    /* CSS Document */
    /* 标题 */
    .tb-father .tb-caption{
    	font-family: 微软雅黑;
    	font-size: 26px;
    	font-weight: bold;
    	color: #255e95;
      	height: 60px;
       line-height: 60px;
      	border-bottom: 1px dashed rgb(204, 204, 204);
      	margin: 0 !important;
    }
    .tb-father .tb-remark {
      margin-top: 8px !important;
      margin-bottom: 12px !important;
    }
    .text-right {
      text-align: right;
    }
    /* 表格基本样式 */
    .tb-father th {
      position: sticky; /* 固定首行 */
      top: 0;
      z-index: 2; /* 次级,最高级在固定左右侧 */
      border: 1px solid rgb(204, 204, 204); 
      background: #e9faff;
      height:25px;
      line-height:150%;
      text-align: center;
    }
    .tb-father th:last-child {
      z-index: 3;
    }
    .tb-father th:last-child, tr[id^='row']>td:last-child{
    	position: sticky; /* 固定尾列 */
       right: 0;
    }
		/* 粘性元素的边框在滚动时变得不可见,直接把原来的border改成outline影响宽高样式;因此在涉及th和td的伪类元素上用outline */
    .tb-father th::before,
		td.left-outline::before,
		tr[id^='row']>td:last-child::before {
		content: "";
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      outline: 1px solid rgb(204, 204, 204);
      z-index: -1;
    }
    .tb-father td{
    	background-color:#ffffff;
    	height:25px;
    	line-height:150%;
       word-wrap: break-all;
       text-align: center;
    }
    .tb-father tr[id^='row']>td {
      border: 1px solid rgb(204, 204, 204);
    }
    .tb-father input[type='text'],textarea,select {
      width: 90%;
    }
    .titfont {
    	font-family: 微软雅黑;
    	font-size: 16px;
    	font-weight: bold;
    	color: #255e95;
    	background-color:#e9faff;
    }
    /* 父子表格 */
    .tb-father {
      table-layout: fixed;
      border-collapse: collapse;   
    }
    .tb-son-container {
      padding: 0;
      border: none;
    }
    .tb-son {
      width: 100%;
      height: 100%;
      border-collapse: collapse; /* 设置这个tr的边框才生效 */
    }
    .tb-son tr:not(:last-child) {
      border-bottom: 1px solid rgb(204, 204, 204);
    }
    .tb-son td:not(:last-child){
      border-right: 1px solid rgb(204, 204, 204);
    }
    .width60 {
      width: 60px;
    }
    .tb-son width60{
      padding: 0 2px;
    }
    .width80 {
    	width: 80px;
    }
    .width100 {
    	width: 100px;
    }
    /* 显示隐藏 */
    .showVal {
      display: block;
    }
    .changeVal {
      display: none;
    }

(3)JS

// 封装原生JS获取元素NodeList并遍历方法
    // 看到有涉及封装原生JS修改this指向的博文,待细看 https://www.cnblogs.com/surfaces/p/5169774.html
      function getElementListAndForeach(fatherEle, selectStr, fn) {
        // 获取子元素nodelist,把nodelist转换成array
        let domList = [].slice.call(fatherEle.querySelectorAll(selectStr));
        domList.forEach(item => {
          fn(item);
        })
       
      }
    
    // 点击编辑
    function edit(trKey) {
       let fatherTr = document.querySelector("#" + trKey); 
       getElementListAndForeach(fatherTr, '.showVal', function(item) {
         item.style.display = 'none';
    });
      	getElementListAndForeach(fatherTr, '.changeVal', function(item) {
         item.style.display = 'block';
    });
    }
      
      // 点击取消,重设各类选项值
      // 流程:提交输入框 data-id="key值";原始值存储框 data-id="key值Val";点击取消键,取原始值置入提交输入框
      function reset(trKey) {
        let fatherTr = document.querySelector("#" + trKey);
        getElementListAndForeach(fatherTr, '.showVal', function(item) {
           item.style.display = 'block';
         });
        // 遍历array修改展示状态 & 重设各类选项值
        getElementListAndForeach(fatherTr, '.changeVal', function(item) {
           item.style.display = 'none';
             // 1.获取存放原始value的obj 一般以input type="hidden" 写在代码里
          let originValObj = item.querySelector("[data-id$='Val']");
             // 1.1.获取原始value
          let originVal = originValObj?.value;
             // 1.2.获取key,根据key获取需提交的元素
          let submitKey = originValObj?.getAttribute('data-id')?.substring(0, originValObj?.getAttribute('data-id')?.length - 3);
             let submitObj = item.querySelector("[data-id="+ submitKey +"]");
             // 2.获取元素标签名,不同类型的设置值
             let submitTagName = submitObj?.tagName;
             if(submitTagName) {
           switch(submitTagName) {
            case "INPUT": 
              let submitInputType = submitObj?.getAttribute('type');
              switch(submitInputType) {
                case "radio": 
                  let radioList = item.querySelectorAll("[data-id="+ submitKey +"]");
                  radioList = [].slice.call(radioList);
                  radioList.forEach(radioItem => {
                    if(radioItem.value == originVal) radioItem.checked = true;
                  })
                  break;
                case "checkbox": 
                  let originValList = [];
                  if(originVal != "") originValList = originVal.split(",");
                  let checkboxList = item.querySelectorAll("[data-id="+ submitKey +"]");
                  checkboxList = [].slice.call(checkboxList);
                  checkboxList.forEach(checkboxItem => {
                    if(originValList.indexOf(checkboxItem.value) != -1) {
                      checkboxItem.checked = true;
                    } else {
                      checkboxItem.checked = false;
                    }
                  })
                  break;
                default: submitObj.value = originVal; break;
               }
              break;
            default: submitObj.value = originVal; break;
          		}
            }
         });
      }
      
      // 点击确定提交
      function submit(trKey) {
        // 1.初始化上传参数
        let params = {};
        // 2.获取父tr元素
        let fatherTr = document.querySelector("#" + trKey);
        getElementListAndForeach(fatherTr, '.changeVal', function(item) {
          // 2.1.获取表格需提交的元素和key名
             let originValObj = item.querySelector("[data-id$='Val']");
          let submitKey = originValObj?.getAttribute('data-id')?.substring(0, originValObj?.getAttribute('data-id')?.length - 3);
             let submitObj = item.querySelector("[data-id="+ submitKey +"]");
          let submitPrefix = submitObj?.getAttribute("data-submit-prefix");
          let submitValue = "";
          // 2.2.获取元素标签名,不同类型的设置值
          let submitTagName = submitObj?.tagName;
             if(submitPrefix && submitTagName) {
            switch(submitTagName) {
                case "INPUT": 
                  let submitInputType = submitObj?.getAttribute('type');
                  switch(submitInputType) {
                    case "radio": 
                      let radioList = item.querySelectorAll("[data-id="+ submitKey +"]");
                      radioList = [].slice.call(radioList);
                      radioList.forEach(radioItem => {
                        if(radioItem?.checked && radioItem?.value) submitValue = radioItem?.value;
                      })
                      break;
                    case "checkbox": 
                      let checkboxList = item.querySelectorAll("[data-id="+ submitKey +"]");
                      checkboxList = [].slice.call(checkboxList);
                      let submitValueList = [];
                      checkboxList.forEach(checkboxItem => {
                        if(checkboxItem?.checked && checkboxItem?.value) submitValueList.push(checkboxItem?.value);
                       });
                      if(submitValueList.length > 0) submitValue = submitValueList.join(","); 
                      break;
                    default: 
                       if(submitObj?.value) submitValue = submitObj?.value; break;
                      }
                  break;
                case "SELECT": submitValue = submitObj?.options[submitObj?.selectedIndex].value; break;
                default: if(submitObj?.value) submitValue = submitObj?.value; break;
            }
            params[submitPrefix + "." + submitKey] = submitValue;
          }
        })
        console.log(params);
        // 调接口送params信息
      }
     
      // 固定左侧几列(这个有局限性,只能固定合并列)
      function fixLeftColumn (num) {
        console.log('触发固定左侧几列');
        for(let i = 1, leftOffset = 0; i <= num; i++) {
          let fatherTable = document.querySelector(".tb-father");
          let th = fatherTable.querySelector("th:nth-child(" + i + ")");
          th.style.position = "sticky";
          th.style.left = leftOffset + "px";
          th.style.zIndex = "3";
          let td = getElementListAndForeach(fatherTable, "tr[id^='row']>td:nth-child(" + i + ")", function (item) {
            item.style.position = "sticky";
            item.style.left = leftOffset + "px";
            item.setAttribute("class", item.getAttribute("class") + " left-outline");
          })
          leftOffset += Number(th.offsetWidth);
        } 
      }
       
      // 模拟数据
      function mockData () {
        // 模拟数据
        let sonTable = document.querySelector(".tb-son").querySelector("tbody");
        // 复制子表格单行
        let sonTableSampleTr = sonTable.querySelector(".tb-son-tr").cloneNode(true);
        // 由于同一name的radio只能选其一的特殊性,需要加上唯一标识符和子表格for循环渲染的index
        getElementListAndForeach(sonTableSampleTr, "input[data-id=name13]", function(item) {
          item.setAttribute("name", "row_key_1_sonObj.name13");
        });
        // 便于提交在data-submit-prefix设入循环index
        getElementListAndForeach(sonTableSampleTr, "[data-submit-prefix^='fatherObj.sonObj']", function(item) {
          item.setAttribute("data-submit-prefix", "fatherObj.sonObj[1]");
        });
        sonTable.appendChild(sonTableSampleTr);
        // 复制子表格单行
        let sonTableSampleTr1 = sonTable.querySelector(".tb-son-tr").cloneNode(true);
        // 由于同一name的radio只能选其一的特殊性,需要加上唯一标识符和子表格for循环渲染的index
        getElementListAndForeach(sonTableSampleTr1, "input[data-id=name13]", function(item) {
          item.setAttribute("name", "row_key_2_sonObj.name13");
        });
     	// 便于提交在data-submit-prefix设入循环index
        getElementListAndForeach(sonTableSampleTr1, "[data-submit-prefix^='fatherObj.sonObj']", function(item) {
          item.setAttribute("data-submit-prefix", "fatherObj.sonObj[2]");
        });
        sonTable.appendChild(sonTableSampleTr1);
        let fatherTable = document.querySelector(".tb-father").querySelector("tbody");
      }
      
      // 节流函数(时间戳节流,立即触发,下次在N秒后才能触发)
      function throttle (fn, waiting) {
        // 上一次执行时间
        let previous = 0;
        return function (...args) {
          // 当前时间
          let now = +new Date();
          if (now - previous > waiting) {
            previous = now;
          	 fn.apply(this, args);
          }
        }
      }
                
      window.onload = function () {
        // 1.模拟数据
        mockData();
        // 2.固定左侧5列
        fixLeftColumn(5);
        // 如果缩放浏览器,left值固定,显示会有问题;所以增加监听事件改变left值
        // resize触发太频繁影响性能,用节流函数套一层
        let throttleFn = throttle(fixLeftColumn, 100);
        window.addEventListener("resize", function () { throttleFn(5); });
      }

  • 9
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值