Quantcast
Channel: Active questions tagged svelte - Stack Overflow
Viewing all articles
Browse latest Browse all 1736

Click on any item from page 2 onwards and it immediately jumps to the top of the list. How to conditionally retain scroll positions?

$
0
0

Image

How to reproduce

  • View the page with enough width (say a desktop) where list and detail are shown side by side
  • Click show more, go to page 2 or 3 or any other page than 1
  • Click on any item
  • Boom, we jump straight to the top

Codesandbox Link

Here is the codesandbox clearly illustrating the issue

Tried a different virtual list library and still getting the same problem

Third attempt with svelte-virtual-scroll-list and still no luck

  • Removing that virtual list and simply looping through items doesn't have this issue

  • Not sure which file is causing it but for stackoverflow purposes, I'll share the +layout.ts and +layout.svelte file and the state file

lib/state/LatestNewsItems.svelte.ts

import type { Cursor } from '$lib/types/Cursor';import type { NewsFilter } from '$lib/types/NewsFilter';import type { NewsItem } from '$lib/types/NewsItem';export class LatestNewsState {    cursor: Cursor = $state({ lastFeedItemId: undefined, lastPubdate: undefined });    filter: NewsFilter = $state('latest');    mapNewsItemIdToTrue = new Map<string, boolean>();    newsItems: NewsItem[] = $state([]);    search: string = $state('');    appendNewsItems(items: NewsItem[]) {        for (let i = 0; i < items.length; i++) {            const item = items[i];            if (!this.mapNewsItemIdToTrue.get(item.id)) {                this.newsItems.push(item);                this.mapNewsItemIdToTrue.set(item.id, true);            }        }        const lastItem = this.newsItems[this.newsItems.length - 1];        if (lastItem) {            this.cursor.lastFeedItemId = lastItem.id;            this.cursor.lastPubdate = lastItem.pubdate;        }    }    clearNewsItems() {        this.newsItems.splice(0, this.newsItems.length);        this.cursor.lastFeedItemId = undefined;        this.cursor.lastPubdate = undefined;        this.mapNewsItemIdToTrue.clear();    }    hasFilterOrSearchChanged(filter: NewsFilter, search: string) {        return !this.isEqualFilter(filter) || !this.isEqualSearch(search);    }    isEqualFilter(filter: NewsFilter) {        return filter === this.filter;    }    isEqualSearch(search: string) {        return search === this.search;    }}

+layout.ts

import { browser } from '$app/environment';import { requestProperties } from '$lib/config';import {    getNewsListFirstPageEndpoint,    getNewsListWithPinnedItemFirstPageEndpoint} from '$lib/endpoints/backend';import { isDetailRoute } from '$lib/functions';import { latestNewsState } from '$lib/state';import type { NewsFilter } from '$lib/types/NewsFilter';import type { NewsItem } from '$lib/types/NewsItem';import type { LayoutLoad } from './$types';const fetchNewsItemsFromCache = (    newsItems: NewsItem[]): Promise<{ status_code: number; data: NewsItem[] }> => {    return new Promise((resolve) => {        resolve({ status_code: 200, data: newsItems });    });};const fetchNewsItemsFromAPI = (    endpoint: string,    fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>): Promise<{ status_code: number; data: NewsItem[] }> => {    return new Promise((resolve, reject) => {        fetch(endpoint, requestProperties)            .then((response) => {                if (!response.ok) {                    throw new Error(                        `Something went wrong while loading news items ${response.status} ${response.statusText}`                    );                }                return response.json();            })            .then(resolve)            .catch(reject);    });};const getNewsListEndpoint = (    filter: NewsFilter,    id: string | undefined,    search: string,    title: string | undefined) => {    let endpoint;    if (isDetailRoute(id, title)) {        endpoint = getNewsListWithPinnedItemFirstPageEndpoint(filter, id, search);    } else {        endpoint = getNewsListFirstPageEndpoint(filter, search);    }    return endpoint;};const shouldFetchItemsFromAPI = (filter: NewsFilter, id: string | undefined, search: string) => {    const hasFilterOrSearchChanged = latestNewsState.hasFilterOrSearchChanged(filter, search);    const doesItemExistInCache = latestNewsState.mapNewsItemIdToTrue.has(id as string);    return hasFilterOrSearchChanged || !doesItemExistInCache;};const loadOnBrowser = (    filter: NewsFilter,    fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,    id: string | undefined,    search: string,    title: string | undefined) => {    let latestNewsPromise;    if (shouldFetchItemsFromAPI(filter, id, search)) {        console.log('oh no lets load');        const endpoint = getNewsListEndpoint(filter, id, search, title);        latestNewsState.clearNewsItems();        latestNewsState.filter = filter;        latestNewsState.search = search;        latestNewsPromise = fetchNewsItemsFromAPI(endpoint, fetch);    } else {        latestNewsPromise = fetchNewsItemsFromCache(latestNewsState.newsItems);    }    return {        filter,        id,        latestNewsPromise,        search,        title    };};const loadOnServer = (    filter: NewsFilter,    fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,    id: string | undefined,    search: string,    title: string | undefined) => {    const endpoint = getNewsListEndpoint(filter, id, search, title);    return {        filter,        id,        latestNewsPromise: fetchNewsItemsFromAPI(endpoint, fetch),        search,        title    };};export const load: LayoutLoad = ({ fetch, params, url }) => {    const filter = (url.searchParams.get('filter') as NewsFilter) || 'latest';    const id = params.id;    const search = url.searchParams.get('search') || '';    const title = params.title;    if (browser) {        return loadOnBrowser(filter, fetch, id, search, title);    } else {        return loadOnServer(filter, fetch, id, search, title);    }};

+layout.svelte

<script lang="ts">    import { VList } from 'virtua/svelte';    import '$lib/css/main.css';    import { page } from '$app/state';    import { MediaQuery } from 'svelte/reactivity';    import type { NewsItem } from '$lib/types/NewsItem.js';    import { isDetailRoute } from '$lib/functions';    import {        getNewsListNextPageEndpoint,        getNewsListWithPinnedItemNextPageEndpoint    } from '$lib/endpoints/backend';    import { getNewsDetailEndpoint, getNewsListEndpoint } from '$lib/endpoints/frontend.js';    import { goto } from '$app/navigation';    import type { NewsFilter } from '$lib/types/NewsFilter.js';    import { latestNewsState } from '$lib/state/index.js';    import { requestProperties } from '$lib/config/index.js';    const large = new MediaQuery('min-width: 800px');    const { children, data } = $props();    const hasNoDetailSelected = $derived.by(() => {        return (            page.url.pathname === '/' ||            page.url.pathname === '/news' ||            page.url.pathname === `/news/${page.params.tag}`        );    });    const filter = $derived(data.filter);    const id = $derived(data.id);    const search = $derived(data.search);    const title = $derived(data.title);    let newSearch = $state('');    $effect(() => {        data.latestNewsPromise            .then((items) => {                latestNewsState.appendNewsItems(items.data);            })            .catch((error: Error) => {                console.error(`Something went wrong when loading news items ${error.message}`);            });    });    async function showMore() {        try {            let endpoint;            if (isDetailRoute(page.params.id, page.params.title)) {                endpoint = getNewsListWithPinnedItemNextPageEndpoint(                    latestNewsState.cursor,                    filter,                    id,                    search                );            } else {                endpoint = getNewsListNextPageEndpoint(latestNewsState.cursor, filter, search);            }            const response = await fetch(endpoint, requestProperties);            if (!response.ok) {                throw new Error(                    `Something went wrong when loading news items on page N ${response.status} ${response.statusText}`                );            }            const { data: items }: { data: NewsItem[] } = await response.json();            latestNewsState.appendNewsItems(items);        } catch (error) {            console.log(                `Something when wrong when executing show more ${error instanceof Error ? error.message : ''}`            );        }    }    function onFilterChange(e: Event) {        const newFilterValue = (e.target as HTMLSelectElement).value;        let to;        if (isDetailRoute(page.params.id, page.params.title)) {            to = getNewsDetailEndpoint(newFilterValue as NewsFilter, id, search, title);        } else {            to = getNewsListEndpoint(newFilterValue as NewsFilter, search);        }        return goto(to);    }    function onSearchChange(e: KeyboardEvent) {        if (e.key === 'Enter') {            let to;            if (isDetailRoute(page.params.id, page.params.title)) {                to = getNewsDetailEndpoint(filter as NewsFilter, id, newSearch, title);            } else {                to = getNewsListEndpoint(filter as NewsFilter, newSearch);            }            return goto(to);        }    }</script><header><div><a data-sveltekit-preload-data="off" href="/">TestNewsApp</a></div><div>        On desktop, list + detail are shown side by side, on mobile you'll see either the list or the        detail depending on the url</div></header>{#if large.current}<main style="flex-direction:row;"><div class="list"><section class="panel"><span>Filter: {filter}</span><span>Search: {search}</span></section><br /><div class="panel"><section class="list-filter" onchange={onFilterChange}><select>                        {#each ['latest', 'likes', 'dislikes', 'trending'] as filterValue}<option selected={filter === filterValue}>{filterValue}</option>                        {/each}</select></section><section><input                        placeholder="Search for 'china'"                        type="search"                        name="search"                        value={search}                        oninput={(e: Event) => {                            newSearch = (e.target as HTMLInputElement).value;                        }}                        onkeydown={onSearchChange}                    /></section></div><nav>                {#await data.latestNewsPromise}<span>Loading items...</span>                {:then}<VList data={latestNewsState.newsItems} getKey={(newsItem: NewsItem) => newsItem.id}>                        {#snippet children(newsItem, index)}<div class="list-item" class:selected={page.params.id === newsItem.id}><a                                    data-sveltekit-preload-data="off"                                    href={getNewsDetailEndpoint(filter, newsItem.id, search, newsItem.title)}>{index + 1} {newsItem.title}</a></div>                            {#if index === latestNewsState.newsItems.length - 1}<footer><button onclick={showMore}>Show More</button></footer>                            {/if}                        {/snippet}</VList>                {/await}</nav></div><div class="detail">            {@render children()}</div></main>{:else if !large.current && hasNoDetailSelected}<main style="flex-direction:column;"><div class="list"><section class="panel"><span>Filter: {filter}</span><span>Search: {search}</span></section><br /><div class="panel"><section class="list-filter" onchange={onFilterChange}><select>                        {#each ['latest', 'likes', 'dislikes', 'trending'] as filterValue}<option selected={filter === filterValue}>{filterValue}</option>                        {/each}</select></section><section><input                        placeholder="Search for 'china'"                        type="search"                        name="search"                        value={search}                        oninput={(e: Event) => {                            newSearch = (e.target as HTMLInputElement).value;                        }}                        onkeydown={onSearchChange}                    /></section></div><nav>                {#await data.latestNewsPromise}<span>Loading items...</span>                {:then}<VList data={latestNewsState.newsItems} getKey={(newsItem: NewsItem) => newsItem.id}>                        {#snippet children(newsItem, index)}<div class="list-item" class:selected={page.params.id === newsItem.id}><a                                    data-sveltekit-preload-data="off"                                    href={getNewsDetailEndpoint(filter, newsItem.id, search, newsItem.title)}>{index + 1} {newsItem.title}</a></div>                            {#if index === latestNewsState.newsItems.length - 1}<footer><button onclick={showMore}>Show More</button></footer>                            {/if}                        {/snippet}</VList>                {/await}</nav></div></main>{:else}<div class="detail">        {@render children()}</div>{/if}<style>    .detail {        background-color: lightcyan;        flex: 1;    }    .list {        background-color: lightyellow;        flex: 1;        display: flex;        flex-direction: column;        padding: 1rem;    }    .list-item {        border-bottom: 1px dotted lightgray;        padding: 0.5rem 0;    }    .panel {        display: flex;        font-size: x-small;        justify-content: space-between;    }    .selected {        background-color: yellow;    }    footer {        display: flex;        justify-content: center;    }    main {        background-color: lightgoldenrodyellow;        display: flex;        flex: 1;        overflow: hidden;    }    nav {        flex: 1;    }</style>

Questions

  • When filter or search is changed, scroll position should be back to 0
  • But when clicking on items, scroll position should be retained
  • How to achieve this?

Viewing all articles
Browse latest Browse all 1736

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>