DEV Community

John Au-Yeung
John Au-Yeung

Posted on • Updated on

Create Web Components with Vue.js

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

Component-based architecture is the main architecture for front end development today. The World Wide Web Consortium (W3C) has caught up to the present by creating the web components API. It lets developers build custom elements that can be embedded in web pages. The elements can be reused and nested anywhere, allowing for code reuse in any pages or apps.

The custom elements are nested in the shadow DOM, which is rendered separately from the main DOM of a document. This means that they are completely isolated from other parts of the page or app, eliminating the chance of conflict with other parts,

There are also template and slot elements that aren’t rendered on the page, allowing you to reused the things inside in any place.

To create web components without using any framework, you have to register your element by calling CustomElementRegistry.define() and pass in the name of the element you want to define. Then you have to attach the shadow DOM of your custom element by calling Element.attachShawdow() so that your element will be displayed on your page.

This doesn’t include writing the code that you want for your custom elements, which will involve manipulating the shadow DOM of your element. It is going to be frustrating and error-prone if you want to build a complex element.

Vue.js abstracts away the tough parts by letting you build your code into a web component. You write code by importing and including the components in your Vue components instead of globally, and then you can run commands to build your code into one or more web components and test it.

We build the code into a web component with Vue CLI by running:

npm run build -- --target wc --inline-vue --name custom-element-name

The --inline-vue flag includes a copy of view in the built code, --target wc builds the code into a web component, and --name is the name of your element.

In this article, we will build a weather widget web component 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.

We will use Vue.js to build the web component. To begin building it, we start with creating the project with Vue CLI. Run npx @vue/cli create weather-widget to create the project. In the wizard, select Babel, SCSS 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 install some packages that we need for building the web component. 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";  
import store from "../store";  
import { BListGroup, BListGroupItem } from "bootstrap-vue";  
import 'bootstrap/dist/css/bootstrap.css'  
import 'bootstrap-vue/dist/bootstrap-vue.css'

export default {  
  store,  
  name: "CurrentWeather",  
  mounted() {},  
  mixins: [requestsMixin],  
  components: {  
    BListGroup,  
    BListGroupItem  
  },  
  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 is 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.

Note that we’re importing all the BootstrapVue components individually here. This is because we aren’t building an app. main.js in our project will not be run, so we cannot register components globally by calling Vue.use. Also, we have to import the store here, so that we have access to the Vuex store in the component.

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";  
import store from "../store";  
import { BListGroup, BListGroupItem } from "bootstrap-vue";  
import 'bootstrap/dist/css/bootstrap.css'  
import 'bootstrap-vue/dist/bootstrap-vue.css'

export default {  
  store,  
  name: "Forecast",  
  mixins: [requestsMixin],  
  components: {  
    BListGroup,  
    BListGroupItem  
  },  
  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](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 , 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";  
import store from "./store";  
import {  
  BTabs,  
  BTab,  
  BButton,  
  BForm,  
  BFormGroup,  
  BFormInvalidFeedback,  
  BNavbar,  
  BNavbarBrand,  
  BFormInput  
} from "bootstrap-vue";  
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";  
import { required } from "vee-validate/dist/rules";  
extend("required", required);

export default {  
  store,  
  name: "App",  
  components: {  
    CurrentWeather,  
    Forecast,  
    ValidationProvider,  
    ValidationObserver,  
    BTabs,  
    BTab,  
    BButton,  
    BForm,  
    BFormGroup,  
    BFormInvalidFeedback,  
    BNavbar,  
    BNavbarBrand,  
    BFormInput  
  },  
  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 lang="scss">  
@import "./../node_modules/bootstrap/dist/css/bootstrap.css";  
@import "./../node_modules/bootstrap-vue/dist/bootstrap-vue.css";  
.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 to check for form validity.

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.

Like in the previous components, we import and register all the components and the Vuex store in this component, so that we can use the BootstrapVue components here. We also called Vee-Validate’s extend function so that we can use its required form validation rule for checking the input.

In style section of this file, we import the BootstrapVue styles, so that they can be accessed in this and the child components. We also add the page class so that we can add some padding to the page.

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.

Finally, in package.json , we add 2 scripts to the scripts section of the file:

"wc-build": "npm run build -- --target wc --inline-vue --name weather-widget","wc-test": "cd dist && live-server --port=8080 --entry-file=./demo.html"

The wc-build script builds our code into a web component as we described before, and the wc-test runs a local web server so that we can see what the web component looks like when it’s included in a web page. We use the live-server NPM package for serving the file. The --entry-file option specifies that we server demo.html as the home page, which we get when we run npm run wc-build .

Top comments (6)

Collapse
 
brianlovega profile image
Brian Loveless

Awesome article, love the explanations. thewebdev.info is now bookmarked as there is a lot for me to learn still. But as a newbie, I have a few questions with following along with this example.

When you say "create a .env file in the root folder and add VUE_APP_APIKEY as the key and the API key as the value."

Does it matter what we name it? And I know security is at the forefront of many web issues but, what can someone really do with my free API key for free weather info?
Run up the usage so that the site doesn't work? There is no credit card info or anything given for the free key so why hide it? And if it is in a file that GitHub ignores how do we have the key available to use when the site is deployed?

Also wondering what do we call the file used here... "Next, we create a mixins folder in the src folder and add: ..."

Since you can't just add code to an empty folder.

I have also followed along with all the installs and steps and at the end of the article, I do not seem to have a "store.js" file to replace any code. Do I just create it? if so where?

Is the full code / example available anywhere?
Is there a live demo version?

Sorry to be a pest but I would love to view the weather with VUE and this is one of the closest to complete articles I have found.

Collapse
 
aumayeung profile image
John Au-Yeung

Thanks so much for reading.

As long as the env variable starts with VUE_ it's fine.

Yes. Whatever you don't have you create it in the folder specified.

The OpenWeatherApi has a free version so you can use it for free.

I can send you the link for the code repo once I find it.

I am happy that you read stuff and I am happy to answer your questions.

Collapse
 
cappuccino32 profile image
Andreas ☕💻🏟

Thank you very much for this detailed article, which is very helpful to me. Is it possible to get a link to the code or repository?

Collapse
 
aumayeung profile image
John Au-Yeung

Hi Andreas, thanks so much for reading. I'll find it and link it here.

Collapse
 
patrafter profile image
Marek Be

Hi John

Very informative article. It would be helpful if you linked your repository :)

Thread Thread
 
aumayeung profile image
John Au-Yeung

Thanks for reading.

The repo is at bitbucket.org/hauyeung/vue-web-com...