Web Components with Vue.js framework

by Javier Puerto
Tags: Open Source , Web , Web Components

In our previous post, we showcased a small example with Web Components and the xtag library. We brought component-based development to the Web, an approach commonly used in Non-Web UI developments and frameworks. In this post we focus on the component-based development pattern and implement our be-dog and be-cat components with Vue.js.

Tools to build something bigger. Building with Web Components. (Foto: pexels.com)

Components-based software engineering (CBSE)

This programming paradigm emphasizes the separation of concerns and promotes the reusability across components that share common functionality. The components should be loosely coupled, allowing them to be reused in different projects or scenarios. In the previous post be-cat was basically a copy of the be-dog component. Then we identified the differences and with the help of Web Components features, we were able to abstract the base functionality into a be-media component.

What is Vue.js?

Vue is a progressive framework for building user interfaces. It is designed from the ground up to be incrementally adoptable, and can easily scale between a library and a framework depending on different use cases. It consists of an approachable core library that focuses on the view layer only, and an ecosystem of supporting libraries that helps you tackle complexity in large Single-Page Applications.

Vue logo. Vue.js logo.

Vue.js does not work directly with Web Component related APIs, instead it takes the specs as a reference and implement it their own. This is something that I should not recommend as the code will depend on the framework instead of the HTML standards. But since Web Components are currently not widely adopted, Vue.js allow us to write beautiful apps based in CBSE paradigms. Also, it includes certain facilities that Web Components does not provide. And as we will see at the end of the article, we can create Web Components based in Vue.js framework but due to an issue we can not create Web Components from Vue.js at this moment.

Defining our components

Let's define the following components for Vue.js. If you followed our previous post, you will be familiar with the structure. Also separation of concerns is fulfilled here.

  • be-media: component that displays an image or video passed to the src attribute.
  • be-dog: component that retrieves a random dog image or video and display it.
  • be-cat: component that retrieves a random cat image or video and display it.

Our components hierarchy:

be-media
├ be-dog
└ be-cat

Creating projects with vue-cli

As we just identified our components, let's start implementing them. Also we have the code from our previous post that we could reuse somehow.

We could create all the component by hand but Vue.js has an excellent tool to simplify this task: vue-cli.

First we will have to install the tool.

npm install -g @vue/cli
# OR
yarn global add @vue/cli

Then we can start creating our Vue.js projects with:

vue create my-project
# OR
vue ui

The first command will prompt us with some questions in the CLI while the second command will show a UI Web View that will allow us to do the same, plus some interesting options to manage the project. That is outside the scope of our tutorial but I encourage you to try the ui option as it is very powerful.

Creating the project

I can not hide my preferences for TypeScript rather than plain JavaScript, so we are going to use it to create our components. When vue-cli asks for the preset, just select manual and include TypeScript. We leave the remaining options to it's default values.

$ vue create vue-web-components-tutorial
Vue CLI v3.0.0
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Linter
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript for auto-detected polyfills? Yes
? Pick a linter / formatter config: TSLint
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

Now we are ready to test our new project, running the suggested command leads to a sample web app that we can open in the browser.

 $ cd be-media
 $ npm run serve

Now it is turn to start implementing the component itself. By default, vue-cli creates a structure as follows:

babel.config.js
node_modules
package.json
package-lock.json
public
├── favicon.ico
└── index.html
README.md
src
├── App.vue
├── assets
│   └── logo.png
├── components
│   └── HelloWorld.vue
├── main.ts
├── shims-tsx.d.ts
└── shims-vue.d.ts
tsconfig.json
tslint.json
  • babel.config.js contains the transpilation configuration for babel tool.
  • package.json contains the project definition, build configuration and dependencies.
  • public directory contains the files to serve the application demo.
  • src directory contains the vue.js components, assets and main application.
  • tsconfig.json contains the typescript configuration for compile the source code.
  • tslint.json contains the tslint configuration for code inspection.

Implementing be-media component

We can safely delete the HelloWorld.vue file with the example component and start creating the be-media component. As seen in our previous post, this component just takes a src attribute and renders an image or a video tag based on the attribute extension. Let's see how we implement it with Vue.js.

<template>
  <img v-if="isImage()" v-bind:src="src"/>
  <video v-else autoplay> <source v-bind:src="src" type="video/mp4"> </video>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class BeMedia extends Vue {
  @Prop() protected src!: string;
  private isImage() {
    return this.src.endsWith('mp4') ? false : true;
  }
}
</script>

<style>
</style>

Vue.js components can be defined across multiple files or use a single file with a .vue extension. We choose to keep everything in a single file as the implementation is not complex. Here we can identify the different sections of the component.

  • template tag keeps the view layer. For our use case, we simply use the directive v-if in combination with v-else (all vue.js directives starts with the prefix v-). The directive calls the component's method isImage that returns true or false based on the content of the attribute src. Finally, both templates binds the src attribute to the HTML tag's attribute.
  • script tag keeps the model and controller layer. Here we can define the component behaviour and model. We are using TypeScript for the implementation, so we can use annotations and classes to define our component. In this case we define a mandatory property, denoted by the annotation and the ! character. There is also a method to determine the type of the source.
  • style tag allows to define the styles for our component. Vue.js can define scoped styles for our component or on the other hand, allow them to affect other elements outside the scope of the component. For our use case, we do not need any styles.

If we compare the Vue.js implementation with the X-Tag implementation, we notice that we do not need to define the class attribute. The reason is because Vue.js automatically binds the class attribute to the root element of our custom component.

Implementing be-dog and be-cat components

First of all, we have to install the axios library to make requests to the different services.

$ npm install --save-dev axios

Then we can create our implementations easily extending from the be-media component.

<template>
    <be-media v-bind:src="src" @click.native="refresh()"/>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import BeMedia from './BeMedia.vue';
import Axios from 'axios';

@Component({
components: {
  BeMedia,
},
})

export default class BeDog extends BeMedia {

  constructor() {
    super();
    this.refresh();
  }

  public refresh() {
    Axios.get('https://random.dog/woof.json')
      .then((response) => this.src = response.data.url)
      .catch((error) => console.error(error));
  }
}
</script>

<style>
</style>

This time we are using the be-media component in the template and binding the src attribute to the component's attribute. For the click event, we use the @ shorthand for the v-on directive used for event binding. As we did not expose any events in the be-media component, we will use the sufix .native to bind the native click event to the root element.

For the controller, we just have to import the be-media compoenent and the axios library. We define a new public method refresh to retrieve a random dog image and update the src attrivute with the value. As we want to initially display a dog or a cat, we override also the default constructor to call the refreshmethod.

The code shows the implementation for the be-dog component but be-cat component is similar.

Testing the new components

Using the example implementation, we are going to update it to display our new components and play a bit with the dog and cat images. The entry point for the build is the file main.ts. Looking at the code, it takes the App.vue file as main component and then bootstraps the Vue.js framework.

We also want to provide some styles to the new contents so again we are going to use the bootstrap framework. Vue provides a bootstrap integration that can be installed with:

$ npm install --save-dev bootstrap-vue

Then we can update our main app to include bootstrap and our new components.

<template>
  <div id="app" class="container">
    <div class="row">
      <div class="col-6">
        <be-dog class="img-fluid"/>
      </div>
      <div class="col-6">
        <be-cat class="img-fluid"/>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import BeDog from './components/BeDog.vue';
import BeCat from './components/BeCat.vue';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';

@Component({
  components: {
    BeDog,
    BeCat,
  },
})
export default class App extends Vue {}
</script>

<style>
</style>

We can now test our application by just running:

$ npm run serve

Creating a Web Component

Unfortunately the Web Component wrapper did not work as expected and I had to open an issue in their bug tracker. I will update the blog post in case my issue gets resolved.

Check the issue in the vue-web-component-wrapper repository.

Thanks for reading

You can find the repository for all components developed in this blog post on GitHub.

I hope you enjoyed and learnt something from this tutorial. If not you may at least had fun with all the dogs and cats. See you in the next blog post.