基于Vanilla JS的动态输入表单验证需求实现问询
动态表单验证功能实现与修复
需求说明
- 注意:第二个标签页中的price输入项是从第一个标签页添加元素时动态生成的。
- 具体需求:
- 从第一个标签页添加元素后点击保存按钮,需弹出提示“存在2个空字段”。
- 保存按钮默认禁用,向第二个标签页添加元素后启用;点击该按钮弹出“存在3个空字段”提示;所有字段填满后点击按钮弹出“字段已填满”提示。
- 向第二个标签页的表格添加元素时,点击按钮提示对应空字段数,每新增一个元素空字段数递增(如新增后提示“存在4个空字段”)。
- 向空input填写内容时,按钮上的空字段数递减;点击保存按钮提示“存在n个空输入框”,同时焦点定位到第一个空input。
现有问题
现有代码部分功能可用,但动态生成的input验证存在问题(如添加5次后第4个输入项为空时,验证无法正确统计),需完善实现。
修复后的完整代码
HTML代码
<html> <head> <title>动态表单验证</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css"> </head> <body> <section class="section"> <div class="tabs"> <ul class="tabs-menu"> <li class="is-active" data-target="first-tab"><a>标签页一</a></li> <li data-target="second-tab"><a>标签页二</a></li> </ul> </div> <div class="tab-content" id="first-tab"> <table id="articulos" class="table"> <thead> <tr> <TH>id</TH> <TH>产品id</TH> <TH>可用数量</TH> <TH>鞋码</TH> <TH>添加</TH> <TH>添加</TH> </tr> </thead> <tbody id="productos_content"> <tr class="tr"> <td class="id_n">15</td> <td class="cantidad_disponible">3</td> <td class="talla">36</td> <td class="td"><button id="15" onclick="add(15)" class="button">添加</button></td> </tr> <tr class="tr"> <td class="id_n">1</td> <td class="cantidad_disponible">7</td> <td class="talla">40</td> <td class="td"><button id="1" onclick="add(1)" class="button">添加</button></td> </tr> </tbody> </table> </div> <div class="tab-content" id="second-tab"> <div class="shopping-cart-items shoppingCartItemsContainer"> <form id="form2"> <input type="text" name="campo" id="campo" class="form-control" required> <input type="text" id="content" class="form-control"> <input type="submit" id="guardardatos" value="保存" disabled /> <table id="tabla" class="table"> <tr> <TH>id</TH> <TH>产品id</TH> <TH>可用数量</TH> <TH>鞋码</TH> <TH>数量</TH> <TH>价格</TH> </tr> <tbody id="carrito_content" class="tbody"></tbody> </table> <div class="float-right"> 总计:<input type="text" id="resultado_total" value="0" disabled /> </div> </form> </div> </div> </section> <script> const tabSystem = { init(){ document.querySelectorAll('.tabs-menu').forEach(tabMenu => { Array.from(tabMenu.children).forEach((child, ind) => { child.addEventListener('click', () => { tabSystem.toggle(child.dataset.target); }); if(child.className.includes('is-active')){ tabSystem.toggle(child.dataset.target); } }); }); }, toggle(targetId){ document.querySelectorAll('.tab-content').forEach(contentElement=>{ contentElement.style.display = contentElement.id === targetId ? 'block' : 'none'; document.querySelector(`[data-target="${contentElement.id}"]`).classList[contentElement.id === targetId ? 'add' : 'remove']('is-active'); }) }, }; tabSystem.init() </script> </body> </html>
JavaScript代码
function updateCantidadNuevo(input) { if(input.checkValidity()){ input.setAttribute("data-oldvalue", input.value); } else { input.value = input.getAttribute("data-oldvalue") || input.min; } } function add(id, cantidad=1){ let input = tbody.querySelector('.ItemCarrito>.form__element-input_cantidad>#shoppingCartItemQuantity_'+id); if (input === null) { return; } if (typeof input.value === 'undefined' || input.value === null) { input.value = cantidad; } else { input.value = (input.valueAsNumber + cantidad); } updateCantidadNuevo(input); validarFormulario(); } const tbody = document.querySelector('#second-tab >.shoppingCartItemsContainer > #form2 >#tabla >.tbody') const addToShoppingCartButtons = document.querySelectorAll('#first-tab > #articulos > tbody > tr > .td > .button') const form = document.getElementById("form2"); const botonGuardar = document.getElementById("guardardatos"); addToShoppingCartButtons.forEach((addToCartButton) => { addToCartButton.addEventListener('click', addToCartClicked); }); function addToCartClicked(event) { const button = event.target; const item = button.closest('.tr'); const id_prod = item.querySelector('.id_n').textContent; const cantidad_disponible = item.querySelector('.cantidad_disponible').textContent; const talla = item.querySelector('.talla').textContent; addItemToShoppingCart(id_prod, cantidad_disponible, talla); } function addItemToShoppingCart(id_prod,cantidad_disponible,talla) { const elementsTitle = document.getElementsByClassName('title'); for (let i = 0; i < elementsTitle.length; i++) { if (elementsTitle[i].innerText === id_prod) { return; } } const tr = document.createElement('tr') tr.classList.add('ItemCarrito') tr.setAttribute("id", `tr_${id_prod}`); const Content = ` <th scope="row">${id_prod}</th> <td class="table__productos"> <input id="id_productos_${id_prod}" type="text" value="${id_prod}" class="id_productos" style="border: none; border-color: transparent;outline: 0;width: 40px;"> <h6 id="title_${id_prod}" class="title" hidden>${id_prod}</h6> </td> <td class="table__cantidad_disponible"> <input id="cantidad_disponible_${id_prod}" type="text" value="${cantidad_disponible}" class="cantidad_disponible" style="border: none; border-color: transparent;outline: 0;width: 40px;" > </td> <td class="table__talla"> <input id="talla_${id_prod}" type="text" value="${talla}" class="talla" style="border: none; border-color: transparent;outline: 0;width: 40px;"> </td> <td class="form__element-input_cantidad"> <input id="shoppingCartItemQuantity_${id_prod}" max="${cantidad_disponible}" class="shopping-cart-quantity-input cantidad" type="number" value="1" min="1"> </td> <td class="form__element-input_precio"> <input id="precio_${id_prod}" type="text" class="precio" min="0"> </td> <td class="table__delete"> <button class="delete btn btn-danger">x</button> </td> ` tr.innerHTML = Content tbody.append(tr); // 给新元素绑定事件 const newInputs = tr.querySelectorAll('input'); newInputs.forEach(input => { input.addEventListener('input', validarFormulario); }); tr.querySelector('.delete').addEventListener('click', () => { tr.remove(); validarFormulario(); }); validarFormulario(); } // 表单提交逻辑 form.addEventListener("submit", (event) => { event.preventDefault(); const camposVacios = getEmptyFieldsCount(); const firstEmptyInput = getFirstEmptyInput(); if (camposVacios > 0) { alert(`存在${camposVacios}个空输入框`); firstEmptyInput?.focus(); } else { alert("字段已填满"); } }); // 统计空字段数量 function getEmptyFieldsCount() { let count = 0; form.querySelectorAll("input").forEach(input => { if (input.value.trim() === "") count++; }); return count; } // 获取第一个空输入框 function getFirstEmptyInput() { for (const input of form.querySelectorAll("input")) { if (input.value.trim() === "") return input; } return null; } // 更新按钮状态与显示文本 function validarFormulario() { const camposVacios = getEmptyFieldsCount(); const hasItems = tbody.querySelectorAll('.ItemCarrito').length > 0; if (camposVacios > 0) { botonGuardar.disabled = !hasItems; botonGuardar.value = `存在${camposVacios}个空字段`; } else { botonGuardar.disabled = false; botonGuardar.value = "保存"; } } // 初始化绑定事件 document.querySelectorAll("#form2 input").forEach(input => { input.addEventListener('input', validarFormulario); }); // 初始验证 validarFormulario();
关键修改说明
- 动态元素事件绑定:在添加新表格行后,给所有动态生成的input绑定输入事件,同时给删除按钮绑定移除事件,确保元素变化后能实时触发验证。
- 实时输入框统计:每次验证时重新获取所有输入框,避免缓存导致动态添加的元素无法被统计。
- 按钮启用逻辑:只有当表格中存在添加的元素时,才根据空字段数决定按钮状态,满足“默认禁用,添加元素后启用”的需求。
- 提交交互优化:点击保存按钮时,精准弹出空字段提示,并自动定位到第一个空输入框。
- 代码拆分复用:将空字段统计、空输入框查找拆分为独立函数,提升代码可读性和维护性。
内容的提问来源于stack exchange,提问作者FelipeDeveloper
相关产品推荐
相关产品推荐

