Skip to content

update for v3 #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module"
},
"plugins": ["svelte3"],
"settings": {
"svelte3/extensions": ["html"]
},
"env": {
"browser": true
}
}
73 changes: 27 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,23 @@ yarn add @sveltejs/svelte-virtual-list
## Usage

```html
<VirtualList items={things} component={RowComponent} />

<script>
import VirtualList from '@sveltejs/svelte-virtual-list';
import RowComponent from './RowComponent.html';

export default {
components: { VirtualList },

data() {
return {
things: [
// these can be any values you like
{ name: 'one', number: 1 },
{ name: 'two', number: 2 },
{ name: 'three', number: 3 },
// ...
{ name: 'six thousand and ninety-two', number: 6092 }
],
RowComponent
};
}
};
</script>
```

The component constructor you supply to `<VirtualList>` will be instantiated for each visible member of `items`:
const things = [
// these can be any values you like
{ name: 'one', number: 1 },
{ name: 'two', number: 2 },
{ name: 'three', number: 3 },
// ...
{ name: 'six thousand and ninety-two', number: 6092 }
];
</script>

```html
<!-- RowComponent.html -->
<div>
<strong>{number}</strong>
<span>{name}</span>
</div>
<VirtualList items={things} let:item>
<!-- this will be rendered for each currently visible item -->
<p>{item.number}: {item.name}</p>
</VirtualList>
```


Expand All @@ -54,37 +37,35 @@ The component constructor you supply to `<VirtualList>` will be instantiated for
You can track which rows are visible at any given by binding to the `start` and `end` values:

```html
<VirtualList items={things} component={RowComponent} bind:start bind:end />
<VirtualList items={things} bind:start bind:end>
<p>{item.number}: {item.name}</p>
</VirtualList>

<p>showing {start}-{end} of {things.length} rows</p>
```

You can rename them with e.g. `bind:start=a bind:end=b`.
You can rename them with e.g. `bind:start={a} bind:end={b}`.


## `itemHeight`
## `height`

You can optimize initial display and scrolling when the height of items is known in advance.
By default, the `<VirtualList>` component will fill the vertical space of its container. You can specify a different height by passing any CSS length:

```html
<VirtualList items={things} component={RowComponent} itemHeight={48} />
<VirtualList height="500px" items={things} let:item>
<p>{item.number}: {item.name}</p>
</VirtualList>
```


## Additional properties

You can add arbitrary properties to `<VirtualList>` and they will be forwarded to the rows:
## `itemHeight`

```html
<VirtualList class="funky" answer={42} items={things} component={RowComponent} />
```
You can optimize initial display and scrolling when the height of items is known in advance. This should be a number representing a pixel value.

```html
<!-- RowComponent.html -->
<div class="{number === answer ? 'the-answer' : ''}">
<strong>{number}</strong>
<span>{name}</span>
</div>
<VirtualList itemHeight={48} items={things} let:item>
<p>{item.number}: {item.name}</p>
</VirtualList>
```


Expand Down
170 changes: 170 additions & 0 deletions VirtualList.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<script>
import { onMount, tick } from 'svelte';

// props
export let items;
export let height = '100%';
export let itemHeight;

let foo;

// read-only, but visible to consumers via bind:start
export let start = 0;
export let end = 0;

// local state
let height_map = [];
let rows;
let viewport;
let contents;
let viewport_height = 0;
let visible;
let mounted;

let top = 0;
let bottom = 0;
let average_height;

$: visible = items.slice(start, end).map((data, i) => {
return { index: i + start, data };
});

// whenever `items` changes, invalidate the current heightmap
$: if (mounted) refresh(items, viewport_height, itemHeight);

async function refresh(items, viewport_height, itemHeight) {
const { scrollTop } = viewport;

await tick(); // wait until the DOM is up to date

let content_height = top - scrollTop;
let i = start;

while (content_height < viewport_height && i < items.length) {
let row = rows[i - start];

if (!row) {
end = i + 1;
await tick(); // render the newly visible row
row = rows[i - start];
}

const row_height = height_map[i] = itemHeight || row.offsetHeight;
content_height += row_height;
i += 1;
}

end = i;

const remaining = items.length - end;
average_height = (top + content_height) / end;

bottom = remaining * average_height;
height_map.length = items.length;

}

async function handle_scroll() {
const { scrollTop } = viewport;

const old_start = start;

for (let v = 0; v < rows.length; v += 1) {
height_map[start + v] = itemHeight || rows[v].offsetHeight;
}

let i = 0;
let y = 0;

while (i < items.length) {
const row_height = height_map[i] || average_height;
if (y + row_height > scrollTop) {
start = i;
top = y;

break;
}

y += row_height;
i += 1;
}

while (i < items.length) {
y += height_map[i] || average_height;
i += 1;

if (y > scrollTop + viewport_height) break;
}

end = i;

const remaining = items.length - end;
average_height = y / end;

while (i < items.length) height_map[i++] = average_height;
bottom = remaining * average_height;

// prevent jumping if we scrolled up into unknown territory
if (start < old_start) {
await tick();

let expected_height = 0;
let actual_height = 0;

for (let i = start; i < old_start; i +=1) {
if (rows[i - start]) {
expected_height += height_map[i];
actual_height += itemHeight || rows[i - start].offsetHeight;
}
}

const d = actual_height - expected_height;
viewport.scrollTo(0, scrollTop + d);
}

// TODO if we overestimated the space these
// rows would occupy we may need to add some
// more. maybe we can just call handle_scroll again?
}

// trigger initial refresh
onMount(() => {
rows = contents.getElementsByTagName('svelte-virtual-list-row');
mounted = true;
});
</script>

<style>
svelte-virtual-list-viewport {
position: relative;
overflow-y: auto;
-webkit-overflow-scrolling:touch;
display: block;
}

svelte-virtual-list-contents, svelte-virtual-list-row {
display: block;
}

svelte-virtual-list-row {
overflow: hidden;
}
</style>

<svelte-virtual-list-viewport
bind:this={viewport}
bind:offsetHeight={viewport_height}
on:scroll={handle_scroll}
style="height: {height};"
>
<svelte-virtual-list-contents
bind:this={contents}
style="padding-top: {top}px; padding-bottom: {bottom}px;"
>
{#each visible as row (row.index)}
<svelte-virtual-list-row>
<slot item={row.data}>Missing template</slot>
</svelte-virtual-list-row>
{/each}
</svelte-virtual-list-contents>
</svelte-virtual-list-viewport>
20 changes: 11 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
{
"name": "@sveltejs/svelte-virtual-list",
"version": "2.2.1",
"version": "3.0.0-alpha.1",
"description": "A <VirtualList> component for Svelte apps",
"svelte": "src/VirtualList.html",
"module": "index.mjs",
"main": "index.js",
"main": "VirtualList.html",
"svelte": "VirtualList.html",
"scripts": {
"build": "rollup -c",
"dev": "rollup -cw",
"prepublishOnly": "npm test",
"test": "node test/runner.js",
"test:browser": "npm run build && serve test/public",
"pretest": "npm run build"
"pretest": "npm run build",
"lint": "eslint src/VirtualList.html"
},
"devDependencies": {
"eslint": "^5.12.1",
"eslint-plugin-svelte3": "git+https://github.com/sveltejs/eslint-plugin-svelte3.git",
"port-authority": "^1.0.5",
"puppeteer": "^1.9.0",
"rollup": "^0.66.6",
"rollup": "^1.1.2",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-svelte": "^4.3.2",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-svelte": "^5.0.1",
"sirv": "^0.2.2",
"svelte": "^2.13.5",
"svelte": "^3.0.0-beta.2",
"tap-diff": "^0.1.1",
"tap-dot": "^2.0.0",
"tape-modern": "^1.1.1"
Expand Down
8 changes: 3 additions & 5 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ import pkg from './package.json';

export default [
{
input: 'src/VirtualList.html',
output: [
{ file: pkg.module, 'format': 'es' },
{ file: pkg.main, 'format': 'umd', name: 'VirtualList' }
],
input: 'test/src/index.js',
output: { file: 'test/public/bundle.js', 'format': 'iife' },
plugins: [
resolve(),
commonjs(),
svelte()
]
},
Expand Down
Loading