英文:
What is the correct way of chaining transitions with multiple elements in Svelte?
问题
我正在使用 Svelte 编写一个小应用程序,向用户呈现一个可折叠面板列表,其中包含文本,要求他们选择其中一些面板,然后当用户点击按钮时,将这些面板分组到另一个不同列表中的可折叠面板中。从概念上讲,这类似于 延迟过渡教程 ,但在一端多了一个额外的层级。
我已经准备了一个 REPL 供您参考。替换 App.svelte
中的组件以切换实现。
Original.svelte
组件说明了问题:
我正在使用 Svelte 的 crossfade
过渡来发送和接收,当分组的面板处于折叠状态时,一切看起来都很好。然而,任何打开的面板在两个列表之间传递时,都会尴尬地扭曲到关闭状态,我理解这是 crossfade
的效果。解决方案很明显:首先关闭面板,然后再将它们发送到目标位置。
现在,我的问题是,在 Svelte 中,如何以惯用和最优的方式做到这一点?
WithDelay.svelte
组件展示了我尝试使用 delay
来延迟个别面板过渡的尝试,具体取决于面板是否打开。正如显而易见的那样,这会延迟发送/接收过渡,但仍然会扭曲面板,无论延迟多长时间(尝试一秒或更长时间)。
我的第二个直觉是收集要移动的面板到一个中间列表中,关闭它们,然后最终使用过渡事件 on:outrostart
/on:outroend
来完成移动。然而,这个逻辑太冗长,需要多个辅助函数和额外的数组来跟踪元素。一旦多个面板同时触发它们的事件,一切都变得一团糟。防止竞态条件变得如此复杂,以至于我完全放弃了这个尝试。
我的第三个尝试可以在 WithTimeout.svelte
中看到,我不得不使用 setTimeout
来延迟关闭面板后实际移动元素的操作。如预期的那样,这正是我想要实现的方式,但是使用 setTimeout
感觉不对。尽管如此,我觉得我不应该需要使用 requestAnimationFrame
来实现自己的 wait
函数,因为 Svelte 本身似乎没有内置的用于延迟的函数。在内部,它在一个使用 requestAnimationFrame
的循环结构中运行过渡。
在继续使用超时之前,我想听听更有经验的意见。在继续之前,我甚至不确定我的担忧是否合理。也许 requestAnimationFrame
只在执行实际的过渡操作时才重要,而 setTimeout
在分配变量时是可以的?非常感谢您的帮助!
英文:
I'm writing a small app using Svelte that presents a list of collapsible panels with text to the user, has them select a set of those panels, and, when the user clicks a button, groups these panels into another collapsible panel in a different list. Conceptually, it's the deferred transitions tutorial with an extra layer on one end.
I have prepared a REPL so you can follow along. Replace the component in App.svelte
to switch the implementations.
The Original.svelte
component illustrates the problem:
I'm using Svelte's crossfade
transition to send and receive, and everything looks great when the grouped panels are collapsed. Any open panel, however, will warp awkwardly to its closed state when sent between the lists, which I understand is an effect of crossfade
. The solution is obvious: close the panels first, then send them over to their target.
My question is, now, what is the idiomatic/optimal way to do that in Svelte?
The WithDelay.svelte
component shows my attempt using delay
on the individual panel transitions based on whether a panel was open. As is evident, this delays the send/receive transitions but still warps the panels, no matter how long the delay is (try a second or longer).
My second intuition was to collect the panels to be moved in an intermediate list, close them, and finally use the transition events on:outrostart
/on:outroend
to finalize the move. However, the logic was too lengthy to be correct, requiring multiple helper functions and extra arrays to track the elements. Once multiple panels had their events fire simultaneously, everything just went haywire. Preventing race conditions turned out so complicated that I scrapped that attempt entirely.
My third attempt can be seen in WithTimeout.svelte
, where I resorted to using setTimeout
to delay the actual moving of elements after the panels have been closed. As expected, that works exactly how it should and should be used as a reference for what I want to achieve, but using setTimeout
just feels wrong. Still, I feel like I should not need to implement my own wait
function using requestAnimationFrame
since Svelte itself seems to have no built-in function for that. Internally, it runs the transition in a loop construct that uses requestAnimationFrame
.
I'd go on with the timeout, but I would like to hear more experienced opinions before proceeding. I'm not even sure if my concerns are even justified. Maybe requestAnimationFrame
only matters when doing actual transition stuff, and setTimeout
is fine for assigning variables? Thanks a lot for your help!
答案1
得分: 2
A different approach could be to handle the closing inside the component via an exported function that returns a Promise that resolves when the out transition has ended.
Panel.svelte
<script lang="ts">
...
let resCb = () => {}
export function close() {
return new Promise(res => {
if (open) {
resCb = res
open = false
} else {
res()
}
})
}
function handleOutroEnd() {
resCb()
}
</script>
...
{#if open}
<div class="panel-content"
in:slide|local="{{ duration: slideDuration }}"
out:slide|local="{{ duration: slideDuration }}"
on:outroend={handleOutroEnd}
>
<slot />
</div>
{/if}
(The |local
flag added so that the slide transition doesn't play when the panels are added as a slot to the Panel
component for the group. Setting up an extra component for the group might be better.)
The function can be called on the component references that can be organized in an object with either the panel.id
as the key or a combination with the group.key
(crypto.randomUUID()
instead of Symbol()
)
<script>
...
const panelRefs = {}
function panelRefKey(panel, group) {
const key = (group.key ?? '') + panel.id
return key
}
</script>
<Panel header={panel.header}
bind:selected={panel.selected}
bind:this={panelRefs[panel.id]}
>
{panel.content}
</Panel>
...
<Panel header={panel.header}
bind:this="{panelRefs[panelRefKey(panel, group)]}"
onDelete={() => deletePanel(group, panel)}
>
{panel.content}
</Panel>
Since close()
directly resolves if the component wasn't opened, tracking bind:open
on the data/component is redundant, and the function can simply be called on all component references before the data changes sides.
async function deleteGroup(group) {
const groupEntries = Object.entries(panelRefs).filter(([key, _]) => key.startsWith(group.key))
const panelsToClose = groupEntries.filter(([_, panelRef]) => panelRef && panelRef.close).map(([_, panelRef]) => panelRef)
await Promise.all(panelsToClose.map(panelRef => panelRef.close()))
groupEntries.forEach(([key, _]) => delete panelRefs[key])
...
}
async function deletePanel(group, panel) {
const key = panelRefKey(panel, group)
await panelRefs[key].close()
...
}
英文:
A different approach could be to handle the closing inside the component via an exported function that returns a Promise that resolves when the out transition has ended.
Panel.svelte
<script lang="ts">
...
let resCb = () => {}
export function close() {
return new Promise(res => {
if(open) {
resCb = res
open = false
}else {
res()
}
})
}
function handleOutroEnd() {
resCb()
}
</script>
...
{#if open}
<div class="panel-content"
in:slide|local="{{ duration: slideDuration }}"
out:slide|local="{{ duration: slideDuration }}"
on:outroend={handleOutroEnd}
>
<slot />
</div>
{/if}
(The |local
flag added so that the slide transition doesn't play when the panels are added as slot to the Panel
component for the group. Setting up an extra component for the group might be better.)
The function can be called on the component references that can be organised on an object with either the panel.id
as key or a combination with the group.key
(crypto.randomUUID()
instead of Symbol()
)
<script>
...
const panelRefs = {}
function panelRefKey(panel, group) {
const key = (group.key ?? '') + panel.id
return key
}
</script>
<Panel header={panel.header}
bind:selected={panel.selected}
bind:this={panelRefs[panel.id]}
>
{panel.content}
</Panel>
...
<Panel header={panel.header}
bind:this="{panelRefs[panelRefKey(panel, group)]}"
onDelete={() => deletePanel(group, panel)}
>
{panel.content}
</Panel>
Since close()
directly resolves if the component wasn't opened, tracking bind:open
on the data/component is redundant and the function can simply be called on all component references before the data changes sides.
async function deleteGroup(group) {
const groupEntries = Object.entries(panelRefs).filter(([key, _]) => key.startsWith(group.key))
const panelsToClose = groupEntries.filter(([_, panelRef])=> panelRef && panelRef.close).map(([_, panelRef]) => panelRef)
await Promise.all(panelsToClose.map(panelRef => panelRef.close()))
groupEntries.forEach(([key, _]) => delete panelRefs[key])
...
}
async function deletePanel(group, panel) {
const key = panelRefKey(panel, group)
await panelRefs[key].close()
...
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论