在动态表单场景下,用户经常需要点击按钮追加输入框,或者删除某一行后让剩余输入框的序号和辅助说明保持连续。如果只操作 DOM 而不同步数据源,新增项的 title 属性容易写成固定值,删除中间项后后面的 title 也不会自动前移,导致无障碍访问和提示信息错乱。正确的做法是让 title 完全由数据索引决定,任何增删都先改数据,再由渲染逻辑生成正确的 title。

为什么 title 属性需要跟随数据重排
title 属性在浏览器中表现为鼠标悬停提示,同时也被部分屏幕阅读器用于朗读表单控件的描述。当表单支持动态删除时,假如第二项被移除,原本第三项的 title 若仍写作“第3项”,对用户而言就变成了逻辑上的“第2项”,产生认知偏差。此外,手动用 setAttribute 去改每一个 DOM 节点的 title,在频繁操作时会遗漏节点,尤其在虚拟 DOM 或异步渲染环境中更不可靠。
从维护角度看,把 title 硬编码在 HTML 里,意味着增删逻辑和视图描述耦合在一起。一旦产品要求 title 文案变化,比如从“第N项”改成“候选人N”,就得到处改代码。用数据索引生成 title,只需改一处模板或渲染函数,所有项自动一致,也天然支持删除后的重排序。
原生 JavaScript 实现方案
下面用最基础的 JS 展示一个可新增、可删除、且 title 自动连续的表单。核心是一个数组 items,渲染函数根据数组下标输出 input,并在 title 中放入当前索引加一的值。删除时调用 splice 改变数组,再重新渲染即可。
let items = ['', ''];
function renderForm() {
const box = document.getElementById('formBox');
box.innerHTML = '';
items.forEach((val, idx) => {
const wrap = document.createElement('div');
const input = document.createElement('input');
input.type = 'text';
input.value = val;
// title 由索引决定,删除后重渲染自动连续
input.title = '第' + (idx + 1) + '项';
input.oninput = (e) => {
items[idx] = e.target.value;
};
const del = document.createElement('button');
del.textContent = '删除';
del.onclick = () => {
items.splice(idx, 1);
renderForm();
};
wrap.appendChild(input);
wrap.appendChild(del);
box.appendChild(wrap);
});
}
document.getElementById('addBtn').onclick = () => {
items.push('');
renderForm();
};
renderForm();
上述代码在每次增删后都整体重渲染,title 必然正确。但它的缺点是重渲染会丢失已有输入框的焦点,且如果输入框内有复杂状态,直接 innerHTML 清空会打断交互。在原生方案中,若想保留焦点,可以改为只增删差异节点,但那样就需要手动同步 title,复杂度上升。
一个折中办法是在删除时只移除对应 DOM 并遍历后续节点重设 title,而不重建全部。这样既能保焦点,又让 title 连续。不过只要数据仍是单一数据源,逻辑就不容易出错。下面的 Vue 示例展示了数据驱动如何天然规避这些麻烦。
Vue 中的声明式实现
在 Vue 里,我们用 v-for 渲染列表,并将 title 绑定为索引表达式。由于 Vue 的响应式系统会在数组变化后高效更新 DOM,title 会随着 key 的排布自动更新,不需要手写重排逻辑。
<div id="app">
<div v-for="(item, idx) in items" :key="idx">
<input type="text" v-model="items[idx]" :title="'第' + (idx + 1) + '项'">
<button @click="remove(idx)">删除</button>
</div>
<button @click="add">新增</button>
</div>
<script>
new Vue({
el: '#app',
data() {
return { items: ['', ''] };
},
methods: {
add() { this.items.push(''); },
remove(i) { this.items.splice(i, 1); }
}
});
</script>
这里 :title 绑定了 '第' + (idx + 1) + '项',删除中间项后 Vue 会复用并更新对应 DOM 的 title,不会出现断号。需要注意,使用 index 作为 key 在数组 splice 时可能导致输入值错位,如果输入框内有独立状态,建议给每个 item 加稳定 id 作为 key,title 仍由显示索引计算。
如果担心 splice 带来的复用问题,可以把数据项设计为对象数组,例如 items: [{id: 1, val: ''}],key 用 id,title 用 items.indexOf(item) + 1 来算。这样删除后 indexOf 变化,title 自动前移,且输入框的值不会因位置变动而串号。
重排序时的常见坑与建议
第一,不要试图在删除按钮里用 DOM 查找去改后续兄弟节点的 title,这种命令式做法在框架中会和虚拟 DOM 冲突,造成状态不一致。第二,如果表单需要提交,提交前应从数据数组取值,而不是读 DOM,这样 title 的排布和值的位置天然对应。
第三,当输入项很多时,整体重渲染可能影响性能,此时应依赖框架的差异化更新,或原生中使用 DocumentFragment 批量操作。最后,title 只是辅助提示,复杂说明应配合 aria-label 或 <label> 标签,但排布原则相同:都由数据索引生成,删除即重排。
小结
动态表单中新增项与删除后重排序的本质,是把视图描述交给数据索引。无论是原生 JS 的 render 函数,还是 Vue 的 v-for 绑定,只要 title 由 idx + 1 算出,就能保证新增有正确提示、删除后不断号。避免手动改属性,不仅减少 bug,也让代码更易扩展。
dynamic_forminput_titleauto_reorder修改时间:2026-08-07 05:51:14