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
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. 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.
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 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:
<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:
<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:
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:
<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:
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:
<!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.