Building external modules in VueJS

building-external-modules-vuejs-header.png

Learn how take advantage of VueJS' component based architecture to create isolated, reusable code modules. Write Vue modules, then use them in a fully functional app built with Node and Webpack.

Introduction

VueJS is one of the most popular JavaScript frameworks out there and for good reason. Vue makes developing applications easy and fast, and the documentation is very easy to follow.

Vue has a component based architecture that makes it easy to create isolated, reusable components. This is especially useful in large projects where, if no care is taken, things can get pretty confusing.

This article will focus on how you can create external modules for your application.

So let’s get started.

Requirements

To follow along in this tutorial you’ll need:

When you have the requirements listed above, let’s continue.

Why would you need external modules

So why should you create external modules? You can definitely create a module and use it for the project without having to make it external. However, if you are working on it can get bloated easily. With external modules, we can build our modules once and separate them from the codebase, thus keeping it light.

Smaller projects too can benefit from decoupling modules and making them external, though you have to be careful not to over complicate things by creating an external module when you do not need one.

External modules have the following benefits

They are reusable across projects
When we create an external module, we can use the same module across multiple projects. This will make it easier to maintain the code because if you change it once, all you need to do to get the changes across projects is updating your NPM packages.

Make your project, well, modular
Developing modular parts for our application can reduce it’s complexity and make it easier to maintain the separate modules. In larger teams, small groups can be responsible for maintaining certain modules without having to really interact with other parts of the application.

Building an external module

To build our external Vue module, we need to make sure it is one that can be reused across multiple projects and not just specific to a single project.

External modules are commonly referred to as plugins and there is no strictly defined scope for a plugin. There are several types of plugins you can write, and they can perform any the following:

  • Add some global methods or properties. An example of this is vue-custom-element.
  • Add one or more global assets: directives/filters/transitions etc. An example of this is vue-touch.
  • Add some component options by global mixin. An example of this is vue-router.
  • Add some Vue instance methods by attaching them to Vue.prototype.
  • Provide an API of its own, while at the same time injecting some combination of the above. An example of this is vue-router.

When making our external module, we will:

  • Build the component – Every module has a component or a set of components at it’s core.
  • Configure the build system – The build system will compile the module.
  • Deploy the module – Because this has been covered many times, we will link to resources that show how to publish your module to the NPM registry.

Creating a sample module

In order to understand the process we are going to build a sample module. We are going to replicate a user preview feature similar to the one on Twitter where you see the profile when you hover over the profile link:

screenshot of functionality we will replicate

The goal is to to illustrate how to build VueJS modules, not to make a polished finished module. The source to the Module will be linked at the bottom of the article if you want to explore it.

To get started, we need to create a directory to store our module. In that directory, create a package.json file. This file will contain the dependencies we need to build our module. We will be using Webpack to build our package. In the package.json file paste the following code:

1{
2        "main": "src/index.js",
3        "scripts": {
4            "dev": "node_modules/webpack/bin/webpack.js --progress --config build/webpack.config.js",
5            "build": "node build/build.js",
6            "build-docs": "node build/build-docs.js",
7            "build-main": "node build/build-main.js"
8        },
9        "dependencies": {
10            "vue": "^2.5.13"
11        },
12        "devDependencies": {
13            "babel-core": "^6.22.1",
14            "babel-helper-vue-jsx-merge-props": "^2.0.3",
15            "babel-loader": "^7.1.1",
16            "babel-plugin-syntax-jsx": "^6.18.0",
17            "babel-plugin-transform-runtime": "^6.22.0",
18            "babel-plugin-transform-vue-jsx": "^3.5.0",
19            "babel-preset-env": "^1.3.2",
20            "babel-preset-stage-2": "^6.22.0",
21            "chalk": "^2.0.1",
22            "copy-webpack-plugin": "^4.0.1",
23            "css-loader": "^0.28.0",
24            "extract-text-webpack-plugin": "^3.0.0",
25            "file-loader": "^1.1.4",
26            "friendly-errors-webpack-plugin": "^1.6.1",
27            "html-webpack-plugin": "^2.30.1",
28            "webpack-bundle-analyzer": "^2.9.0",
29            "node-notifier": "^5.1.2",
30            "postcss-import": "^11.0.0",
31            "postcss-loader": "^2.0.8",
32            "postcss-url": "^7.2.1",
33            "semver": "^5.3.0",
34            "shelljs": "^0.7.6",
35            "optimize-css-assets-webpack-plugin": "^3.2.0",
36            "uglifyjs-webpack-plugin": "^1.1.1",
37            "url-loader": "^0.5.8",
38            "vue-loader": "^13.3.0",
39            "vue-style-loader": "^3.0.1",
40            "vue-template-compiler": "^2.5.2",
41            "portfinder": "^1.0.13",
42            "webpack": "^3.6.0",
43            "webpack-dev-server": "^2.9.1",
44            "webpack-merge": "^4.1.0"
45        }
46    }

Next, run the following command to install the dependencies:

1$ npm install

This will install the dependencies we specified above. Take note that these dependencies are the basic requirements for setting up our build system and other components needed for our module.

Next, create a src directory and create a file index.js then paste in the following code:

1import Vue from 'vue'
2    import UserPop from './UserPop.vue'
3
4    export default {
5        install(Vue, options) {
6            Vue.component('user-pop', UserPop)
7        }
8    }

This file, src/index.js, is the main entry point for our module and it defines it as a plugin. Next we’ll create the user pop component referenced on line 2.

Create a file named UserPop.vue in the src directory. In that file paste in the following code:

1<template>
2      <div class="inline" v-if="user">
3          <a href="#" class="user-pop" v-on:mouseover="hover" v-on:mouseout="hoverOut">
4            {{ main }}
5          </a>
6          <div class="user-popover" v-if="showPopup" transition="fade" v-on:mouseover="hoverInfo" v-on:mouseout="hoverOutInfo">
7            <div class="user-popover--img" v-bind:style="{ backgroundImage: 'url('+user.profile.profile_image+')' }">
8              <h3 class="img-inner">{{ user.name }}</h3>
9            </div>
10            <div class="col-md-12">
11              <p>{{ user.username }}</p>
12            </div>
13            <div class="col-md-12" v-if="user.email">
14              <p>{{ user.email }}</p>
15            </div>
16          </div>
17        </div>
18    </template>

In the code above we have defined the HTML part of our Vue component. In the same file, paste the following code below the closing template tag:

1<script>
2    export default {
3        props: ['user', 'main'],
4        data() {
5            return {
6                timer: '',
7                isInInfo: false,
8                showPopup: false,
9            }
10        },
11        methods: {
12            hover: () => {
13                this.timer = setTimeout(() => this.showPopover(), 600)
14            },
15            hoverOut: () => {
16                clearTimeout(this.timer);
17                this.timer = setTimeout(() => {
18                    if ( ! this.isInInfo) {
19                        this.closePopover();
20                    }
21                }, 200);
22            },
23            hoverInfo: () => this.isInInfo = true,
24            hoverOutInfo: () => {
25                this.isInInfo = false;
26                this.hoverOut()
27            },
28            showPopover: () => this.showPopup = true,
29            closePopover: () => this.showPopup = false,
30        }
31    }
32    </script>

Finally we add some styling using the style tag. Paste the following below the closing script tag:

1<style>
2    .user-pop{
3      color: inherit;
4      text-decoration: none;
5    }
6    .user-pop:hover{
7        text-decoration: none;
8        color: inherit;
9    }
10    .user-popover{
11            position: absolute;
12            width: 200px;
13            background: #fff;
14            border: none;
15            border-radius: 5px;
16            box-shadow: 0 6px 6px rgba(16, 16, 16, 0.04), 0 6px 6px rgba(0, 0, 0, 0.05);
17            z-index:999;
18            text-align: left;
19     }
20     .user-popover--img{
21      background: rgb(237, 27, 27);
22      background-position: center !important;
23      background-size: cover !important;
24      height: 100px;
25      width: 100%;
26      padding: 12px;
27      text-align:left;
28      vertical-align: bottom;
29    }
30    .user-popover--inner{
31      padding: 10px;
32    }
33    .img-inner{
34      color:rgb(237, 27, 27);
35      font-size: 17px;
36    }
37    </style>

Now we have a standalone UI component. Importable into any Vue project. Now let’s build our module using Webpack.

Building our sample module with Webpack

As it is right now, our module can be used without building it. We can also decide to build it into a single JS file so we can include it into an HTML page using the script tag.

Most of what we need to do concerning publishing the module is done with Webpack. For our module, there are two ways we want it to be used, and we will setup webpack configurations for both instances. These are:

  • Browser-based implementation, using the script tag.
  • Node-based implementation.

To do this, we are going to use a common webpack configuration located in the ./webpack.config.js file:

1const webpack = require('webpack');
2    const merge = require('webpack-merge');
3    const path = require('path');
4
5    var commonConfig = {
6      output:{
7        path: path.resolve(__dirname + '/dist/')
8      },
9      module:{
10        loaders: [...]
11      },
12      externals:{...},
13      plugins:{...}
14    }
15
16    module.exports=[
17
18      // for the browser based implementation
19      merge(commonConfig,{
20
21      }),
22
23      // for the node based implementation
24      merge(commonConfig, {
25
26      })
27    ]

Browser based implementation

Using the plugin file as the base entry point for the application we can specify the output to a file for the browser based implementation, vue-preview.min.js.

Modify the Webpack configuration file to support this using the code below:

1module.exports = [
2      [...]
3
4      // browser based implementation
5      merge(config, {
6        entry: path.resolve(__dirname + '/../src/index.js'),
7        output: {
8          filename: 'vue-preview.min.js'
9        }
10      }),
11
12      [...]
13    ]

This allows us to import the library via HTML. The build system allows us to have our module available as a min.js file, which we can link to in a script tag. We can also use it via the import statement, importing the entry file (src/index.js)

Exporting as a library

Using the libraryTarget option you can have a plugin accessible to the window object. Paste this in the output option of the webpack.config.js file:

1output: {
2      filename: 'vue-preview.min.js',
3      libraryTarget: 'window',
4      library: 'VuePreview'
5    }

With this the module is accessible via the window object using window.VuePreview().

Cleaning up the Package (.json)

To prepare our package is ready for publishing, we need to make sure that our package.json gives information as to what our package does and its entry point. You can copy and paste the JSON code below (everything except the devDependencies section):

1{
2      "name": "preview-component",
3      "version": "1.0.12",
4      "main": "src/index.js",
5      "scripts": {
6        "dev": "node_modules/webpack/bin/webpack.js --progress --config build/webpack.config.js",
7        "build": "node build/build.js",
8        "build-docs": "node build/build-docs.js",
9        "build-main": "node build/build-main.js"
10      },
11      "license": "MIT",
12      "dependencies": {
13        "vue": "^2.5.13"
14      },
15      "devDependencies": {
16        [...]
17      }
18    }

The most important things to note here are:

  • The main option which points to the bundled entry file.
  • The dependencies option that lists the packages needed.

Using the module

So now that our module is complete, here is an example on how we can use it in a Vue application.

First we initalize a vue app using the vue cli tool. If you don’t have it you can install it using the command:

1$ npm install --global @vue/cli

Now you can initialize a Vue project using the command:

1$ vue init webpack sampleapp

? Vue-CLI will show some option settings, you can decide to use the defaults or customize as you wish.

Next we proceed to structure our app:
We intend to create a minimalist chat interface with, with the usernames on the right. On hovering over the names the preview modal is supposed to pop up.

First we add the preview-component. At the moment its hosted in GitHub, but can be linked to a project using the git+ssh link as the repository address:

1{
2      [...]
3      "dependencies": {
4        [...]
5        "preview-component": "git+https://git@github.com/neoighodaro-articles/Vue-Slack-Preview.git",
6        [...]
7      },
8    }

? You should clone the repository and upload the code to GitHub then set as the dependency for your application.

As seen above, we simply added the preview-component to the dependencies option of the package.json file created by Vue CLI. You should run the command below to install the component:

1$ npm install

In the ./app/main.js file add following before the new Vue instance:

1// [...]
2    import PreviewComponent from "preview-component";
3
4    // [...]
5
6    Vue.use(PreviewComponent);
7
8    // [...]

In the sample code above, you can see where the package is imported as PreviewComponent.

After it’s imported, it’s linked to the Vue instance with the use method. This makes it available to Vue globally. Next, we create our Chat and Message components. The Chat component will hold the messages which the Message component will use to display messages. The Message component will be using the p``review-component to show a preview of the users profile.

Let’s create the chat component. Create a new file src/components/ChatComponent.vue and paste in the following code:

1<template>
2      <div class="chat-main">
3        <Message v-for="message in messages" :key="message.username" :message="message"/>
4      </div>
5    </template>
6
7    <script>
8    import Message from './Message'
9    export default {
10      data(){
11        return {
12          messages:[
13            {
14              user: {
15                  name: 'Neo Ighodaro',
16                  username: 'neoighodaro', 
17                  email: 'neo@gmail.com',
18                  profile: {
19                      profile_image:'https://avatars3.githubusercontent.com/u/807318'
20                  },
21              },
22              message: 'How have you been'
23            },
24            {
25              user: {
26                  name: 'Osita Chibuike',
27                  username: 'mozartted', 
28                  email: 'mozart@gmail.com', 
29                  profile: {
30                      profile_image:'https://avatars2.githubusercontent.com/u/11639772'
31                  },
32              },
33              message: "I'm alright, This is an article right??"
34            },
35            {
36              user: {
37                  name: 'Neo Ighodaro',
38                  username: 'neoighodaro',
39                  email: 'neo@gmail.com',
40                  profile: {
41                      profile_image:'https://avatars3.githubusercontent.com/u/807318'
42                  },
43              },
44              message: 'Yea, I think so'
45            },
46          ]
47        }
48      },
49      components: { 
50          Message
51      }
52    }
53    </script>
54
55    <style>
56    .chat-main {
57      width:500px;
58      height: auto;
59      box-shadow: 0px 1px 4px #202020;
60      border: none;
61      padding: 12px;
62      margin:0 auto;
63    }
64    </style>

Next, let’s create the message component. Create a new file src/components/MessageComponent.vue and paste in the following code:

1<template>
2      <div class="chat-message">
3        <div class="chat-side--bar">
4          <user-pop :user="message.user" :main="message.user.name"></user-pop>
5        </div>
6        <div class="chat-inner--sec">
7          {{message.message}}
8        </div>
9      </div>
10    </template>
11
12    <script>
13    export default {
14      props: ['message'],
15      data() {
16        return {
17        }
18      }
19    }
20    </script>
21
22    <style>
23    .chat-message {
24      width: 100%;
25      border:none;
26      box-shadow: 0px 1px 4px #202020;
27      padding: 4px;
28      min-height: 50px;
29      margin-top:12px;
30    }
31    .chat-side--bar {
32      width:20%;
33    }
34    .chat-inner--sec {
35      width: 80%;
36    }
37    </style>

This component accepts a prop which we use to display the message. The prop also contains the user which contains the user details.

The component then uses the UserPop component which has been defined in the entry file of the imported module to create the preview functionality.

To preview simply run npm run dev and checkout the served URL displayed on the terminal. Here is a sample of it in action:

finished app

Publishing your module to NPM

You can decide to publish your module to NPM if you want it to be available to other developers. Here is a great video that shows how you can do this:

https://youtu.be/BkotrAFtBM0

You can also follow along using the tutorial on NPM’s official website here.

A note on abstraction

External modules make code reuse easier, but their real power is displayed when they have been used from the beginning of a large project.

Consider a series of applications with similar frontend feature sets. For example, Google, and its suite of applications including Gmail, Youtube, Newspaper, Calendar etc. They all have a design standard that makes them similar in many ways. These similar UI features and sections can be abstracted as external modules used by the in-house team to build a generic UI for any of their products.

In order to always maintain the Google standard and style, it’s important to understand when it’s advisable architecture wise to abstract the UI with the intention of creating a developer friendly implementation and environment for building new products.

Many have taken these thought process and created implementations of generic UI elements of some of the most popular apps around. For example https://github.com/airyland/vux, implements most of WeUI components in a smooth style.

Conclusion

This tutorial has shown how easy it is to create external modules with Vue.js, allowing developers to make their frontend components reusable and shareable.

External modules solve a large variety of problems and thus speed up the development process of subsequent applications.

The source code for the module and its sample usage are available on GitHub here and here.