Categories
JavaScript Vue

Create Custom Filters with Vue.js

In a Vue app, we can use filters to transform data in a template to a format that we want to display the items in.

In this article, we’ll look at how to create filters and use them in our template.

Global Filter

We can define a global filter by using the Vue.filter method to define a filter.

Global filters are available in any part of the app.

It takes the name of the filter as the first argument and a function that takes a value and returns the value transformed to something we want as the second argument.

For example, we can define a global filter and use it as follows:

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      {{date | localeString}}
    </div>
    <script src="index.js"></script>
  </body>
</html>

index.js:

Vue.filter("localeString", function(value) {
  if (!value instanceof Date) return "";
  return value.toLocaleString();
});

new Vue({
  el: "#app",
  data: {
    date: new Date()
  }
});

In the code above, we defined a global filter with:

Vue.filter("localeString", function(value) {
  if (!value instanceof Date) return "";
  return value.toLocaleString();
});

It takes a value parameter, checks if it’s a Date instance, and returns value.toLocaleString() if it is to format the date into a date string.

We then used it by writing:

{{date | localeString}}

in our template.

Local Filter

We can define a local filter by adding the filter method to our filters object in our Vue component.

We can write it as follows:

index.js

new Vue({
  el: "#app",
  data: {
    date: new Date()
  },
  filters: {
    localeString(value) {
      if (!value instanceof Date) return "";
      return value.toLocaleString();
    }
  }
});

In the code above, we moved our filter function into the root component. We reference the same way as we did we the global filter.

Filters can be chained. We just have to separate them with pipes. For example, we can write:

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      {{date | localeString | lowercase}}
    </div>
    <script src="index.js"></script>
  </body>
</html>

index.js:

Vue.filter("localeString", function(value) {
  if (!value instanceof Date) return "";
  return value.toLocaleString();
});

Vue.filter("lowercase", function(value) {
  return value.toLowerCase();
});

new Vue({
  el: "#app",
  data: {
    date: new Date()
  }
});

We should then see our date string in lower case.

Also, we can pass in arguments into filters as follows:

{{ message | filterA('arg1', arg2) }}

Then 'arg1' will be passed in as the second argument and arg2 as the 3rd argument of our filter function.

Conclusion

We can define filters to transform data into a format that we want to displayed in.

They can be defined globally or locally.

They can be chained and they can also pass in arguments.

Categories
JavaScript Vue

Add Multiple Selection Drop Down with Vue-Multiselect

Multiple selection is often a feature added to web apps to let users select multiple items at once.

Vue apps can use the Vue-Multiselect library to add this feature.

In this article, we’ll look at how to use this package to add a multi-select drop down.

Basic Single Select

We can get started by writing the following code:

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vue-multiselect@2.1.0"></script>
    <link
      rel="stylesheet"
      href="https://unpkg.com/vue-multiselect@2.1.0/dist/vue-multiselect.min.css"
    />
  </head>
  <body>
    <div id="app">
      <vue-multiselect v-model="value" :options="options"></vue-multiselect>
    </div>
    <script src="index.js"></script>
  </body>
</html>

index.js:

Vue.component("vue-multiselect", window.VueMultiselect.default);

new Vue({
  el: "#app",
  data: {
    value: "",
    options: ["apple", "orange", "grape"]
  }
});

In the code above, we added the Vue-Multiselect library with the following script and link tags in index.html:

<script src="https://unpkg.com/vue-multiselect@2.1.0"></script>
<link
  rel="stylesheet"
  href="https://unpkg.com/vue-multiselect@2.1.0/dist/vue-multiselect.min.css"
/>

for the code and styles respectively.

Then we registered the component with:

Vue.component("vue-multiselect", window.VueMultiselect.default);

Then we added the options for our dropdown choices:

options: ["apple", "orange", "grape"]

Finally, in our template, we added:

<vue-multiselect v-model="value" :options="options"></vue-multiselect>

After that, we should see a drop down showning on the screen with those choices listed in the options.

Single Select with Objects

In the code above, we have an array of strings, but we can also use an array of objects as options.

We just have to change our example above slightly to let user select objects:

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vue-multiselect@2.1.0"></script>
    <link
      rel="stylesheet"
      href="https://unpkg.com/vue-multiselect@2.1.0/dist/vue-multiselect.min.css"
    />
  </head>
  <body>
    <div id="app">
      <vue-multiselect
        track-by="name"
        label="name"
        v-model="value"
        :options="options"
      ></vue-multiselect>
      <p>{{value}}</p>
    </div>
    <script src="index.js"></script>
  </body>
</html>

index.js:

Vue.component("vue-multiselect", window.VueMultiselect.default);

new Vue({
  el: "#app",
  data: {
    value: "",
    options: [
      { name: "Vue.js", language: "JavaScript" },
      { name: "Rails", language: "Ruby" },
      { name: "Sinatra", language: "Ruby" }
    ]
  }
});

In the code above, we added the track-by1 andlabelprops to enable the library to track changes in object by itsname` property.

Multiple Select

All we have to do to enable multiple selection is to set the multiple prop to true.

We just have to revise the example above by changing index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vue-multiselect@2.1.0"></script>
    <link
      rel="stylesheet"
      href="https://unpkg.com/vue-multiselect@2.1.0/dist/vue-multiselect.min.css"
    />
  </head>
  <body>
    <div id="app">
      <vue-multiselect
        track-by="name"
        label="name"
        v-model="value"
        :options="options"
        :multiple="true"
      ></vue-multiselect>
      <p>{{value}}</p>
    </div>
    <script src="index.js"></script>
  </body>
</html>

Then we’ll see the values that are selected in the p tag.

Async Selection

The options doesn’t have to be set synchronously. We can also populate it with data from an async source as in the following example:

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vue-multiselect@2.1.0"></script>
    <link
      rel="stylesheet"
      href="https://unpkg.com/vue-multiselect@2.1.0/dist/vue-multiselect.min.css"
    />
  </head>
  <body>
    <div id="app">
      <vue-multiselect
        track-by="name"
        label="name"
        v-model="value"
        :options="options"
        :multiple="true"
      ></vue-multiselect>
      <p>{{value}}</p>
    </div>
    <script src="index.js"></script>
  </body>
</html>

index.js:

Vue.component("vue-multiselect", window.VueMultiselect.default);

new Vue({
  el: "#app",
  data: {
    value: "",
    options: []
  },
  async beforeMount() {
    this.options = await Promise.resolve([
      { name: "Vue.js", language: "JavaScript" },
      { name: "Rails", language: "Ruby" },
      { name: "Sinatra", language: "Ruby" }
    ]);
  }
});

The code above populates the options from the resolved value of a promise, but we don’t have to make any changes to the template code.

Conclusion

The Vue-Multiselect lets us create a dropdown that works in various ways like single select, multiselect, and displaying a searchable drop down.

It can take options from synchronous and asynchronous sources.

Categories
JavaScript Vue

How to Add Virtual Scrolling to a Vue App

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.

Categories
JavaScript Vue

How to Create a Progressive Web App with Vue.js

Progressive web app (PWAs) is a web app that does somethings that a native app does. It can work offline and you can install it from a browser with one click.

PWAs should run well on any device. They should be responsive. Performance should be good in any device. People can link to it easily, and it should have an icon for different size devices.

To build a PWA, we have to register service workers for handling installation and adding offline capabilities to make a regular web app a PWA. Service works also lets PWAs use some native capabilities like notifications, caching for when the device is offline, and app updates.

Building a PWA with Vue CLI 3.x is easy. There is the @vue/cli-plugin-pwa plugin. All we have to do is to run vue add pwa to convert our web app to a progressive web app. Optionally, we can configure our app with the settings listed at https://www.npmjs.com/package/@vue/cli-plugin-pwa.

In this article, we will build a progressive web app that displays article snippets from the New York Times API and a search page where users can enter a keyword to search the API.

The desktop layout will have a list of item names on the left and the article snippets on the right. The mobile layout will have a drop down for selecting the section to display and the cards displaying the article snippets below it.

The search page will have a search form on top and the article snippets below it regardless of screen size.

To start building the app, we start by running the Vue CLI. We run:

npx @vue/cli create nyt-app

to create the Vue.js project. When the wizard shows up, we choose ‘Manually select features’. Then we choose to include Vue Router and Babel in our project.

Next we add our own libraries for styling and making HTTP requests. We use BootstrapVue for styling, Axios for making requests, VueFilterDateFormat for formatting dates and Vee-Validate for form validation.

To install all the libraries, we run:

npm i axios bootstrap-vue vee-validate vue-filter-date-format

After all the libraries are installed, we can start building our app.

First, we use slots yo build our layouts for our pages. Create BaseLayout.vue in the components folder and add:

<template>
  <div>
    <div class="row">
      <div class="col-md-3 d-none d-lg-block d-xl-none d-xl-block">
        <slot name="left"></slot>
      </div>
      <div class="col">
        <div class="d-block d-sm-none d-none d-sm-block d-md-block d-lg-none">
          <slot name="section-dropdown"></slot>
        </div>
        <slot name="right"></slot>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: "BaseLayout"
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

In this file, we make use of Vue slots to create the responsive layout for the home page. We have the left , right , and section-dropdown slots in this file. The left slot only displays when the screen is large since we added the d-none d-lg-block d-xl-none d-xl-block classes to the left slot. The section-dropdown slot only shows on small screens since we added the d-block d-sm-none d-none d-sm-block d-md-block d-lg-none classes to it. These classes are the responsive utility classes from Bootstrap.

We make use of Vue.js slots in this file. Slots is a useful feature of Vue.js that allows you to separate different parts of a component into an organized unit. With your component compartmentalized into slots, you can reuse components by putting them into the slots you defined. It also makes your code cleaner since it lets you separate the layout from the logic of the app.

Also, if you use slots, you no longer have to compose components with the parent-child relationship since you can put any components into your slots.

The full list of responsive utility classes are at https://getbootstrap.com/docs/4.0/utilities/display/

Next, create a SearchLayout.vue file in the components folder and add:

<template>
  <div class="row">
    <div class="col-12">
      <slot name="top"></slot>
    </div>
    <div class="col-12">
      <slot name="bottom"></slot>
    </div>
  </div>
</template>
<script>
export default {
  name: "SearchLayout"
};
</script>

to create another layout for our search page. We have the top and bottom slots taking up the whole width of the screen.

Then we create a mixins folder and in it, create a requestsMixin.js file and add:

const axios = require("axios");
const APIURL = "https://api.nytimes.com/svc";
export const requestsMixin = {
  methods: {
    getArticles(section) {
      return axios.get(
        `${APIURL}/topstories/v2/${section}.json?api-key=${process.env.VUE_APP_API_KEY}`
      );
    },
searchArticles(keyword) {
      return axios.get(
        `${APIURL}/search/v2/articlesearch.json?api-key=${process.env.VUE_APP_API_KEY}&q=${keyword}`
      );
    }
  }
};

to create a mixin for making HTTP requests to the New York Times API. process.env.VUE_APP_API_KEY is the API key for the New York Times API, and we get it from the .env file in the project’s root folder, with the key of the environment variable being VUE_APP_API_KEY.

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

<template>
  <div class="page">
    <h1 class="text-center">Home</h1>
    <BaseLayout>
      <template v-slot:left>
        <b-nav vertical pills>
          <b-nav-item
            v-for="s in sections"
            :key="s"
            :active="s == selectedSection"
            @click="selectedSection = s; getAllArticles()"
          >{{s}}</b-nav-item>
        </b-nav>
      </template>
<template v-slot:section-dropdown>
        <b-form-select
          v-model="selectedSection"
          :options="sections"
          @change="getAllArticles()"
          id="section-dropdown"
        ></b-form-select>
      </template>
<template v-slot:right>
        <b-card
          v-for="(a, index) in articles"
          :key="index"
          :title="a.title"
          :img-src="(Array.isArray(a.multimedia) && a.multimedia.length > 0 && a.multimedia[a.multimedia.length-1].url) || ''"
          img-bottom
        >
          <b-card-text>
            <p>{{a.byline}}</p>
            <p>Published on: {{new Date(a.published_date) | dateFormat('YYYY.MM.DD hh:mm a')}}</p>
            <p>{{a.abstract}}</p>
          </b-card-text>
<b-button :href="a.short_url" variant="primary" target="_blank">Go</b-button>
        </b-card>
      </template>
    </BaseLayout>
  </div>
</template>
<script>
// @ is an alias to /src
import BaseLayout from "@/components/BaseLayout.vue";
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "home",
  components: {
    BaseLayout
  },
  mixins: [requestsMixin],
  data() {
    return {
      sections: `arts, automobiles, books, business, fashion,
      food, health, home, insider, magazine, movies, national,
      nyregion, obituaries, opinion, politics, realestate, science,
      sports, sundayreview, technology, theater,
      tmagazine, travel, upshot, world`
        .split(",")
        .map(s => s.trim()),
      selectedSection: "arts",
      articles: []
    };
  },
  beforeMount() {
    this.getAllArticles();
  },
  methods: {
    async getAllArticles() {
      const response = await this.getArticles(this.selectedSection);
      this.articles = response.data.results;
    },
    setSection(ev) {
      this.getAllArticles();
    }
  }
};
</script>
<style scoped>
#section-dropdown {
  margin-bottom: 10px;
}
</style>

We use the slots defined in BaseLayout.vue in this file. In the left slot, we put the list of section names in there to display the list on the left when we have a desktop-sized screen.

In the section-dropdown slot, we put the drop-down that only shows in mobile screens as defined in BaseLayout.

Then in the right slot, we put the Bootstrap cards for displaying the article snippets, also as defined in BaseLayout.

We put all the slot contents inside BaseLayout and we use v-slot outside the items we want to put into the slots to make the items show in the designated slot.

In the script section, we get the articles by section by defining the getAllArticles function from requestsMixin.

Next create a Search.vue file and add:

<template>
  <div class="page">
    <h1 class="text-center">Search</h1>
    <SearchLayout>
      <template v-slot:top>
        <ValidationObserver ref="observer" v-slot="{ invalid }">
          <b-form @submit.prevent="onSubmit" novalidate id="form">
            <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>
      </template>
<template v-slot:bottom>
        <b-card v-for="(a, index) in articles" :key="index" :title="a.headline.main">
          <b-card-text>
            <p>By: {{a.byline.original}}</p>
            <p>Published on: {{new Date(a.pub_date) | dateFormat('YYYY.MM.DD hh:mm a')}}</p>
            <p>{{a.abstract}}</p>
          </b-card-text>
<b-button :href="a.web_url" variant="primary" target="_blank">Go</b-button>
        </b-card>
      </template>
    </SearchLayout>
  </div>
</template>
<script>
// @ is an alias to /src
import SearchLayout from "@/components/SearchLayout.vue";
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "home",
  components: {
    SearchLayout
  },
  mixins: [requestsMixin],
  data() {
    return {
      articles: [],
      form: {}
    };
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      const response = await this.searchArticles(this.form.keyword);
      this.articles = response.data.response.docs;
    }
  }
};
</script>
<style scoped>
</style>

It’s very similar to Home.vue. We put the search form in the top slot by putting it inside the SearchLayour, and we put our slot content for the top slot by putting our form inside the <template v-slot:top> element.

We use the ValidationObserver to validate the whole form, and ValidationProvider to validate the keyword input. They are both provided by Vee-Validate.

Once the Search button is clicked, we call this.$refs.observer.validate(); to validate the form. We get the this.$refs.observer since we wrapped the ValidationObserver outside the form.

Then once form validation succeeds, by this.$refs.observer.validate() resolving to true , we call searchArticles from requestsMixin to search for articles.

In the bottom slot, we put the cards for displaying the article search results. It works the same way as the other slots.

Next in App.vue , we put:

<template>
  <div>
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">New York Times 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="/search" :active="path == '/search'">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>
.page {
  padding: 20px;
}
</style>

to add the BootstrapVue b-navbar here and watch the route as it changes so that we can set the active prop to the link of the page the user is currently in.

Next we change main.js ‘s code to:

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 VueFilterDateFormat from "vue-filter-date-format";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
Vue.use(VueFilterDateFormat);
Vue.use(BootstrapVue);
extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

We import all the app-wide packages we use here, like BootstrapVue, Vee-Validate and the calendar and date-time picker widgets.

The styles are also imported here so we can see them throughout the app.

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 Search from "./views/Search.vue";
Vue.use(Router);
export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    },
    {
      path: "/search",
      name: "search",
      component: Search
    }
  ]
});

to set the routes for our app, so that when users enter the given URL or click on a link with it, they can see our page.

Finally, we replace the code in index.html 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>New York Times App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-slots-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 app’s title.

Next to make our app a progressive web app by running vue add pwa. Then we run npm run build to build the app. After it runs we can use browser-sync package to serve our app. Run npm i -g browser-sync to install the server. Then we run browser-sync start — server from the dist folder of our project folder, which should be created when we run npm run build.

Then on the top right corner of your browser, you should get an install button on the right side of the URL input in Chrome.

You should also get an entry in the chrome://apps/ page for this app.

Categories
JavaScript Vue

How To Add Parallax Scrolling to Your Vue.js App

Parallax scrolling is the effect where the background image scrolls slower than the elements in the foreground, creating an illusion of depth of the page.

Websites often use this for informational pages, where you have some text in the foreground and an image in the background that scrolls more slowly to create a more interesting experience for the user.

https://www.mockplus.com/blog/post/parallax-scrolling-websites has some examples of web pages with parallax scrolling.

With React, it is quick and simple to create the parallax scrolling effect with the Vue-Parallaxy library, located at https://github.com/apertureless/vue-parallax.

In this article, we will make an app that displays a list of images in the background with tags text in the foreground. The images will be provided by the Pixabay API. You can register for an API key at Pixabay.

To start the project, we create the project by running:

npx @vue/cli create photo-app

Then we select ‘Manually select features’ then choose to include Babel and Vue Router.

We need to install Axios to get images from the Pixabay API, BootstrapVue for styling, and Vue-Parallaxy to create the parallax scrolling effect. To install the packages run:

npm i axios bootstra-vue vue-parallaxy

With all the packages installed, we can start building the app. To start, we add a mixin for making the HTTP requests. Create a mixins folder in the src folder, then in the folder, create requestsMixins.js . In the file, add:

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}`  
      );  
    }  
  }  
};

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

<template>  
  <div class="page">  
    <div v-for="(img, i) of images" :key="i" class="parallax-container">  
      <parallax :speed-factor="0.5" direction="down" :parallax="true">  
        <div>  
          <img :src="img.largeImageURL" :alt="img.tags" style="image" />  
          <h1 class="parallax-title">{{img.tags}}</h1>  
        </div>  
      </parallax>  
      <br />  
    </div>  
  </div>  
</template>

<script>  
// @ is an alias to /src  
import { requestsMixin } from "../mixins/requestsMixin";  
import Parallax from "vue-parallaxy";

export default {  
  name: "home",  
  mixins: [requestsMixin],  
  components: {  
    Parallax  
  },  
  data() {  
    return {  
      images: []  
    };  
  },  
  methods: {  
    async getImagesByPage() {  
      const response = await this.getImages();  
      this.images = response.data.hits;  
    }  
  },  
  beforeMount() {  
    this.getImagesByPage();  
  }  
};  
</script>

<style>  
.parallax-container {  
  position: relative;  
  height: 1000px;  
}

.parallax-title {  
  position: absolute;  
  top: 30%;  
  left: 0;  
  right: 0;  
  padding: 20px;  
  color: white;  
  text-align: center;  
}

.image {  
  height: 700px;  
}  
</style>

We include the Vue-Parallaxy component in this component by adding Parallax in the components object. Then we get the images by calling the this.getImages function from the requestsMixin we just created. We call the getImagesByPage function in the beforeMount hook to get the images when the page loads.

In the template, we use the parallax component provided by Vue-Parallaxy to create the parallax scrolling effect. The parallax serves as the container for the parallax effect. We make the speed of the scrolling different from the foreground with the speed-factor prop, we set the direction to down so that it scrolls down. parallax prop is set to true so that we get the different scrolling speed between the foreground and background.

We change the height of the parallax-container divs to the same height of 1000px, and the images to 700px to keep the spacing consistent.

In the component, we loop through the images and show some text from the Pixbay API. We position the text inside the photo by specifying:

<style>  
.parallax-container {  
  position: relative;  
}

.parallax-title {  
  position: absolute;  
  top: 30%;  
  left: 0;  
  right: 0;  
  padding: 20px;  
  color: white;  
  text-align: center;  
}  
</style>

We place the text in the center of the images and change the text color to white.

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

<template>  
  <div id="app">  
    <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-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;  
}  
</style>

We add some padding to the pages with the page class, and we add the BootstrapVue navigation bar to the top of the page. Also, we have the router-view so that we see the home page.

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 "bootstrap/dist/css/bootstrap.css";  
import "bootstrap-vue/dist/bootstrap-vue.css";  
import BootstrapVue from "bootstrap-vue";

Vue.config.productionTip = false  
Vue.use(BootstrapVue);

new Vue({  
  router,  
  store,  
  render: h => h(App)  
}).$mount('#app')

to add the BootstrapVue libraries and styles to the app so we can use the code in our app and see the styling in the whole app.

Then in router.js , replace the existing code with:

import Vue from "vue";  
import Router from "vue-router";  
import Home from "./views/Home.vue";

Vue.use(Router);

export default new Router({  
  mode: "history",  
  base: process.env.BASE_URL,  
  routes: [  
    {  
      path: "/",  
      name: "home",  
      component: Home  
    }  
  ]  
});

We added the home page route so that we can see the page we built.

Then in the root folder of the project, we add an .env file so store the API key:

VUE_APP_API_KEY='Pixabay API key'

We can use this keep by referencing process.env.VUE_APP_API_KEY like we have in the requestsMixin.js.

Next in index.html , 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-parallax-scrolling-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.