Virtual scroller in Vue.js

by Edward

HTML

<script src="https://unpkg.com/vue/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>


<div id="app">
  <virtual-scroller :items="items" :renderers="renderers" item-height="42"></virtual-scroller>
</div>

CSS

body {
  font-family: sans-serif;
}

.virtual-scroller {
  overflow: auto;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.item-container {
  box-sizing: border-box;
}

.item {
  height: 42px;
  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;
}

JavaScript

function getData(count) {
  const raw = {}

  const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split('')

  for(var l of alphabet) {
    raw[l] = []
  }

  for(var i = 0; i < count; i++) {
    const item = {
      name: faker.name.findName(),
    }
    const letter = item.name.charAt(0).toLowerCase()
    raw[letter].push(item)
  }

  const data = []
	let index = 0
  
  for(var l of alphabet) {
    raw[l] = _.sortBy(raw[l], 'name')
    data.push({
    	index: index++,
      type: 'letter',
      value: l,
    })
    for(var item of raw[l]) {
      data.push({
      	index: index++,
        type: 'person',
        value: item,
        sex: 'f',
      })
    }
  }

  return data;
}

Vue.component('virtual-scroller', {
	props: ['items', 'renderers', 'itemHeight'],
  template: `<div class="virtual-scroller" @scroll="updateVisibleItems">
  	<div class="item-container" :style="containerStyle">
      <div class="items">
        <div class="item" v-for="item in visibleItems" :key="item.index">
          <component :is="renderers[item.type]" :item="item"></component>
        </div>
      </div>
    </div>
  </div>`,
  data: () => ({
  	visibleItems: [],
    containerStyle: null,
    itemsStyle: null,
  }),
  methods: {
  	updateVisibleItems() {
      const el = this.$el;
      const scroll = {
        top: el.scrollTop,
        bottom: el.scrollTop + el.clientHeight,
      }
      const startIndex = Math.floor(scroll.top/this.itemHeight)
      const endIndex = Math.ceil(scroll.bottom/this.itemHeight)
      this.visibleItems = this.items.slice(startIndex, endIndex)
      this.containerStyle = {
        paddingTop: startIndex * this.itemHeight + 'px',
        height: this.items.length * this.itemHeight + 'px',
      }
      console.log(items.length, startIndex, endIndex)
    },
  },
  mounted() {
    this._lastUpdate = new Date().getTime()
  	this.updateVisibleItems()
  },
  updated() {
  	const time = new Date().getTime()
    console.log('updated', time - this._lastUpdate)
   ...