Svelte 的 details 被无关状态更新重新折叠
Svelte 中 details 的 open 表达式可能在同一更新组重写 DOM,覆盖用户刚展开的状态。用 bind:open 明确状态归属,并保留初始值与后续控制的区别。
点击 <details> 展开一项,页面上的计数更新了,刚展开的内容却马上折回。若元素写着 open={item === initial},还在 toggle 时修改了其他状态,应检查框架是否把初始表达式重新写回了 open。
Svelte 5.56.3、5.57.3 有这种具体表现。用户操作已改变 DOM,但控制 open 的表达式仍保持原值;一次相关更新可能再次执行属性赋值,覆盖刚才的展开状态。
表达式没变,DOM 状态已经变了
details.open 是可由浏览器交互改变的 DOM 属性。open={...} 把应用表达式单向交给元素,并不自动把用户展开后的值写回那个表达式。
Svelte 5.57.3 对普通 DOM property 可以直接生成赋值;value、checked、selected 则有各自的专用处理。不能由这些表单属性的行为推断 open 必然跳过相同值的重复赋值。
先检查 toggle 后究竟更新了哪些变量,以及同一元素的属性是否进入相同更新组。也要排除应用显式关闭其他项的手风琴逻辑、重新创建列表节点或变化的 key;这些行为同样会让展开状态丢失。
让用户操作进入明确的状态
如果要求每个条目记住自己的展开状态,可以把初值放进模型,并使用双向绑定:
<script>
let items = $state([
{ id: 'a', label: '第一项', open: true },
{ id: 'b', label: '第二项', open: false },
]);
</script>
{#each items as item (item.id)}
<details bind:open={item.open}>
<summary>{item.label}</summary>
<p>该项的内容</p>
</details>
{/each}
这里的 open 同时记录浏览器交互和应用主动修改,不再把“最初展开哪项”的表达式反复当成当前状态。接入真实列表时,应按稳定 id 保存状态;每次刷新数据都用初始值重建整个模型,仍会让用户操作丢失。
如果业务要求只能展开一项,应在同一份状态模型中实现切换规则,而不是一边 bind、一边另写一个始终返回初值的 open 属性。初始值只应决定首次状态,后续外部控制则需要明确的更新入口,两者不能混用。
保留展开、关闭和列表更新的验收
检查用户打开原本关闭的一项后,计数、筛选或其他不相关状态变化不会把它折回;再检查用户主动关闭、应用主动关闭及数据刷新仍符合业务约定。
不要仅移除 toggle 里的计数更新就宣布解决。那会改变功能,也可能只是让覆盖 open 的同组更新不再触发。直接在 DOM 上强制重新打开,也会与状态模型继续争夺控制权。
这里的绑定处理适用于需要保存用户展开状态的页面;若 open 本来就应由父组件强制控制,先明确控制契约,再决定如何同步用户操作。


