Vue.js Tutorial: From jQuery to Vue.js
Published on by Paul Redmond
As far as JavaScript libraries are concerned, there’s never been a more popular library than jQuery. It made (and still does) DOM traversal a cinch using CSS selectors at a time when browser compatibility was a significant issue for developers.
In fact, jQuery is so universal I thought it would be a great way to segue into why I love writing UIs with Vue using component-based JavaScript. In this Vue tutorial, we will first walk through building a UI with jQuery, and then rewrite it using Vue.
The Project
It’s pretty typical to have a form that needs the ability to add multiple inputs with JavaScript dynamically. Imagine we have an online checkout form which allows a user to purchase multiple tickets that require a name and email address for each ticket:
Building this in jQuery first is a good segue into how we might make the same thing in Vue. Many developers are familiar with jQuery, and it provides an excellent contrast to the very different approach you must take to build dynamic interfaces.
If you want to cheat, I’ve created a working example of the jQuery version and the Vue version on Code Pen.
The jQuery Version
There are a dozen ways we could build this UI with jQuery. For instance, we could create the form with one set of inputs in the HTML markup, and then let jQuery take over by dynamically adding more inputs to the DOM when the user adds more.
We could also use a <script type="text/template>
tag as a row template and add one by default on DOMContentLoaded
, which is the approach we’ll take.
jQuery HTML Template
Using a script template for the row is more in line with how we might build a component in Vue. Here’s how the HTML markup might look:
<html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>jQuery Checkout UI</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"> <style type="text/css"> body { margin: 3em } button { cursor: pointer; } .unit-price { margin-right: 2rem; color: #999; } </style></head><body> <div class="container" id="app"> <form> <!-- A placeholder for the list of attendee inputs --> <div class="attendee-list"></div> <div class="row justify-content-center"> <div class="col-sm-6"></div> <div class="col-sm-2"> <button type="button" class="btn btn-secondary add-attendee">Add Attendee</button> </div> </div> <hr> <div class="row justify-content-center"> <div class="col-sm-6"> <!-- A placeholder for the unit price --> <span id="unit-price" class="unit-price"></span> </div> <div class="col-sm-2 text-left"> <button type="submit" id="checkout-button" class="btn btn-primary"> Pay <!-- A placeholder for the checkout total --> <span class="amount"></span></button> </div> </div> </form> </div> <script type="text/template" data-template="attendee"> <div class="attendee row justify-content-center"> <div class="col-sm-3"> <div class="form-group"> <label class="sr-only">Name</label> <input class="form-control" placeholder="Enter name" name="attendees[][name]" required> </div> </div> <div class="col-sm-3"> <div class="form-group"> <label class="sr-only">Email address</label> <input type="email" class="form-control" placeholder="Enter email" name="attendees[][email]" required> </div> </div> <div class="col-sm-2 text-left"> <button type="button" class="btn btn-light remove-attendee"> <span aria-hidden="true">×</span> Remove </button> </div> </div> </script> <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script> <script src="app.js"></script></body></html>
We are using the Bootstrap 4 beta version for the layout. We’ve defined a few placeholders jQuery will populate with data on $(document).ready()
, but it’s tough to tell from the markup what is going to happen. You will have to compare the HTML and JavaScript side-by-side to make any sense of the functionality intended. Coming back to this project months later will require a decent amount of mental overhead to figure out what’s going on.
In our app.js
file, we will populate the unit price of a single ticket and the total price that will appear on the checkout button with JavaScript. Each time a user clicks “Add Attendee” we will add a new row to the placeholder container <div class="attendee-list"></div>
from the template.
To populate the attendee list of repeated form inputs, we use a <script>
tag as a client-side template. Browsers will ignore the script because of the type="text/template"
, which means it won’t be executed.
Near the closing <body>
tag use a recent version of jQuery and app.js
where we’ll start working on the dynamic UI updates.
jQuery JavaScript Init
To start our jQuery version, let’s initialize the form by calculating the total, adding a row by default, and setting the unit price from data:
// app.js $(document).ready(function () { var data = { cost: 9.99 }; /** * Get the attendee count */ function getAttendeeCount() { return $('.attendee-list .row.attendee').length; } function addAttendee() { $('.attendee-list').append( $('script[data-template="attendee"]').text() ); } function syncPurchaseButton() { // Total up the count for the checkout button total $('#checkout-button span.amount').html( '$' + data.cost * getAttendeeCount() ); } // // Initialize the form // // Set up the unit cost of one ticket $('#unit-price').html('$' + data.cost + ' ea.'); // Add one attendee by default on init addAttendee(); syncPurchaseButton();});
The first part of the code sets an object literal for the data, containing a single price
property. Price is the unit price of a single ticket. You might want to set the price of a single ticket dynamically, but for our purposes, it’s just hard-coded.
We have a couple of helper functions, including getting the attendee count using a DOM query. Using the DOM is the only accurate way to determine this value using jQuery.
The second helper function adds a new attendee to the list using the script template in our markup.
The syncPurchaseButton()
function uses the getAttendeeCount()
to calculate and populate the purchase button with the checkout total.
If you wanted the same purchase total anywhere else in the template, you would need to sync all instances in the DOM using a class selector, but we are being specific in this case and targeting just one.
If you load the page at this point, the form will be initialized with one attendee, the unit price, and the total in the checkout button:
Adding Attendees With jQuery
Next, let’s tackle the ability to add and remove attendees. jQuery has excellent event handling, including triggering custom events. Let’s start with the code necessary to add new attendees:
function addAttendee() { $('.attendee-list').append( $('script[data-template="attendee"]').text() ); // Sync remove button UI syncRemoveButtons();} function syncRemoveButtons() { // If only one attendee, hide the first remove button // otherwise, show all remove buttons if (getAttendeeCount() === 1) { $('.attendee-list .attendee .remove-attendee').first().hide(); } else { $('.attendee-list .attendee .remove-attendee').show(); }} function syncPurchaseButton() { // Total up the count for the checkout button total $('#checkout-button span.amount').html( '$' + data.cost * getAttendeeCount() );} // Events$('.add-attendee').on('click', function (event) { event.preventDefault(); addAttendee(); $(this).trigger('attendee:add');}).on('attendee:add', function () { syncPurchaseButton(); syncRemoveButtons();});
The syncRemoveButtons()
ensures the user cannot remove an input when only one remains, but the user can remove any row when multiple rows exist.
We now call syncRemoveButtons()
in the addAttendee()
function, which means if you refresh the page, the remove button is hidden because the attendee count is only one.
The event handler for adding an attendee calls the addAttendee()
function and then triggers the attendee:add
custom event.
In the custom event handler, we sync the total price, so that the checkout button is accurate, and then we call syncRemoveButtons()
to update the remove button status as already described.
Syncing state can get out of hand as your jQuery UI grows. We have to explicitly manage state and sync it when it changes from an event, and we must absorb the specific way state syncs with each application.
Managing state in jQuery requires some mental overhead because it can be handled in various ways, and ties back to the DOM. When the state is dependent on the DOM and not the other way around, DOM queries to keep track of state get complicated. Furthermore, the method used to manage state isn’t predictable and varies from script to script and from developer to developer.
Removing Attendees With jQuery
At this point, if you refresh the page, you can add new rows to the form. As you add the first additional attendee, the remove button will be shown for each row, allowing you to remove a row.
Next, let’s wire up the remove event and make sure the UI state is reflected after the removal:
// Attach an event handler to the dynamic row remove button$('#app').on('click', '.attendee .remove-attendee', function (event) { event.preventDefault(); var $row = $(event.target).closest('.attendee.row'); $row.remove(); $('#app').trigger('attendee:remove');}); $('#app').on('attendee:remove', function () { syncPurchaseButton(); syncRemoveButtons();});
We’ve added a click event listener on the #app
DOM ID, which allows us to respond to click event for newly added rows dynamically. Inside this handler, we prevent the default button event and then find the closest ancestor .row
in the DOM tree.
Once the parent $row
is located, we remove it from the DOM and trigger a custom attendee:remove
event.
In the attendee:remove
event handler, we sync our purchase button and remove button state.
The Finished jQuery Version
At this point, we have a working jQuery prototype of our ticket form UI that we can use to compare to our Vue version.
Here’s the complete app.js
file:
$(document).ready(function () { var data = { cost: 9.99 }; /** * Get the attendee count */ function getAttendeeCount() { return $('.attendee-list .row.attendee').length; } function addAttendee() { $('.attendee-list').append( $('script[data-template="attendee"]').text() ); syncRemoveButtons(); } function syncRemoveButtons() { // If only one attendee, hide the first remove button // otherwise, show all remove buttons if (getAttendeeCount() === 1) { $('.attendee-list .attendee .remove-attendee').first().hide(); } else { $('.attendee-list .attendee .remove-attendee').show(); } } function syncPurchaseButton() { // Total up the count for the checkout button total $('#checkout-button span.amount').html( '$' + data.cost * getAttendeeCount() ); } // Events $('.add-attendee').on('click', function (event) { event.preventDefault(); addAttendee(); $(this).trigger('attendee:add'); }).on('attendee:add', function () { syncPurchaseButton(); syncRemoveButtons(); }); // Attach an event handler to the dynamic row remove button $('#app').on('click', '.attendee .remove-attendee', function (event) { event.preventDefault(); var $row = $(event.target).closest('.attendee.row'); $row.remove(); $('#app').trigger('attendee:remove'); }); $('#app').on('attendee:remove', function () { syncPurchaseButton(); syncRemoveButtons(); }); // // Initialize the form // // Set up the unit cost of one ticket $('#unit-price').html('$' + data.cost + ' ea.'); // Add one attendee by default on init addAttendee(); syncPurchaseButton();});
The goal of this example is to paint a picture of a UI you’re likely to have written, and then show you what it’s like with Vue.js. The important takeaway here is state is tied directly to the DOM, and you must query the DOM to reason about the state.
JQuery still makes it convenient to write UI, but let’s see how you can now write the same functionality using Vue.
Introduction to Vue
Most people have probably heard of Vue at this point, but for those unfamiliar with Vue, the guide is an excellent place to start.
The comparison with other frameworks is also helpful to get a feel for Vue in contrast to other frameworks with which you might already be familiar.
I suggest you install the Vue devtools extension available on Chrome and Firefox. The developer tools will provide you excellent debugging information as you learn and develop applications with Vue.
The Vue Version
Our Vue version is going to be written with plain JavaScript to avoid having to worry about ES6 tooling and focus instead on the component example at hand.
You will see how Vue helps separate the data from the display of the UI, with the data driving the display in a reactive way. We also don’t need to traverse the DOM to calculate values, which starts to feel clunky when you compare how it’s done in jQuery compared to React or Vue.
Getting Started
Before we write our template and JavaScript, let’s discuss our approach to building the form. Thinking about the data associated with the form, I picture a collection (array) of attendees and a unit price.
The object-literal might look as follows:
var data = { attendees: [ { name: 'Example', email: 'user@example.com' } ], cost: 9.99,};
If we were to update the data by adding another attendee, Vue is listening and ready to react to that change in data:
data.attendees.push({ name: 'Example 2', email: 'user2@example.com'});
With that in mind let’s build out the rough HTML markup and JavaScript skeleton for our UI.
Vue HTML Template
We will build out the JavaScript and HTML incrementally to walk you through each feature we’ve already covered in the jQuery version.
Here’s the starting HTML markup for the Vue portion of this tutorial:
<html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Vue Checkout UI</title> <linkrel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"> <style type="text/css"> body { margin: 3em } button { cursor: pointer; } .unit-price { margin-right: 2rem; color: #999; } </style></head><body> <div class="container" id="app"> <form> <div class="row justify-content-center" v-for="(attendee, index) in attendees" :key="index" > <div class="col-sm-3"> <div class="form-group"> <label class="sr-only">Name</label> <input class="form-control" aria-describedby="emailHelp" placeholder="Enter name" v-model="attendee.name" name="attendees[][name]" required > </div> </div> <div class="col-sm-3"> <div class="form-group"> <label class="sr-only">Email address</label> <input type="email" class="form-control" placeholder="Enter email" v-model="attendee.email" name="attendees[][email]" required > </div> </div> <div class="col-sm-2 text-left"> <button type="button" class="btn btn-light"> <span aria-hidden="true">×</span> Remove</button> </div> </div> <div class="row justify-content-center"> <div class="col-sm-6"></div> <div class="col-sm-2"> <button type="button" class="btn btn-secondary">Add Attendee</button> </div> </div> <hr> <div class="row justify-content-center"> <div class="col-sm-6"> <span class="unit-price">${{ cost }} ea.</span> </div> <div class="col-sm-2 text-left"> <button type="submit" class="btn btn-primary">Pay</button> </div> </div> <form> </div> <script src="https://unpkg.com/vue@2.4.4/dist/vue.js"></script> <script src="app.js"></script></body></html>
The markup is very similar to our jQuery version, but perhaps you noticed the variable for the unit price:
<span class="unit-price">${{ cost }} ea.</span>
Vue uses declarative rendering to render data to the DOM. The {{ cost }}
is data binding at work using the “Mustache” syntax and the $
just the dollar character.
Remember the data object with the cost
property?
var data = { cost: 9.99};
The mustache tag gets replaced with the value of data.cost
when the bound data changes.
Next, note the v-for="(attendee, index) in attendees"
line which is a loop using the attendees
data array, and will iterate over the array and render form inputs for each attendee.
The v-for
attribute is a directive, which is designed to “reactively apply side effects to the DOM when the value of its expression changes.” In our example when the data.attendees
array is updated, the DOM will update as a result of this directive.
You should begin to see a pattern: we modify data (state), and the UI responds to those changes. As a result, your code is more declarative and easier to write.
Vue JavaScript Init
At the bottom of the HTML markup, we have an app.js
script tag for our Vue code.
To initialize a Vue instance on the page, we need to mount Vue to a DOM node. We’ve provided a container <div id="app"></div>
which means any markup within this DOM element will be linked to Vue and is reactive to data change:
(function () { var app = new Vue({ el: '#app', data: { attendees: [{ name: '', email: '' }], cost: 9.99, }, });})();
We create a new Vue instance bound to the #app
DOM element and define the main data
object. The data object includes our unit cost and an array of attendees. We’ve added one empty attendee so our form will render with one set of inputs by default.
If you were to remove the attendees and make it an empty array, you would not see any name and email inputs.
The whole thing is wrapped in an immediately-invoked function expression (IIFE) to keep our instance out of the global scope.
Calculating the Total Price
In the jQuery version, we calculated the total price by syncing the total with the DOM on an event to either remove or add an attendee. In Vue, as you might guess, we use data, and then the view reacts to those changes automatically.
We could do something like the following, and it would still be far better than querying the DOM:
<button type="submit" class="btn btn-primary"> Pay ${{ cost * attendees.length }}</button>
However, putting too much logic in your templates makes them less expressive and harder to maintain. Instead, we can use computed properties:
(function () { var app = new Vue({ el: '#app', data: { attendees: [{ name: '', email: '' }], cost: 9.99, }, computed: { quantity: function () { return this.attendees.length; }, checkoutTotal: function () { return this.cost * this.quantity; } } });})();
We’ve defined two computed properties. The first property is the ticket quantity, which is calculated by the length of attendees.
Second, we define the checkoutTotal
computed property which uses the first computed property to multiply the unit cost and the quantity.
Now, we can update the checkout button to use the computed property. Notice how descriptive the computed property name is as a result:
<button type="submit" class="btn btn-primary"> Pay ${{ checkoutTotal }}</button>
If you refresh your browser, you should see the checkout total calculated in the button automatically.
When you add an attendee, the computed property is automatically updated and reflected in the DOM.
Adding Attendees With Vue
We are ready to look at how we would add attendees using Vue using events.
In jQuery we used a DOM event handler:
$('.add-attendee').on('click', function () {});
In Vue, we hook up the event in the template. In my opinion, it makes the HTML easier to read, because we have an expressive way of knowing which events are associated with a given element.
You can either use the v-on:click="addAttendee"
:
<!-- Using v-on: --><button type="button" class="btn btn-secondary" v-on:click="attendees.push({ name: '', email: ''})"> Add Attendee</button>
Or, the shorthand `@click=”addAttendee”:
<!-- Using @click --><button type="button" class="btn btn-secondary" @click="attendees.push({ name: '', email: ''})"> Add Attendee</button>
It’s okay to use either style, but also good form to stick to the same method throughout. I prefer the shorthand style.
When the button is clicked, we push a new object to the attendees
array in the template. I wanted to show you this style so you could understand that you can just run some JavaScript in the attribute.
Most of the time it’s better to use event handlers because usually, events have more complex logic associated with them:
<button type="button" class="btn btn-secondary" @click="addAttendee"> Add Attendee</button>
Vue accepts a methods
property on the main Vue object (and components) which will allow us to define an event handler method:
(function () { var app = new Vue({ el: '#app', data: { attendees: [{ name: '', email: '' }], cost: 9.99, }, computed: { quantity: function () { return this.attendees.length; }, checkoutTotal: function () { return this.cost * this.quantity; } }, methods: { addAttendee: function (event) { event.preventDefault(); this.attendees.push({ name: '', email: '', }); } } });})();
We prevent the default action and push a new object onto the attendees
array. Now, if you add attendees, you will see new inputs added and the checkoutTotal
matches the row count:
Notice the handler receives an event object we can use to prevent the default. Since it’s common to prevent the default event action or stop propagation, Vue provides event modifiers used with a dot (.) as part of the attribute:
<button type="button" class="btn btn-secondary" @click.prevent="addAttendee"> Add Attendee</button>
Your methods are focused on data, and Vue automatically deals with DOM events using event attribute modifiers.
Removing Attendees With Vue
Removing attendees is similar to adding them, but instead of adding an object to the array, we need to remove one based on the array index with another event handler:
<button type="button" class="btn btn-light" @click.prevent="removeAttendee(index)"> <span aria-hidden="true">×</span> Remove</button>
We are using the array index to reference the correct attendee we want to remove. If you recall in our v-for
loop, we defined an index:
<div class="row justify-content-center" v-for="(attendee, index) in attendees" :key="index"> <!-- Attendee inputs --></div>
Inside our Vue instance, we define the removeAttendee
method which uses splice
to remove one item from the array based on the index:
methods: { removeAttendee: function (index) { this.attendees.splice(index, 1); }, // ...}
With the removeAttendee
event handler in place, you can now add and remove attendees!
We also want to match the business requirement of only displaying the “Remove” button when multiple attendees are added. We don’t want to allow the user to remove all inputs.
We can do that with the built-in v-show
conditional directive:
<button type="button" class="btn btn-light" @click.prevent="removeAttendee(index)" v-show="quantity > 1"> <span aria-hidden="true">×</span> Remove</button>
We used the quantity
computed property to show the remove button when the quantity is greater than one.
We could have also hidden the button with the v-if
conditional. I recommend reading the documentation to understand the nuances of how they both work.
In our case, we use v-show
to show and hide the button with CSS. If you switch it out with v-if
and inspect the DOM, you will see Vue removes the element from the DOM.
The Finished Vue Version
Here is the final Vue version:
(function () { var app = new Vue({ el: '#app', data: { attendees: [{ name: '', email: '' }], cost: 9.99, }, computed: { quantity: function () { return this.attendees.length; }, checkoutTotal: function () { return this.cost * this.quantity; } }, methods: { removeAttendee: function (index) { this.attendees.splice(index, 1); }, addAttendee: function (event) { event.preventDefault(); this.attendees.push({ name: '', email: '', }); } } });})();
We now have the same functionality in both versions! My goal was to illustrate moving from a DOM-based workflow to modifying the data and having the UI update as a side-effect of those changes.
The Vue version’s markup is more expressive in conveying the functionality of the component than the jQuery version. It’s impossible to determine which elements will have event handling attached in the jQuery version. Furthermore, we cannot anticipate how the UI will react to change from the HTML markup.
What’s Next?
If you don’t have much experience with Vue yet, I recommend you read the guide from end-to-end. Much like the Laravel documentation, the guide reads like a book. The documentation will walk you through everything you need to know to start using Vue.
Vue also released an official style guide you should read once you have started using Vue.