如何将Neo4j返回的嵌套JSON从Node.js传递到EJS渲染多级下拉表单
EJS 多级下拉选择框渲染解决方案
问题根因
你之前用ul/li可以正常嵌套渲染是因为列表标签本身支持多层嵌套,但<select>属于表单控件,HTML语法明确规定其内部仅允许嵌套<option>、<optgroup>两类子元素,直接在<select>里嵌套另一个<select>属于语法错误,浏览器解析时会自动把嵌套的<select>移到外层,最终导致页面结构混乱。
实现方案
首先假设你服务端传递到EJS的四级嵌套数据结构如下(可根据你的实际字段调整):
const neo4jData = { name: 'UofO', campBusList: [ { id: 'cb_001', name: '主校区', schColUnitList: [ { id: 'scu_001', name: '计算机学院', deptList: [ { id: 'dept_001', name: '软件工程系' }, { id: 'dept_002', name: '网络安全系' } ] } ] } ] }
提供两种常用实现方案可选:
方案1:静态全量渲染(适合数据量小的场景)
不嵌套<select>标签,每一级下拉独立渲染,用optgroup给同父级的选项做分组:
<!-- 一级选择:CampBus --> <select name="campBus" id="campBus"> <% neo4jData.campBusList.forEach(cb => { %> <option value="<%= cb.id %>"><%= cb.name %></option> <% }) %> </select> <!-- 二级选择:SchColUnit --> <select name="schColUnit" id="schColUnit"> <% neo4jData.campBusList.forEach(cb => { %> <optgroup label="<%= cb.name %>" data-parent-cbid="<%= cb.id %>"> <% cb.schColUnitList.forEach(scu => { %> <option value="<%= scu.id %>"><%= scu.name %></option> <% }) %> </optgroup> <% }) %> </select> <!-- 三级选择:Dept --> <select name="dept" id="dept"> <% neo4jData.campBusList.forEach(cb => { %> <% cb.schColUnitList.forEach(scu => { %> <optgroup label="<%= scu.name %>" data-parent-scuid="<%= scu.id %>"> <% scu.deptList.forEach(dept => { %> <option value="<%= dept.id %>"><%= dept.name %></option> <% }) %> </optgroup> <% }) %> <% }) %> </select>
如果需要联动效果,只需加少量前端JS监听上一级下拉的change事件,隐藏不符合父级ID的optgroup即可。
方案2:动态联动渲染(适合数据量大的场景)
EJS只输出基础下拉结构和全量数据,后续层级的选项根据上一级的选择动态生成,性能更高:
<!-- 把全量数据挂载到全局供JS调用 --> <script> window.fullTreeData = <%- JSON.stringify(neo4jData) %> </script> <!-- 各级下拉空结构 --> <select name="campBus" id="campBus"></select> <select name="schColUnit" id="schColUnit" disabled></select> <select name="dept" id="dept" disabled></select> <script> const cbSelect = document.getElementById('campBus') const scuSelect = document.getElementById('schColUnit') const deptSelect = document.getElementById('dept') // 初始化一级选项 fullTreeData.campBusList.forEach(cb => { const opt = document.createElement('option') opt.value = cb.id opt.textContent = cb.name cbSelect.appendChild(opt) }) // 一级变更触发二级渲染 cbSelect.addEventListener('change', () => { const selectedCb = fullTreeData.campBusList.find(cb => cb.id === cbSelect.value) scuSelect.innerHTML = '' deptSelect.innerHTML = '' deptSelect.disabled = true // 渲染二级选项 selectedCb.schColUnitList.forEach(scu => { const opt = document.createElement('option') opt.value = scu.id opt.textContent = scu.name scuSelect.appendChild(opt) }) scuSelect.disabled = false scuSelect.dispatchEvent(new Event('change')) }) // 二级变更触发三级渲染 scuSelect.addEventListener('change', () => { const selectedCb = fullTreeData.campBusList.find(cb => cb.id === cbSelect.value) const selectedScu = selectedCb.schColUnitList.find(scu => scu.id === scuSelect.value) deptSelect.innerHTML = '' selectedScu.deptList.forEach(dept => { const opt = document.createElement('option') opt.value = dept.id opt.textContent = dept.name deptSelect.appendChild(opt) }) deptSelect.disabled = false }) // 初始化渲染 cbSelect.dispatchEvent(new Event('change')) </script>
内容的提问来源于stack exchange,提问作者Redink
相关产品推荐
相关产品推荐

