Basic List
Multi Select List
API Users List
API Users List (cursor mode)
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:
Option | Type | Default | Description |
---|---|---|---|
collection | string | 'items' | Collection name for API-connected lists |
baseUrl | string | 'http://localhost:4000/api' | Base URL for API requests |
renderItem | Function | Required | Function that renders each item |
items | Array | [] | Static items for non-API lists |
itemHeight | number | 48 | Default height for items in pixels |
pageSize | number | 20 | Number of items to load per page |
renderBufferSize | number | 5 | Extra items to render above/below viewport |
overscanCount | number | 3 | Extra items to keep in DOM but invisible |
loadThreshold | number | 0.8 | Load more when scrolled past this fraction |
throttleMs | number | 16 | Throttle scroll event (ms) |
dedupeItems | boolean | true | Remove duplicate items based on ID |
trackSelection | boolean | true | Track item selection state |
multiSelect | boolean | false | Allow multiple items to be selected |
initialSelection | string[] | [] | Initially selected item IDs |
scrollStrategy | string | 'scroll' | Scroll detection strategy ('scroll' , 'intersection' , or 'hybrid' ) |
ariaLabel | string | '' | ARIA label for accessibility |
class | string | '' | Additional CSS classes |
API Reference
The List component provides the following methods:
Data Management
refresh()
: Reloads all data and resets the listloadMore()
: Manually triggers loading of more itemsgetVisibleItems()
: Returns currently visible itemsgetAllItems()
: Returns all loaded itemsisLoading()
: Returns whether data is currently loadinghasNextPage()
: 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 itemsgetSelectedItemIds()
: Returns IDs of selected itemsisItemSelected(itemId)
: Checks if an item is selectedselectItem(itemId)
: Selects an itemdeselectItem(itemId)
: Deselects an itemclearSelection()
: Clears all selectionssetSelection(itemIds)
: Sets selection to specified IDs
Event Handling
on(event, handler)
: Adds an event listeneroff(event, handler)
: Removes an event listener
Lifecycle
destroy()
: Cleans up resources and removes event listeners
Events
The List component emits the following events:
Event | Description | Data |
---|---|---|
select | Fired when an item is selected | { item, element, selectedItems, originalEvent } |
load | Fired when items are loaded | { loading, hasNext, hasPrev, items, allItems } |
scroll | Fired 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
Event | Description |
---|---|
change | The collection data has changed |
add | Items were added to the collection |
update | Items were updated in the collection |
remove | Items were removed from the collection |
error | An error occurred during an operation |
loading | Loading 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:
Metric | Without Recycling | With Recycling | Improvement |
---|---|---|---|
Memory Usage | ~60MB | ~12MB | 80% reduction |
DOM Nodes | Fluctuating | Constant | More stable |
Scroll Jank | Common | Rare | Smoother experience |
CPU Usage During Scroll | 35-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/songstransforms.playlist
: For music playliststransforms.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;
}