lit-virtualizer

Virtual scrolling for lit-html and LitElement

Usage no npm install needed!

<script type="module">
  import litVirtualizer from 'https://cdn.skypack.dev/lit-virtualizer';
</script>

README

lit-virtualizer

Lit-virtualizer provides tools for implementing virtual scrolling with lit-html and LitElement.

This package provides two main exports to be used alongside lit-html and LitElement:

  • scroll: a lit-html directive that turns its parent element into a virtually scrolling container.
  • <lit-virtualizer>: a subclass of LitElement that wraps the scroll directive.

You can find documentation below, or view live examples. The examples are deployed from the uni-virtualizer-examples directory in this monorepo.

⚠️ lit-virtualizer is in prerelease. All changes may be breaking until 1.0.

Getting Started

Get this package:

npm i lit-virtualizer

The package is shipped using ES modules. It also uses bare specifiers to refer to other node modules such as lit-html. Shipping the package this way affords you control as a developer over your bundle delivery. For example, you could do code splitting. You will, however, have to resolve these module names when bundling your code.

As an example, here's how you can do module resolution with rollup.

index.html

...
<script type="module" src="build/main.js">
...

src/main.js

import { LitVirtualizer } from 'lit-virtualizer';

// use <lit-virtualizer> element or LitVirtualizer class

Install rollup and the rollup-plugin-node-resolve plugin.

npm i rollup rollup-plugin-node-resolve

rollup.config.js

import resolve from 'rollup-plugin-node-resolve';

export default [
  {
    input: 'src/main.js',
    output: {
      dir: 'build',
      format: 'esm'
    },
    plugins: [
      resolve(),
    ]        
  }
];

Roll it up.

npx rollup --config

Rollup will output build/main.js, with properly resolved module names.

Other small chunks will also be present. Lit-virtualizer utilizes dynamic imports in a few places to avoid loading code unnecessarily. This allows rollup to split the code and emit several chunks.

Documentation

scroll directive

scroll is a lit-html directive that turns its parent element into a virtual scrolling area. It requires a renderItem function for rendering virtual children, and an array of items to render.

Say we are building an index page for a blog, and need a list of link to all blog posts. The blog has thousands of posts, so we want the list to have virtual scrolling. Here's our index.html...

<body>
  <h1>All posts</h1>
  <div id="post-list"></div>

  <script type="module" src="index.js"></script>
</body>

...and here's how we can use the lit-virtualizer's scroll directive to render the list of post links.

import scroll from 'lit-virtualizer'
import { html, render } from 'lit-html'

// Post metadata that we want to virtually scroll.
const posts = [
  {title: "10 essential products", link: "./post-1", date: "June 20 2019"},
  {title: "How to tie a tie", link: "./post-2", date: "June 21 2019"},
  ...
];

// Building our post link template.
// renderPostLink is a function that takes an item (our metadata)
// and uses the `html` tag to build DOM structure with it.
const renderPostLink = post => html`
  <div>
    <h2><a href="${post.link}">${post.title}</a></h2>
    <p>${post.date}</p>
  </div>
`;

// Pass the post metadata and the render to the scroll directive...
const templateResult = html`${
  scroll({
    items: posts,
    renderItem: renderPostLink
  })
}`

// ... and render it into our container!
render(templateResult, document.querySelector("#post-list"));

In this example, just items and renderItem were configured. You can also specify the scroll target and whether or not to use shadow DOM.

scroll config options

items

Type: Array<T>

A list of items to display via the renderItem function. The type of the items should match the first argument of the renderItem function.

renderItem

Type: (item: T, index?: number) => lit-html.TemplateResult

A function that returns a lit-html TemplateResult. It will be used to generate the DOM for each item in the virtual list.

scrollTarget

Type: Element | Window

Optional. An element that receives scroll events for the virtual scroller. If not specified, the directive's parent element will be the target.

useShadowDOM

Type: boolean

Optional. Whether to build the virtual scroller within a shadow DOM.

totalItems

Type: number

Optional. Limit for the number of items to display. Defaults to the length of the items array.

scrollToIndex

Type: {index: number, position?: string}

where position is: 'start'|'center'|'end'|'nearest'

Optional. Scroll to the item at the give index. Place the item at the given position within the scroll view. For example, if index is 100 and position is end, then the bottom of the item at index 100 will be at the bottom of the scroll view. Position defaults to start.

Note: Rendering with scrollToIndex will cause the scroll view to fix at the given position until the user manually scrolls. If a lit-html template using the scroll directive is re-rendered, note that the view will be re-scrolled to respect the given scrollToIndex option. Take care to set or unset the scrollToIndex option upon subsequent re-renders for the desired behavior.

Events

rangechange event

Thi event is an instance of RangeChangeEvent, which has the following properties regarding the currently rendered range of items:

  • first: the index of the first item currently rendered. This may not be the first visible item.
  • last: the index of the last item currently rendered. This may not be the first visible item.
  • firstVisible: the index of the first item visible. That is, the first item to intersect the scroll view.
  • lastVisible: the index of the last item visible. That is, the last item to intersect the scroll view.

This event is fired when any of these values change, e.g. because the user scrolled. To listen for this event, attach a listener to the host of the scroll directive. You can do this inline with lit-html.

Example usage:

const handleEvent = (e) => {
  console.log("The first visible index is", e.firstVisible);
  console.log("The last visible index is", e.lastVisible);
}

const example = (contacts) => html`
  <section @rangechange=${handleEvent}>
    ${scroll({
      items: contacts,
      renderItem: ({ mediumText }) => html`<p>${mediumText}</p>`,
    })}
  </section>
`;

<lit-virtualizer> element

<lit-virtualizer> is a LitElement wrapper for the scroll directive. It simply provides different usage ergonomics. It doesn't add extra functionality. If your project does not otherwise use LitElement, using the scroll directive with tree-shaking will save you bytes by not unnecessarily importing LitElement.

Here's how to redo the blog post example, using <lit-virtualizer> instead. Construction of the final templateResult is the only difference:

const templateResult = html`
  <lit-virtualizer
    .items=${posts}
    .renderItem=${renderPostLink}
  ></lit-virtualizer>
`
render(templateResult, document.querySelector("#post-list"));

With <lit-virtualizer>, you pass configuration as properties to the HTML Element.

<lit-virtualizer> API

items property

Type: Array<T>

A list of items to display via the renderItem function. The type of the items should match the first argument of the renderItem function.

renderItem property

Type: (item: T, index?: number) => lit-html.TemplateResult

A function that returns a lit-html TemplateResult. It will be used to generate the DOM for each item in the virtual list.

scrollTarget property

Type: Element | Window

Optional. An element that receives scroll events for the virtual scroller. If not specified, the <lit-virtualizer> element itself will be the scroll target.

scrollToIndex method

Type: (index: number, position?: string) => void

where position is: 'start'|'center'|'end'|'nearest'

Scroll to the item at the give index. Place the item at the given position within the scroll view. For example, if index is 100 and position is end, then the bottom of the item at index 100 will be at the bottom of the scroll view. Position defaults to start.

Example usage:

const virtualizer = document.createElement('lit-virtualizer');
virtualizer.items = contacts;
virtualizer.renderItem = renderContactItem;
// Scroll to the 100th item and put it in the center of the scroll view.
virtualizer.scrollToIndex(100, 'center');

Complete example

index.html

<html>
  <script type="module" src="build/index.js"></script>
  <contact-list></contact-list>
</html>

src/index.js (uncompiled)

import 'lit-virtualizer'
import { LitElement, html } from 'lit-element'

class ContactList extends LitElement {
    static get properties() {
      return {
        data: {type: Array}
      };
    }

    constructor() {
      super();
      this.data = [];
    }

    async firstUpdated() {
      this.data = [
        {name: 'Name 1', phone: '123 456-7890'},
        {name: 'Name 2', phone: '555 555-5555'}
      ]
    }

    render() {
        return html`
            <lit-virtualizer
              .scrollTarget=${window}
              .items=${this.data}
              .renderItem=${(contact) => html`
                <div><b>${contact.name}</b>: ${contact.phone}</div>
              `}>
            </lit-virtualizer>
        `;
    }
}

customElements.define('contact-list', ContactList);