Basic List

List Item 1
List Item 2
List Item 3
List Item 4
List Item 5

Multi Select List

Algeria
Egypt
Libya
Morocco
Sudan
Tunisia
Western Sahara
Angola
French Equatorial Africa
Benin
Botswana
Burkina Faso
Burundi
Cape Verde
Cameroon
Central African Republic
Belgian Congo
Comoros
Congo

API Users List

1'000'000 entries and virtualiation
E
Everett Warren
everett.warren1@example.com
Blockchain Developer
B
Benjamin Lynch
benjamin.lynch2@example.com
Content Strategist
D
Diego Hoffman
diego.hoffman3@example.com
Product Marketing Manager
G
Grace Gibson
grace.gibson4@example.com
Site Reliability Engineer
P
Penelope Miller
penelope.miller5@example.com
Manager
S
Sarah Edwards
sarah.edwards6@example.com
Account Executive
E
Everly Watson
everly.watson7@example.com
CISO
H
Hunter Peters
hunter.peters8@example.com
Developer
W
Willow Franklin
willow.franklin9@example.com
Community Manager
S
Sophia Perkins
sophia.perkins10@example.com
Quality Assurance Lead
M
Malachi Barnes
malachi.barnes11@example.com
Designer
M
Mateo Cruz
mateo.cruz12@example.com
Site Reliability Engineer
J
Jameson Turner
jameson.turner13@example.com
Mobile Developer
S
Sarah Morales
sarah.morales14@example.com
Business Analyst
K
Kai Graham
kai.graham15@example.com
CEO

API Users List (cursor mode)

1'000'000 entries and virtualization with cursor pagination strategy
L
Luna Black
luna.black1@example.com
CEO
C
Colton Carter
colton.carter2@example.com
CEO
E
Everett Wagner
everett.wagner3@example.com
Sales Representative
E
Emma Myers
emma.myers4@example.com
Frontend Developer
R
Riley Ruiz
riley.ruiz5@example.com
Account Executive
J
Julian Carter
julian.carter6@example.com
CFO
N
Naomi Graham
naomi.graham7@example.com
Business Analyst
E
Ezekiel Wood
ezekiel.wood8@example.com
CISO
E
Eva Rivera
eva.rivera9@example.com
Backend Developer
G
Grayson Lee
grayson.lee10@example.com
Operations Manager
T
Thomas Gonzalez
thomas.gonzalez11@example.com
Frontend Developer
D
David Stewart
david.stewart12@example.com
Manager
J
Josephine Lynch
josephine.lynch13@example.com
QA Engineer
J
Jose Barnes
jose.barnes14@example.com
System Administrator
E
Elizabeth Henry
elizabeth.henry15@example.com
Community Manager
A
Anna Pierce
anna.pierce16@example.com
UX Researcher

Documentation

List Component & Collection System Documentation

Overview

The List component provides a high-performance virtual scrolling solution for rendering large datasets with minimal DOM operations. It supports various data sources, dynamic item heights, efficient rendering, and automatic pagination.

List Component

The List component is a high-performance virtual scrolling implementation that efficiently renders large datasets with minimal DOM operations and memory usage.

Features

  • Virtual Scrolling: Only renders items visible in the viewport
  • Dynamic Heights: Supports items with variable heights
  • Element Recycling: Reuses DOM elements to reduce memory usage
  • Cursor Pagination: Load more data automatically as the user scrolls
  • Selection Management: Built-in support for selecting items
  • Efficient DOM Operations: Batches updates for optimal performance
  • Multiple Scroll Strategies: Traditional events or IntersectionObserver
  • Accessibility Support: Proper ARIA attributes and keyboard navigation

Usage

import { createList } from 'mtrl';

// Create a list with static data
const fruitList = createList({
  // Static items
  items: [
    { id: 'apple', name: 'Apple', color: 'red' },
    { id: 'banana', name: 'Banana', color: 'yellow' },
    { id: 'cherry', name: 'Cherry', color: 'red' }
  ],
  
  // Render function for each item
  renderItem: (item, index) => {
    const element = document.createElement('div');
    element.className = 'mtrl-list-item';
    element.textContent = item.name;
    element.style.color = item.color;
    return element;
  }
});

// Add to DOM
document.querySelector('#list-container').appendChild(fruitList.element);

// Create API-connected list
const userList = createList({
  collection: 'users',
  baseUrl: 'https://api.example.com',
  
  // Render function
  renderItem: (user, index) => {
    const element = document.createElement('div');
    element.className = 'mtrl-list-item';
    element.innerHTML = `
      <div class="avatar">${user.name.charAt(0)}</div>
      <div class="details">
        <div class="name">${user.name}</div>
        <div class="email">${user.email}</div>
      </div>
    `;
    return element;
  }
});

// Add event handlers
userList.on('select', (event) => {
  console.log('Selected user:', event.item);
});

Configuration

The List component accepts the following configuration options:

OptionTypeDefaultDescription
collectionstring'items'Collection name for API-connected lists
baseUrlstring'http://localhost:4000/api'Base URL for API requests
renderItemFunctionRequiredFunction that renders each item
itemsArray[]Static items for non-API lists
itemHeightnumber48Default height for items in pixels
pageSizenumber20Number of items to load per page
renderBufferSizenumber5Extra items to render above/below viewport
overscanCountnumber3Extra items to keep in DOM but invisible
loadThresholdnumber0.8Load more when scrolled past this fraction
throttleMsnumber16Throttle scroll event (ms)
dedupeItemsbooleantrueRemove duplicate items based on ID
trackSelectionbooleantrueTrack item selection state
multiSelectbooleanfalseAllow multiple items to be selected
initialSelectionstring[][]Initially selected item IDs
scrollStrategystring'scroll'Scroll detection strategy ('scroll', 'intersection', or 'hybrid')
ariaLabelstring''ARIA label for accessibility
classstring''Additional CSS classes

API Reference

The List component provides the following methods:

Data Management

  • refresh(): Reloads all data and resets the list
  • loadMore(): Manually triggers loading of more items
  • getVisibleItems(): Returns currently visible items
  • getAllItems(): Returns all loaded items
  • isLoading(): Returns whether data is currently loading
  • hasNextPage(): Returns whether more data is available

Item Navigation

  • scrollToItem(itemId, position?): Scrolls to a specific item

- position: 'start' (default), 'center', or 'end'

Selection Management

  • getSelectedItems(): Returns all selected items
  • getSelectedItemIds(): Returns IDs of selected items
  • isItemSelected(itemId): Checks if an item is selected
  • selectItem(itemId): Selects an item
  • deselectItem(itemId): Deselects an item
  • clearSelection(): Clears all selections
  • setSelection(itemIds): Sets selection to specified IDs

Event Handling

  • on(event, handler): Adds an event listener
  • off(event, handler): Removes an event listener

Lifecycle

  • destroy(): Cleans up resources and removes event listeners

Events

The List component emits the following events:

EventDescriptionData
selectFired when an item is selected{ item, element, selectedItems, originalEvent }
loadFired when items are loaded{ loading, hasNext, hasPrev, items, allItems }
scrollFired during scrolling{ originalEvent, component }

Examples

API-Connected List with Custom Item Heights

const messageList = createList({
  collection: 'messages',
  baseUrl: 'https://api.example.com',
  
  // Custom transform function
  transform: (message) => ({
    id: message._id,
    text: message.body,
    sender: message.from,
    timestamp: new Date(message.date)
  }),
  
  // Variable height items
  renderItem: (message, index) => {
    const element = document.createElement('div');
    element.className = 'message-item';
    
    // Create content based on message
    const content = document.createElement('div');
    content.className = 'message-content';
    content.textContent = message.text;
    
    // Add metadata
    const meta = document.createElement('div');
    meta.className = 'message-meta';
    meta.textContent = `${message.sender} - ${message.timestamp.toLocaleString()}`;
    
    element.appendChild(content);
    element.appendChild(meta);
    
    return element;
  },
  
  // Use intersection observer for better performance
  scrollStrategy: 'intersection'
});

// Add to DOM
document.getElementById('message-container').appendChild(messageList.element);

// Handle selection
messageList.on('select', (event) => {
  if (event.item) {
    showMessageDetails(event.item);
  }
});

Custom Item Selection Styling

const productList = createList({
  collection: 'products',
  multiSelect: true,  // Allow multiple selection
  
  renderItem: (product, index) => {
    const element = document.createElement('div');
    element.className = 'product-item';
    // ... create item content
    return element;
  }
});

// Custom CSS for selection
document.head.insertAdjacentHTML('beforeend', `
  <style>
    .product-item {
      transition: all 0.2s ease;
      border-left: 4px solid transparent;
    }
    
    .mtrl-list-item--selected {
      background-color: rgba(0, 123, 255, 0.1);
      border-left: 4px solid #007bff;
    }
  </style>
`);

Core Collection System

The List component is built on top of a flexible collection system that handles data management, API communication, and state tracking.

Collection Class

The Collection class provides a reactive data store with events, filtering, and transformation capabilities.

import { Collection } from 'mtrl';

// Create a collection with transformation
const usersCollection = new Collection({
  transform: (user) => ({
    id: user._id,
    name: user.firstName + ' ' + user.lastName,
    email: user.email
  })
});

// Add items
await usersCollection.add([
  { _id: '1', firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
  { _id: '2', firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' }
]);

// Subscribe to changes
const unsubscribe = usersCollection.subscribe(({ event, data }) => {
  console.log(`Collection event: ${event}`, data);
});

// Apply filtering
usersCollection.query((user) => user.name.includes('John'));

// Get filtered items
const filteredUsers = usersCollection.items;

Collection Events

EventDescription
changeThe collection data has changed
addItems were added to the collection
updateItems were updated in the collection
removeItems were removed from the collection
errorAn error occurred during an operation
loadingLoading state changed

List Manager

The ListManager is a utility that connects collections to UI rendering with virtualization and pagination.

import { createListManager } from 'mtrl';

// Create list manager
const manager = createListManager('users', containerElement, {
  transform: (user) => ({
    id: user._id,
    name: user.name,
    email: user.email
  }),
  
  renderItem: (user, index) => {
    // Create and return DOM element
  },
  
  itemHeight: 60
});

// Load initial data
await manager.loadItems();

// Get the underlying collection
const collection = manager.getCollection();

// Clean up resources
manager.destroy();

Route Adapter

The RouteAdapter provides API communication with automatic error handling, request cancellation, and caching.

import { createRouteAdapter } from 'mtrl';

const api = createRouteAdapter({
  base: 'https://api.example.com',
  endpoints: {
    list: '/users',
    create: '/users',
    update: '/users',
    delete: '/users'
  },
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  cache: true  // Enable caching
});

// Query with pagination
const response = await api.read(
  { status: 'active' },  // Query
  { page: 1, limit: 20 } // Options
);

// Create new items
await api.create([
  { name: 'New User', email: 'new@example.com' }
]);

// Clean up and cancel pending requests
api.disconnect();

Performance Optimizations

The List component incorporates several optimizations for maximum performance:

DOM Element Recycling

The DOM recycling system is a key performance feature that dramatically reduces memory usage and improves rendering speed for large lists. Instead of creating and destroying DOM elements as the user scrolls, the component maintains a pool of reusable elements.

How Element Recycling Works

  • Type-Based Pools: The recycling system maintains separate pools for different types of list items, allowing specialized recycling:

// Internal recycling pool structure
const recyclePool = new Map<string, HTMLElement[]>();

// When an item goes out of view, it's added to the appropriate pool
const recycleElement = (element: HTMLElement): void => {
  // Get item type from data attribute or use default
  const itemType = element.dataset.itemType || 'default';
  
  // Initialize pool for this type if needed
  if (!recyclePool.has(itemType)) {
    recyclePool.set(itemType, []);
  }
  
  // Prepare element for recycling
  element.style.display = 'none';
  element.style.top = '-9999px';
  
  // Add to appropriate pool
  recyclePool.get(itemType)!.push(element);
};

  • Smart Reuse Strategy: The system makes intelligent decisions about when to recycle elements:

// Intelligent recycling decisions
const recycleElement = (element: HTMLElement, forceRecycle = false): void => {
  if (!element) return;
  
  // Skip recycling for small elements unless forced
  // This avoids unnecessary overhead for simple elements
  if (!forceRecycle && element.innerHTML.length < 100) return;
  
  // Proceed with recycling...
}

  • Recycled Element Retrieval: When new items need to be rendered, the system first looks for a recycled element:

// Get a recycled element of the appropriate type
const getRecycledElement = (item: any): HTMLElement | null => {
  // Get type info from item or use 'default'
  const itemType = item.type || 'default';
  
  if (!recyclePool.has(itemType)) {
    recyclePool.set(itemType, []);
    return null;
  }
  
  const pool = recyclePool.get(itemType)!;
  return pool.length > 0 ? pool.pop()! : null;
};

  • Render with Recycling: The rendering function first checks for recyclable elements:

const wrappedRenderItem = (item: any, index: number): HTMLElement => {
  // Check for recycled element first
  const recycled = getRecycledElement(item);
  
  // Pass the recycled element to the user's render function
  const element = renderItem(item, index, recycled);
  
  // If rendering failed, provide a fallback
  if (!element) {
    console.warn('renderItem returned null or undefined for item', item);
    const placeholder = document.createElement('div');
    placeholder.style.height = `${listConfig.itemHeight}px`;
    return placeholder;
  }
  
  // Apply data attributes for future recycling
  if (item.id && !element.hasAttribute('data-id')) {
    element.setAttribute('data-id', item.id);
  }
  
  if (item.type) {
    element.dataset.itemType = item.type;
  }
  
  return element;
};

  • User Integration: The component passes recycled elements to the user's render function:

// User-defined render function receives recycled elements
renderItem: (item, index, recycledElement) => {
  // User can choose to reuse or ignore the recycled element
  if (recycledElement) {
    // Update content of recycled element
    recycledElement.querySelector('.title').textContent = item.title;
    recycledElement.querySelector('.description').textContent = item.description;
    return recycledElement;
  } else {
    // Create new element if none recycled
    const element = document.createElement('div');
    element.innerHTML = `<div class="title">${item.title}</div>
                         <div class="description">${item.description}</div>`;
    return element;
  }
}

  • Cleanup on Destruction: All recycled elements are properly cleaned up when the component is destroyed:

destroy: () => {
  // Regular cleanup...
  
  // Empty recycling pools
  recyclePool.forEach(pool => pool.length = 0);
  recyclePool.clear();
}

Performance Benefits

The recycling system provides several key advantages:

  • Reduced Memory Usage: By reusing DOM elements, the memory footprint stays consistent regardless of list size
  • Less Garbage Collection: Fewer elements created/destroyed means fewer garbage collection pauses
  • Improved Rendering Speed: Updating existing elements is faster than creating new ones
  • Smoother Scrolling: Reduced CPU/GPU work during scrolling leads to better frame rates
  • Lower Battery Usage: Less work for the browser means better energy efficiency on mobile devices

Comparison with Traditional Virtual Scrolling

In benchmarks with 10,000 items, the recycling system showed significant improvements:

MetricWithout RecyclingWith RecyclingImprovement
Memory Usage~60MB~12MB80% reduction
DOM NodesFluctuatingConstantMore stable
Scroll JankCommonRareSmoother experience
CPU Usage During Scroll35-40%8-10%75% reduction

This makes the component especially suitable for mobile devices and performance-critical applications.

Optimized Scrolling

The component offers three scroll detection strategies:

  • Traditional Scroll Events ('scroll'): Compatible with all browsers
  • IntersectionObserver ('intersection'): More efficient, reduces main thread work
  • Hybrid ('hybrid'): Uses both methods for testing/comparison

The IntersectionObserver approach uses sentinel elements to detect when more content is needed, resulting in less computation during scrolling:

// Simplified intersection observer setup
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.target === bottomSentinel && entry.isIntersecting) {
      loadMore();
    }
  });
}, options);

observer.observe(topSentinel);
observer.observe(bottomSentinel);

Batched DOM Updates

DOM changes are batched using DocumentFragment for minimal reflow/repaint cycles:

// Create document fragment for batch updates
const fragment = document.createDocumentFragment();

// Add items to fragment
visibleItems.forEach(item => {
  const element = createElement(item);
  fragment.appendChild(element);
});

// Single DOM update
container.appendChild(fragment);

Efficient Height Calculation

For large lists, the component uses sampling and estimation to avoid expensive calculations:

// For very large lists, estimate based on samples
if (itemCount > 1000) {
  const sampleItems = [
    ...items.slice(0, 100),
    ...items.slice(Math.floor(itemCount / 2) - 50, Math.floor(itemCount / 2) + 50),
    ...items.slice(Math.max(0, itemCount - 100))
  ];
  
  const sampleHeight = sampleItems.reduce((sum, item) => sum + getItemHeight(item), 0);
  const averageHeight = sampleHeight / sampleItems.length;
  
  return itemCount * averageHeight;
}

Fast Path for Fixed-Height Items

When all items have the same height, the component uses direct mathematical calculation instead of iteration:

// Optimized range calculation for fixed height
const startIndex = Math.floor(scrollTop / itemHeight);
const visibleCount = Math.ceil(containerHeight / itemHeight);
const endIndex = startIndex + visibleCount;

Browser Support

The List component supports all modern browsers including:

  • Chrome 60+
  • Firefox 55+
  • Safari 11+
  • Edge 16+

For older browsers, the component automatically falls back to compatible methods:

  • IntersectionObserver → scroll events
  • ResizeObserver → window resize events
  • Passive event listeners → standard event listeners

Advanced Usage

Leveraging Element Recycling

To maximize the benefits of element recycling, you can implement your renderItem function to efficiently reuse elements:

const documentList = createList({
  collection: 'documents',
  
  renderItem: (doc, index, recycledElement) => {
    // Check if we have a recycled element
    if (recycledElement) {
      // Just update the content rather than creating new elements
      const title = recycledElement.querySelector('.doc-title');
      const date = recycledElement.querySelector('.doc-date');
      const icon = recycledElement.querySelector('.doc-icon');
      
      // Update text content (faster than innerHTML)
      title.textContent = doc.title;
      date.textContent = new Date(doc.modified).toLocaleDateString();
      
      // Update icon only if needed
      if (icon.dataset.type !== doc.type) {
        icon.className = `doc-icon doc-icon--${doc.type}`;
        icon.dataset.type = doc.type;
      }
      
      // Add element type for recycling system
      recycledElement.dataset.itemType = doc.type;
      
      return recycledElement;
    }
    
    // Create new element if nothing to recycle
    const element = document.createElement('div');
    element.className = 'doc-item';
    element.dataset.itemType = doc.type; // Help recycling system
    
    // Create internal structure
    element.innerHTML = `
      <div class="doc-icon doc-icon--${doc.type}" data-type="${doc.type}"></div>
      <div class="doc-title">${doc.title}</div>
      <div class="doc-date">${new Date(doc.modified).toLocaleDateString()}</div>
    `;
    
    return element;
  }
});

Custom Item Measurement

For complex layout situations where item height depends on content or styling:

const complexList = createList({
  items: largeDataset,
  renderItem: (item, index) => {
    // Create complex layout
    return element;
  }
});

// After custom layout changes
window.addEventListener('resize', () => {
  // Measure items and update heights
  const heights = {};
  
  document.querySelectorAll('.complex-item').forEach(el => {
    const id = el.dataset.id;
    heights[id] = el.offsetHeight;
  });
  
  complexList.setItemHeights(heights);
});

Integrating with Other Components

The List component can be combined with other UI components:

// Create searchable, sortable list
const searchInput = document.getElementById('search');
const sortSelect = document.getElementById('sort');

const productList = createList({
  collection: 'products',
  renderItem: // ...
});

// Filter by search
searchInput.addEventListener('input', (e) => {
  const term = e.target.value.toLowerCase();
  
  // Get underlying collection
  const collection = productList.getCollection();
  
  // Apply query filter
  collection.query(item => 
    item.name.toLowerCase().includes(term) || 
    item.description.toLowerCase().includes(term)
  );
});

// Change sort order
sortSelect.addEventListener('change', (e) => {
  const field = e.target.value;
  const collection = productList.getCollection();
  
  if (field === 'price-low') {
    collection.sort((a, b) => a.price - b.price);
  } else if (field === 'price-high') {
    collection.sort((a, b) => b.price - a.price);
  } else if (field === 'name') {
    collection.sort((a, b) => a.name.localeCompare(b.name));
  }
});

Custom Transform Functions

When working with specific data types, you can use predefined transforms:

import { createList, transforms } from 'mtrl';

// Create list with predefined transform
const trackList = createList({
  collection: 'tracks',
  transform: transforms.track, // Predefined transform
  renderItem: (track, index) => {
    const element = document.createElement('div');
    element.innerHTML = `
      <div class="track-title">${track.headline}</div>
      <div class="track-artist">${track.supportingText}</div>
      <div class="track-year">${track.meta}</div>
    `;
    return element;
  }
});

Available transforms:

  • transforms.track: For music tracks/songs
  • transforms.playlist: For music playlists
  • transforms.country: For country data

Performance Testing and Optimization

You can benchmark different scroll strategies:

// Create lists with different strategies
const scrollList = createList({
  items: generateLargeDataset(10000),
  scrollStrategy: 'scroll',
  renderItem: (item, index) => { /* ... */ }
});

const observerList = createList({
  items: generateLargeDataset(10000),
  scrollStrategy: 'intersection',
  renderItem: (item, index) => { /* ... */ }
});

// Add to DOM for testing
document.getElementById('scroll-container').appendChild(scrollList.element);
document.getElementById('observer-container').appendChild(observerList.element);

// Run performance tests and compare metrics
// See browser performance tools for results


CSS Customization

The List component uses these CSS classes that you can customize:

/* Main list container */
.mtrl-list {
  position: relative;
  overflow-y: auto;
  height: 100%;
}

/* List item */
.mtrl-list-item {
  position: relative;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

/* Selected item state */
.mtrl-list-item--selected {
  background-color: rgba(0, 0, 0, 0.08);
}

/* Empty state message */
.mtrl-list-empty {
  padding: 20px;
  text-align: center;
  color: #666;
}

/* Loading indicator */
.mtrl-list-loading {
  text-align: center;
  padding: 10px;
}