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.