PolymerLabs/virtual-list

Name: virtual-list

Owner: PolymerLabs

Description: null

Created: 2018-03-07 01:48:50.0

Updated: 2018-03-31 13:45:42.0

Pushed: 2018-03-30 09:06:11.0

Homepage: https://polymerlabs.github.io/virtual-list/

Size: 525

Language: JavaScript

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

<virtual-list>

<virtual-list> maps a provided set of JavaScript objects onto DOM nodes, and renders only the DOM nodes that are currently visible, leaving the rest “virtualized”.

This document is an early-stage explainer for <virtual-list> as a potential future web platform feature, as part of the layered API project. The repository also hosts a proof-of-concept implementation that is being co-evolved with the design.

The (tentative) API design choices made here, as well as the list's capabilities, take inspiration from the infinite list study group research.

Example
ipt type="module"
    src="std:virtual-list|https://some.cdn.com/virtual-list.js">
ript>

tual-list></virtual-list>

ipt type="module">
nst list = document.querySelector('virtual-list');
nst myItems = new Array(200).fill('item');

 Setting this is required; without it the list does not function.
st.newChild = (index) => {
const child = document.createElement('section');
child.textContent = index + ' - ' + myItems[index];
child.onclick = () => console.log(`clicked item #${index}`);
return child;


 This will automatically cause a render of the visible children
 (i.e., those that fit on the screen).
st.totalItems = myItems.length;
ript>

Checkout more examples in demo/index.html.

API
newChild property

Type: function(itemIndex: number) => Element

Set this property to configure the virtual list with a factory that creates an element the first time a given item at the specified index is ready to be displayed in the DOM.

This property is required. Without it set, nothing will render.

updateChild property

Type: function(child: Element, itemIndex: number)

Set this property to configure the virtual list with a function that will update the element with data at a given index.

If set, this property is invoked in two scenarios:

For more on the interplay between newChild and updateChild, and when each is appropriate, see the example below

recycleChild property

Type: function(child: Element, itemIndex: number)

Set this property to replace the default behavior of removing an item's element from the DOM when it is no longer visible.

This is often used for node-recycling scenarios, as seen in the example below.

We are discussing the naming and API for this functionality in #25.

childKey property

Type: function(itemIndex: number) => any

Set this property to provide a custom identifier for the element corresponding to a given data index.

This is often used for more efficient re-ordering, as seen in the example below.

totalItems property

Type: number

Set this property to control how many items the list will display. The items are mapped to elements via the newChild property, so this controls the total number of times newChild could be called, as the user scrolls to reveal all the times.

Can also be set as an attribute (all lower-case) on the element, e.g. <virtual-list totalitems="10"></virtual-list>

layout property

Type: string

One of:

Can also be set as an attribute on the element, e.g. <virtual-list layout="horizontal-grid"></virtual-list>

requestReset() method

This re-renders all of the currently-displayed elements, updating them from their source data using updateChild.

This can be useful when you mutate data without changing the totalItems. Also see the example below.

We are discussing the naming of this API, as well as whether it should exist at all, in #26. The aforementioned #29 is also relevant.

rangechange” event

Bubbles: false / Cancelable: false / Composed: false

Fired when the list has finished rendering a new range of items, e.g. because the user scrolled. The event is an instance of RangeChangeEvent, which has the following properties:

Also see the example below.

More examples
Using newChild and updateChild

The rule of thumb for these two options is:

Thus, for completely static lists, you only need to set newChild:

myItems = ['a', 'b', 'c', 'd'];

.newChild = index => {
nst child = document.createElement('div');
ild.textContent = myItems[index];
turn child;


alls newChild four times (assuming the screen is big enough)
.totalItems = myItems.length;

In this example, we are statically displaying a virtual list with four items, which we never plan to update. This can be useful for use cases where you would otherwise use static HTML, but want to get the performance benefits of virtualization. (Admittedly, we'd need more than four items to see that happen in reality.)

Note that even if we invoke requestReset(), nothing new would render in this case:

oes nothing
imeout(() => {
Items = ['A', 'B', 'C', 'D'];
st.requestReset();
00);

Note: see #15 for why we included a setTimeout here.

If you plan to update your items, you're likely better off using newChild to set up the “template” for each item, and using updateChild to fill in the data. Like so:

.newChild = () => {
turn document.createElement('div');


.updateChild = (child, index) => {
ild.textContent = myItems[index];


myItems = ['a', 'b', 'c', 'd'];
alls newChild + updateChild four times
.totalItems = myItems.length;

his now works: it calls updateChild four times
imeout(() => {
Items = ['A', 'B', 'C', 'D'];
st.requestReset();
00);
DOM recycling using recycleChild

You can recycle DOM by using the recycleChild function to collect DOM, and reuse it in newChild.

When doing this, be sure to perform DOM updates in updateChild, as recycled children will otherwise have the data from the previous item.

t myItems = ['a', 'b', 'c', 'd'];
t nodePool = [];

ct.assign(list, {
wChild() {
return nodePool.pop() || document.createElement('div');

dateChild(child, index) {
child.textContent = myItems[index];

cycleChild(child) {
nodePool.push(child);


Data manipulation using requestReset()

The <virtual-list> element will automatically rerender the displayed items when totalItems changes. For example, to add a new item to the end, you could do:

ems.push('new item');
.totalItems++;

If you want to keep the same number of items or change an item's properties, you can use requestReset() to notify the list about changes, and cause a rerender of currently-displayed items. If you do this, you'll also need to set updateChild, since the elements will already be created. For example:

.updateChild = (child, index) => {
ild.textContent = index + ' - ' + myItems[index];


ems[0] = 'item 0 changed!';

.requestReset();

In this case, newChild will be called for the newly-added item once it becomes visible, whereas updateChild will every item, including the ones that already had corresponding elements in the old items indexes.

Efficient re-ordering using childKey

<virtual-list> keeps track of the generated DOM via an internal key/Element map to limit the number of created nodes.

The default key is the array index, but can be customized through the childKey property.

Imagine we have a list of 3 contacts:

t myContacts = ['A', 'B', 'C'];
ualList.totalItems = myContacts.length;
ualList.newChild = () => document.createElement('div');
ualList.updateChild = (div, index) => div.textContent = myContacts[index];

This renders 3 contacts, and the <virtual-list> key/Element map is:

div>A</div>
div>B</div>
div>C</div>

We want to move the first contact to the end:

tion moveFirstContactToEnd() {
nst contact = myContacts[0];
Contacts.splice(0, 1); // remove it
Contacts.push(contact); // add it to the end
rtualList.requestReset(); // notify virtual-list

With the default childKey, we would relayout and repaint all the contacts when invoking moveFirstContactToEnd():

div>B</div> (was <div>A</div>)
div>C</div> (was <div>B</div>)
div>A</div> (was <div>C</div>)

This is suboptimal, as we just needed to move the first DOM node to the end.

We can customize the key/Element mapping via childKey:

ualList.childKey = (index) => myContacts[index];

This updates the <virtual-list> key/Element map to:

div>A</div>
div>B</div>
div>C</div>

Now, invoking moveFirstContactToEnd() will only move the first contact DOM node to the end.

See demo/sorting.html as an example implementation.

Performing actions as the list scrolls using the “rangechange” event

Listen for the “rangechange” event to get notified when the displayed items range changes.

.addEventListener('rangechange', (event) => {
 (event.first === 0) {
console.log('rendered first item.');

 (event.last === list.totalItems - 1) {
console.log('rendered last item.');
// Perhaps you would want to load more data for display!


Scrolling

<virtual-list> needs to be sized in order to determine how many items should be rendered. Its default height is 150px, similar to CSS inline replaced elements like images and iframes.

Main document scrolling will be achievable through document.rootScroller

tual-list style="height: 100vh"></virtual-list>
ipt type="module">
cument.rootScroller = document.querySelector('virtual-list');
ript>
Development

To work on the proof-of-concept implementation, ensure you have installed the npm dependencies and serve from the project root

m install
thon -m SimpleHTTPServer 8081

Then, navigate to the url: http://localhost:8081/demo/

For more documentation on the internal pieces that we use to implement our <virtual-list> prototype, see DESIGN.md.


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.