Skip to content

Commit

Permalink
refactor: extract vaadin-virtual-list-mixin and split tests (#6810)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomivirkki authored Nov 20, 2023
1 parent 1c38056 commit 22ddd00
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 195 deletions.
30 changes: 30 additions & 0 deletions dev/virtual-list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Virtual list</title>
<script type="module" src="./common.js"></script>

<script type="module">
import '@vaadin/virtual-list';

const items = Array.from({ length: 100000 }).map((_, i) => {
return { label: `Item ${i}` };
});

const renderer = (root, _, { item }) => {
root.innerHTML = `<div>${item.label}</div>`;
};

const list = document.querySelector('vaadin-virtual-list');
list.items = items;
list.renderer = renderer;
</script>
</head>

<body>
<vaadin-virtual-list style="height: 400px"></vaadin-virtual-list>
</body>
</html>
1 change: 1 addition & 0 deletions packages/virtual-list/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"web-component"
],
"dependencies": {
"@open-wc/dedupe-mixin": "^1.3.0",
"@polymer/polymer": "^3.0.0",
"@vaadin/component-base": "24.3.0-alpha10",
"@vaadin/lit-renderer": "24.3.0-alpha10",
Expand Down
68 changes: 68 additions & 0 deletions packages/virtual-list/src/vaadin-virtual-list-mixin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @license
* Copyright (c) 2021 - 2023 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import type { Constructor } from '@open-wc/dedupe-mixin';
import type { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';
import type { VirtualList } from './vaadin-virtual-list.js';

export type VirtualListDefaultItem = any;

export interface VirtualListItemModel<TItem> {
index: number;
item: TItem;
}

export type VirtualListRenderer<TItem> = (
root: HTMLElement,
virtualList: VirtualList<TItem>,
model: VirtualListItemModel<TItem>,
) => void;

export declare function VirtualListMixin<TItem, T extends Constructor<HTMLElement>>(
base: T,
): Constructor<ControllerMixinClass> & Constructor<VirtualListMixinClass<TItem>> & T;

export declare class VirtualListMixinClass<TItem = VirtualListDefaultItem> {
/**
* Gets the index of the first visible item in the viewport.
*/
readonly firstVisibleIndex: number;

/**
* Gets the index of the last visible item in the viewport.
*/
readonly lastVisibleIndex: number;

/**
* Custom function for rendering the content of every item.
* Receives three arguments:
*
* - `root` The render target element representing one item at a time.
* - `virtualList` The reference to the `<vaadin-virtual-list>` element.
* - `model` The object with the properties related with the rendered
* item, contains:
* - `model.index` The index of the rendered item.
* - `model.item` The item.
*/
renderer: VirtualListRenderer<TItem> | undefined;

/**
* An array containing items determining how many instances to render.
*/
items: TItem[] | undefined;

/**
* Scroll to a specific index in the virtual list.
*/
scrollToIndex(index: number): void;

/**
* Requests an update for the content of the rows.
* While performing the update, it invokes the renderer passed in the `renderer` property for each visible row.
*
* It is not guaranteed that the update happens immediately (synchronously) after it is requested.
*/
requestContentUpdate(): void;
}
146 changes: 146 additions & 0 deletions packages/virtual-list/src/vaadin-virtual-list-mixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* @license
* Copyright (c) 2021 - 2023 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
import { OverflowController } from '@vaadin/component-base/src/overflow-controller.js';
import { processTemplates } from '@vaadin/component-base/src/templates.js';
import { Virtualizer } from '@vaadin/component-base/src/virtualizer.js';

/**
* @polymerMixin
* @mixes ControllerMixin
*/
export const VirtualListMixin = (superClass) =>
class VirtualListMixinClass extends ControllerMixin(superClass) {
static get properties() {
return {
/**
* An array containing items determining how many instances to render.
* @type {Array<!VirtualListItem> | undefined}
*/
items: { type: Array },

/**
* Custom function for rendering the content of every item.
* Receives three arguments:
*
* - `root` The render target element representing one item at a time.
* - `virtualList` The reference to the `<vaadin-virtual-list>` element.
* - `model` The object with the properties related with the rendered
* item, contains:
* - `model.index` The index of the rendered item.
* - `model.item` The item.
* @type {VirtualListRenderer | undefined}
*/
renderer: Function,

/** @private */
__virtualizer: Object,
};
}

static get observers() {
return ['__itemsOrRendererChanged(items, renderer, __virtualizer)'];
}

/**
* Gets the index of the first visible item in the viewport.
*
* @return {number}
*/
get firstVisibleIndex() {
return this.__virtualizer.firstVisibleIndex;
}

/**
* Gets the index of the last visible item in the viewport.
*
* @return {number}
*/
get lastVisibleIndex() {
return this.__virtualizer.lastVisibleIndex;
}

/** @protected */
ready() {
super.ready();

this.__virtualizer = new Virtualizer({
createElements: this.__createElements,
updateElement: this.__updateElement.bind(this),
elementsContainer: this,
scrollTarget: this,
scrollContainer: this.shadowRoot.querySelector('#items'),
});

this.__overflowController = new OverflowController(this);
this.addController(this.__overflowController);

processTemplates(this);
}

/**
* Scroll to a specific index in the virtual list.
*
* @param {number} index Index to scroll to
*/
scrollToIndex(index) {
this.__virtualizer.scrollToIndex(index);
}

/** @private */
__createElements(count) {
return [...Array(count)].map(() => document.createElement('div'));
}

/** @private */
__updateElement(el, index) {
if (el.__renderer !== this.renderer) {
el.__renderer = this.renderer;
this.__clearRenderTargetContent(el);
}

if (this.renderer) {
this.renderer(el, this, { item: this.items[index], index });
}
}

/**
* Clears the content of a render target.
* @private
*/
__clearRenderTargetContent(element) {
element.innerHTML = '';
// Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
// When clearing the rendered content, this part needs to be manually disposed of.
// Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
delete element._$litPart$;
}

/** @private */
__itemsOrRendererChanged(items, renderer, virtualizer) {
// If the renderer is removed but there are elements created by
// a previous renderer, we need to request an update from the virtualizer
// to get the already existing elements properly cleared.
const hasRenderedItems = this.childElementCount > 0;

if ((renderer || hasRenderedItems) && virtualizer) {
virtualizer.size = (items || []).length;
virtualizer.update();
}
}

/**
* Requests an update for the content of the rows.
* While performing the update, it invokes the renderer passed in the `renderer` property for each visible row.
*
* It is not guaranteed that the update happens immediately (synchronously) after it is requested.
*/
requestContentUpdate() {
if (this.__virtualizer) {
this.__virtualizer.update();
}
}
};
66 changes: 10 additions & 56 deletions packages/virtual-list/src/vaadin-virtual-list.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,16 @@
* Copyright (c) 2021 - 2023 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import type {
VirtualListDefaultItem,
VirtualListItemModel,
VirtualListMixinClass,
VirtualListRenderer,
} from './vaadin-virtual-list-mixin.js';

export type VirtualListDefaultItem = any;

export interface VirtualListItemModel<TItem> {
index: number;
item: TItem;
}

export type VirtualListRenderer<TItem> = (
root: HTMLElement,
virtualList: VirtualList<TItem>,
model: VirtualListItemModel<TItem>,
) => void;
export { VirtualListDefaultItem, VirtualListItemModel, VirtualListRenderer };

/**
* `<vaadin-virtual-list>` is a Web Component for displaying a virtual/infinite list of items.
Expand Down Expand Up @@ -46,51 +40,11 @@ export type VirtualListRenderer<TItem> = (
* @extends HTMLElement
* @mixes ElementMixin
* @mixes ThemableMixin
* @mixes VirtualListMixin
*/
declare class VirtualList<TItem = VirtualListDefaultItem> extends ElementMixin(
ControllerMixin(ThemableMixin(HTMLElement)),
) {
/**
* Gets the index of the first visible item in the viewport.
*/
readonly firstVisibleIndex: number;

/**
* Gets the index of the last visible item in the viewport.
*/
readonly lastVisibleIndex: number;
declare class VirtualList<TItem = VirtualListDefaultItem> extends ThemableMixin(ElementMixin(HTMLElement)) {}

/**
* Custom function for rendering the content of every item.
* Receives three arguments:
*
* - `root` The render target element representing one item at a time.
* - `virtualList` The reference to the `<vaadin-virtual-list>` element.
* - `model` The object with the properties related with the rendered
* item, contains:
* - `model.index` The index of the rendered item.
* - `model.item` The item.
*/
renderer: VirtualListRenderer<TItem> | undefined;

/**
* An array containing items determining how many instances to render.
*/
items: TItem[] | undefined;

/**
* Scroll to a specific index in the virtual list.
*/
scrollToIndex(index: number): void;

/**
* Requests an update for the content of the rows.
* While performing the update, it invokes the renderer passed in the `renderer` property for each visible row.
*
* It is not guaranteed that the update happens immediately (synchronously) after it is requested.
*/
requestContentUpdate(): void;
}
interface VirtualList<TItem = VirtualListDefaultItem> extends VirtualListMixinClass<TItem> {}

declare global {
interface HTMLElementTagNameMap {
Expand Down
Loading

0 comments on commit 22ddd00

Please sign in to comment.