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 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:
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:
<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:
<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:
<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:
<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:
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:
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:
<!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.