基于JavaScript实现递归HTML表格树:子节点展开功能需求
好的,咱们来把这个表格改成支持递归展开的树形结构!先分析下你现有代码的局限:原来的逻辑只处理了顶级父节点(带parent类的行),点击后展开后面的行直到下一个parent,但没法处理子节点本身还有下一级子行的情况。要实现递归展开,咱们得调整标记、事件逻辑和样式这几个部分。
核心实现逻辑
- 统一标记可展开节点:给所有包含子节点的行(不管是顶级父节点还是子级父节点)添加通用类
has-children,同时用data-level属性记录节点层级(比如顶级是0,子级是1,孙子级是2)——既方便识别可点击节点,也能做样式缩进区分层级。 - 精准匹配子行范围:点击某个
has-children行时,只处理它后面层级比当前行高1级的行,直到遇到层级小于等于当前行的行为止,这样就能准确圈出当前节点的直接子节点,不会误操作其他层级的内容。 - 递归控制显示状态:切换子行显示时,子行里的
has-children节点默认保持折叠,用户点击时再展开它的下一级,形成递归展开的效果;同时折叠父节点时,要递归隐藏所有下级子行。 - 视觉层级优化:根据
data-level给不同层级的行添加左侧缩进,让树形结构更直观。
修改后的完整代码
HTML(带层级标记)
<div class="container"> <table class="table" id="products"> <thead> <tr> <th>Product</th> <th>Price</th> <th>Destination</th> <th>Updated on</th> </tr> </thead> <tbody> <!-- 顶级节点,层级0 --> <tr class="has-children" data-level="0"> <td><a href="#"></a>Oranges</td> <td>100</td> <td>On Store</td> <td>22/10</td> </tr> <!-- 子节点,层级1,自身带下级 --> <tr class="has-children" data-level="1"> <td><a href="#"></a>121</td> <td>120</td> <td>City 1</td> <td>22/10</td> </tr> <!-- 孙子节点,层级2 --> <tr data-level="2"> <td>121-1</td> <td>130</td> <td>Suburb 1</td> <td>22/10</td> </tr> <tr data-level="2"> <td>121-2</td> <td>135</td> <td>Suburb 2</td> <td>22/10</td> </tr> <tr data-level="1"> <td>212</td> <td>140</td> <td>City 2</td> <td>22/10</td> </tr> <!-- 另一个顶级节点 --> <tr class="has-children" data-level="0"> <td><a href="#"></a>Apples</td> <td>100</td> <td>On Store</td> <td>22/10</td> </tr> <tr data-level="1"> <td>120</td> <td>120</td> <td>City 1</td> <td>22/10</td> </tr> </tbody> </table> </div>
CSS(层级缩进+显示控制)
/* 默认隐藏所有行 */ tbody tr { display: none; } /* 默认显示顶级节点(层级0) */ tbody tr[data-level="0"] { display: table-row; } /* 展开状态的行显示 */ tbody tr.open { display: table-row; } /* 不同层级的缩进样式,视觉区分 */ tbody tr[data-level="1"] td:first-child { padding-left: 20px; } tbody tr[data-level="2"] td:first-child { padding-left: 40px; } /* 可展开节点的箭头切换样式 */ .has-children a { text-decoration: none; margin-right: 8px; } .has-children.open a::before { content: "-"; } .has-children:not(.open) a::before { content: "+"; }
JavaScript(递归展开逻辑)
document.getElementById("products").addEventListener("click", function(e) { if (e.target.tagName === "A") { e.preventDefault(); const currentRow = e.target.closest("tr"); const currentLevel = parseInt(currentRow.dataset.level); // 切换当前节点的展开状态 currentRow.classList.toggle("open"); // 遍历当前行之后的所有行,处理直接子节点 let nextRow = currentRow.nextElementSibling; while (nextRow) { const nextLevel = parseInt(nextRow.dataset.level); if (nextLevel === currentLevel + 1) { // 父节点展开时显示子行,折叠时隐藏并递归折叠子行的下级 if (currentRow.classList.contains("open")) { nextRow.style.display = "table-row"; // 子节点默认保持折叠状态 nextRow.classList.remove("open"); } else { nextRow.style.display = "none"; hideChildRows(nextRow, nextLevel); } } else if (nextLevel <= currentLevel) { // 遇到同层级或更高层级的节点,停止遍历 break; } nextRow = nextRow.nextElementSibling; } } }); // 递归隐藏子行的所有下级节点 function hideChildRows(row, parentLevel) { let nextRow = row.nextElementSibling; while (nextRow) { const nextLevel = parseInt(nextRow.dataset.level); if (nextLevel === parentLevel + 1) { nextRow.style.display = "none"; nextRow.classList.remove("open"); // 继续递归隐藏下级 hideChildRows(nextRow, nextLevel); } else if (nextLevel <= parentLevel) { break; } nextRow = nextRow.nextElementSibling; } }
关键逻辑说明
- 层级标记:通过
data-level明确节点的嵌套关系,让JS能精准识别当前节点的子范围,同时CSS可以基于层级做视觉区分。 - 子行范围判断:只处理层级为
当前层级+1的行,避免误操作其他层级的内容,确保递归展开的准确性。 - 递归隐藏:折叠父节点时,调用
hideChildRows函数递归隐藏所有下级子行,避免出现父节点折叠但子节点下级还显示的混乱情况。
内容的提问来源于stack exchange,提问作者njeoo
相关产品推荐
相关产品推荐

