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.

Categories
JavaScript Vue

How to Make a Vue.js App with Buefy Widget Library

Buefy is a lightweight UI component library for Vue.js. It is based on the Bulma CSS framework, which is a framework similar to Bootstrap and Material Design libraries like Vuetify and Vue Material. It provides components like form inputs, tables, modals, alerts, etc, which are the most common components that Web apps use. The full list of components is located at https://buefy.org/documentation.

In this article, we will build a password manager using Buefy and Vue.js. It is a simple app with inputs for entering name, URL, username, and password. The user can edit or delete any entry they entered.

Getting Started

To start building the app, we run the Vue CLI to scaffold the project. We run npx @vue/cli create password-manager to generate the app. In the wizard, we choose ‘Manually select features’ and select Babel, Vuex, and Vue Router.

Next, we install some libraries that we use. We need Axios for making HTTP requests, the Buefy library, and Vee-Validate for form validation. To install them, we run:

npm i axios buefy vee-validate

Building the App

After installing the libraries, we can start building our app. First, in the components folder, create a file namedPasswordForm.vue and add:

<template>  
  <ValidationObserver ref="observer" v-slot="{ invalid }">  
    <form @submit.prevent="onSubmit" novalidate>  
      <ValidationProvider name="name" rules="required" v-slot="{ errors }">  
        <b-field  
          label="Name"  
          :type="errors.length > 0 ? 'is-danger': '' "  
          :message="errors.length > 0 ? 'Name is required': ''"  
        >  
          <b-input type="text" name="name" v-model="form.name"></b-input>  
        </b-field>  
      </ValidationProvider> <ValidationProvider name="url" rules="required|url" v-slot="{ errors }">  
        <b-field  
          label="URL"  
          :type="errors.length > 0 ? 'is-danger': '' "  
          :message="errors.join('. ')"  
        >  
          <b-input type="text" name="url" v-model="form.url"></b-input>  
        </b-field>  
      </ValidationProvider> <ValidationProvider name="username" rules="required" v-slot="{ errors }">  
        <b-field  
          label="Username"  
          :type="errors.length > 0 ? 'is-danger': '' "  
          :message="errors.length > 0 ? 'Username is required': ''"  
        >  
          <b-input type="text" name="username" v-model="form.username"></b-input>  
        </b-field>  
      </ValidationProvider> <ValidationProvider name="password" rules="required" v-slot="{ errors }">  
        <b-field  
          label="Password"  
          :type="errors.length > 0 ? 'is-danger': '' "  
          :message="errors.length > 0 ? 'Password is required': ''"  
        >  
          <b-input type="password" name="password" v-model="form.password"></b-input>  
        </b-field>  
      </ValidationProvider> 

      <br /> 

      <b-button type="is-primary" native-type="submit" style="margin-right: 10px">Submit</b-button> 

      <b-button type="is-warning" native-type="button" @click="cancel()">Cancel</b-button>  
    </form>  
  </ValidationObserver>  
</template>

<script>  
import { requestsMixin } from "@/mixins/requestsMixin";

export default {  
  name: "PasswordForm",  
  mixins: [requestsMixin],  
  props: {  
    edit: Boolean,  
    password: Object  
  },  
  methods: {  
    async onSubmit() {  
      const isValid = await this.$refs.observer.validate();  
      if (!isValid) {  
        return;  
      }
      if (this.edit) {  
        await this.editPassword(this.form);  
      } 
      else {  
        await this.addPassword(this.form);  
      }  
      const response = await this.getPasswords();  
      this.$store.commit("setPasswords", response.data);  
      this.$emit("saved");  
    },  
    cancel() {  
      this.$emit("cancelled");  
    }  
  },  
  data() {  
    return {  
      form: {}  
    };  
  },  
  watch: {  
    password: {  
      handler(p) {  
        this.form = JSON.parse(JSON.stringify(p || {}));  
      },  
      deep: true,  
      immediate: true  
    }  
  }  
};  
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->  
<style scoped lang="scss">  
</style>

This component has the form for users to enter a password entry. We use the ValidationObserver component to watch for the validity of the form inside the component and ValidationProvider to check for the validation rule of the inputted value of the input inside the component.

Inside the ValidationProvider, we have our Buefy b-field input. We get the errors array from the ValidationProvider ‘s slot and check if any errors exist in the type and message props. The label prop corresponds to the label tag of the input. The b-input is the actual input field. We bind to our form model here.

Below the inputs, we have the b-button components, which are rendered as buttons. We use the native-type prop to specify the type of the button, and the type prop is used for specifying the style of the button.

Once the user clicks Save, the onSubmit function is called. Inside the function, this.$refs.observer.validate(); is called to check for form validity. observer is the ref of the ValidationObserver . The observed form of validity value is here. If it resolves to true , then we call editPassword or addPassword depending if the edit prop is true. These 2 functions are from requestsMixin which we will create later. If that succeeds, then we call getPasswords which is also from the mixin, and then this.$store.commit is called to store the latest password entries in our Vuex store. After that, we emit the saved event to close the modal that the form is in.

Next, we create a mixins folder in the src folder and then create requestsMixin.js in the mixins folder. Then we add:

const APIURL = "http://localhost:3000";  
const axios = require("axios");
export const requestsMixin = {  
  methods: {  
    getPasswords() {  
      return axios.get(`${APIURL}/passwords`);  
    }, 

    addPassword(data) {  
      return axios.post(`${APIURL}/passwords`, data);  
    }, 

    editPassword(data) {  
      return axios.put(`${APIURL}/passwords/${data.id}`, data);  
    }, 

    deletePassword(id) {  
      return axios.delete(`${APIURL}/passwords/${id}`);  
    }  
  }  
};

This adds the code to make requests to the back end to save our password data.

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

<template>  
  <div class="page">  
    <h1 class="center">Password Manager</h1>  
    <b-button @click="openAddModal()">Add Password</b-button> <b-table :data="passwords">  
      <template scope="props">  
        <b-table-column field="name" label="Name">{{props.row.name}}</b-table-column>  
        <b-table-column field="url" label="URL">{{props.row.url}}</b-table-column>  
        <b-table-column field="username" label="Username">{{props.row.username}}</b-table-column>  
        <b-table-column field="password" label="Password">******</b-table-column>  
        <b-table-column field="edit" label="Edit">  
          <b-button @click="openEditModal(props.row)">Edit</b-button>  
        </b-table-column>  
        <b-table-column field="delete" label="Delete">  
          <b-button @click="deleteOnePassword(props.row.id)">Delete</b-button>  
        </b-table-column>  
      </template>  
    </b-table> <b-modal :active.sync="showAddModal" :width="500" scroll="keep">  
      <div class="card">  
        <div class="card-content">  
          <h1>Add Password</h1>  
          <PasswordForm @saved="closeModal()" @cancelled="closeModal()" :edit="false"></PasswordForm>  
        </div>  
      </div>  
    </b-modal> <b-modal :active.sync="showEditModal" :width="500" scroll="keep">  
      <div class="card">  
        <div class="card-content">  
          <h1>Edit Password</h1>  
          <PasswordForm  
            @saved="closeModal()"  
            @cancelled="closeModal()"  
            :edit="true"  
            :password="selectedPassword"  
          ></PasswordForm>  
        </div>  
      </div>  
    </b-modal>  
  </div>  
</template>

<script>  
// @ is an alias to /src  
import { requestsMixin } from "@/mixins/requestsMixin";  
import PasswordForm from "@/components/PasswordForm";

export default {  
  name: "home",  
  data() {  
    return {  
      selectedPassword: {},  
      showAddModal: false,  
      showEditModal: false  
    };  
  },  
  components: {  
    PasswordForm  
  },  
  mixins: [requestsMixin],  
  computed: {  
    passwords() {  
      return this.$store.state.passwords;  
    }  
  },  
  beforeMount() {  
    this.getAllPasswords();  
  },  
  methods: {  
    openAddModal() {  
      this.showAddModal = true;  
    },  
    openEditModal(password) {  
      this.showEditModal = true;  
      this.selectedPassword = password;  
    },  
    closeModal() {  
      this.showAddModal = false;  
      this.showEditModal = false;  
      this.selectedPassword = {};  
    },  
    async deleteOnePassword(id) {  
      await this.deletePassword(id);  
      this.getAllPasswords();  
    },  
    async getAllPasswords() {  
      const response = await this.getPasswords();  
      this.$store.commit("setPasswords", response.data);  
    }  
  }  
};  
</script>

We add a table to display the password entries here by using the b-table component, and add the b-table-column column inside the b-table to display custom columns. The b-table component takes a data prop which contains an array of passwords, then the data is exposed for use by the b-table-column components by getting the props from the scoped slot. Then we display the fields, by using the prop.row property. In the last 2 columns, we add 2 buttons to let the user open the edit modal and delete the entry respectively. The entries are loaded when the page loads, by calling getAllPasswords in the beforeMount hook.

This page also has 2 modals, one for the add view and one for editing the entry. In each modal, we nest the PasswordForm component that we created earlier inside. We call the openEditModal to open the edit modal. In the function, we set the selectedPassword field to pass it onto the PasswordForm so that users can edit it and set this.showEditModal to true. The openAddModal function opens the add password modal by changing this.showAddModal to true .

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

<template>  
  <div>  
    <b-navbar type="is-warning">  
      <template slot="brand">  
        <b-navbar-item tag="router-link" :to="{ path: '/' }">Password Manager</b-navbar-item>  
      </template>  
      <template slot="start">  
        <b-navbar-item :to="{ path: '/' }" :active="path  == '/'">Home</b-navbar-item>  
      </template>  
    </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;  
}

button {  
  margin-right: 10px;  
}

.center {  
  text-align: center;  
}

h1 {  
  font-size: 32px !important;  
}  
</style>

This adds the Buefy b-navbar component, which is the top navigation bar component provided by Buefy. The b-navbar contains different slots for adding items to the different parts of the left bar. The brand slot folds the app name of the top left, and the start slot has the links in the top left.

We also have the router-view for showing our routes. In the scripts section, we watch the $route variable to get the current route the user has navigated to set the active prop of the the b-navbar-item , which highlights the link if the user has currently navigated to the page with the URL referenced.

In the styles section, we add some padding to our pages and margin for the buttons, and also center some text and change the heading size.

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 Buefy from "buefy";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
import "buefy/dist/buefy.css";
extend("required", required);
extend("url", {
  validate: value => {
    return /^(http://www.|https://www.|http://|https://)?[a-z0-9]+([-.]{1}[a-z0-9]+)*.[a-z]{2,5}(:[0-9]{1,5})?(/.*)?$/.test(
      value
    );
  },
  message: "URL is invalid."
});
Vue.use(Buefy);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

This adds the Buefy library and styles to our app and adds the validation rules that we need. Also, we added the ValidationProvider and ValidationObserver to our app so we can use it in the PasswordForm .

Next in router.js , we 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  
    }  
  ]  
})

This includes the home page route.

Then in store.js , we replace the existing code with:

import Vue from "vue";  
import Vuex from "vuex";Vue.use(Vuex);export default new Vuex.Store({  
  state: {  
    passwords: []  
  },  
  mutations: {  
    setPasswords(state, payload) {  
      state.passwords = payload;  
    }  
  },  
  actions: {}  
});

This adds our passwords state to the store so we can observe it in the computed block of PasswordForm and HomePage components. We have the setPasswords function to update the passwords state and we use it in the components by call this.$store.commit(“setPasswords”, response.data); like we did in PasswordForm . Also, we imported the Bootstrap CSS in this file to get the styles.

After all the hard work, we can start our app by running npm start.

Fake App Backend

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{  
  "passwords": [  
  ]  
}

So we have the passwords endpoints defined in the requests.js available.