Telerik blogs
VueT2 Dark_1200x303

jQuery used to be a common choice for a lot of applications in the past. However, there are smaller, faster, more feature-rich solutions now. We’ll cover how to migrate from jQuery to Vue 3 a lot of common use cases, such as handling events, forms, API requests, conditional and dynamic content, and more.

In the past, for many years, jQuery was the staple choice for any kind of project. It provides a lot of useful helper methods for traversing the DOM, animations, API requests and more. What’s more, it ensures cross-browser compatible functionality, which was quite a big deal in the past, as browsers vendors did not implement features in a consistent way.

However, jQuery was not a silver bullet, and there were problems with it as projects grew. For instance, the code written in jQuery was imperative, and it was very easy to end up with a lot of nested spaghetti code. We had to update the DOM manually every time we needed to make a change. Modern frameworks, such as Vue, React, etc., have simplified this and allowed us to write declarative code. For example, instead of explicitly specifying how the DOM should be updated, we only need to write how the DOM should look, and modern frameworks will handle the rest for us.

In this article, we will cover a few common use cases and how to migrate them from jQuery to Vue 3.

Adding jQuery and Vue to a Project

Adding jQuery to a project is very simple, as we can do it by including one script tag.

<body>
  <div id="app">
    <!-- Content goes here -->
  </div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  <!-- other scripts -->
</body>

The most popular ways to create a Vue project usually involve a toolchain, such as Vue CLI or Vite. However, Vue is a progressive framework and can be used without all the bells and whistles. It’s especially useful when you want to slowly migrate to Vue or if you want to add some dynamic interactions here and there in your app. Similarly to jQuery, it can be added to a project with just a script tag.

<body>
  <div id="app">
    <!-- Content goes here -->
  </div>
  <script src="https://unpkg.com/vue@3.2.21"></script>
  <!-- other scripts -->
</body>

Now that we covered how to include jQuery and Vue in a project, let’s have a look at common use cases jQuery usually was used for and how to do them in Vue.

Handling Event Listeners

Handling event listeners is the first common use case. For example, if a user clicks on a button, we might want to perform some kind of action like showing a modal, or if a user focuses an input field, we could display a helpful tooltip.

In jQuery, before running any code, we would wait for the document to be ready by using the $(document).ready() method. Then, to add event listeners, we would retrieve DOM elements by using the $('selector') method and then chain an event we want to listen to, such as click or focus. Below you can see an example.

<button id="clickMeBtn">Click me</button>

<div style="margin-top: 1rem">
  <label>Focus me</label>
  <input type="text" id="focus-input" />
</div>

<script type="text/javascript">
  $(document).ready(() => {
    $("#clickMeBtn").click(() => {
      console.log("Button clicked");
    });

    $("#focus-input").focus(() => {
      console.log("Input focused");
    });

    $("#focus-input").blur(() => {
      console.log("Input lost focus");
    });
  });
</script>

When using Vue, we first need to create a Vue app by using the createApp method and mount it on a DOM element. Vue will take control over all DOM manipulations inside of that element.

A great thing about Vue is that in comparison to jQuery, the code we write with Vue is declarative, not imperative. While in jQuery we have to explicitly retrieve DOM elements to attach event listeners, we don’t have to do that with Vue. Rather, we only have to specify what events should be attached to an element by using the v-on directive, and Vue handles the rest for us (@ is a shorthand for v-on) . Below you can see the code example for Vue.

<div id="handling-events">
  <button @click="onBtnClick">Click me</button>
  <div style="margin-top: 1rem">
    <label>Focus me</label>
    <input
    	type="text"
    	id="focus-input"
    	@focus="onFocusInput"
    	@blur="onFocusBlur"
   	/>
  </div>
</div>
<script type="text/javascript">
  const app = Vue.createApp({
    setup() {
      return {
        onBtnClick() {
          console.log("Button clicked");
        },
        onFocusInput() {
          console.log("Input focused");
        },
        onFocusBlur() {
          console.log("Input lost focus");
        },
      };
    },
  }).mount("#handling-events");
</script>

Again, we have handlers for three events. We listen for a button click and focus and blur events on the input element by attaching the @click directive to the button and focus and @blur directives on the input. All the directives receive appropriate handlers that are defined in the setup method: onBtnClick, onFocusInput and onFocusBlur. Anything that is returned in an object from the setup method will be available in the markup.

A nice advantage of Vue that I think is worth mentioning here is, when we look at the DOM markup, we can clearly see what kind of events we are listening to, as the directives are defined directly on elements. With jQuery, however, this isn’t the case. To figure out what events we are listening to, we would need to dive into the jQuery implementation.

Handling Forms

A lot of websites contain forms for the sign-up, login, contact, etc. With jQuery, we explicitly query for the form element by using its id and then attach a submit event listener. Then, to get access to input values, we retrieve them directly and get their values using the val() method.

 <div id="login-form-container">
   <form id="login-form">
     <div class="form-row">
       <label for="email">Email </label>
       <input type="email" name="email" />
     </div>
     <div class="form-row">
       <label for="password">Password </label>
       <input type="password" name="password" />
     </div>
     <div>
       <button type="submit">Submit Form</button>
     </div>
   </form>
</div>
<script type="text/javascript">
  $(document).ready(() => {
    $("#login-form").submit(e => {
      e.preventDefault();
      const email = $('[name="email"]').val();
      const password = $('[name="password"]').val();
      console.log("form submitted", {
        email,
        password,
      });
    });
  });
</script>

Vue is a reactive state-driven framework. In a nutshell, Vue will create and update the DOM based on the reactive state, instead of us doing it imperatively. There are a few ways to create a reactive state, and one of them is by using the ref method.

 <div id="login-form-container">
   <form id="login-form" @submit.prevent="onSubmit">
     <div class="form-row">
       <label for="email">Email </label>
       <input v-model="email" type="email" name="email" />
     </div>
     <div class="form-row">
       <label for="password">Password </label>
       <input v-model="password" type="password" name="password" />
     </div>
     <div>
       <button type="submit">Submit Form</button>
     </div>
   </form>
</div>
<script type="text/javascript">
Vue.createApp({
  setup() {
    const email = Vue.ref("");
    const password = Vue.ref("");

    const onSubmit = () => {
      console.log("form submitted", {
        email: email.value,
        password: password.value,
      });
    };
    return {
      email,
      password,
      onSubmit,
    };
  },
}).mount("#login-form-container");
</script>

We have two reactive refs: email and password. Both of them and the onSubmit method are returned from the setup. In the markup, we use the v-model directive to create two-way data bindings to the form inputs. Two-way data binding basically means that whenever the state is updated, the input fields will be updated accordingly, and when the input fields are updated, so will the state.

Last but not least, we have the @submit.prevent event listener attached on the form element. Note that .prevent, which was chained to the @submit listener, is one of the event modifiers supported by Vue. In jQuery, we explicitly had to call e.preventDefault() to prevent the default form behavior and stop it from being sent by the browser to the server, as we do it using JavaScript.

Showing and Hiding Content

There are many use cases for showing specific markup only in certain situations, such as form validation messages, alerts or helper messages. Below you can see an example of how to toggle the visibility of a message paragraph in jQuery.

<div id="conditional-section">
  <button id="messageBtn">Hide message</button>
  <p id="message">Hello world</p>
</div>

<script type="text/javascript">
  $(document).ready(() => {
    const $btn = $("#messageBtn");
    const $message = $("#message");
    $btn.click(() => {      
      const isVisible = $message.is(":visible");
      if (isVisible) {
        $message.hide();
        $btn.text("Show message");
      } else {
        $message.show();
        $btn.text("Hide message");
      }
    });
  });
</script>

Vue is a state-driven framework and this helps a lot with use cases such as this one, as we can easily render different content based on the state. For instance, as the code below shows, depending on the value of the isMessageVisible ref, the button’s text will either have a Hide or Show string. On top of that, we use the v-show directive to control whether the message should be visible or not.

<div id="conditional-section">
  <button
		id="messageBtn"
		@click="isMessageVisible = !isMessageVisible"
	>
    {{isMessageVisible ? 'Hide' : 'Show'}} message
  </button>
  <p v-show="isMessageVisible" id="message">Hello world</p>
</div>

<script type="text/javascript">
  Vue.createApp({
    setup() {
      const isMessageVisible = Vue.ref(true);
      return {
        isMessageVisible,
      };
    },
  }).mount("#conditional-section");
</script>

This is another great example of the difference between jQuery and Vue. The code written with jQuery is very imperative, as we explicitly query for elements and update their text and visibility. On the other hand, Vue is declarative, and the DOM updates are automatically performed by Vue based on the state.

Besides the v-show directive that toggles the display style, Vue also provides the v-if directive that can create and remove content from the DOM.

Rendering a List of Items

If we want to render a list of items, we usually need to generate the markup dynamically. We can loop through a list by either using one of the native array methods or with the each method provided by jQuery. We can use the loop to generate markup for each list item, and, after the loop, we just append the content to the desired HTML element.

<div id="list-container"></div>
<script type="text/javascript">
  const fruits = ["apple", "banana", "orange"];
  let content = [];
  $.each(fruits, (idx, fruit) => {
    content.push(`<li>${fruit}</li>`);
  });

  $("#list-container").append(`
  	<ul>
  		${content.join("\n")}
    </ul>
  `);
</script>

Vue is a bit more pragmatic when it comes to rendering lists, as it provides a directive called v-for that can be used to loop through iterables and generate new content. All the markup is again defined directly in HTML. In the example below, we have the v-for directive on the li element, as we want to create li elements for each item in the fruits array. Note that Vue requires us to provide a unique key attribute that is used for tracking changes and optimizing performance. In the setup method, we just define and return the fruits array.

<div id="list-container">
  <ul>
    <li v-for="(fruit, idx) of fruits" :key="idx">{{fruit}}</li>
  </ul>
</div>
<script type="text/javascript">
  Vue.createApp({
    setup() {
      const fruits = ["apple", "banana", "orange"];
      return {
        fruits,
      };
    },
  }).mount("#list-container");
</script>

Toggling Classes

I can’t even remember how many times I had to toggle classes with jQuery, but believe me, it was a lot. A good example of a feature that requires it, is tabs. If a user clicks on a tab, it should change to an active state, and if there was a previously active tab, it should be changed to a normal inactive state. Below you can see a very simple example in jQuery.

<style>
  .tab {
    background: none;
    border: none;
  }

  .active {
    background-color: aquamarine;
  }
</style>
<div>
  <button class="tab" id="tab-one">Tab 1</button>
  <button class="tab" id="tab-two">Tab 2</button>
  <button class="tab" id="tab-three">Tab 3</button>
</div>
<script type="text/javascript">
  $(document).ready(() => {
    $(".tab").click(e => {
      // Remove current active classes
      $(".tab.active").toggleClass("active");
      const $btn = $(e.currentTarget);
      // Turn on active class for the clicked tab
      $btn.toggleClass("active");
    });
  });
</script>

Similarly to the handling events example, we query all tabs that contain the tab class and attach a click event listener to them. When one of the tabs is clicked, we first try to find a tab with the active class and then toggle it. Finally, we toggle the active class on the tab that was just clicked. Now, let’s have a look at how to implement the same functionality with Vue.

<style>
  .tab {
    background: none;
    border: none;
  }

  .active {
    background-color: aquamarine;
  }
</style>
<div id="toggling-classes">
  <button
  	class="tab"
  	:class="{active: activeTabId === 'tab-one'}"
  	id="tab-one"
  	@click="activeTabId = 'tab-one'"
  >
    Tab 1
  </button>
  <button
    class="tab"
    :class="{active: activeTabId === 'tab-two'}"
    id="tab-two"
    @click="activeTabId = 'tab-two'"
  >
    Tab 2
  </button>
  <button
		class="tab"
		:class="{active: activeTabId === 'tab-three'}"
		id="tab-three"
		@click="activeTabId = 'tab-three'"
	>
    Tab 3
  </button>
</div>
<script type="text/javascript">
  Vue.createApp({
    setup() {
      const activeTabId = Vue.ref(null);
      return {
        activeTabId,
      };
    },
  }).mount("#toggling-classes");
</script>

The activeTabId value is a reactive ref and is returned from the setup method. Now, whenever the activeTabId value changes, the DOM will be updated automatically. In the markup, we again have three buttons, and each of them have the @click directive attached. However, instead of passing a method as a handler, we pass a JavaScript expression that is used to update the reactive activeTabId ref. Besides having the tab class, each button has a dynamic active class that is applied conditionally when the expression activeTabId === 'value' evaluates to true.

To be honest, the Vue example actually involves many more lines of code than jQuery one, as we have to add dynamic classes and directives on each button separately. However, we can simplify the code for it by utilizing the v-for loop and creating the buttons dynamically. The example below provides the same result but is much cleaner and more concise. We define the tabs array in the setup method and then return it. What’s more, instead of using an id to figure out which button should be active, we just use the array index.

<div id="toggling-classes">
  <button
		v-for="(tab, idx) of tabs"
		:key="idx"
		class="tab"
		:class="{active: activeTabIdx === idx}"
		@click="activeTabIdx = idx"
	>
    {{tab}}
  </button>
</div>
<script type="text/javascript">
  Vue.createApp({
    setup() {
      const activeTabIdx = Vue.ref(null);
      const tabs = ["Tab 1", "Tab 2", "Tab 3"];
      return {
        activeTabIdx,
        tabs,
      };
    },
  }).mount("#toggling-classes");
</script>

Performing API Requests

Web applications often need to perform API requests, for example, to fetch some data or submit a form. It’s also common to dynamically create content based on the data received from a server.

The jQuery example below utilizes the ajax method to perform a GET request to fetch a list of quotes. When the data is fetched successfully, we loop through it to dynamically create HTML markup. Finally, we query for the div with quotes id and append the dynamically generated content to it.

<div id="quotes"></div>

<script type="text/javascript">
  $.ajax("https://type.fit/api/quotes", {
    method: "GET",
    success(data, textStatus, jqXHR) {
      console.log("success", data);
      let content = [];

      JSON.parse(data)
        .slice(0, 10)
        .forEach(({ text, author }) => {
          content.push(`
            <li class="quote-list-item">
            	<div>
            		<span class="quote-text">${text}</span>
            		<br />
            		<span class="quote-author">${author}</span>
  						</div>
            </li>
          `);
      });

      $("#quotes").append(`
        <ul class="quotes-list">
      	  ${content.join("\n")}
        </ul>
      `);
    },
    error(jqXHR, textStatus, error) {
      console.log("error", jqXHR, textStatus, error);
    },
  });
</script>

Here’s the Vue implementation:

<div id="quotes">
  <ul class="quotes-list">
    <li
      v-for="(quote, idx) of quotes"
      :key="idx"
      class="quote-list-item"
    >
      <div>
        <span class="quote-text">{{quote.text}}</span>
        <br />
        <span class="quote-author">{{quote.author}}</span>
      </div>
    </li>
  </ul>
</div>
<script type="text/javascript">
  Vue.createApp({
    setup() {
      const quotes = Vue.ref([]);
      Vue.onMounted(async () => {
        try {
          const response = await fetch(
            "https://type.fit/api/quotes",
            {
              method: "get",
            }
          );
          const result = await response.json();
          quotes.value = result.slice(0, 10);
        } catch (error) {
          console.log("error", error);
        }
      });

      return {
        quotes,
      };
    },
  }).mount("#quotes");
</script>

In contrast to the jQuery example where we only had an empty div with quotes id, in Vue, we can define the markup for our quotes data directly in the HTML. We just need to use the v-for directive to loop through the quotes. Initially, the quotes ref has an empty array as the initial value. However, when the quotes ref is updated and populated with the data fetched from the server, Vue will re-render and update the DOM accordingly.

The API request to fetch the data is performed in the onMounted lifecycle hook. Vue has multiple lifecycle hooks that are called at different lifecycle stages of the Vue app.* The onMounted hook is executed when Vue finished taking over the DOM and has rendered and committed updates to the DOM for the first time.

* You can read more about Vue lifecycle hooks here and here.

Summary

We have covered how to convert a few common functionalities from jQuery to Vue. jQuery served developers well for many years, but now there are solutions that can not only do the same things better but also provide more features. Let me note that we’ve covered only a gist of what can be done with Vue, and I highly recommend going through the official documentation, as there are many concepts we did not talk about, such as components, slots, computed properties, watchers and more.

You can find a full code example for this article in this GitHub repo.


Editor’s note: We know there are many reasons developers have for using either jQuery or Vue, or some combination of the two. Whatever your preference, Kendo UI has you covered! We will continue to offer support to both our jQuery and Vue component libraries (as well as React and Angular) for years to come.


Thomas Findlay-2
About the Author

Thomas Findlay

Thomas Findlay is a 5-star rated mentor, full-stack developer, consultant, technical writer and the author of “React - The Road To Enterprise” and “Vue - The Road To Enterprise.” He works with many different technologies such as JavaScript, Vue, React, React Native, Node.js, Python, PHP and more. Thomas has worked with developers and teams from beginner to advanced and helped them build and scale their applications and products. Check out his Codementor page, and you can also find him on Twitter.

Related Posts