在Svelte中,如何正确地链接多个元素的过渡效果?

huangapple go评论94阅读模式
英文:

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.

REPL

Panel.svelte
&lt;script lang=&quot;ts&quot;&gt;

	...

	let resCb = () =&gt; {}

	export function close() {
		return new Promise(res =&gt; {
			if(open) {
				resCb = res
				open = false				
			}else {
				res()
			}
		})
	}

	function handleOutroEnd() {
		resCb()
	}

&lt;/script&gt;

...

	{#if open}
	&lt;div class=&quot;panel-content&quot;
			 in:slide|local=&quot;{{ duration: slideDuration }}&quot;
			 out:slide|local=&quot;{{ duration: slideDuration }}&quot; 
			 on:outroend={handleOutroEnd}			 
			 &gt;
		&lt;slot /&gt;
	&lt;/div&gt;
	{/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())

&lt;script&gt;
	
    ...

	const panelRefs = {}

	function panelRefKey(panel, group) {
		const key = (group.key ?? &#39;&#39;) + panel.id
		return key
	}

&lt;/script&gt;


				&lt;Panel header={panel.header}
							 bind:selected={panel.selected}
							 bind:this={panelRefs[panel.id]}
							 &gt;
					{panel.content}
				&lt;/Panel&gt;
			
                ...

							&lt;Panel header={panel.header}
										 bind:this=&quot;{panelRefs[panelRefKey(panel, group)]}&quot;
										 onDelete={() =&gt; deletePanel(group, panel)}
								&gt;
								{panel.content}
							&lt;/Panel&gt;

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, _]) =&gt; key.startsWith(group.key))
		const panelsToClose = groupEntries.filter(([_, panelRef])=&gt; panelRef &amp;&amp; panelRef.close).map(([_, panelRef]) =&gt; panelRef)

		await Promise.all(panelsToClose.map(panelRef =&gt; panelRef.close()))
		groupEntries.forEach(([key, _]) =&gt; delete panelRefs[key])

		...
	}

	async function deletePanel(group, panel) {
		const key = panelRefKey(panel, group)
		await panelRefs[key].close()

		...
	}

huangapple
  • 本文由 发表于 2023年5月26日 09:18:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/76337089.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定