Giter Site home page Giter Site logo

jonasgeiler / svelte-tiny-virtual-list Goto Github PK

View Code? Open in Web Editor NEW
366.0 8.0 23.0 127 KB

A tiny but mighty list virtualization library for Svelte, with zero dependencies ๐Ÿ’ช Supports variable heights/widths, sticky items, scrolling to index, and more!

Home Page: https://svelte-tiny-virtual-list.jonasgeiler.com

License: MIT License

JavaScript 78.55% Svelte 21.45%
svelte svelte-components virtual list scroll infinite loading component plugin

svelte-tiny-virtual-list's Introduction

ListLogo

svelte-tiny-virtual-list

A tiny but mighty list virtualization library, with zero dependencies ๐Ÿ’ช

NPM VERSION NPM DOWNLOADS DEPENDENCIES

About โ€ข Features โ€ข Installation โ€ข Usage โ€ข Examples โ€ข License

About

Instead of rendering all your data in a huge list, the virtual list component just renders the items that are visible, keeping your page nice and light.
This is heavily inspired by react-tiny-virtual-list and uses most of its code and functionality!

Features

  • Tiny & dependency free โ€“ Only ~5kb gzipped
  • Render millions of items, without breaking a sweat
  • Scroll to index or set the initial scroll offset
  • Supports fixed or variable heights/widths
  • Vertical or Horizontal lists
  • svelte-infinite-loading compatibility

Installation

If you're using this component in a Sapper application, make sure to install the package to devDependencies!
More Details

With npm:

$ npm install svelte-tiny-virtual-list

With yarn:

$ yarn add svelte-tiny-virtual-list

With pnpm (recommended):

$ npm i -g pnpm
$ pnpm install svelte-tiny-virtual-list

From CDN (via unpkg):

<!-- UMD -->
<script src="https://unpkg.com/svelte-tiny-virtual-list@^1/dist/svelte-tiny-virtual-list.js"></script>

<!-- ES Module -->
<script src="https://unpkg.com/svelte-tiny-virtual-list@^1/dist/svelte-tiny-virtual-list.mjs"></script>

Usage

<script>
  import VirtualList from 'svelte-tiny-virtual-list';

  const data = ['A', 'B', 'C', 'D', 'E', 'F', /* ... */];
</script>

<VirtualList
    width="100%"
    height={600}
    itemCount={data.length}
    itemSize={50}>
  <div slot="item" let:index let:style {style}>
    Letter: {data[index]}, Row: #{index}
  </div>
</VirtualList>

Also works pretty well with svelte-infinite-loading:

<script>
  import VirtualList from 'svelte-tiny-virtual-list';
  import InfiniteLoading from 'svelte-infinite-loading';

  let data = ['A', 'B', 'C', 'D', 'E', 'F', /* ... */];

  function infiniteHandler({ detail: { complete, error } }) {
    try {
      // Normally you'd make an http request here...

      const newData = ['G', 'H', 'I', 'J', 'K', 'L', /* ... */];
      
      data = [...data, ...newData];
      complete();
    } catch (e) {
      error();
    }
  }
</script>

<VirtualList
    width="100%"
    height={600}
    itemCount={data.length}
    itemSize={50}>
  <div slot="item" let:index let:style {style}>
    Letter: {data[index]}, Row: #{index}
  </div>

  <div slot="footer">
    <InfiniteLoading on:infinite={infiniteHandler} />
  </div>
</VirtualList>

Props

Property Type Required? Description
width number | string* โœ“ Width of List. This property will determine the number of rendered items when scrollDirection is 'horizontal'.
height number | string* โœ“ Height of List. This property will determine the number of rendered items when scrollDirection is 'vertical'.
itemCount number โœ“ The number of items you want to render
itemSize number | number[] | (index: number) => number โœ“ Either a fixed height/width (depending on the scrollDirection), an array containing the heights of all the items in your list, or a function that returns the height of an item given its index: (index: number): number
scrollDirection string Whether the list should scroll vertically or horizontally. One of 'vertical' (default) or 'horizontal'.
scrollOffset number Can be used to control the scroll offset; Also useful for setting an initial scroll offset
scrollToIndex number Item index to scroll to (by forcefully scrolling if necessary)
scrollToAlignment string Used in combination with scrollToIndex, this prop controls the alignment of the scrolled to item. One of: 'start', 'center', 'end' or 'auto'. Use 'start' to always align items to the top of the container and 'end' to align them bottom. Use 'center' to align them in the middle of the container. 'auto' scrolls the least amount possible to ensure that the specified scrollToIndex item is fully visible.
scrollToBehaviour string Used in combination with scrollToIndex, this prop controls the behaviour of the scrolling. One of: 'auto', 'smooth' or 'instant' (default).
stickyIndices number[] An array of indexes (eg. [0, 10, 25, 30]) to make certain items in the list sticky (position: sticky)
overscanCount number Number of extra buffer items to render above/below the visible items. Tweaking this can help reduce scroll flickering on certain browsers/devices.
estimatedItemSize number Used to estimate the total size of the list before all of its items have actually been measured. The estimated total height is progressively adjusted as items are rendered.
getKey (index: number) => any Function that returns the key of an item in the list, which is used to uniquely identify an item. This is useful for dynamic data coming from a database or similar. By default, it's using the item's index.

* height must be a number when scrollDirection is 'vertical'. Similarly, width must be a number if scrollDirection is 'horizontal'

Slots

  • item - Slot for each item
    • Props:
      • index: number - Item index
      • style: string - Item style, must be applied to the slot (look above for example)
  • header - Slot for the elements that should appear at the top of the list
  • footer - Slot for the elements that should appear at the bottom of the list (e.g. InfiniteLoading component from svelte-infinite-loading)

Events

  • afterScroll - Fired after handling the scroll event
    • detail Props:
      • event: ScrollEvent - The original scroll event
      • offset: number - Either the value of wrapper.scrollTop or wrapper.scrollLeft
  • itemsUpdated - Fired when the visible items are updated
    • detail Props:
      • start: number - Index of the first visible item
      • end: number - Index of the last visible item

Methods

  • recomputeSizes(startIndex: number) - This method force recomputes the item sizes after the specified index (these are normally cached).

VirtualList has no way of knowing when its underlying data has changed, since it only receives a itemSize property. If the itemSize is a number, this isn't an issue, as it can compare before and after values and automatically call recomputeSizes internally. However, if you're passing a function to itemSize, that type of comparison is error prone. In that event, you'll need to call recomputeSizes manually to inform the VirtualList that the size of its items has changed.

Use the methods like this:

<script>
  import { onMount } from 'svelte';
  import VirtualList from 'svelte-tiny-virtual-list';

  const data = ['A', 'B', 'C', 'D', 'E', 'F', /* ... */];
  
  let virtualList;
  
  function handleClick() {
    virtualList.recomputeSizes(0);
  }
</script>

<button on:click={handleClick}>Recompute Sizes</button>

<VirtualList
        bind:this={virtualList}
        width="100%"
        height={600}
        itemCount={data.length}
        itemSize={50}>
  <div slot="item" let:index let:style {style}>
    Letter: {data[index]}, Row: #{index}
  </div>
</VirtualList>

Styling

You can style the elements of the virtual list like this:

<script>
  import VirtualList from 'svelte-tiny-virtual-list';

  const data = ['A', 'B', 'C', 'D', 'E', 'F', /* ... */];
</script>

<div class="list">
  <VirtualList
      width="100%"
      height={600}
      itemCount={data.length}
      itemSize={50}>
    <div slot="item" let:index let:style {style}>
      Letter: {data[index]}, Row: #{index}
    </div>
  </VirtualList>
</div>

<style>
  .list :global(.virtual-list-wrapper) {
    background-color: #0f0;
    /* ... */
  }
  
  .list :global(.virtual-list-inner) {
    background-color: #f00;
    /* ... */
  }
</style>

Examples / Demo

License

MIT License

svelte-tiny-virtual-list's People

Contributors

jonasgeiler avatar mskocik avatar rlawisch avatar wd-david avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

svelte-tiny-virtual-list's Issues

Clarification on footer and header behavior

Hello! I'm looking for some clarification on the footer and header behavior. When adding a footer and header, I'd expect it to

  • not affect the overflow of the parent container. At this moment, when you add a footer and a header, there doesn't appear to be a way for the container to not show a scrollbar.
  • the footer to be positioned immediately after the last element

Here's a REPL illustrating the issue - I'd expect the footer to be visible right after the last number, and for there to be no scrollbar on the container. Is there a way currently to achieve such behavior?

Thanks!

ids do not have a stability property.

First, thanks for creating this version of VirtualList. The 'sticky' feature is something I have been wanting.

Now the problem I am facing:

When a scroll list comes from a dynamic database and items are added or removed, a stability property is needed. That is to say for a given item, the loop id should always bind to the same item. From reading the source code, the id right now is item.index which when a database inserts new item, is not stable.

A possible solution:
svelte-virtual-list has the same issue. However I've addressed it in that code very simply and the same could be done here.
First add:

    export let indexing;

and use it in the loop:

        <div class="virtual-list-inner" style={innerStyle}>
		{#each items as item (indexing ? indexing(item.index) : item.index)}
			<slot name="item" style={item.style} index={item.index} />
		{/each}
	</div>

Alternatively, the call to indexing could be done inside the refresh function.

Wrapper offset after scrollToIndex is modified

Hey there,

I was wondering if there is a way to get the result of getWrapperOffset() after scrollToIndex prop is modified? After regular scrolling the afterScroll event is dispatched and the offset can be obtained that way. However, the event is not dispatched after scrollToIndex is modified.

Thanks for your time,
winterSteve25

estimatedItemSize - details

Hi

first of all, many thanks for that great component! :-) Works really nice and smooth.

I do have a question about the 'estimateItemSize' though:
I was under the impression that this was used to calculate the total scroll-height. So,...that it should basically match itemCount * itemSize in case of equal height items.

If that's right, I also thought that this would prevent that growing scrollbar during scrolling. It seems that as items are being recomputed, my scrollbar grows. So I tried to use estimatedItemSize as described above. But that gives me another weird behavior (scrolling to the bottom after a few initial correctly scrolled items). So maybe I got that totally wrong,...:-)

afterScroll not firing

Hi, the afterScroll is not firing for me, any tips ?

<VirtualList
            width="100%"
            height={400}
            itemCount={$messages.length}
            itemSize={92}
            {scrollOffset}
            {scrollToBehaviour}
            on:afterScroll={() => console.log("well, hello there")}
    >
        <svelte:fragment slot="item" let:index let:style>
            {@const message = $messages[index]}
            <div class="flex gap-x-6 px-8 py-6 relative" {style}>
               // thingy
            </div>
        </svelte:fragment>
</VirtualList>

Sveltekit v2 error

Happy belated New Year.

[vite-plugin-svelte] WARNING: The following packages have a svelte field in their package.json but no exports condition for svelte.
[email protected]

With all the changes coming to Svelte 5 and the strong recommendation to upgrade to Sveltekit 2 would you suggest using an alternate component, or do you plan to update the code?

I hope you do, and I thank you for creating such a useful component.

Unnecessary item creation / destruction

Hi

I have an image viewer that might display thousands of images, and the user can zoom into the images using the mouse-wheel and the 'control' key. I noticed that if the list is large and the current position is not somewhere at the beginning (like the first 100 items? not sure where the limit is), items that are on screen are being rerendered when zooming.

To see what I mean:

https://svelte.dev/repl/69d2cb5333304d29a661c40b2d5808d0?version=4.2.1

If you press 'Ctrl' and scroll while at the beginning of the list, the time of the items do not update. If you scroll down 50% into the list and do the same, the time updates with every "zoom tick". And the time only updates if 'onMount' is called in the rendered Item...why is that? That should not be I think, should it?

Virtualized Table?

Would it be possible to create a virtualized table like React Virtuoso's table: https://virtuoso.dev/table-fixed-headers/?
Edit: petyosi/react-virtuoso#42 <- this issue is basically the issue I have

For instance, I tried:

<script>
    import VirtualList from "svelte-tiny-virtual-list'';
  
    const data = ['A', 'B', 'C', 'D', 'E', 'F', /* ... */];
</script>
  
<table>
  <thead>
    <tr>
      <td>
        Col 1
      </td>
      <td>
        Col 2
      </td>
    </tr>
  </thead>
  <tbody>
    <VirtualList
        width="100%"
        height={600}
        itemCount={data.length}
        itemSize={50}>
      <div slot="item" let:index let:style {style}>
        <tr>
          <td>{index}</td>
          <td>
            Letter: {data[index]}, Row: #{index}
          </td>
        </tr>
      </div>
    </VirtualList>
  </tbody>
</table>

and the result is:
image

I'm guessing that this is because the 2 divs generated by svelte make the table not recognize the row as a proper row.

Any thoughts/help would be appreciated!

Horizontal Infinite Virtual list scroll support

Should the infinite loader be initiated through a dispatched event when last item is reached?

The footer slot is positioned under the list and gets immediately triggered - I tried flex row position on the virtual-list-wrapper.

  <div slot="footer">
    <InfiniteLoading on:infinite={infiniteHandler} />
  </div>

Thanks

Error when initializing virtual list with defined scrollToIndex

When property scrollToIndex has value, it result in following error. See REPL

can't access lexical declaration 'state' before initialization

This is due initialization check not expecting scrollToIndex can have initial value;

Fix is easy - add check also for items.length for state.offset initialization:

// VirtualList.svelte: 62
let state = {
  offset:             scrollOffset || (scrollToIndex != null && items.length && getOffsetForIndex(scrollToIndex)) || 0,
  scrollChangeReason: SCROLL_CHANGE_REASON.REQUESTED,
};

ScrollOffset doesnt set scroll position of wrapper on initial render

I cant get the List to render at a certain scroll offset on first render, if the scrollOffset props changes subsequent renders are positioned correctly, but on initial render the wrapper is always positioned at the top. The correct virtual items are rendered (sometimes out of view if the offset is higher enough) and they can be seen in the devtools as being in the right spot.

Using scrollToIndex with sticky headers

Background

I'd like to make a virtual table with a few characteristics:

  1. a sticky header row
  2. keyboard shortcuts (โฌ†๏ธ and โฌ‡๏ธ) to change the selected row
  3. auto-scroll to reveal a newly-focused row if it's partly offscreen

Attempts

I tried to implement this in two ways shown in this example code, but each had an issue ๐Ÿ˜…

  1. Put the header in slot="header"

    • Auto-scrolling reveals only part of a row when navigating down. This seems to happen because the header row shifts the other rows down, and auto-scrolling based on scrollToIndex doesn't take that shift into consideration. Based on this comment about invisible elements, I think this is the expected behavior in this case.
  2. Put the header in slot="item"

    • Auto-scrolling reveals only part of a row when navigating up. This seems to happen because the sticky header row hides the topmost row.

Questions

  1. Is there another way to do what I have in mind?
  2. If not, would you be open to a PR that tweaks existing behavior to fit this use case?

Thanks in advance for any insight you can offer! ๐Ÿ™

Content aware dynamic height.

I'm having a difficult time making dynamic heights work. I am writing a basic chat window with messages of different heights (depending on amount of text inside. Currently I am trying to solve it by binding offsetHeight of the message to itemSize but it causes a lot of jumping and flickering with constant rerendering of components. It is obvious that I am doing something wrong and I would really appreciate any hints on what is wrong or what is the right way of doing it.

Here's REPL link to my code: https://svelte.dev/repl/6a5c26b0b7fa4351a9d30f3f907e00cf?version=3.48.0

Function for itemSize makes scroll offset not able to go all the way to the bottom

Hi I am working on using the virtual list in a chat type layout and want to always have the offset start at the end of the list. However the elements are dynamically sized and I am using the itemSize as a function. This is working fin but I am not able to scroll all the way to the offset initially unless I overestimate the size of the elements and then the scroll bar gets jittery. Is there any better way to handle this?

How to calculate/recalculate row height with expandable content?

I need to build expandable table, on button click content as sub-row must appear.
I have big problem figuring out how to calculate dynamic height on render (page crashes) and to recalculate height on row expand. Please give me some hint.

<script>
	import VirtualList from 'svelte-tiny-virtual-list';
	let virtualList;
	let expanded = [1, 5];
        let rows = [1,2,3,4,5,6,7,8,9,10]
	let columns = [
		{
			id: 'testCol'
		}
	];

	let heights = [];
	$: console.log(heights);
</script>

<div>
	<div class="w-[500px]">
		<VirtualList
			width="100%"
			height={600}
			itemCount={rows.length}
			itemSize={(i) => heights[i]}
			bind:this={virtualList}
		>
			<div class="" slot="item" let:index let:style {style} bind:clientHeight={heights[index]}>
				{#each columns as column}
					{@const row = rows[index]}
					<div class=" px-sm table-cell">
						<button
							on:click={() => {
								let newArr = [];

								if (!expanded.includes(index)) {
									expanded = [...expanded, index];
									return;
								}

								expanded.map((i) => {
									if (i != index) {
										newArr.push(i);
									}
								});
								expanded = newArr;

								virtualList.recomputeSizes(0);
							}}>Expand trigger button</button
						>
					</div>
				{/each}
				{#if expanded.includes(index)}
					<div class="bg-red-200 w-full">
						<div class="text-right">Expended row content</div>
					</div>
				{/if}
			</div>
		</VirtualList>
	</div>
</div>

Abuse of function arguments to trigger update

This is just a suggestion regarding how the size recalculation is triggered in this line:

$: if (mounted) recomputeSizes(0, height, width, stickyIndices); // call scroll.reset;

At first glance this looks like a mistake because the function only accepts one argument.
I would instead write it like this:

$: {
  height, width, stickyIndices;
  if (mounted) recomputeSizes(0); // call scroll.reset;
}

REPL example of this approach

<VirtualList> is not a valid SSR component.

<VirtualList> is not a valid SSR component. You may need to review your build config to ensure that dependencies are compiled, rather than imported as pre-compiled modules
Error: <VirtualList> is not a valid SSR component. You may need to review your build config to ensure that dependencies are compiled, rather than imported as pre-compiled modules

sveltejs/kit 1.0.0-next.396

Dynamic heights?

I tried putting images into the VirtualList. After a bit of tinkering it works, but only until you resize the window. Because the image gets taller when the list gets wider, the VirtualList becomes misaligned. Is there an intended way to fix this or could a feature like this be implemented?

Tiny virtual grid?

Nice work! Any plans to implement svelte-tiny-virtual-grid or perhaps advice on how to go about that?

Improve performance on scroll & dynamic heights.

I was looking at twitter.com and some thoughts..

Instead of setting top style on the items - if I replace with transform the performance appeared far smoother on older mobiles ie tested on samsung s9 also for large datasets the scrollbar on desktop had less lag in chrome.

transform:translateY(${offset}px)

Also looking at the dynamic heights instead of fixed or having an array of calculated heights..
It looks like twitter looks at previous sibling item element and any changes to the height of that element - auto changes all the following next sibling items top position so that height does not need to be defined and is horizontally responsive - I'm sure there is more logic to calculate full initial size and resize and scroll position this was just an initial observation as twitters approach appears very similar.

Enable CodeQL

Enable CodeQL in the settings after releasing 3.0.0

Add new item and scroll to it

If I push a new item to the array tracked by VirtualList and change itemCount property on VirtualList component and simultaniosly change scrollToIndex to index of newly added item. Scrolling does not work as expected.

        items = [...items, 'new item']
        scrollToIndex = items.length-1

Expected behavior: Newly created item should appear in the visible area.
Actual behavior: Item does not appear there. Instead I see that the list is scrolled to the last element that existed in the array before I pushed a new Item. So it scrolls to the index items.length-2.

It should be related to the fact that scrollTo function can not jump to the item that is not yet placed.
Workaround: Wait a moment after updating items and before setting new scrollToIndex.

        items = [...items, 'new item']
        // wait a moment before setting scrollToIndex
	await new Promise(res => setTimeout(res, 0))
        scrollToIndex = items.length-1

This is a demo that demonstrates the issue.

If items.length changes to zero (e.g. in the case of filtering), a fatal error results

	getSizeAndPositionForIndex(index) {
		if (index < 0 || index >= this.itemCount) {
			throw Error(
				`Requested index ${index} is outside of range 0..${this.itemCount}`,
			);
		}

		return this.justInTime
			? this.getJustInTimeSizeAndPositionForIndex(index)
			: this.itemSizeAndPositionData[index];
	}

I have a list of items but if filtering on terms for instance and there are no results for the entered term, then the above code generates an error when used in conjunction with scrollToIndex.

    beforeUpdate(async () => {
        await tick();
        scrollToIndex = filteredItems.length;
    });

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.