How You Can Make a Browser Extension with Vue.js
Tags: webdev, html, javascript, tutorial
Published: 2020-01-15 03:04: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**

The most popular web browsers, Chrome and Firefox support extensions. Extensions are small apps that you can add to your browser to get the functionality that is not included in your browser. This makes extending browser functionality very easy. All a user has to do is to add browser add-ons from the online stores like the Chrome Web Store or the Firefox Store to add browser add-ons.

Browser extensions are just normal HTML apps packages in a specific way. This means that we can use HTML, CSS, and JavaScript to build our own extensions.

Chrome and Firefox extensions follow the Web Extension API standard. The full details are at [https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions)

In this article, we will make a Chrome extension that displays the weather from the OpenWeatherMap API. We will add a search to let users look up the current weather and forecast from the API and display it in the extension’s popup box.

We will use Vue.js to build the browser extension. To begin building it, we start with creating the project with Vue CLI. Run `npx @vue/cli create weather-app` to create the project. In the wizard, select Babel and Vuex.

The OpenWeatherMap API is available at [https://openweathermap.org/api](https://openweathermap.org/api). You can register for an API key here. Once you got an API key, create an `.env` file in the root folder and add `VUE_APP_APIKEY` as the key and the API key as the value.

Next, we use the vue-cli-plugin-browser-extension to add the files for writing and compiling the Chrome extension. The package settings and details are located at [https://www.npmjs.com/package/vue-cli-plugin-browser-extension](https://www.npmjs.com/package/vue-cli-plugin-browser-extension).

To add it to our project, we run `vue add browser-extension` to add the files needed to build the extension. The command will change the file structure of our project.

After that command is run, we have to remove some redundant files. We should remove `App.vue` and `main.js` from the `src` folder and leave the files with the same name in the `popup` folder alone. Then we run `npm run serve` to build the files as we modify the code.

Next, we have to install the Extension Reload to reload the extension as we are changing the files. Install it from [https://chrome.google.com/webstore/detail/extensions-reloader/fimgfedafeadlieiabdeeaodndnlbhid](https://chrome.google.com/webstore/detail/extensions-reloader/fimgfedafeadlieiabdeeaodndnlbhid) to get hot reloading of our extension in Chrome.

Then we go to the chrome://extensions/ URL in Chrome and toggle on Developer Mode. We should see the Load unpacked button on the top left corner. Click that, and then select the `dist` folder in our project to load our extension into Chrome.

Next, we have to install some libraries that we will use. We need Axios for making HTTP requests, BootstrapVue for styling, and Vee-Validate for form validation. To install them, we run `npm i axios bootstrap-vue vee-validate` to install them.

With all the packages installed we can start writing our code. Create `CurrentWeather.vue` in the `components` folder and add:

```javascript
<template>  
  <div>  
    <br />  
    <b-list-group v-if="weather.main">  
      <b-list-group-item>Current Temparature: {{weather.main.temp - 273.15}} C</b-list-group-item>  
      <b-list-group-item>High: {{weather.main.temp_max - 273.15}} C</b-list-group-item>  
      <b-list-group-item>Low: {{weather.main.temp_min - 273.15}} C</b-list-group-item>  
      <b-list-group-item>Pressure: {{weather.main.pressure }}mb</b-list-group-item>  
      <b-list-group-item>Humidity: {{weather.main.humidity }}%</b-list-group-item>  
    </b-list-group>  
  </div>  
</template>

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

export default {  
  name: "CurrentWeather",  
  mounted() {},  
  mixins: [requestsMixin],  
  computed: {  
    keyword() {  
      return this.$store.state.keyword;  
    }  
  },  
  data() {  
    return {  
      weather: {}  
    };  
  },  
  watch: {  
    async keyword(val) {  
      const response = await this.searchWeather(val);  
      this.weather = response.data;  
    }  
  }  
};  
</script>

<style scoped>  
p {  
  font-size: 20px;  
}  
</style>
```

This component displays the current weather from the OpenWeatherMap API as the `keyword` from the Vuex store is updated. We will create the Vuex store later. The `this.searchWeather` function is from the `requestsMixin` , which is a Vue mixin that we will create. The `computed` block gets the `keyword` from the store via `this.$store.state.keyword` and return the latest value.

Next, create `Forecast.vue` in the same folder and add:

```javascript
<template>  
  <div>  
    <br />  
    <b-list-group v-for="(l, i) of forecast.list" :key="i">  
      <b-list-group-item>  
        <b>Date: {{l.dt_txt}}</b>  
      </b-list-group-item>  
      <b-list-group-item>Temperature: {{l.main.temp - 273.15}} C</b-list-group-item>  
      <b-list-group-item>High: {{l.main.temp_max - 273.15}} C</b-list-group-item>  
      <b-list-group-item>Low: {{l.main.temp_min }}mb</b-list-group-item>  
      <b-list-group-item>Pressure: {{l.main.pressure }}mb</b-list-group-item>  
    </b-list-group>  
  </div>  
</template>

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

export default {  
  name: "Forecast",  
  mixins: [requestsMixin],  
  computed: {  
    keyword() {  
      return this.$store.state.keyword;  
    }  
  },  
  data() {  
    return {  
      forecast: []  
    };  
  },  
  watch: {  
    async keyword(val) {  
      const response = await this.searchForecast(val);  
      this.forecast = response.data;  
    }  
  }  
};  
</script>

<style scoped>  
p {  
  font-size: 20px;  
}  
</style>
```

It’s very similar to `CurrentWeather.vue`. The only difference is that we are getting the current weather instead of the weather forecast.

Next, we create a `mixins` folder in the `src` folder and add:

```javascript
const APIURL = "http://api.openweathermap.org";  
const axios = require("axios");

export const requestsMixin = {  
  methods: {  
    searchWeather(loc) {  
      return axios.get(  
        `${APIURL}/data/2.5/weather?q=${loc}&appid=${process.env.VUE_APP_APIKEY}`  
      );  
    }, 

    searchForecast(loc) {  
      return axios.get(  
        `${APIURL}/data/2.5/forecast?q=${loc}&appid=${process.env.VUE_APP_APIKEY}`  
      );  
    }  
  }  
};
```

These functions are for getting the current weather and the forecast respectively from the OpenWeatherMap API. `process.env.VUE_APP_APIKEY` is obtained from our `.env` file that we created earlier.

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

```javascript
<template>  
  <div>  
    <b-navbar toggleable="lg" type="dark" variant="info">  
      <b-navbar-brand href="#">Weather App</b-navbar-brand>  
    </b-navbar>  
    <div class="page">  
      <ValidationObserver ref="observer" v-slot="{ invalid }">  
        <b-form @submit.prevent="onSubmit" novalidate>  
          <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><br /> 

      <b-tabs>  
        <b-tab title="Current Weather">  
          <CurrentWeather />  
        </b-tab>  
        <b-tab title="Forecast">  
          <Forecast />  
        </b-tab>  
      </b-tabs>  
    </div>  
  </div>  
</template>

<script>  
import CurrentWeather from "@/components/CurrentWeather.vue";  
import Forecast from "@/components/Forecast.vue";

export default {  
  name: "App",  
  components: { CurrentWeather, Forecast },  
  data() {  
    return {  
      form: {}  
    };  
  },  
  methods: {  
    async onSubmit() {  
      const isValid = await this.$refs.observer.validate();  
      if (!isValid) {  
        return;  
      }  
      localStorage.setItem("keyword", this.form.keyword);  
      this.$store.commit("setKeyword", this.form.keyword);  
    }  
  },  
  beforeMount() {  
    this.form = { keyword: localStorage.getItem("keyword") || "" };  
  },  
  mounted(){  
    this.$store.commit("setKeyword", this.form.keyword);  
  }  
};  
</script>

<style>  
html {  
  min-width: 500px;  
}

.page {  
  padding: 20px;  
}  
</style>
```

We add the BootstrapVue `b-navbar` here to add a top bar to show the extension’s name. Below that, we added the form for searching the weather info. 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 for form validation.

If `isValid` resolves to `true` , then we set the `keyword` in local storage, and also in the Vuex store by running `this.$store.commit(“setKeyword”, this.form.keyword);` .

In the `beforeMount` hook, we set the `keyword` so that it will be populated when the extension first loads if a `keyword` was set in local storage. In the `mounted` hook, we set the keyword in the Vuex store so that the tabs will get the `keyword` to trigger the search for the weather data.

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: {  
    keyword: ""  
  },  
  mutations: {  
    setKeyword(state, payload) {  
      state.keyword = payload;  
    }  
  },  
  actions: {}  
});
```

to add the Vuex store that we referenced in the components. We have the `keyword` state for storing the search keyword in the store, and the `setKeyword` mutation function so that we can set the `keyword` in our components.

Next in `popup/main.js` , we replace the existing code with:

```javascript
import Vue from 'vue'  
import App from './App.vue'  
import store from "../store";  
import "bootstrap/dist/css/bootstrap.css";  
import "bootstrap-vue/dist/bootstrap-vue.css";  
import BootstrapVue from "bootstrap-vue";  
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";  
import { required } from "vee-validate/dist/rules";/\* eslint-disable no-new \*/

extend("required", required);  
Vue.component("ValidationProvider", ValidationProvider);  
Vue.component("ValidationObserver", ValidationObserver);  
Vue.use(BootstrapVue);Vue.config.productionTip = false;new Vue({store,  
  render: h => h(App)  
}).$mount("#app");
```

We added the validation rules that we used in the previous files here, as well as include 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 be used by the templates by calling `extend` from Vee-Validate. We called `Vue.use(BootstrapVue)` to use BootstrapVue in our app.

Finally in `index.html` , we replace the existing code with:

```html
<!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>Weather App</title>  
  </head>  
  <body>  
    <noscript>  
      <strong  
        >We're sorry but vue-chrome-extension-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 replace the title.