Use Vue.js to create custom web components


Web Components let you define new HTML tags, referred to as custom elements. These tags can then be used in your app’s HTML code directly, like this :

<div id="my-app">
  <p>Introduction text</p>
  <share-buttons/>
</div>

In this example, <share-buttons/> will be interpreted by the browser and “replaced” by the HTML markup you have defined. This will result in :

<div id="my-app">
  <p>Introduction text</p>
  <share-buttons>
    <div id="share-buttons">
      <a href="#">Facebook</a>
      <a href="#">Twitter</a>
    </div>
  </share-buttons>
</div>

It may also include custom JS logic, for instance the Facebook and Twitter links could listen to click events and share the current page when a link is clicked.

Web components are similar to Vue.js components. They have a lifecycle, properties, and can be nested. They have a different API that is less powerful but standard, defined by W3C specs.

Problem : web components are not fully supported by browsers yet. See browser support for Web Components on are-we-componentized-yet or caniuse.com

But, with a bit of JS magic you can now turn your Vue.js component into web components, enabling you to use it in any web application, even using React, Angular or <name-your-favorite-framework>.

How to turn your Vue.js component into universal web components

vue-custom-element is a library written by @karol-f that allows you to use a Vue.js component as a custom element.

The HTML way

Once you register the custom element, you can insert its tag into standard HTML, SPA’s, React, Angular or Vue.js projects.

<div id="my-app">
  <p>Introduction text</p>
  <share-buttons gplus="false"/>
</div>

<template id="share-buttons-template">
  <div id="share-buttons">
    <a href="#" @click.prevent="share" v-if="facebook">Facebook</a>
    <a href="#" @click.prevent="share" v-if="twitter">Twitter</a>
    <a href="#" @click.prevent="share" v-if="gplus">Google+</a>
  </div>
</template>

<script src="vue.min.js"></script>
<script src="vue-custom-element.min.js"></script>
<script>
Vue.customElement('share-buttons', {
  template: '#share-buttons-template',
  props: {
    facebook: { type: Boolean, default: true },
    twitter: { type: Boolean, default: true },
    gplus: { type: Boolean, default: true }
  },
  methods: {
    share ($event) {
      window.alert('Share on ' + $event.target.innerHTML);
    }
  }
});
</script>

This implementation requires Vue.js core files and the vue-custom-element library to be included in your page.

Props are automatically interpreted to their native type (“false” as an attribute is interpreted as the boolean value false).

The custom element’s API is accessible like any HTML element : document.getElementsByTagName('share-buttons').

This example uses an HTML <template> tag but you can pass the template string directly to the template property of your component.

Pros and cons

This implies two things :

  • script dependencies have to be included in the final HTML file
  • the component behavior is readable directly from the source code

These can be good or bad things, depending on your use case.

A common use case I can imagine is distributing a Vue.js component in form of a widget across multiple websites. So let’s bundle all this code in a single file.

Bundle Vue.js components in a single .js file

Check the vue-customelement-bundler repository that contains :

  • Webpack configuration
  • This example component code (ES2015 in a .vue file)
  • NPM dependencies to get your own up and running

It takes Vue.js components code (in form of .vue files) and output a single .js file embedding Vue itself, the vue-custom-element lib and your Vue.js components, registered to be used as custom elements. You can then use your components in any HTML/JS app, like this :

<html>
  <body>

    ...

    <my-vue-component/>

    <!-- allows multiples instances of the same component -->
    <my-vue-component my-prop="true"/>

    <script src="my-vue-component.js"></script>

  </body>
</html>

Note : The output file weights 266kB. This is too much. I have tried to minify it but my Webpack skills don’t go this far. UglifyJsPlugin threw me an error I couldn’t solve. So if you can, please let me know how to optimize my setup, I am sure it could get much better.

Edit 5/4 : Thanks to @anthonygore and UglifyJS, the output file weights 113kB.

Edit 6/4 : Anthony on fire, gets another 22kB off the bundle. The output file now weights 91kB (32kB gzipped) !

Edit 11/4 : @chimon2000 offered an alternative using rollup.js, the file weights 76kB (24kB gzipped), thanks Ryan ! Check it out on the rollup branch of the repo.

A bit about Web Components API

This section is not essential to use Vue.js components as web components but it is often good to know how things work to write better code.

Web Components include the following specs :

Custom Element

Custom Element is the main API that allows developers to define a new HTML tag that can be interpreted by the browser.

It features lifecycle callbacks (aka reactions) :

  • constructor itself (element upgraded, meaning inserted in the DOM via JS or already in the DOM)
  • connectedCallback (inserted in the DOM)
  • disconnectedCallback (removed from the DOM)
  • adoptedCallback (moved into a new document)
  • attributeChangedCallback

Sounds familiar ? Yes, this does look like the Vue.js components lifecycle !

However, writing custom elements is much more verbose. One of the reasons is that you don’t benefit from Vue.js’s reactive properties magic, or the sweet template syntax such as v-if statements.

Registering a custom element is done by using window.customElements.define

class MyElement extends HTMLElement {

  constructor() {
    super();
    this.msg = 'Hello, World!';
  }

  connectedCallback() {
    this.innerHTML = `<p>${this.msg}</p>`;
  }
}

window.customElements.define('my-element', MyElement);

Note that connectedCallback is called when the element is inserted

Also, the component’s tag has to meet a few requirements :

  • contain an hyphen
  • not include uppercase letters
  • not be one of the restricted names (annotation-xml, color-profile, etc.)

More details in the W3C specs

HTML Template

The custom element’s markup can be embedded in a <template> tag inside the DOM.

<template id="share-buttons-template">
  <div id="share-buttons">
    <a href="#">Facebook</a>
    <a href="#">Twitter</a>
  </div>
</template>

This markup can be imported and cloned into the custom element on createdCallback.

HTML Import

Defines HTML markup and JS logic in a single file that can be imported in your app with a single <link> tag.

<html>
  <head>
    <link rel="import" href="share-buttons.html">
    ...
  </head>
  <body>
    ...
    <share-buttons></share-buttons>
    ...
  </body>
</html>

Content of share-buttons.html :

<html>
  <template>
    <div id="share-buttons">
      <a href="#">Facebook</a>
      <a href="#">Twitter</a>
    </div>
  </template>
  <script>
    (function() {
      ...
      document.registerElement('share-buttons', { prototype: MyCustomElement });
    });
  </script>
</html>

Sounds more and more familiar, doesn’t it ?

Components communication

Component communicate with its host using events, just as in Vue.js. Events are emitted using the dispatchEvent method of HTML elements.

this.dispatchEvent(new Event('content-shared'));

Host can communicate with the component in two ways :

  • using attributes
  • using the component prototype’s methods

The first method is the same as in Vue.js. You can set attributes, they just won’t be reactive so you may have to use attributeChangedCallback of you want to trigger logic when an attribute’s value is changed.

The second method works in Vue.js, although it is not recommended.

Support and polyfills

Current support for these can be followed on caniuse.com or are-we-componentized-yet.

Polyfill libraries allow you to use these APIs in browsers that don’t support them yet. Here are some links :

Note : I haven’t mentioned Shadow DOM in this post. I thought it would confuse developers that focus on porting their Vue.js components to custom elements. Check out the links at the end of the post for more resources.

Latest posts

Use a global event bus instead of a state manager

Emit and listen to events across every Vue components of your app

Vue.js Component Style Guide

General purpose recommandations for writing Vue.js components code

How to use Docker containers for Vue.js applications

Get started with Docker and Vue.js

Hide elements during loading using "v-cloak"

You probably need this directive and didn't know it

Using Bootstrap with Vue.js

Question is : do you really need Boostrap ?

Build, test and deploy your Vue.js app easily with Gitlab

Introduction to Continuous Integration using free Gitlab tools

Another way to declare Vuex modules

`vuex-module` provides a syntax to write your store modules' state, actions, getters, etc. You may want to use it.

Simple state management, simpler than Vuex

Vue Stash makes it easy to share reactive data between components. An alternative to Vuex.

Hybrid Vue.js app in Cordova : desktop or mobile ?

A simple hack to know if the code is executed in a website or in the Cordova version of your app

Does Vue.js require jQuery ?

No it doesn't. But you might find it useful, here's why.