Vue CLI makes using them in your app easy by allowing variables in an .env file that have keys starting with VUE_APP to be used in your app.
Environment variables are often used for things that do not belong in your code like API keys and URLs. Vue CLI makes using them in your app easy by allowing variables in an .env
file that have keys starting with VUE_APP
to be used in your app.
The variables can be accessed by using the process.env
object. For example, if you have VUE_APP_API_KEY
in your .env
file, then you can access it by using process.env.VUE_APP_API_KEY
.
You can also have an .env
file for other environments by adding an extension to the .env
file. For example, you can use .env.staging
for the staging environment if you want to include your staging URLs when you want to deploy to your staging server.
In this article, we will build a photo app that allows users to search for images and display images in a masonry grid. The image grid will have infinite scroll to get more images. We will use the vue-masonry library for render the image grid, and vue-infinite-scroll for the infinite scrolling effect.
To implement the masonry effect, we have to set the width of the image proportional to the screen width and set the image height to be proportional to the aspect ratio of the image.
This is a pain to do if it’s done without any libraries, so people have made libraries to create this effect.
Our app will display images from the Pixabay API. You can view the API documentation and register for a key at https://pixabay.com/api/docs/
We will store the API key in the .env
file on the project’s root folder.
Once we have the Pixabay API key, we can start writing our app. To start, we create a project called photo-app
. To do this, run:
npx @vue/cli create photo-app
This will create the files for our app and install the packages for the built-in libraries. We choose ‘manually select features’ and choose Babel, Vue Router and CSS Preprocessor.
Next, we install our own packages. We need the vue-masonry library and vue-infinite-scroll we mentioned above. In addition, we need BootstrapVue for styling, Axios for making HTTP requests and Vee-Validate for form validation.
We install all the packages by running:
npm i axios bootstrap-vue vee-validate vue-infinite-scroll vue-masonry
With all the packages installed, we can start writing our app. Create a mixins
folder in the src
folder and create a requestsMixin.js
file.
Then we add the following to the file:
const axios = require("axios");
const APIURL = "https://pixabay.com/api";
export const requestsMixin = {
methods: {
getImages(page = 1) {
return axios.get(`${APIURL}/?page=${page}&key=${process.env.VUE_APP_API_KEY}`);
},
searchImages(keyword, page = 1) {
return axios.get(
`${APIURL}/?page=${page}&key=${process.env.VUE_APP_API_KEY}&q=${keyword}`
);
}
}
};
We call the endpoints to search for images here. process.env.VUE_APP_API_KEY
is retrieved from the .env
file in the root folder of our project. Note that the environment variables we use have to have keys that begin with VUE_APP
.
Next, in Home.vue
, replace the existing code with:
<template>
<div class="page">
<h1 class="text-center">Home</h1>
<div
v-infinite-scroll="getImagesByPage"
infinite-scroll-disabled="busy"
infinite-scroll-distance="10"
>
<div
v-masonry="containerId"
transition-duration="0.3s"
item-selector=".item"
gutter="5"
fit-width="true"
class="masonry-container"
>
<div>
<img
:src="item.previewURL"
v-masonry-tile
class="item"
v-for="(item, index) in images"
:key="index"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { requestsMixin } from "../mixins/requestsMixin";
export default {
name: "home",
mixins: [requestsMixin],
data() {
return {
images: [],
page: 1,
containerId: null
};
},
methods: {
async getImagesByPage() {
const response = await this.getImages(this.page);
this.images = this.images.concat(response.data.hits);
this.page++;
}
},
beforeMount() {
this.getImagesByPage();
}
};
</script>
We use the vue-infinite-scroll and vue-masonry packages here. Note that we specified the transition-duration
to tweak the transition from showing nothing to showing the images, fit-width
makes the columns fit the container. gutter
specifies the width of the space between each column in pixels. We also set a CSS class name in the v-masonry
container to change the styles later.
Inside the v-masonry
div, we loop through the images, we set the v-masonry-tile
to indicate that it is tile so that it will resize them to a masonry grid.
In the script
object, we get the images when the page loads with the beforeMount
hook. Since we are adding infinite scrolling, we keep adding images to the array as the user scrolls down. We call getImagesByPage
as the user scrolls down as indicated by the v-infinite-scroll
prop. We set infinite-scroll-disabled
to busy
to set disable scrolling if busy
is set to true
. infinite-scroll-distance
indicates the distance from the bottom of the page in percent for scrolling to be triggered.
Next create ImageSearchPage.vue
in the views
folder and add:
<template>
<div class="page">
<h1 class="text-center">Image Search</h1>
<ValidationObserver ref="observer" v-slot="{ invalid }">
<b-form @submit.prevent="onSubmit" novalidate>
<b-form-group label="Keyword" label-for="keyword">
<ValidationProvider name="keyword" rules="required" v-slot="{ errors }">
<b-form-input
:state="errors.length == 0"
v-model="form.keyword"
type="text"
required
placeholder="Keyword"
name="keyword"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">Keyword is required</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-button type="submit" variant="primary">Search</b-button>
</b-form>
</ValidationObserver>
<br />
<div
v-infinite-scroll="searchAllImages"
infinite-scroll-disabled="busy"
infinite-scroll-distance="10"
>
<div
v-masonry="containerId"
transition-duration="0.3s"
item-selector=".item"
gutter="5"
fit-width="true"
class="masonry-container"
>
<div>
<img
:src="item.previewURL"
v-masonry-tile
class="item"
v-for="(item, index) in images"
:key="index"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { requestsMixin } from "../mixins/requestsMixin";
export default {
mixins: [requestsMixin],
data() {
return {
form: {},
page: 1,
containerId: null,
images: []
};
},
methods: {
async onSubmit() {
const isValid = await this.$refs.observer.validate();
if (!isValid) {
return;
}
this.page = 1;
await this.searchAllImages();
},
async searchAllImages() {
if (!this.form.keyword) {
return;
}
const response = await this.searchImages(this.form.keyword, this.page);
if (this.page == 1) {
this.images = response.data.hits;
} else {
this.images = this.images.concat(response.data.hits);
}
this.page++;
}
}
};
</script>
The infinite scrolling and masonry layout are almost the same, except when the keyword
changes, we reassign the this.images
array to the new items instead of keep adding them to the existing array so that users see the new results.
The form is wrapped inside the ValidationObserver
so that we can get the validation status of the whole form inside the ValidationObserver
. In the form, we wrap the input with ValidationProvider
so that the form field can be validated and a validation error message displayed for the input. We check if keyword
is filled in.
Once the user clicks Search, onSubmit
is run, which runs await this.$refs.observer.validate();
to get the form validation status. If that results to true
, then searchAllImages
will be run to get the images.
Next, we replace the existing code in App.vue
with:
<template>
<div>
<b-navbar toggleable="lg" type="dark" variant="info">
<b-navbar-brand href="#">Photo App</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="/imagesearch" :active="path == '/imagesearch'">Image Search</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;
}
.item {
width: 30vw;
}
.masonry-container {
margin: 0 auto;
}
</style>
We add the BootstrapVue b-navbar
here to display a top bar with links to our pages. In the script
section, we watch the current route by getting this.$route.path
. We set the active
prop by check the path against our watched path
to highlight the links.
In the style
section, we set the padding of our pages with the page
class, we set the photo width with the item
class as indicated in the item-selector
of our v-masonry
div, and we set the masonry-container
‘s margin to 0 auto
so that it will be centered in the page.
Next in main.js
, replace the existing code with:
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import BootstrapVue from "bootstrap-vue";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
import { VueMasonryPlugin } from "vue-masonry";
import infiniteScroll from "vue-infinite-scroll";
Vue.config.productionTip = false;
extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(VueMasonryPlugin);
Vue.use(infiniteScroll);
Vue.use(BootstrapVue);
new Vue({
router,
render: h => h(App)
}).$mount("#app");
to add all the libraries we used in the components and the Vee-Validate validation rules that we used. Also, we import our Bootstrap styles here so that we see the styles everywhere.
Next in router.js
, replace the existing code with:
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import ImageSearchPage from "./views/ImageSearchPage.vue";
Vue.use(Router);
export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/imagesearch",
name: "imagesearch",
component: ImageSearchPage
}
]
});
to add our routes.
Finally, 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>Photo App</title>
</head>
<body>
<noscript>
<strong
>We're sorry but vue-masonry-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 rename the title of our app.