core
Source
// Basic List — Vue implementation
// Interactive control panel demonstrating core vlist API: item count, overscan,
// scrollToIndex, and data operations (append, prepend, remove).
import { createApp, ref, computed, watch, onMounted, onUnmounted } from "vue";
import { vlist, withSelection } from "vlist";
import {
DEFAULT_COUNT,
ITEM_HEIGHT,
makeUser,
makeUsers,
itemTemplate,
} from "../shared.js";
// =============================================================================
// App Component
// =============================================================================
const App = {
setup() {
// State
const users = ref(makeUsers(DEFAULT_COUNT));
const nextId = ref(DEFAULT_COUNT + 1);
const overscan = ref(3);
const scrollIndex = ref(0);
const scrollAlign = ref("start");
const selectedIndex = ref(-1);
const stats = ref({ dom: 0, total: DEFAULT_COUNT });
// Refs
const containerRef = ref(null);
let instance = null;
// Create vlist instance
function createListInstance() {
if (!containerRef.value) return;
if (instance) {
instance.destroy();
instance = null;
}
instance = vlist({
container: containerRef.value,
ariaLabel: "User list",
overscan: overscan.value,
items: users.value,
item: {
height: ITEM_HEIGHT,
template: itemTemplate,
},
})
.use(withSelection({ mode: 'single' }))
.build();
instance.on('selection:change', ({ selected }) => {
selectedIndex.value = selected.length > 0 ? selected[0] : -1;
});
instance.on("range:change", ({ range }) => {
stats.value = {
dom: range.end - range.start + 1,
total: users.value.length,
};
});
instance.on("scroll", () => {
const domNodes =
containerRef.value?.querySelectorAll(".vlist-item").length ?? 0;
stats.value = { ...stats.value, dom: domNodes };
});
}
// Watch overscan to recreate
watch(overscan, () => {
createListInstance();
});
// Watch container ref
watch(containerRef, (newVal) => {
if (newVal) createListInstance();
});
// Cleanup
onUnmounted(() => {
if (instance) {
instance.destroy();
instance = null;
}
});
// Computed
const memorySaved = computed(() => {
return stats.value.total > 0
? Math.round((1 - stats.value.dom / stats.value.total) * 100)
: 0;
});
const visiblePercent = computed(() => {
return stats.value.total > 0
? Math.round((stats.value.dom / stats.value.total) * 100)
: 0;
});
// Navigation
const handleGoToIndex = () => {
const clamped = Math.max(
0,
Math.min(scrollIndex.value, users.value.length - 1),
);
instance?.scrollToIndex(clamped, {
align: scrollAlign.value,
behavior: "smooth",
duration: 400,
});
};
const scrollToFirst = () => {
instance?.scrollToIndex(0, { behavior: "smooth", duration: 300 });
};
const scrollToMiddle = () => {
instance?.scrollToIndex(Math.floor(users.value.length / 2), {
align: "center",
behavior: "smooth",
duration: 500,
});
};
const scrollToLast = () => {
instance?.scrollToIndex(users.value.length - 1, {
align: "end",
behavior: "smooth",
duration: 500,
});
};
// Count slider
const handleCountChange = (e) => {
const count = parseInt(e.target.value, 10);
users.value = makeUsers(count);
nextId.value = count + 1;
stats.value = { ...stats.value, total: count };
createListInstance();
};
// Overscan slider
const handleOverscanChange = (e) => {
overscan.value = parseInt(e.target.value, 10);
};
// Data operations
const handleAppend = () => {
const newUser = makeUser(nextId.value);
nextId.value++;
users.value = [...users.value, newUser];
instance?.appendItems([newUser]);
stats.value = { ...stats.value, total: users.value.length };
};
const handlePrepend = () => {
const newUser = makeUser(nextId.value);
nextId.value++;
users.value = [newUser, ...users.value];
instance?.prependItems([newUser]);
stats.value = { ...stats.value, total: users.value.length };
};
const handleAppend100 = () => {
const batch = makeUsers(100, nextId.value);
nextId.value += 100;
users.value = [...users.value, ...batch];
instance?.appendItems(batch);
stats.value = { ...stats.value, total: users.value.length };
};
const handleRemove = () => {
if (users.value.length === 0) return;
const idx = selectedIndex.value >= 0 && selectedIndex.value < users.value.length
? selectedIndex.value
: users.value.length - 1;
users.value = users.value.filter((_, i) => i !== idx);
instance?.clearSelection();
selectedIndex.value = -1;
instance?.setItems(users.value);
stats.value = { ...stats.value, total: users.value.length };
};
const handleClear = () => {
users.value = [];
instance?.setItems([]);
stats.value = { dom: 0, total: 0 };
};
const handleReset = () => {
users.value = makeUsers(DEFAULT_COUNT);
nextId.value = DEFAULT_COUNT + 1;
overscan.value = 3;
stats.value = { ...stats.value, total: DEFAULT_COUNT };
createListInstance();
};
return {
containerRef,
users,
overscan,
scrollIndex,
scrollAlign,
stats,
memorySaved,
visiblePercent,
ITEM_HEIGHT,
DEFAULT_COUNT,
handleGoToIndex,
scrollToFirst,
scrollToMiddle,
scrollToLast,
handleCountChange,
handleOverscanChange,
handleAppend,
handlePrepend,
handleAppend100,
handleRemove,
handleClear,
handleReset,
selectedIndex,
};
},
template: `
<div class="container">
<header>
<h1>Basic List</h1>
<p class="description">
Vue implementation — the core of <code>@floor/vlist</code>.
Use the control panel to explore item count, overscan, scroll-to,
and data operations in real time.
</p>
</header>
<div class="split-layout">
<div class="split-main">
<div ref="containerRef" id="list-container"></div>
</div>
<aside class="split-panel">
<!-- Items -->
<section class="panel-section">
<h3 class="panel-title">Items</h3>
<div class="panel-row">
<label class="panel-label">Count</label>
<span class="panel-value">{{ users.length.toLocaleString() }}</span>
</div>
<div class="panel-row">
<input
type="range"
class="panel-slider"
min="100"
max="100000"
step="100"
:value="Math.min(users.length, 100000)"
@change="handleCountChange"
/>
</div>
<div class="panel-row">
<label class="panel-label">Overscan</label>
<span class="panel-value">{{ overscan }}</span>
</div>
<div class="panel-row">
<input
type="range"
class="panel-slider"
min="0"
max="10"
step="1"
:value="overscan"
@change="handleOverscanChange"
/>
</div>
</section>
<!-- Scroll To -->
<section class="panel-section">
<h3 class="panel-title">Scroll To</h3>
<div class="panel-row">
<div class="panel-input-group">
<input
type="number"
class="panel-input"
placeholder="Index"
min="0"
v-model.number="scrollIndex"
@keydown.enter.prevent="handleGoToIndex"
/>
<select class="panel-select" v-model="scrollAlign">
<option value="start">start</option>
<option value="center">center</option>
<option value="end">end</option>
</select>
<button class="panel-btn" @click="handleGoToIndex">Go</button>
</div>
</div>
<div class="panel-row">
<div class="panel-btn-group">
<button class="panel-btn panel-btn--icon" title="First" @click="scrollToFirst">
<i class="icon icon--up"></i>
</button>
<button class="panel-btn panel-btn--icon" title="Middle" @click="scrollToMiddle">
<i class="icon icon--center"></i>
</button>
<button class="panel-btn panel-btn--icon" title="Last" @click="scrollToLast">
<i class="icon icon--down"></i>
</button>
</div>
</div>
</section>
<!-- Data Operations -->
<section class="panel-section">
<h3 class="panel-title">Data</h3>
<div class="panel-row">
<div class="panel-btn-group">
<button class="panel-btn" title="Prepend 1 item" @click="handlePrepend">
<i class="icon icon--add"></i> Prepend
</button>
<button class="panel-btn" title="Append 1 item" @click="handleAppend">
<i class="icon icon--add"></i> Append
</button>
<button class="panel-btn" title="Append 100 items" @click="handleAppend100">
<i class="icon icon--add"></i> +100
</button>
</div>
</div>
<div class="panel-row">
<div class="panel-btn-group">
<button class="panel-btn" title="Remove last item" @click="handleRemove">
<i class="icon icon--remove"></i> Remove
</button>
<button class="panel-btn" title="Clear all items" @click="handleClear">
<i class="icon icon--trash"></i> Clear
</button>
<button class="panel-btn" title="Reset to 10,000 items" @click="handleReset">
<i class="icon icon--shuffle"></i> Reset
</button>
</div>
</div>
</section>
</aside>
</div>
<footer class="example-footer" id="example-footer">
<div class="example-footer__left">
<span class="example-footer__stat">
<strong>{{ visiblePercent }}%</strong>
</span>
<span class="example-footer__stat">
{{ stats.dom }} / <strong>{{ stats.total.toLocaleString() }}</strong>
<span class="example-footer__unit"> items</span>
</span>
<span class="example-footer__stat">
<strong>{{ memorySaved }}%</strong>
<span class="example-footer__unit"> saved</span>
</span>
</div>
<div class="example-footer__right">
<span class="example-footer__stat">
height <strong>{{ ITEM_HEIGHT }}</strong><span class="example-footer__unit">px</span>
</span>
<span class="example-footer__stat">
overscan <strong>{{ overscan }}</strong>
</span>
</div>
</footer>
</div>
`,
};
// =============================================================================
// Mount
// =============================================================================
createApp(App).mount("#vue-root");
<div id="vue-root"></div>
// Shared data and utilities for basic list example variants
// This file is imported by all framework implementations to avoid duplication
import { makeUser, makeUsers } from '../../src/data/people.js';
// =============================================================================
// Constants
// =============================================================================
export const DEFAULT_COUNT = 10_000;
export const ITEM_HEIGHT = 56;
// =============================================================================
// Data Generation (re-export for convenience)
// =============================================================================
export { makeUser, makeUsers };
// =============================================================================
// Templates
// =============================================================================
export const itemTemplate = (user, i) => `
<div class="item__avatar" style="background:${user.color}">${user.initials}</div>
<div class="item__text">
<div class="item__name">${user.name}</div>
<div class="item__email">${user.email}</div>
</div>
<span class="item__index">#${i + 1}</span>
`;
/* Basic List — example styles */
/* ============================================================================
Item (styles live on .vlist-item — no wrapper div needed)
============================================================================ */
.vlist-item {
display: flex;
align-items: center;
gap: 12px;
padding: 0 16px;
}
.item__avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 15px;
flex-shrink: 0;
}
.item__text {
flex: 1;
min-width: 0;
}
.item__name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item__email {
font-size: 13px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item__index {
font-size: 12px;
color: var(--text-muted);
min-width: 48px;
text-align: right;
font-variant-numeric: tabular-nums;
}
/* ============================================================================
Selected state
============================================================================ */