
Guide to Vue.js for jQuery developers
Introduction to writing Vue.js components for developers with jQuery background.
Today we’re going to create a simple Vue.js component based on a jQuery dropdown button, in order to learn the most important differences between these technologies.
As I wrote last week, Vue.js can be used not only to create large applications, but also for simple components. I also showed you how to transform your .vue files into a “bundle” which can be loaded by the browser, so that you can get started with Vue.js. If you’re not familiar with this already, make sure you read that article first.
Mixing Vue.js and jQuery
Before we start, I’d like to answer the following question: Isn’t it possible to mix Vue.js and jQuery on the same page?
There is nothing “magical” in Vue.js, it translates to plain JavaScript and can be mixed together with any other libraries, including jQuery. Nothing stops you from using Vue.js and jQuery components on the same page.
But each library in your bundle adds a certain cost. It may not seem large, jQuery is about 95 kilobytes and the Vue.js runtime is about 55 kilobytes. However, it all adds up to the total size of your application and increases the time needed to load and parse the code.
Also you might be tempted to use jQuery to manipulate DOM in your Vue.js components, because that may seem easier or it might just be something that you’re used to.
However, Vue.js has an internal representation of all the DOM elements, called the virtual DOM. This way, the actual DOM doesn’t have to be analyzed every time a change is made. When you manipulate the DOM using jQuery (or directly using the DOM API), it no longer matches the virtual representation used by Vue.js. This can lead to unexpected behavior.
In general, you can mix Vue.js and jQuery components on the same page, if necessary, but you shouldn’t use jQuery to manipulate a Vue.js component. In other words, when you use Vue.js, just stick to it. Once you get used to it, you will see the benefits and eventually you will find out that writing Vue.js components is actually much easier than using jQuery.
A simple dropdown component
I will show you how to write our own simple Vue.js component. I will use the Boostrap dropdown as an example, because it’s simple to understand.
Note that there are lots of existing Vue.js components, including Bootstrap ports, but sooner or later you will need to create a custom component, so let’s learn how to do that.
The HTML markup of the dropdown looks like this:
<div class="dropdown">
<button class="btn btn-default dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
My button
</button>
<ul class="dropdown-menu">
<li><a href="#">Item 1</a></li>
<li><a href="#">Item 2</a></li>
<li><a href="#">Item 3</a></li>
</ul>
</div>
It’s just a button with an initially invisible dropdown menu. Pressing the button toggles the menu on and off. The outer <div>
wraps these elements together to ensure correct positioning of the menu.
If you’re not familiar with Bootstrap, it looks like this:

Importing styles
For simplicity, we will include the Bootstrap style sheet in our bundle, so we don’t have to focus on writing CSS. Just install the Bootstrap module using npm
and add the following line to your main.js
file:
import 'bootstrap/less/bootstrap.less'
I’m assuming that your bundle is configured like in the previous article, so you can use Less and import style sheets directly from a script.
Note that you can also selectively import individual files if you don’t want to include the entire Bootstrap.
Implementation in Vue.js
Let’s create a file called DropdownButton.vue
. It will contain the template of our component:
<template>
<div class="dropdown" v-bind:class="{ open }">
<button ref="button" type="button"
v-bind:class="[ 'btn', btnClass, 'dropdown-toggle' ]"
aria-haspopup="true"
v-bind:aria-expanded="open ? 'true' : 'false'"
v-on:click="toggle"
v-on:keydown="keyDown">
{{ text }}
</button>
<div v-if="open" class="dropdown-backdrop" v-on:click="close">
</div>
<ul ref="menu"
class="dropdown-menu"
v-on:click="close"
v-on:keydown="keyDown">
<slot></slot>
</ul>
</div>
</template>
As you can see, it’s fairly similar to the original HTML markup, with a few important differences:
- The wrapper
<div>
has an optionalopen
class. It is only added when theopen
property of the component is set to true. - The
btn-default
class of the button is replaced with thebtnClass
property of the component. - The
aria-expanded
attribute is set to true or false depending on the open property. - The button text is replaced with the
text
property. - There is an additional
dropdown-backdrop
element which is only inserted into the DOM whenopen
is true. - The content of the dropdown menu is replaced by a
<slot>
placeholder element. - Some functions are assigned to the
click
andkeydown
events on the button and the menu element. - The button and menu elements have a
ref
attribute.
To register this component in Vue.js, add the following code to main.js
:
import DropdownButton from '@/components/DropdownButton'Vue.component( 'dropdown-button', DropdownButton );
Now we can insert it into other Vue.js components using the following simplified markup:
<dropdown-button text="My Button">
<li><a href="#">Item 1</a></li>
<li><a href="#">Item 2</a></li>
<li><a href="#">Item 3</a></li>
</dropdown-button>
The custom <dropdown-button>
element is automatically replaced with the component’s template. The child list items are inserted in place of the <slot>
element. This brevity is the first significant benefit of using Vue.js.
Imperative vs. declarative programming
Code written using jQuery is imperative. It means that for every event, such as a mouse click, some operations must be executed which change the state of the DOM elements. The following example, which toggles the state of the menu, is a very common pattern when using jQuery:
if ( parentEl.hasClass( 'open' ) ) {
el.attr( 'aria-expanded', 'false' );
parentEl.removeClass( 'open' );
$( '.dropdown-backdrop' ).remove();
} else {
el.attr( 'aria-expanded', 'true' );
parentEl.addClass( 'open' );
$( '<div>' ).addClass( 'dropdown-backdrop' ).insertAfter( el );
}
To check the current state of the menu, we examine the state of some DOM element. We also have to modify all elements whenever the state needs to change. In this example it includes changing an attribute, adding or removing a class and inserting or removing a DOM element.
This code is verbose and hard to understand. Also, when the state can be changed in many different ways, it’s easy to forget some operation in one place. This leads to subtle bugs which can be hard to find.
In Vue.js the data properties of a component represents its actual state, that’s why they are sometimes called the “single source of truth”. The component is just a virtual object in memory. The properties of various DOM elements are derived from these source properties. Whenever you change the property of a component, the whole DOM is automatically updated.
Such coding style is declarative, because the relations between the DOM elements and the data properties are declared up front in the component’s template. Such code is easier to read. Also it’s no longer possible to forget about some update, because they are performed automatically.
So in Vue.js, the entire code which toggles the state of the menu can be written like this:
this.open = !this.open;
Code of the component
Let’s add some code to our component:
<script>
export default {
props: {
btnClass: { type: String, default: 'btn-default' },
text: String
},
data() {
return {
open: false
}
},
methods: {
toggle() {
this.open = !this.open;
},
close() {
this.open = false;
}
}
}
</script>
The components has two properties. The btnClass
defaults to 'btn-default'
and text
defaults to an undefined value. The values of these properties are specified as attributes when the component is created, for example:
<dropdown-button text="My Button">
... items ...
</dropdown-button>
This creates a button with 'My Button'
text. It also uses the default button class, 'btn-default'
, because there is no btn-class
attribute.
The data()
function returns the initial state of the component. The state consists of a single property, open
. It is initially set to false and it can be changed by simply assigning a different value. That’s what the two methods, toggle()
and close()
do.
The methods are invoked by click events, so the menu can be toggled by clicking on the button. When the menu is opened, a backdrop element is created to detect mouse clicks outside of the dropdown and close the menu. The backdrop is simply a fully transparent <div>
element which fills the whole page.
Handling keyboard
Adding keyboard support is a bit more tricky. Handling the focus of DOM elements requires interacting directly with the DOM elements, because focus can be changed not only by our code, but also by the user, for example by using the Tab key. It might be tempting to use jQuery for that, but I will show you that it’s not so difficult to write code without it.
Here’s the implementation of the keyboard handler which mimics the behavior of the Bootstrap dropdown:
keyDown( e ) {
if ( e.keyCode == 38 || e.keyCode == 40 ) { // up and down keys
if ( !this.open ) {
this.open = true;
} else {
const items = this.$refs.menu.querySelectorAll( 'li a' );
if ( items.length > 0 ) {
let index = Array.prototype.indexOf.call( items, e.target );
if ( e.keyCode == 38 && index > 0 )
index--;
else if ( e.keyCode == 40 && index < items.length - 1 )
index++;
if ( index < 0 )
index = 0;
items[ index ].focus();
}
}
e.preventDefault();
} else if ( e.keyCode == 27 ) { // Esc key
this.$refs.button.focus();
this.close();
}
}
Some elements in our component’s template have the ref
attribute. We can access them directly using the $refs
object.
We need to find all child link elements of the dropdown list. Instead of jQuery, we can use the standard querySelectorAll()
method. It works in all modern browsers and it supports the CSS selectors known from jQuery.
We also need to find the index of the currently focused item (which triggered the keyboard event) in the list. In jQuery we would use the index()
method, which can be replaced by the indexOf()
method of an Array
.
The collection returned by querySelectorAll()
is a NodeList
, not an Array
. But we can still call this method using the Array
prototype. You can also call other array methods using this simple trick.
The code changes the focus to the previous or next list item when the up or down arrow key is pressed, or opens the menu if it’s still closed. Pressing Esc closes the menu and sets the focus back to the button. We also call e.preventDefault()
to prevent the page from scrolling up or down.
Dynamically creating Vue.js components
When we register our component using Vue.component()
, we can use it anywhere in the templates of other Vue.js components.
But what if we want to use it on a page which is generated on the server side, for example by PHP code? This could be helpful if you want to use Vue.js components just like you would use traditional jQuery components.
The problem is that Vue.js will not process the <dropdown-button>
tags in the page body. That only works within other Vue.js components. Instead, the browser will attempt to render it as a regular <div>
element and you will just see a list of items:

In order to replace the <dropdown-button>
elements on the page with the actual components, we have to find all occurrences of these elements, extract information from them and create matching components. There are several ways to do that.
The easiest way is to extract the markup of the element and convert it to a dynamically compiled Vue.js component:
const elements = document.querySelectorAll( 'dropdown-button' );for ( let i = 0; i < elements.length; i++ ) {
const element = elements[ i ];
const compiled = Vue.compile( element.outerHTML );
new Vue( {
el: element,
render: compiled.render
} );
}
The loop iterates over all <dropdown-button>
elements. The code extracts the outerHTML
markup of each element. It compiles it into a render function and creates a new Vue.js component which uses that function and replaces the original element.
It’s a very powerful mechanism, but you have to be very careful. The compiler will interpret the HTML as a Vue.js template, which means that, for example, curly brackets {{ }}
can be used to insert any JavaScript expression. This can lead to unexpected behavior, and even an XSS attack (Vue-injection?) is possible when a user deliberately inserts such expression.
Also, using Vue.compile()
requires the full version of the Vue.js script which is slightly larger than the Vue.js runtime.
Another solution is to manually analyze the content of the element, extract all information that we need and pass it to the render function:
const elements = document.querySelectorAll( 'dropdown-button' );for ( let i = 0; i < elements.length; i++ ) {
const element = elements[ i ];
const text = element.getAttribute( 'text' );
const links = element.querySelectorAll( 'li a' );
const items = [];
for ( let j = 0; j < links.length; j++ ) {
items.push( {
text: links[ j ].innerText,
href: links[ j ].getAttribute( 'href' )
} );
}
new Vue( {
el: element,
render( h ) {
return h( DropdownButton, {
props: { text },
scopedSlots: {
default: props => items.map( item => h( 'li', [
h( 'a', {
attrs: { href: item.href }
}, item.text )
] ) )
}
} );
}
} );
}
The code extracts the value of the text
attribute of each element. Then it finds all child links and the inner loop populates an items
array containing the text and URL of each link.
Then a new Vue.js component is created, which replaces the original element. Our manually created render function creates a DropdownButton
component, passing the extracted text as its text
property. It also uses the default slot to insert list items with links which correspond to the extracted items.
If you’re scratching your head right now, don’t worry. Writing a render function manually like this can be difficult, that’s why we normally use templates in Vue.js. But sometimes it can be a very useful technique.
Converting HTML to Vue.js components
In most cases, the HTML rendered on the server side will just contain the regular markup with the button and menu, not a custom <dropdown-button>
element.
Actually, using such regular markup is often a better solution. When you use custom tags on your page, you may see the list of items briefly before it’s replaced by the proper component.
The simplicity of Bootstrap is that the script just adds some dynamic behavior to existing markup instead of replacing it completely. However, a similar effect can also be achieved when using Vue.js.
We can slightly modify the code shown above to convert the standard markup of a dropdown menu into our Vue.js component:
const elements = document.querySelectorAll(
'[data-toggle="dropdown"]' );for ( let i = 0; i < elements.length; i++ ) {
const element = elements[ i ];
const text = element.innerText;
const links = element.parentElement.querySelectorAll(
'ul.dropdown-menu li a' );
const items = [];
for ( let j = 0; j < links.length; j++ ) {
items.push( {
text: links[ j ].innerText,
href: links[ j ].getAttribute( 'href' )
} );
}
new Vue( {
el: element.parentElement,
render( h ) {
// ... same as above ...
}
} );
}
We changed the selector to [data-toggle="dropdown"]
. It returns all button elements which have this special attribute. The standard Bootstrap script uses the same selector.
We extract the text from each button. Then we query the parent <div>
element to find all child links in the associated dropdown menu and we populate a list of items in the same way as in the previous example.
Finally, the Vue.js component is mounted on the parent <div>
element of the button. That replaces the original element with the same content, so the change is invisible to the user, but the component is now “live” and can be manipulated using mouse and keyboard.
Note that the technique I’m showing here is similar to server-side rendering, but it’s not the same. When using SSR, the Vue.js components actually run on the server, which requires using Node.js. Here I’m assuming that we just have a traditional web application, written in PHP or another language.
If you have read so far, it might seem an overkill to implement a dropdown button in Vue.js, because it just works “out of the box” when using jQuery. But remember, that’s just a very simple example. Once you start writing and using more complex components written in Vue.js, you will appreciate how powerful and easy it is.