Documentation

List Manager

The List Manager is a high-performance, virtualized list component for efficiently rendering large datasets with minimal DOM operations. It provides optimized scrolling, recycling of DOM elements, and support for both static data and API-connected data sources.

Overview

The List Manager provides efficient virtualized list rendering, essential for displaying large datasets without performance degradation. It maintains a minimal DOM footprint by rendering only the visible items and those just outside the viewport, making it ideal for mobile applications and performance-critical scenarios.

Features

  • Virtualized Rendering: Only renders items visible in the viewport plus a configurable buffer
  • DOM Recycling: Reuses DOM elements to minimize creation/destruction operations
  • Dynamic Item Heights: Support for both fixed and variable height items
  • Efficient Scrolling: Optimized scroll handling with customizable strategies
  • API Connection: Built-in support for loading data from APIs
  • Pagination: Supports cursor-based, page-based, and offset-based pagination
  • Memory Optimization: Careful memory management to prevent leaks in long-lived applications
  • Highly Configurable: Extensive options for adapting to various use cases

Usage

Basic Example

This example shows how to create a basic List Manager with static data:

import { createListManager } from '../core/collection/list-manager';

// Container element for the list
const container = document.getElementById('my-list-container');

// Create list manager
const listManager = createListManager('items', container, {
  // Function to render each item
  renderItem: (item, index) => {
    const element = document.createElement('div');
    element.className = 'list-item';
    element.textContent = item.headline;
    return element;
  },
  
  // Static data (no API connection)
  staticItems: [
    { id: '1', headline: 'Item 1' },
    { id: '2', headline: 'Item 2' },
    { id: '3', headline: 'Item 3' },
    // ... more items
  ],
  
  // Default item height (optional, improves performance)
  itemHeight: 48,
  
  // Callback after items are loaded
  afterLoad: (result) => {
    console.log(`Loaded ${result.items.length} items`);
  }
});

// Later, when done with the list, clean up
// listManager.destroy();

API-Connected Example

This example connects the list to a REST API:

import { createListManager } from '../core/collection/list-manager';

const container = document.getElementById('api-list-container');

// Create an API-connected list manager
const listManager = createListManager('users', container, {
  // API base URL
  baseUrl: 'https://api.example.com/api',
  
  // Transform API response items
  transform: (user) => ({
    id: user.id,
    headline: user.name,
    supportingText: user.email,
    meta: user.role
  }),
  
  // Render function for items
  renderItem: (item, index) => {
    const element = document.createElement('div');
    element.className = 'user-item';
    
    element.innerHTML = `
      <h3>${item.headline}</h3>
      <p>${item.supportingText}</p>
      <span class="meta">${item.meta}</span>
    `;
    
    return element;
  },
  
  // Pagination configuration
  pagination: {
    strategy: 'cursor',  // 'cursor', 'page', or 'offset'
    perPageParamName: 'limit'
  },
  
  // Number of items per page
  pageSize: 20
});

// Initial load happens automatically on creation
// You can trigger manual refresh or load more:
listManager.refresh(); // Refresh entire list
listManager.loadMore(); // Load next page

Page Loader

For more control over page loading, use the createPageLoader utility:

import { createListManager, createPageLoader } from '../core/collection/list-manager';

// First, create a list and list manager
const myList = {
  component: document.getElementById('list-container'),
  items: [],
  setItems: (items) => {
    myList.items = items;
    // Update your UI with the new items
  }
};

const listManager = createListManager('posts', myList.component, {
  baseUrl: 'https://api.example.com/api',
  renderItem: (item) => { /* render function */ }
});

// Then create a page loader
const pageLoader = createPageLoader(myList, listManager, {
  onLoad: ({ loading, hasNext, hasPrev, items }) => {
    // Update loading indicators
    document.getElementById('loading-indicator').style.display = loading ? 'block' : 'none';
    
    // Update navigation buttons
    document.getElementById('next-button').disabled = !hasNext;
    document.getElementById('prev-button').disabled = !hasPrev;
    
    // Log the operation
    console.log(`Loaded ${items.length} items`);
  },
  pageSize: 25
});

// Use the page loader for navigation
document.getElementById('next-button').addEventListener('click', () => pageLoader.loadNext());
document.getElementById('prev-button').addEventListener('click', () => pageLoader.loadPrev());

// Initial load
pageLoader.load();

API Reference

Core Functions

createListManager(collection, container, config)

Creates a new list manager instance.

  • Parameters:

- collection (string): Collection name used for API endpoints

- container (HTMLElement): Container element to render the list in

- config (ListManagerConfig): Configuration object

  • Returns: ListManager instance

createPageLoader(list, listManager, config)

Creates a page loader for handling pagination.

  • Parameters:

- list (object): List interface with setItems method

- listManager (ListManager): List manager instance

- config (object): Page loader configuration

  • Returns: PageLoader instance

Configuration Options

The ListManagerConfig interface provides extensive configuration options:

OptionTypeDefaultDescription
transformFunction(item) => itemTransform function applied to items from the API
baseUrlstringnullBase URL for API requests
renderItemFunction(required)Function to render an item element
afterLoadFunctionundefinedCallback function after loading items
staticItemsArray[]Items for static mode (no API)
renderBufferSizenumber5Extra items to render outside the viewport
overscanCountnumber3Extra items to keep in DOM but invisible
itemHeightnumber48Default height for items in pixels
dynamicItemSizebooleanfalseWhether items can have different heights
measureItemsInitiallybooleantrueWhether to measure initial items
pageSizenumber20Number of items per page
loadThresholdnumber0.8Load more when scrolled past this fraction
throttleMsnumber16Throttle scroll event (ms)
dedupeItemsbooleantrueRemove duplicate items based on ID
scrollStrategystring'scroll'Scroll strategy: 'scroll', 'intersection', or 'hybrid'
paginationobjectundefinedPagination configuration object

List Manager Interface

The ListManager interface provides these methods:

MethodParametersReturnsDescription
loadItemsparams (object)PromiseLoads items with the given parameters
loadMore-PromiseLoads the next page of items
refresh-PromiseRefreshes the list with the latest data
updateVisibleItemsscrollTop (number, optional)voidUpdates visible items based on scroll position
scrollToItemitemId (string), position ('start', 'center', 'end')voidScrolls to a specific item
setItemHeightsheightsMap (object)booleanSets custom heights for specific items
getCollection-CollectionGets the underlying collection
getVisibleItems-ArrayGets currently visible items
getAllItems-ArrayGets all items
isLoading-booleanChecks if list is currently loading
hasNextPage-booleanChecks if there are more items to load
isApiMode-booleanChecks if list is in API mode
setRenderHookhookFn (Function)voidSets a hook function for rendering
destroy-voidDestroys the list manager and cleans up

Page Loader Interface

The PageLoader interface provides these methods:

MethodParametersReturnsDescription
loadcursor (string, optional), addToHistory (boolean, default: true)PromiseLoads items at the given cursor position
loadNext-PromiseLoads the next page of items
loadPrev-PromiseLoads the previous page of items
loading-booleanWhether the loader is currently loading
cursor-stringCurrent cursor position

Architecture

Core Components

The List Manager is built from several specialized modules:

  • List Manager (index.ts): Main entry point and API surface
  • Configuration (config.ts): Configuration validation and processing
  • DOM Elements (dom-elements.ts): DOM element creation and manipulation
  • Item Measurement (item-measurement.ts): Item height calculation and caching
  • Renderer (renderer.ts): Efficient DOM updates and element recycling
  • Scroll Tracker (scroll-tracker.ts): Scroll position tracking strategies
  • State Management (state.ts): Internal state tracking and updates
  • Recycling Pool (utils/recycling.ts): DOM element reuse
  • Visibility Calculation (utils/visibility.ts): Determining visible items

Rendering Strategies

The List Manager employs virtualized rendering with three key optimizations:

  • Windowed Rendering: Only renders items visible in the viewport plus a buffer
  • Partial DOM Updates: Only adds, removes, or repositions necessary elements
  • Position Caching: Precomputes and caches item positions for fast access

For large lists, it uses binary search to quickly locate visible items, dramatically improving performance.

DOM Recycling

To minimize expensive DOM operations, the recycling system:

  • Pools removed elements by type
  • Reuses elements when scrolling or refreshing
  • Clears element state before reuse
  • Limits pool size to prevent memory leaks

Scroll Handling

Three scroll tracking strategies are available:

  • Traditional (scroll): Uses optimized scroll events with throttling
  • Intersection Observer (intersection): Uses IntersectionObserver for more efficient tracking
  • Hybrid (hybrid): Combines approaches for optimal performance

The hybrid strategy uses IntersectionObserver for loading more content and minimal scroll events for position tracking.

Item Measurement

For handling item heights, two approaches are available:

  • Uniform Height: All items have the same height (most efficient)
  • Dynamic Height: Each item's height is measured individually (more flexible)

For dynamic heights, measurements are cached and offsets are precomputed for efficient lookup.

Performance Optimizations

The List Manager includes numerous performance optimizations:

  • Throttled Scroll Handling: Limits scroll event processing frequency
  • RequestAnimationFrame: Batches DOM updates to animation frames
  • Binary Search: Efficiently finds visible items in large datasets
  • Partial Updates: Only updates DOM elements that changed
  • DOM Recycling: Reuses DOM elements instead of creating new ones
  • Position Caching: Precomputes item positions for fast lookup
  • Optimized Measurements: Measures only when necessary and caches results
  • Deduplication: Avoids duplicate items when loading more data
  • Lazy Loading: Only loads data when needed
  • Element Pool Limiting: Prevents memory leaks from excessive recycling

Utility Transforms

The List Manager provides transform functions for common collections:

transforms.track

transforms.track = (track) => ({
  id: track._id,
  headline: track.title || 'Untitled',
  supportingText: track.artist || 'Unknown Artist',
  meta: track.year?.toString() || ''
});

transforms.playlist

transforms.playlist = (playlist) => ({
  id: playlist._id,
  headline: playlist.name || 'Untitled Playlist',
  supportingText: `${playlist.tracks?.length || 0} tracks`,
  meta: playlist.creator || ''
});

transforms.country

transforms.country = (country) => ({
  id: country._id,
  headline: country.name || country.code,
  supportingText: country.continent || '',
  meta: country.code || ''
});

Pagination Strategies

The List Manager supports three pagination strategies:

Cursor-Based Pagination

  • Uses a cursor token to retrieve the next set of items
  • Most efficient for large datasets
  • Configuration:

pagination: {
    strategy: 'cursor',
    cursorParamName: 'cursor' // Optional, defaults to 'cursor'
  }

Page-Based Pagination

  • Uses page numbers for navigation
  • Common in many API implementations
  • Configuration:

pagination: {
    strategy: 'page',
    pageParamName: 'page', // Optional, defaults to 'page'
    perPageParamName: 'per_page' // Optional, defaults to 'per_page'
  }

Offset-Based Pagination

  • Uses item offsets for precise positioning
  • Good for random access in large lists
  • Configuration:

pagination: {
    strategy: 'offset',
    offsetParamName: 'offset', // Optional, defaults to 'offset'
    limitParamName: 'limit' // Optional, defaults to 'limit'
  }

Best Practices

For optimal performance:

  • Specify Item Height: Always provide itemHeight when item heights are consistent
  • Use DOM Recycling: Let the List Manager handle element reuse
  • Keep Items Simple: Complex item rendering slows down scrolling
  • Virtualize Large Lists: Always use virtualization for lists over 100 items
  • Debounce External Updates: Avoid frequent external updates to the list
  • Use Image Loading Callbacks: Update heights after images load if sizes vary
  • Limit Item Props: Keep item objects small with only necessary properties
  • Use Appropriate Strategy: Choose scroll strategy based on device performance

Advanced Examples

Variable Height Items

const listManager = createListManager('products', container, {
  dynamicItemSize: true, // Enable variable height measurement
  
  renderItem: (item, index) => {
    const element = document.createElement('div');
    element.className = 'product-item';
    element.innerHTML = `
      <h3>${item.name}</h3>
      <p>${item.description}</p>
      <img src="${item.image}" class="product-image">
    `;
    
    // If images can change height, update measurement after load
    const img = element.querySelector('img');
    if (img) {
      img.onload = () => {
        // Update height for this specific item
        listManager.setItemHeights({
          [item.id]: element.offsetHeight
        });
      };
    }
    
    return element;
  }
});

IntersectionObserver-Based Loading

const listManager = createListManager('feed', container, {
  scrollStrategy: 'intersection', // Use IntersectionObserver
  loadThreshold: 0.9, // Load when user is 90% through content
  
  // Other configuration...
  renderItem: (item) => { /* ... */ }
});

Custom Render Hook

const listManager = createListManager('messages', container, {
  renderItem: (message) => {
    const element = document.createElement('div');
    element.className = 'message';
    element.textContent = message.text;
    return element;
  }
});

// Add custom behavior to each rendered element
listManager.setRenderHook((item, element) => {
  // Add interaction handlers
  element.addEventListener('click', () => {
    console.log('Clicked message:', item.id);
  });
  
  // Add custom styling based on message state
  if (item.isRead) {
    element.classList.add('message--read');
  } else {
    element.classList.add('message--unread');
  }
});