diff --git a/README.md b/README.md index 29a80c5..07d15f2 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ This component can be used two different ways: const data = ['A', 'B', 'C', 'D', 'E', 'F' /* ... */]; - + {#snippet slot({ item, style, index })}
Item: {item}, Row: #{index} @@ -84,10 +84,10 @@ This component can be used two different ways: ``` -You can also perform dynamic loading +You can also perform dynamic loading with a PartialLoader. ```svelte -TODO +TODO: explain that model vs views, which translates into model and items ``` ### Props @@ -98,8 +98,8 @@ The component accepts the following properties | ----------------- | ----------- | :-------: | ------------ | | width | `number` or `string`\* | ✓ | Width of List. This property will determine the number of rendered items when scrollDirection is `'horizontal'`. | | height | `number` or `string`\* | ✓ | Height of List. This property will determine the number of rendered items when scrollDirection is `'vertical'`. | -| items | any[] | ✓ | the model, the data for the items to display in the list. | -| itemCount | `number` | ✓ | The number of items you want to render. | +| model | any[] | ✓ | the model, the data for the items to display in the list. | +| modelCount | `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` | | row | (r:RowAttributes) => SnippetResult | ✓ | Snippet called to render every item, see description below. | | scrollDirection | `string` | | Whether the list should scroll vertically or horizontally. One of `'vertical'` (default) or `'horizontal'`. | @@ -112,16 +112,17 @@ The component accepts the following properties | 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'`_ +_\** `model` is stored, `items` are rendered ### Snippets - `header` - a snippet for the elements that should appear at the top of the list -- `footer` - a snippet for the elements that should appear at the bottom of the list (e.g. `InfiniteLoading` component from `svelte-infinite-loading`) -- `slot` - a required snipper property called to render each row of the list with the signature `slot(r:RowAttributes)` +- `footer` - a snippet for the elements that should appear at the bottom of the list +- `slot` - a required snipper property called to render each row of the list with the signature `slot(r:RowAttributes)` ```typescript -export interface RowAttributes { - item: any // the element from the items array at index +export interface RowAttributes { + item: T // the element from the model array to be rendered index: number; // Item index style: string; // Item style, must be applied to the slot (look above for example) } diff --git a/TODO.md b/TODO.md index 3d6206c..7f6d8b3 100644 --- a/TODO.md +++ b/TODO.md @@ -4,5 +4,5 @@ [x] Rename the row snippet to something else -> slot [ ] add disabled [ ] move the css style logic out of the component row({ item, style, index }) -[ ] considering, items={data} itemCount={data.length} itemSize={50}; model is data, items is the view should be transformed into model={data} modelCount={data.length} itemSize={50}, {@render item(...)} +[x] considering, items={data} itemCount={data.length} itemSize={50}; model is data, items is the view should be transformed into model={data} modelCount={data.length} itemSize={50}, {@render item(...)} [x] implement minimization for the npm package diff --git a/src/lib/SizeAndPositionManager.ts b/src/lib/SizeAndPositionManager.ts index bf36c52..45a4840 100644 --- a/src/lib/SizeAndPositionManager.ts +++ b/src/lib/SizeAndPositionManager.ts @@ -8,18 +8,18 @@ import { ALIGNMENT, type VirtualItemSize, type VirtualRange, type VirtualPosition } from '.'; export default class SizeAndPositionManager { - private items: Array; + private model: Array; + private modelCount: number; private itemSize: VirtualItemSize; - private itemCount: number; private estimatedItemSize?: number; private itemSizeAndPositionData: Record; private lastMeasuredIndex: number; private totalSize?: number; - constructor(items: Array, itemSize: VirtualItemSize, itemCount: number, estimatedItemSize?: number) { - this.items = items; + constructor(model: Array, modelCount: number, itemSize: VirtualItemSize, estimatedItemSize?: number) { + this.model = model; this.itemSize = itemSize; - this.itemCount = itemCount; + this.modelCount = modelCount; this.estimatedItemSize = estimatedItemSize; this.itemSizeAndPositionData = {}; this.lastMeasuredIndex = -1; @@ -35,7 +35,7 @@ export default class SizeAndPositionManager { updateConfig(itemSize: VirtualItemSize, itemCount: number, estimatedItemSize?: number) { if (itemCount !== undefined) { - this.itemCount = itemCount; + this.modelCount = itemCount; } if (estimatedItemSize !== undefined) { @@ -56,7 +56,7 @@ export default class SizeAndPositionManager { } checkForMismatchItemSizeAndItemCount() { - if (Array.isArray(this.itemSize) && this.itemSize.length < this.itemCount) { + if (Array.isArray(this.itemSize) && this.itemSize.length < this.modelCount) { throw Error(`When itemSize is an array, itemSize.length can't be smaller than itemCount`); } } @@ -65,7 +65,7 @@ export default class SizeAndPositionManager { const { itemSize } = this; if (typeof itemSize === 'function') { - return itemSize(this.items[index], index); + return itemSize(this.model[index], index); } return Array.isArray(itemSize) ? itemSize[index] : itemSize; @@ -77,7 +77,7 @@ export default class SizeAndPositionManager { */ computeTotalSizeAndPositionData() { let totalSize = 0; - for (let i = 0; i < this.itemCount; i++) { + for (let i = 0; i < this.modelCount; i++) { const size = this.getSize(i); const offset = totalSize; totalSize += size; @@ -100,8 +100,8 @@ export default class SizeAndPositionManager { * */ getSizeAndPositionForIndex(index: number) { - if (index < 0 || index >= this.itemCount) { - throw Error(`Requested index ${index} is outside of range 0..${this.itemCount}`); + if (index < 0 || index >= this.modelCount) { + throw Error(`Requested index ${index} is outside of range 0..${this.modelCount}`); } return this.justInTime ? this.getJustInTimeSizeAndPositionForIndex(index) : this.itemSizeAndPositionData[index]; @@ -162,7 +162,7 @@ export default class SizeAndPositionManager { return ( lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size + - (this.itemCount - this.lastMeasuredIndex - 1) * this.getEstimatedItemSize() + (this.modelCount - this.lastMeasuredIndex - 1) * this.getEstimatedItemSize() ); } @@ -227,14 +227,14 @@ export default class SizeAndPositionManager { let end = start; - while (offset < maxOffset && end < this.itemCount - 1) { + while (offset < maxOffset && end < this.modelCount - 1) { end++; offset += this.getSizeAndPositionForIndex(end).size; } if (windowOverPadding) { start = Math.max(0, start - windowOverPadding); - end = Math.min(end + windowOverPadding, this.itemCount - 1); + end = Math.min(end + windowOverPadding, this.modelCount - 1); } return { @@ -310,11 +310,11 @@ export default class SizeAndPositionManager { private exponentialSearch(index: number, offset: number): number { let interval = 1; - while (index < this.itemCount && this.getSizeAndPositionForIndex(index).offset < offset) { + while (index < this.modelCount && this.getSizeAndPositionForIndex(index).offset < offset) { index += interval; interval *= 2; } - return this.binarySearch(Math.floor(index / 2), Math.min(index, this.itemCount - 1), offset); + return this.binarySearch(Math.floor(index / 2), Math.min(index, this.modelCount - 1), offset); } } diff --git a/src/lib/VirtualList.svelte b/src/lib/VirtualList.svelte index 92a1ac5..b6f86e0 100644 --- a/src/lib/VirtualList.svelte +++ b/src/lib/VirtualList.svelte @@ -45,10 +45,10 @@ const { height, width = '100%', - items = [], - // total item count, regardless of progressive loading. helps define the viewport size - itemCount, - // provided or calculated size of item n + model = [], + // total model count, typically model.length unless a partial loader is used + modelCount, + // items are the view, size of item n, can be a function itemSize, // usefull when using a partial loader estimatedItemSize, @@ -71,13 +71,12 @@ onVisibleRangeUpdate, onAfterScroll, - ...attributes - + ...props }: { height: number | string; width: number | string; - items: Array; - itemCount: number; + model: Array; + modelCount: number; itemSize: VirtualItemSize; estimatedItemSize?: number; getKey?: (i: number | string) => string; @@ -95,7 +94,6 @@ // events onVisibleRangeUpdate?: (range: VirtualRangeEvent) => void; onAfterScroll?: (event: AfterScrollEvent) => void; - } = $props(); const SCROLL_PROP = { @@ -117,19 +115,19 @@ scrollToIndex?: number; scrollToAlignment?: string; scrollOffset?: number; - itemCount?: number; + modelCount?: number; itemSize?: VirtualItemSize; estimatedItemSize?: number; } - const sizeAndPositionManager = new SizeAndPositionManager(items, itemSize, itemCount, estimatedItemSize); + const sizeAndPositionManager = new SizeAndPositionManager(model, modelCount, itemSize, estimatedItemSize); let mounted: boolean = false; let container: HTMLDivElement; let visibleItems: Array> = $state([]); let curState: VState = $state({ - offset: scrollOffset || (scrollToIndex !== undefined && itemCount && getOffsetForIndex(scrollToIndex)) || 0, + offset: scrollOffset || (scrollToIndex !== undefined && modelCount && getOffsetForIndex(scrollToIndex)) || 0, scrollChangeReason: SCROLL_CHANGE_REASON.REQUESTED }); @@ -139,7 +137,7 @@ scrollToIndex, scrollToAlignment, scrollOffset, - itemCount, + modelCount, itemSize, estimatedItemSize }; @@ -151,7 +149,7 @@ $effect(() => { // listen to updates: //@ts-expect-error unused no side effect - scrollToIndex, scrollToAlignment, scrollOffset, itemCount, itemSize, estimatedItemSize; + scrollToIndex, scrollToAlignment, scrollOffset, modelCount, itemSize, estimatedItemSize; // on update: propsUpdated(); @@ -210,12 +208,12 @@ const scrollPropsHaveChanged = prevProps.scrollToIndex !== scrollToIndex || prevProps.scrollToAlignment !== scrollToAlignment; const itemPropsHaveChanged = - prevProps.itemCount !== itemCount || + prevProps.modelCount !== modelCount || prevProps.itemSize !== itemSize || prevProps.estimatedItemSize !== estimatedItemSize; if (itemPropsHaveChanged) { - sizeAndPositionManager.updateConfig(itemSize, itemCount, estimatedItemSize); + sizeAndPositionManager.updateConfig(itemSize, modelCount, estimatedItemSize); recomputeSizes(); } @@ -228,7 +226,7 @@ }; } else if (typeof scrollToIndex === 'number' && (scrollPropsHaveChanged || itemPropsHaveChanged)) { curState = { - offset: getOffsetForIndex(scrollToIndex, scrollToAlignment, itemCount), + offset: getOffsetForIndex(scrollToIndex, scrollToAlignment, modelCount), scrollChangeReason: SCROLL_CHANGE_REASON.REQUESTED }; @@ -238,7 +236,7 @@ scrollToIndex, scrollToAlignment, scrollOffset, - itemCount, + modelCount, itemSize, estimatedItemSize }; @@ -286,7 +284,7 @@ if (start !== undefined && end !== undefined) { for (let index = start; index <= end; index++) { updatedItems.push({ - item: items[index], + item: model[index], index, style: getStyle(index) }); @@ -333,12 +331,12 @@ function getOffsetForIndex( index: number, align: ALIGNMENT = scrollToAlignment, - _itemCount: number = itemCount + _modelCount: number = modelCount ): number { if (index < 0) { index = 0; - } else if (index >= _itemCount) { - index = _itemCount - 1; + } else if (index >= _modelCount) { + index = _modelCount - 1; } return sizeAndPositionManager.getUpdatedOffsetForIndex( @@ -383,7 +381,7 @@ } -
+
{#if header} {@render header()} {/if} diff --git a/src/routes/examples/events/code.svelte b/src/routes/examples/events/code.svelte index bb3018f..9a1f0eb 100644 --- a/src/routes/examples/events/code.svelte +++ b/src/routes/examples/events/code.svelte @@ -1,5 +1,5 @@
- - {#snippet slot({ item, style, index }: { item: any; style: string; index: number })} + + {#snippet slot({ item, style, index }: SlotAttributes)}
Letter: {item}, Row: #{index}
diff --git a/src/routes/examples/variableheight/code.svelte b/src/routes/examples/variableheight/code.svelte index 88c6b7a..54f4f2c 100644 --- a/src/routes/examples/variableheight/code.svelte +++ b/src/routes/examples/variableheight/code.svelte @@ -1,7 +1,7 @@
- + {#snippet slot({ item, style, index }: SlotAttributes)}
{item.text} diff --git a/src/routes/examples/vertical/code.svelte b/src/routes/examples/vertical/code.svelte index 1f35dd1..9743a02 100644 --- a/src/routes/examples/vertical/code.svelte +++ b/src/routes/examples/vertical/code.svelte @@ -7,7 +7,7 @@ width: string; } - const myItems: Array = new Array(10000).fill(1).map((v, i) => ({ + const myModel: Array = new Array(10000).fill(1).map((v, i) => ({ text: 'Item ' + i, lineHeight: 20 + (i % 20) + 'px', width: 100 + (i % 30) + 'px' @@ -26,7 +26,7 @@
- + {#snippet slot({ item, style, index }: SlotAttributes)}
{item.text}