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?
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}