sveltesvelte-componentsvelte-transition

Svelte animated transition after its style is updated from binded properties


I have a Svelte 5 component who's position is calculated according to its size, which depends on its dynamic content:

    <script>
    import { onMount } from "svelte"
    import { quintIn } from "svelte/easing"
    import { scale } from "svelte/transition"
    
    import { getRandomInt } from "$lib/js/utils.js"
    
    const { htmlContent = null } = $props()
    
    let thisDiv
    let left
    let top
    let clientWidth = $state()
    let clientHeight = $state()
    let positionCalculated = false
    
    onMount(async () => {
        left = getRandomInt(5, window.innerWidth - clientWidth - 5)
        top = getRandomInt(5, window.innerHeight - clientHeight - 5)
        thisDiv.style.left = `${left}px`
        thisDiv.style.top = `${top}px`
        thisDiv.style.zIndex = nb
        positionCalculated = true
    })
    </script>
    
{#if positionCalculated}
    <div
        bind:this={thisDiv}
        bind:clientWidth
        bind:clientHeight
        in:scale={{ duration: 500, easing: quintIn }}
    >
        htmlContent
    </div>
{/if}

T̶h̶e̶ ̶e̶a̶s̶e̶ ̶i̶n̶ ̶a̶n̶i̶m̶a̶t̶i̶o̶n̶ ̶d̶o̶e̶s̶n̶'̶t̶ ̶w̶o̶r̶k̶ ̶a̶t̶ ̶a̶l̶l̶ ̶o̶n̶ ̶t̶h̶i̶s̶ ̶c̶o̶m̶p̶o̶n̶e̶n̶t̶,̶ ̶w̶h̶o̶ ̶a̶p̶p̶e̶a̶r̶s̶ ̶s̶u̶d̶d̶e̶n̶l̶y̶,̶ ̶u̶n̶l̶i̶k̶e̶ ̶o̶t̶h̶e̶r̶ ̶c̶o̶m̶p̶o̶n̶e̶n̶t̶ ̶w̶i̶t̶h̶ ̶t̶h̶e̶ ̶s̶a̶m̶e̶ ̶e̶a̶s̶e̶-̶i̶n̶ ̶a̶n̶i̶m̶a̶t̶i̶o̶n̶.̶ ̶I̶'̶v̶e̶ ̶b̶e̶e̶n̶ ̶t̶r̶y̶i̶n̶g̶:̶ ̶-̶ ̶t̶o̶ ̶a̶d̶d̶ ̶a̶ ̶t̶e̶m̶p̶l̶a̶t̶e̶ ̶c̶o̶n̶d̶i̶t̶i̶o̶n̶ ̶t̶o̶ ̶d̶i̶s̶p̶l̶a̶y̶ ̶t̶h̶e̶ ̶c̶o̶m̶p̶o̶n̶e̶n̶t̶ ̶o̶n̶l̶y̶ ̶a̶f̶t̶e̶r̶ ̶i̶t̶s̶ ̶s̶t̶y̶l̶e̶ ̶i̶s̶ ̶m̶o̶d̶i̶f̶i̶e̶d̶ ̶(̶a̶t̶ ̶t̶h̶e̶ ̶e̶n̶d̶ ̶o̶f̶ ̶o̶n̶M̶o̶u̶n̶t̶)̶,̶ ̶b̶u̶t̶ ̶i̶n̶ ̶t̶h̶a̶t̶ ̶c̶a̶s̶e̶ ̶o̶n̶ ̶l̶o̶a̶d̶i̶n̶g̶ ̶i̶t̶ ̶c̶a̶n̶n̶o̶t̶ ̶f̶i̶n̶d̶ ̶t̶h̶e̶ ̶b̶i̶n̶d̶e̶d̶ ̶c̶o̶m̶p̶o̶n̶e̶n̶t̶ ̶b̶e̶f̶o̶r̶e̶h̶a̶n̶d̶,̶ ̶w̶h̶i̶c̶h̶ ̶m̶a̶k̶e̶s̶ ̶s̶e̶n̶s̶e̶ ̶-̶ ̶t̶o̶ ̶w̶a̶i̶t̶ ̶f̶o̶r̶ ̶a̶ ̶t̶i̶c̶k̶ ̶o̶n̶ ̶m̶o̶u̶n̶t̶,̶ ̶b̶u̶t̶ ̶i̶t̶ ̶d̶o̶e̶s̶n̶'̶t̶ ̶c̶h̶a̶n̶g̶e̶ ̶a̶n̶y̶t̶h̶i̶n̶g̶

EDIT after @brunnerh's remark:

With that conditional transition, binded values cannot be read before positionCalculated is true, causing an error.

Any other idea?


Solution

  • You could use an $effect to wait for the element to be bound. E.g.

    <script>
      import { quintIn } from "svelte/easing"
      import { scale } from "svelte/transition"
      // ...
    
      const { htmlContent = null } = $props()
    
      let mounted = $state(false);
      let thisDiv = $state();
      let clientWidth = $state();
      let clientHeight = $state();
    
      $effect(() => {
        mounted = true;
        if (thisDiv == null)
          return;
    
        const left = getRandomInt(5, window.innerWidth - clientWidth - 5)
        const top = getRandomInt(5, window.innerHeight - clientHeight - 5)
        thisDiv.style.left = `${left}px`
        thisDiv.style.top = `${top}px`
        // ...
      });
    </script>
    
    {#if mounted}
      <div
        bind:this={thisDiv}
        bind:clientWidth
        bind:clientHeight
        in:scale={{ duration: 500, easing: quintIn }}
      >
        {@html htmlContent}
      </div>
    {/if}
    

    You could also use an action instead of all the bindings. Actions get passed the element, so you can be sure that you have a reference. The clientWidth/clientHeight can then directly be read from the element as well.

    <script>
      import { quintIn } from "svelte/easing";
      import { scale } from "svelte/transition";
      // ...
    
      const { htmlContent = null } = $props();
    
      let mounted = $state(false);
      $effect(() => { mounted = true; });
    
      function position(node) {
        const left = getRandomInt(5, window.innerWidth - node.clientWidth - 5);
        const top = getRandomInt(5, window.innerHeight - node.clientHeight - 5);
        node.style.left = `${left}px`;
        node.style.top = `${top}px`;
        // ...
      }
    </script>
    
    {#if mounted}
      <div
        use:position
        in:scale={{ duration: 500, easing: quintIn }}
      >
        {@html htmlContent}
      </div>
    {/if}