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
User | Most Recent Commit | # Commits |
---|
Other Committers
User | Most Recent Commit | # Commits |
---|
<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.
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.
newChild
propertyType: 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
propertyType: 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:
updateChild
is called for all of the newly-visible elements.totalItems
property.requestReset()
, which will call updateChild
for all currently-visible elements. See below for why this can be useful.For more on the interplay between newChild
and updateChild
, and when each is appropriate, see the example below
recycleChild
propertyType: 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
propertyType: 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
propertyType: 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
propertyType: string
One of:
Can also be set as an attribute on the element, e.g. <virtual-list layout="horizontal-grid"></virtual-list>
requestReset()
methodThis 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
” eventBubbles: 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:
first
: a number giving the index of the first item currently rendered.last
: a number giving the index of the last item currently rendered.Also see the example below.
newChild
and updateChild
The rule of thumb for these two options is:
newChild
. It is responsible for actually creating the DOM elements corresponding to each item.updateChild
if you ever plan on updating the items in the list.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);
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);
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.
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.
rangechange
” eventListen 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!
<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>
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.