Getting Started with Vue Plugins

In the last months, I’ve learned a lot about Vue. From building SEO-friendly SPAs to crafting killer blogs or playing with transitions and animations, I’ve experimented with the framework thoroughly.

But there’s been a missing piece throughout my learning: plugins.

Most folks working with Vue have either comes to rely on plugins as part of their workflow or will certainly cross paths with plugins somewhere down the road. Whatever the case, they’re a great way to leverage existing code without having to constantly write from scratch.

Many of you have likely used jQuery and are accustomed to using (or making!) plugins to create anything from carousels and modals to responsive videos and type. We’re basically talking about the same thing here with Vue plugins.

So, you want to make one? I’m going to assume you’re nodding your head so we can get our hands dirty together with a step-by-step guide for writing a custom Vue plugin.

First, a little context…

Plugins aren’t something specific to Vue and — just like jQuery — you’ll find that there’s a wide variety of plugins that do many different things. By definition, they indicate that an interface is provided to allow for extensibility.

Brass tax: they’re a way to plug global features into an app and extend them for your use.

The Vue documentation covers plugins in great detail and provides an excellent list of broad categories that plugins generally fall into:

  1. Add some global methods or properties.
  2. Add one or more global assets: directives/filters/transitions etc.
  3. Add some component options by global mixin.
  4. Add some Vue instance methods by attaching them to Vue.prototype.
  5. A library that provides an API of its own, while at the same time injecting some combination of the above.

OK, OK. Enough prelude. Let’s write some code!

What we’re making

At Spektrum, Snipcart’s mother agency, our designs go through an approval process, as I’m sure is typical at most other shops and companies. We allow a client to comment and make suggestions on designs as they review them so that, ultimately, we get the green light to proceed and build the thing.

We generally use InVision for all this. The commenting system is a core component in InVision. It lets people click on any portion of the design and leave a comment for collaborators directly where that feedback makes sense. It’s pretty rad.

As cool as InVision is, I think we can do the same thing ourselves with a little Vue magic and come out with a plugin that anyone can use as well.

The good news here is they’re not that intimidating. A basic knowledge of Vue is all you need to start fiddling with plugins right away.

Step 1. Prepare the codebase

A Vue plugin should contain an install method that takes two parameters:

  1. The global Vue object
  2. An object incorporating user-defined options

Firing up a Vue project is super simple, thanks to Vue CLI 3. Once you have that installed, run the following in your command line:

$ vue create vue-comments-overlay
# Answer the few questions
$ cd vue-comments-overlay
$ npm run serve

This gives us the classic “Hello World” start we need to crank out a test app that will put our plugin to use.

Step 2. Create the plugin directory

Our plugin has to live somewhere in the project, so let’s create a directory where we can cram all our work, then navigate our command line to the new directory:

$ mkdir src/plugins
$ mkdir src/plugins/CommentsOverlay
$ cd src/plugins/CommentsOverlay

Step 3: Hook up the basic wiring

A Vue plugin is basically an object with an install function that gets executed whenever the application using it includes it with Vue.use().

The install function receives the global Vue object as a parameter and an options object:

// src/plugins/CommentsOverlay/index.js
// 
export default {
  install(vue, opts){   
    console.log('Installing the CommentsOverlay plugin!')
    // Fun will happen here
  }
}

Now, let’s plug this in our “Hello World” test app:

// src/main.js
import Vue from 'vue'
import App from './App.vue'
import CommentsOverlay from './plugins/CommentsOverlay' // import the plugin

Vue.use(CommentsOverlay) // put the plugin to use!

Vue.config.productionTip = false

new Vue({ render: createElement => createElement(App)}).$mount('#app')

Step 4: Provide support for options

We want the plugin to be configurable. This will allow anyone using it in their own app to tweak things up. It also makes our plugin more versatile.

We’ll make options the second argument of the install function. Let’s create the default options that will represent the base behavior of the plugin, i.e. how it operates when no custom option is specified:

// src/plugins/CommentsOverlay/index.js

const optionsDefaults = {
  // Retrieves the current logged in user that is posting a comment
  commenterSelector() {
    return {
      id: null,
      fullName: 'Anonymous',
      initials: '--',
      email: null
    }
  },
  data: {
    // Hash object of all elements that can be commented on
    targets: {},
    onCreate(created) {
      this.targets[created.targetId].comments.push(created)
    },
    onEdit(editted) {
      // This is obviously not necessary
      // It's there to illustrate what could be done in the callback of a remote call
      let comments = this.targets[editted.targetId].comments
      comments.splice(comments.indexOf(editted), 1, editted);
    },
    onRemove(removed) {
      let comments = this.targets[removed.targetId].comments
      comments.splice(comments.indexOf(removed), 1);
    }
  }
}

Then, we can merge the options that get passed into the install function on top of these defaults:

// src/plugins/CommentsOverlay/index.js

export default {
  install(vue, opts){
    // Merge options argument into options defaults
    const options = { ...optionsDefaults, ...opts }
    // ...
  }
}

Step 5: Create an instance for the commenting layer

One thing you want to avoid with this plugin is having its DOM and styles interfere with the app it is installed on. To minimize the chances of this happening, one way to go is making the plugin live in another root Vue instance, outside of the main app’s component tree.

Add the following to the install function:

// src/plugins/CommentsOverlay/index.js

export default {
  install(vue, opts){
    ...
  // Create plugin's root Vue instance
      const root = new Vue({
        data: { targets: options.data.targets },
        render: createElement => createElement(CommentsRootContainer)
      })

      // Mount root Vue instance on new div element added to body
      root.$mount(document.body.appendChild(document.createElement('div')))

      // Register data mutation handlers on root instance
      root.$on('create', options.data.onCreate)
      root.$on('edit', options.data.onEdit)
      root.$on('remove', options.data.onRemove)

      // Make the root instance available in all components
      vue.prototype.$commentsOverlay = root
      ...
  }
}

Essential bits in the snippet above:

  1. The app lives in a new div at the end of the body.
  2. The event handlers defined in the options object are hooked to the matching events on the root instance. This will make sense by the end of the tutorial, promise.
  3. The $commentsOverlay property added to Vue’s prototype exposes the root instance to all Vue components in the application.

Step 6: Make a custom directive

Finally, we need a way for apps using the plugin to tell it which element will have the comments functionality enabled. This is a case for a custom Vue directive. Since plugins have access to the global Vue object, they can define new directives.

Ours will be named comments-enabled, and it goes like this:

// src/plugins/CommentsOverlay/index.js

export default {
  install(vue, opts){

    ...

    // Register custom directive tha enables commenting on any element
    vue.directive('comments-enabled', {
      bind(el, binding) {

        // Add this target entry in root instance's data
        root.$set(
          root.targets,
          binding.value,
          {
            id: binding.value,
            comments: [],
            getRect: () => el.getBoundingClientRect(),
          });

        el.addEventListener('click', (evt) => {
          root.$emit(`commentTargetClicked__${binding.value}`, {
            id: uuid(),
            commenter: options.commenterSelector(),
            clientX: evt.clientX,
            clientY: evt.clientY
          })
        })
      }
    })
  }
}

The directive does two things:

  1. It adds its target to the root instance’s data. The key defined for it is binding.value. It enables consumers to specify their own ID for target elements, like so : <img v-comments-enabled="imgFromDb.id" src="imgFromDb.src" />.
  2. It registers a click event handler on the target element that, in turn, emits an event on the root instance for this particular target. We’ll get back to how to handle it later on.

The install function is now complete! Now we can move on to the commenting functionality and components to render.

Step 7: Establish a “Comments Root Container” component

We’re going to create a CommentsRootContainer and use it as the root component of the plugin’s UI. Let’s take a look at it:

<!-- 
 src/plugins/CommentsOverlay/CommentsRootContainer.vue -->

<template>
  
</template> import CommentsOverlay from "./CommentsOverlay"; export default { components: { CommentsOverlay }, computed: { targets() { return this.$root.targets; } } };

What’s this doing? We’ve basically created a wrapper that’s holding another component we’ve yet to make: CommentsOverlay. You can see where that component is being imported in the script and the values that are being requested inside the wrapper template (target and target.id). Note how the target computed property is derived from the root component’s data.

Now, the overlay component is where all the magic happens. Let’s get to it!

Step 8: Make magic with a “Comments Overlay” component

OK, I’m about to throw a lot of code at you, but we’ll be sure to walk through it:

<!--  src/plugins/CommentsOverlay/CommentsRootContainer.vue -->

<template>
  

{{ getCommentMetaString(comment) }}