Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep updating the state during out transition #6479

Open
aradalvand opened this issue Jun 30, 2021 · 9 comments
Open

Keep updating the state during out transition #6479

aradalvand opened this issue Jun 30, 2021 · 9 comments

Comments

@aradalvand
Copy link

aradalvand commented Jun 30, 2021

Describe the problem

Check out this REPL.
When you click the "Close" button at the top, the div at the center starts its out transition, and as soon as the transition begins, the state inside of the div stops getting updated.
This might seem trivial or unimportant; but I have encountered numerous situations where I would've preferred Svelte to keep updating the contents as the out transition is happening. (If it's not clear to you when anyone would want that, I could provide examples.)

Describe the proposed solution

I'd propose that the state keeps being updated until the element is present in the DOM, either by default, or at least we should have a modifier (similar to local) that leaves the decision up to the developer:

{#if open}
    <div out:fade|keepUpdatingState> <!-- As you can see, I'm horrible at naming, but you get the idea -->
        // Blah blah blah...
    </div>
{/if}

Alternatives considered

Currently, you either have to come to terms with this limitation, or use CSS transitions with CSS classes and then toggle those classes on your element.

Importance

would make my life easier

@aradalvand aradalvand changed the title Continue updating the state when out transition is happening Keep updating the state during out transition Jun 30, 2021
@dummdidumm
Copy link
Member

Could you provide some use cases where this behavior is desired?

@aradalvand
Copy link
Author

aradalvand commented Jul 1, 2021

@dummdidumm Sure. Although the use cases that I personally encountered are a bit niche; I'll try my best to describe them:

  1. Imagine a play/pause button of a video player:
    I've encountered a number of situations of this sort, notice how in the below video, when the play button is clicked, its icon immediately changes to a pause icon, and then it starts to fade out, this wouldn't be possible to achieve with Svelte transitions.
    I should acknowledge that this one isn't a deal-breaker or something, it simply would've been a little nicer if it was possible to do in Svelte:
Rec.0014.mp4
  1. More severe example, when you want to decide whether an element should have a transition or not:
    Imagine again a video player where you have a poster element, and you want this poster to disappear as soon as the video either starts or the user seeks to another point in the video. But:
{#if !hasEverPlayed && !hasEverSeeked}
    <!-- NOTE: If the poster is being removed but not as a result of the video being played but as a result of user's seeking, then we don't want to have a fade out transition since we want the relevant frame of the video to be shown instantly, which is to say no transition for the poster. -->
    <Poster
        {posterUrl}
        shouldHaveTransition={hasEverPlayed && !hasEverSeeked}
    />
{/if}

This wouldn't work, as Svelte doesn't update the state within the {#if} block, namely the value of the shouldHaveTransition, once the condition evaluates to false.
The contents of Poster.svelte:

<script lang="ts">
    // Imports:
    import { fade } from "svelte/transition";

    // Props:
    export let posterUrl: string;
    export let shouldHaveTransition: boolean;
</script>

<img
    src={posterUrl}
    alt="foo"
    class="video-player__poster"
    out:fade|local={{ duration: shouldHaveTransition ? 80 : 0 }}
/>

Again, the examples I'm providing are perhaps a bit too niche, and that's because I'm currently building a full-fledged video player in Svelte and so these were some of the cases where this limitation was annoying.
But I'm sure there are also similar cases in other types of components and UIs, where the behavior I described in my original comment would be more desirable.

@aradalvand
Copy link
Author

aradalvand commented Dec 26, 2021

@dummdidumm Hey, I just stumbled into this again and I actually think this wouldn't make sense logically, so unless I'm missing something, we can close this issue.

Imagine you have something like this:

{#if someObject != null}
    <div transition:fade>
        {someObject.someProperty}
    </div>
{/if}

If Svelte continues to keep the state inside the {#if} block updated during out transition — which is what I originally proposed — in an instance like the code above, we'll get a Cannot read properties of null (reading 'someProperty') error during the out transition while the browser tries to evaluate the expression inside the div (someObject.someProperty).

The larger point being that any time you have an if block, the code inside always assumes the true-ness of the if condition, but if Svelte implements what I originally asked for, namely that it continues to update the state during out transition, that would break this principle.

Let me know what you think. Am I right here?
If yes, then I think it would be nice if this was pointed out somewhere in the docs or the FAQ section, I think we should explain the rationale behind why Svelte couldn't/shouldn't update the state during out transitions, since it isn't really obvious at first, and might lead to confusion.

@bummzack
Copy link

Just encountered the same issue, since I wanted to update a class on an element via the outrostart event. eg.

<script>
	import {fade} from "svelte/transition";

	export let isOpen = false;
	let isAnimatingOut = false;
</script>

{#if isOpen}
<aside
	in:fade
	out:fade
	on:introstart={() => isAnimatingOut = false}
	on:outrostart={() => isAnimatingOut = true}
	class:animate-out={isAnimatingOut}
>
   Content
</aside>
{/if}

In this case, the class animate-out is never set on the element, which would be nice in order to animate or change things before/while animating out.
I think if the outrostart event would trigger just before "freezing" the element, it would be enough for that case

@keoshi
Copy link

keoshi commented Oct 18, 2023

Hi there. Also stumbled into this problem today and thankfully found this GH issue.

I'm trying to create a custom dropdown, but as you can see, the selected option isn't marked when clicked because the transition starts right after. Thought adding the slightest delay (i.e.: 1ms) would do the trick, but was wrong.

Oct-18-2023 22-18-13

Here's the associated code:

{#if isDropdownOpen}
	<ul class="listbox" transition:scale={{ duration: 250, opacity: 0, start: 0.96, easing: quadOut }}>
		{#each options as option}
			<li>
				<button class="option" role="option" on:click={() => handleOptionClick(option)} >
					{#if selectedOption === option}
						<Check size="16" />
					{:else}
						<div class="icon-spacer"></div>
					{/if}

					{option}
				</button>
			</li>
		{/each}
	</ul>
{/if}

See how it compares to https://ui.shadcn.com/docs/components/combobox, where the checkmark is seen before the options fade away.

Edit: forgot to mention that in my case adding a setTimeout() to hide the options works, but it's a sub-optimal workaround.

@robertadamsonsmith
Copy link

The strategy I've used for dealing with this problem, is to use store reactivity, instead of DSL reactivity, so that outroing child components continue to receive updates.

Specifically, just put the relevant state into a store, and then pass the store to each child component so that they can subscribe to it, and then they will update in response to changes to that store even while outroing.

A simple example of doing this for the type of combobox situation that @keoshi mentioned is:

https://svelte.dev/repl/aa236dfb8f68416c86f4ae431265b595?version=4.2.12

Note that the newly selected item is highlighted while outroing.

(I created a generic Subscribe component so that I could write all of the markup in a single component, but having a separate Item component that subscribes to the store would also work and may be preferable.)

@robertadamsonsmith
Copy link

The strategies I outlined in my previous comment don't work with Svelte 5. Despite Svelte 5 having nice granular updates, no DOM updates are carried out at all for components/elements that are outroing.

The workaround I came up with (and which feels a bit hacky), is to have the child components use mount so that they don't know that they are outroing (and so continue to update the DOM). This can even work in a composable way, with the help of a utility Mount component that simply mounts the children snippet (I didn't actually expect to be able to mount the children snippet directly, or for the reactivity to reliably work between separate invocations of mount like this, and so since this is straying into undocumented territory this may well break in interesting ways):

Svelte 5 example

I think it would be good to decide and state in the docs whether or not outroing elements should receive DOM updates. That way it will be clear if this is a bug to fix, a behaviour to work-around, or a limitation to live with.

@CaptainCodeman
Copy link

I just came across this - my use case was an image gallery with a popup preview that showed a larger version of the hovered item. If the popup has a transition applied, the change to the image src frequently stops working so the view gets out of sync, showing the wrong popup for the hovered image.

My quick-fix was to add a use:action to set the src instead, so instead of

<img {src} ...>

you have

<img use:setsrc={src} ... >

action:

function setsrc(node: HTMLImageElement, src: string) {
	function update(src: string) {
		node.src = src
	}
	update(src)
	return {
		update,
	}
}

@robertadamsonsmith
Copy link

I just updated the Svelte 5 workaround I posted previously, to work with the more recent Svelte 5 changes (on:click becoming onclick, and disallowing snippets from being treated as components). It is ergonomically nice enough, but the implementation is hacky (I don't know if calling mount() like this could cause weird things to happen, or performance issues). Update here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants