How to Add Copy to Clipboard Feature to Your Vue.js App
Tags: vue, javascript, webdev, tutorial
Published: 2020-01-18 03:30:30

**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**

Copy to clipboard feature is a popular feature for web apps like password managers, where it is inconvenient for people to highlight text and then copy it. It is an easy feature to add to your own web app.

In this article, we will build a password manager that lets you enter, edit, and delete passwords and let them copy their username and password to the clipboard to use them anywhere they like. We will use Vue.js to build the app.

Getting Started
===============

To start we create the project by running `npx @vue/cli create password-manager`. In the wizard, choose ‘Manually select features’ and choose to include Babel, Vue Router, and Vuex in our app.

Next, we install some libraries we need. We need Axios for making HTTP requests, Bootstrap Vue for styling, V-Clipboard for the copy to clipboard functionality, and Vee-Validate for form validation. We install them by running:

```bash
npm i axios bootstrap-vue v-clipboard vee-validate
```

After we install the libraries, we can start building the app. First, in the `components` folder, create a file called `PasswordForm.vue` for our password form. Then in there, we add:

```javascript
<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group label="Name">
        <ValidationProvider name="name" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.name"
            required
            placeholder="Name"
            name="name"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="URL">
        <ValidationProvider name="url" rules="required|url" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.url"
            required
            placeholder="URL"
            name="url"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Username">
        <ValidationProvider name="username" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.username"
            required
            placeholder="Username"
            name="username"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Username is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Password">
        <ValidationProvider name="password" rules="required" v-slot="{ errors }">
          <b-form-input
            type="password"
            :state="errors.length == 0"
            v-model="form.password"
            required
            placeholder="Password"
            name="password"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Password is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-button type="submit" variant="primary" style="margin-right: 10px">Submit</b-button>
      <b-button type="reset" variant="danger" @click="cancel()">Cancel</b-button>
    </b-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>
```

We have the password form in this component. The form includes name, URL, username, and password fields. All of them are required. We use Vee-Validate to validate the form fields. The `ValidationObserver` component is for validating the whole form, while the `ValidationProvider` component is for validating the form fields that it wraps around.

The validation rule is specified by the `rule` prop of each field. We have a special `url` rule for the URL field. We show the validation error messages when the `errors` object from the scope slot has a non-zero length. The `state` prop is for setting the validation state which shows the green when `errors` has length 0 and red otherwise. The error messages are shown in the `b-form-invalid-feedback` component.

When the user clicks to Save button, the `onSubmit` function is called. We get the validation state of the form by using `this.$refs.observer.validate();` . The ref refers to the ref of the `ValidationObserver` . If it resolves to `true` , then we call `addPassword` or `editPassword` to save the entry depending on the `edit` prop. Then we get the passwords by calling `getPasswords` and then put it in our Vuex store by dispatching the `setPasswords` mutation. Then we emit the `saved` event to close the modal on the home page.

We have a `watch` block mainly used when an existing entry is being edited, we get the `password` prop and set it to `this.form` by making a copy of the prop so that we only update the `form` object and nothing when data is binding.

Next, we create a `mixins` folder and add `requestsMixin.js` inside it. In the file, 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 contains the code to make the HTTP requests in the back end. We include this mixin in our components so that we can make requests to back end from them.

Copy to clipboard functionality
===============================

To copy the username and password buttons, we use the `v-clipboard` directive to let us copy the username and password respectively to the clipboard when the button is clicked.

In `Home.vue`, we replace the existing code with:

```javascript
<template>
  <div class="page">
    <h1 class="text-center">Password Manager</h1>
    <b-button-toolbar>
      <b-button @click="openAddModal()">Add Password</b-button>
    </b-button-toolbar>
    <br />
    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th>Name</b-th>
          <b-th>URL</b-th>
          <b-th>Username</b-th>
          <b-th>Password</b-th>
          <b-th></b-th>
          <b-th></b-th>
          <b-th></b-th>
          <b-th></b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="p in passwords" :key="p.id">
          <b-td>{{p.name}}</b-td>
          <b-td>{{p.url}}</b-td>
          <b-td>{{p.username}}</b-td>
          <b-td>******</b-td>
          <b-td>
            <b-button v-clipboard="() => p.username">Copy Username</b-button>
          </b-td>
          <b-td>
            <b-button v-clipboard="() => p.password">Copy Password</b-button>
          </b-td>
          <b-td>
            <b-button @click="openEditModal(p)">Edit</b-button>
          </b-td>
          <b-td>
            <b-button @click="deleteOnePassword(p.id)">Delete</b-button>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple>
<b-modal id="add-modal" title="Add Password" hide-footer>
      <PasswordForm @saved="closeModal()" @cancelled="closeModal()" :edit="false"></PasswordForm>
    </b-modal>
<b-modal id="edit-modal" title="Edit Password" hide-footer>
      <PasswordForm
        @saved="closeModal()"
        @cancelled="closeModal()"
        :edit="true"
        :password="selectedPassword"
      ></PasswordForm>
    </b-modal>
  </div>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
import PasswordForm from "@/components/PasswordForm";
export default {
  name: "home",
  components: {
    PasswordForm
  },
  mixins: [requestsMixin],
  computed: {
    passwords() {
      return this.$store.state.passwords;
    }
  },
  beforeMount() {
    this.getAllPasswords();
  },
  data() {
    return {
      selectedPassword: {}
    };
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(password) {
      this.$bvModal.show("edit-modal");
      this.selectedPassword = password;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      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>
```

In this file, we have a table to display a list of password entries and let users open and close the add/edit modals. We have buttons in each row to copy the username and passwords, and also to let users edit or delete each entry.

In the `scripts` section, we have the `beforeMount` hook to get all the password entries during page load with the `getPasswords` function we wrote in our mixin. When the Edit button is clicked, the `selectedPassword` variable is set, and we pass it to the `PasswordForm` for editing.

To delete a password, we call `deletePassword` in our mixin to make the request to the back end.

Finishing the app
=================

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

```javascript
<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">Password Manager</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;
}
button {
  margin-right: 10px;
}
</style>
```

This adds a Bootstrap navigation bar to the top of our pages, and a `router-view` to display the routes we define.

Next in `main.js`, replace the code with:

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

To add the libraries we installed to our app so we can use it in our components. We add the V-Clipboard library here so we can use it in our home page. We call `extend` from Vee-Validate to add the form validation rules that we want to use. Also, we imported the Bootstrap CSS in this file to get the styles.

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

to only include our home page.

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` .

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

Demo 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.