How to Add Virtual Scrolling to a Vue App
Tags: javascript, webdev, tutorial, vue
Published: 2020-01-21 00:46:26

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

**Follow me on Twitter at https://twitter.com/AuMayeung**

**Many more articles at https://medium.com/@hohanga**

To display large amount of data in your app, loading everything at once is not a good solution. Loading a big list is taxing on the user’s computer’s resources. Therefore, we need a better solution. The most efficient solution is to load a small amount of data at a time. Only whatever is displayed on the screen should be loaded. This solution is called virtual scrolling.

With Vue.js, we can use the vue-virtual-scroll-list package located at [https://www.npmjs.com/package/vue-virtual-scroll-list](https://www.npmjs.com/package/vue-virtual-scroll-list) to add virtual scrolling to our Vue.js apps. It is one of the easiest package to use for this purpose.

In this article, we will make an app that lets us generate a large amount of fake data and display them in a virtual scrolling list. It will ask how many entries the user wants to create and then create it when the user submits the number.

To get started, we create the Vue.js project with the Vue CLI. We run `npx @vue/cli create data-generator` to create the app. In the wizard, we select ‘Manually select features’, then choose to include Babel and Vue-Router.

Next, we need to install some packages. We need BootstrapVue for styling, Faker for creating the fake data, Vee-Validate for validating user input, and Vue-Virtual-Scroll-List for displaying the list of items in a virtual scrolling list. We install all of them by running:

```bash
npm i bootstrap-vue faker vee-validate vue-virtual-scrolling-list
```

After we install the packages, we add our pages. First, we create `Address.vue` in the `views` folder and add:

```javascript
<template>
  <div class="page">
    <h1 class="text-center">Generate Addresses</h1>
    <ValidationObserver ref="observer" v-slot="{ invalid }">
      <b-form @submit.prevent="onSubmit" novalidate>
        <b-form-group label="Number" label-for="number">
          <ValidationProvider
            name="number"
            rules="required|min_value:1|max_value:100000"
            v-slot="{ errors }"
          >
            <b-form-input
              :state="errors.length == 0"
              v-model="form.number"
              type="text"
              required
              placeholder="Number"
              name="number"
            ></b-form-input>
            <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
          </ValidationProvider>
        </b-form-group>
        <b-button type="submit" variant="primary">Generate</b-button>
      </b-form>
    </ValidationObserver>
<br />
<h2>Addresses</h2>
<virtual-list :size="itemHeight" :remain="3">
      <div v-for="(item, index) of list" :key="index" class="result-row">
        <div class="index">{{index + 1}}</div>
        <div class="column">{{item.streetAddress}}</div>
        <div class="column">{{item.streetName}}</div>
        <div class="column">{{item.city}}</div>
        <div class="column">{{item.county}}</div>
        <div class="column">{{item.state}}</div>
        <div class="column">{{item.country}}</div>
        <div class="column">{{item.zipCode}}</div>
      </div>
    </virtual-list>
  </div>
</template>
<script>
const faker = require("faker");
import virtualList from "vue-virtual-scroll-list";
export default {
  name: "home",
  data() {
    return {
      form: {},
      list: [],
      itemHeight: 80
    };
  },
  components: { "virtual-list": virtualList },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      this.list = Array.from({ length: this.form.number }).map((l, i) => {
        return {
          city: faker.address.city(),
          streetName: faker.address.streetName(),
          streetAddress: faker.address.streetAddress(),
          county: faker.address.county(),
          state: faker.address.state(),
          country: faker.address.country(),
          zipCode: faker.address.zipCode()
        };
      });
    }
  }
};
</script>
<style scoped>
.column {
  padding-right: 20px;
  width: calc(80vw / 7);
  overflow: hidden;
  text-overflow: ellipsis;
}
.result-row {
  height: 80px;
}
</style>
```

In this page, we let users generate fake addresses by letting them enter a number from 1 to 100000 and then once the user enters the number, `onSubmit` is called to generate the items. The Faker library is used for generating the items. Form validation is done by wrapping the form in the `ValidationObserver` component and wrapping the input in the `ValidationProvider` component. We provide the rule for validation in the `rules` prop of `ValidationProvider` . The rules will be added in `main.js` later.

The error messages are displayed in the `b-form-invalid-feedback` component. We get the errors from the scoped slot in `ValidationProvider` . It’s where we get the `errors` object from.

When the user submits the number, the `onSubmit` function is called. This is where the `ValidationObserver` becomes useful as it provides us with the `this.$refs.observer.validate()` function to check for form validity.

If `isValid` resolves to `true` , then we generate the list by using the `Array.from` method to map generate an array with the length the user entered (`this.form.number` ), and then map each entry to the fake address rows.

We add the `virtual-list` component from Vue-Virtual-Scroll-List in the `script` section so we can use it in our template. The items are in the `virtual-list` component so that we only show a few at a time. The `remain` prop is where we specify the number of items to show on the screen at a time. The `size` prop is for setting each row’s height.

Next in `Home.vue` , we replace the existing code with:

```javascript
<template>
  <div class="page">
    <h1 class="text-center">Generate Emails</h1>
    <ValidationObserver ref="observer" v-slot="{ invalid }">
      <b-form @submit.prevent="onSubmit" novalidate>
        <b-form-group label="Number" label-for="number">
          <ValidationProvider
            name="number"
            rules="required|min_value:1|max_value:100000"
            v-slot="{ errors }"
          >
            <b-form-input
              :state="errors.length == 0"
              v-model="form.number"
              type="text"
              required
              placeholder="Number"
              name="number"
            ></b-form-input>
            <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
          </ValidationProvider>
        </b-form-group>
        <b-button type="submit" variant="primary">Generate</b-button>
      </b-form>
    </ValidationObserver>
    <br />
    <h2>Emails</h2>
    <virtual-list :size="itemHeight" :remain="30">
      <div v-for="(item, index) of list" :key="index" class="result-row">
        <div class="index">{{index + 1}}</div>
        <div>{{item}}</div>
      </div>
    </virtual-list>
  </div>
</template>
<script>
const faker = require("faker");
import virtualList from "vue-virtual-scroll-list";
export default {
  name: "home",
  data() {
    return {
      form: {},
      list: [],
      itemHeight: 30
    };
  },
  components: { "virtual-list": virtualList },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      this.list = Array.from({ length: this.form.number }).map((l, i) => {
        return faker.internet.email();
      });
    }
  }
};
</script>
```

It works very similarly to `Address.vue`, except that we are generating emails instead of addresses.

Next create a `Name.vue` file in the `views` folder and add:

```javascript
<template>
  <div class="page">
    <h1 class="text-center">Generate Names</h1>
    <ValidationObserver ref="observer" v-slot="{ invalid }">
      <b-form @submit.prevent="onSubmit" novalidate>
        <b-form-group label="Number" label-for="number">
          <ValidationProvider
            name="number"
            rules="required|min_value:1|max_value:100000"
            v-slot="{ errors }"
          >
            <b-form-input
              :state="errors.length == 0"
              v-model="form.number"
              type="text"
              required
              placeholder="Number"
              name="number"
            ></b-form-input>
            <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
          </ValidationProvider>
        </b-form-group>
        <b-button type="submit" variant="primary">Generate</b-button>
      </b-form>
    </ValidationObserver>
    <br />
    <h2>Names</h2>
    <virtual-list :size="itemHeight" :remain="30">
      <div v-for="(item, index) of list" :key="index" class="result-row">
        <div class="index">{{index + 1}}</div>
        <div>{{item.firstName}} {{item.lastName}}</div>
      </div>
    </virtual-list>
  </div>
</template>
<script>
const faker = require("faker");
import virtualList from "vue-virtual-scroll-list";
export default {
  name: "home",
  data() {
    return {
      form: {},
      list: [],
      itemHeight: 30
    };
  },
  components: { "virtual-list": virtualList },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      this.list = Array.from({ length: this.form.number }).map((l, i) => {
        return {
          firstName: faker.name.firstName(),
          lastName: faker.name.lastName()
        };
      });
    }
  }
};
</script>
```

We generate fake first and last names in this file after the user enters the number of items they want.

Then in `App.vue` , replace the existing code with:

```javascript
<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand to="/">Data Generator</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path  == '/'">Home</b-nav-item>
          <b-nav-item to="/name" :active="path  == '/name'">Name</b-nav-item>
          <b-nav-item to="/address" :active="path  == '/address'">Address</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>
<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>
<style lang="scss">
.page {
  padding: 20px;
}
.result-row {
  display: flex;
  height: calc(50vh / 10);
}
.index {
  padding-right: 20px;
  min-width: 100px;
}
</style>
```

to add our BootstrapVue navigation bar with the links to our pages. In the top bar, we set the `active` prop for the links so that we highlight the link of the current page that is displayed. In the `scripts` section, we watch the `$route` object provided by Vue Router for the current path of the app and assign it to `this.path` so that we can use it to set the `active` prop.

Next in `main.js` , we replace the existing code with:

```javascript
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
import { min_value } from "vee-validate/dist/rules";
import { max_value } from "vee-validate/dist/rules";
extend("required", required);
extend("min_value", min_value);
extend("max_value", max_value);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(BootstrapVue);
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");
```

We added the validation rules that we used in the previous files here, as well as included all the libraries we use in the app. We registered `ValidationProvider` and `ValidationObserver` by calling `Vue.component` so that we can use them in our components. The validation rules provided by Vee-Validate are included in the app so that they can used by the templates by calling `extend` from Vee-Validate. We called `Vue.use(BootstrapVue)` to use BootstrapVue in our app.

In `router.js` we replace the existing code with:

```javascript
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Name from "./views/Name.vue";
import Address from "./views/Address.vue";
Vue.use(Router);
export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    },
    {
      path: "/name",
      name: "name",
      component: Name
    },
    {
      path: "/address",
      name: "address",
      component: Address
    }
  ]
});
```

to include the pages we created in the routes so that users can access them via the links in the top bar or typing in the URLs directly.

Next in `index.html` , we replace the existing code with:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title>Data Generator</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-virtual-scroll-tutorial-app doesn't work properly
        without JavaScript enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
```

to change the title of the app.