Make a Vue.js App with the Buefy Widget Library
Tags: vue, javascript, tutorial, webdev
Published: 2020-01-12 22:16:47

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

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

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

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](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:

```bash
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 named`PasswordForm.vue` and add:

```javascript
<template>  
  <ValidationObserver ref="observer" v-slot="{ invalid }">  
    <form [@submit](http://twitter.com/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:

```javascript
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:

```javascript
<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:

```javascript
<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:

```javascript
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:

```javascript
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:

```javascript
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:

```bash
json-server --watch db.json
```

In `db.json`, change the text to:

```json
{  
  "passwords": [  
  ]  
}
```

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