Search code examples
sveltesveltekit

Scroll to svelte's element


I have a div that I want the page to scroll to when it is mounted. That div has an id which is determined in URL search params.

Not sure why, when I call document.getElementById(#${referProductId}), the result is null. Is it because of the conditional rendering? (note: referProductId exist)

Inside the script tag

let layout: "grid" | "list" = "grid"
let referProductId = ""

page?.subscribe(({ url }) => {
    // set layout value
    // ... similar code to below with .get("layout")

    const product_id = url.searchParams.get('product_id');
    if (product_id) {
        referProductId = product_id;
    }
});

$: if (browser && referProductId) {
    const product = document.getElementById(`#${referProductId}`);
    console.log(product); // this logs null
    if (product) {
        product.scrollIntoView({ behavior: 'smooth' });
    }

Inside the svelte markup

<div
    class={cn(
        'w-full gap-4',
        displayType === 'grid' && 'grid grid-cols-2',
        displayType === 'list' && 'flex flex-col'
    )}
>
    {#await data.products}
        {#if displayType === 'grid'}
            <Skeleton class="aspect-square h-full w-full rounded-lg" />
            <Skeleton class="aspect-square h-full w-full rounded-lg" />
        {:else if displayType === 'list'}
            <div class="flex w-full flex-col gap-4">
                <Skeleton class="h-16 w-full rounded-lg" />
                <Skeleton class="h-16 w-full rounded-lg" />
            </div>
        {/if}
    {:then products}
        {#each products as product, i}
            {#if displayType === 'grid'}
                <div
                    id={String(product.id)}
                    in:fly={{
                        y: -40,
                        opacity: 0,
                        delay: i * 100,
                        duration: 250,
                        easing: cubicInOut
                    }}
                >
                    <component />
                </div>
            {:else if displayType === 'list'}
                <div
                    id={String(product.id)}
                    in:fly={{
                        y: -40,
                        opacity: 0,
                        delay: i * 100,
                        duration: 250,
                        easing: cubicInOut
                    }}
                >
                    <component />
                </div>
            {/if}
        {/each}
    {:catch error}
        <p>Error loading products</p>
    {/await}

Solution

  • Would not recommend using manual subscriptions in component code, they are likely to cause leaks if not properly cleaned up. For focusing, you could approach it the other way around: From the element via an action.

    $: referProductId = getProductId($page.url); // implement get logic in function
    
    function scrollIntoView(node, scroll)
    {
      function update(scroll) {
        if (scroll)
          node.scrollIntoView({ behavior: 'smooth' });
      }
    
      update(scroll);
      return { update };
    }
    
    <!-- on product element -->
    <div
      id={product.id}
      use:scrollIntoView={product.id == referProductId}
      ...
    

    REPL

    You also could try hash-based navigation (just add/use #<id> via goto or links). For smooth scrolling via this mechanism, scroll-behavior: smooth has to be set via CSS.