Virtual scroller
by Christopher O
HTML
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/lodash/4.16.6/lodash.min.js"></script>
<script src="https://cdn.rawgit.com/Marak/faker.js/master/examples/browser/js/faker.js"></script>
<!-- https://github.com/marak/Faker.js/ -->
<div id="app">
<virtual-scroller :items="items" :renderers="renderers" item-height="22"></virtual-scroller>
</div>
CSS
body {
font-family: sans-serif;
}
.virtual-scroller {
overflow: auto;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
height: 90%;
width: 250px;
border: 1px solid blue;
}
.item-container {
box-sizing: border-box;
}
.item {
height: 40px; /* 22 */
padding: 12px;
box-sizing: border-box;
cursor: pointer;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
}
.letter {
text-transform: uppercase;
color: grey;
}
.person:nth-of-type(odd) {
background: #e0e0e0;
}
JavaScript
console.clear();
Vue.component('virtual-scroller', {
props: ['items', 'renderers', 'itemHeight'],
template: `<div class="virtual-scroller" @scroll="updateVisibleItems">
<div class="item-container" ref="container">
<div class="items">
<component class="item" v-for="item in visibleItems" :is="renderers[item.type]" :item="item"></component>
</div>
</div>
</div>`,
data: () => ({
visibleItems: [],
containerStyle: null,
}),
watch: {
items(val) {
this.l = this.items.length
this.updateVisibleItems()
}
},
methods: {
updateVisibleItems() {
const el = this.$el;
const scrollTop = el.scrollTop;
const scrollBottom = scrollTop + el.clientHeight;
let startIndex = Math.floor(scrollTop/this.itemHeight) - 3
if(startIndex < 0) {
startIndex = 0
}
let endIndex = Math.ceil(scrollBottom/this.itemHeight) + 3
if(endIndex >= this.l) {
endIndex = this.l
}
const container = this.$refs.container
container.style.paddingTop = startIndex * this.itemHeight + 'px'
container.style.height = this.l * this.itemHeight + 'px'
this.visibleItems = this.items.slice(startIndex, endIndex)
this.$forceUpdate()
},
},
created() {
this.l = this.items.length
},
mounted() {
this.updateVisibleItems()
},
})
const renderers = {
letter: {
props: ['item'],
template: `<div class="letter">({{item.index}}) {{item.value}}</div>`,
},
person: {
props: ['item'],
template: `<div class="person" @click="edit">({{item.index}}) {{item.value.name}}</div>`,
methods: {
edit() {
this.item.value.name += '*'
},
},
},
}
const count = 100000
console.log('Generating ' + count + ' items...')
let time = new Date().getTime(), time2
const items = Object.freeze(getData(count))
console.log('Generated ' + items.length + ' in ' + ((time2 = new Date().getTime()) - time))
new Vue({
data: () => ({
items,
...