DEV Community

Cover image for From Vanilla JS to Vue.js: A guide to Vue essentials
Peter Mbanugo
Peter Mbanugo

Posted on • Updated on • Originally published at pmbanugo.me

From Vanilla JS to Vue.js: A guide to Vue essentials

Vue.js is a framework for building web applications. It has a reactivity system that allows you to model and manage your application state such that when data changes, it's reflected in the UI, without you having to query the DOM. If you’ve built apps in vanilla JavaScript or with jQuery, you know you need to query DOM elements and update them in order to display some data or show some other application state.

For a large application this becomes difficult to manage. Last week I spent a few hours to learn some basic things, and adopted it in a side-project built in vanilla JS. I want to share with you some of my learnings and compare side by side the differences in code. The project is a shopping list progressive web application which uses Hoodie.

If you want to follow along, you can download the source code in Vanilla JS while I show you how I added in Vue (follow this link if you want to read about how I built the app in Vanilla JS, Hoodie, and Service Worker).

Adding items

The application allows users to add shopping items to their shopping list. This is done in index.html in the public folder. Lines 92 to 124 contains the markup for this:

<div>
  <div class="mdl-grid center-items">
  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="text" id="new-item-name">
      <label class="mdl-textfield__label" for="new-item-name">Item Name</label>
  </div>
  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="number" id="new-item-cost">
      <label class="mdl-textfield__label" for="new-item-cost">Item Cost</label>
  </div>
  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="number" id="new-item-quantity">
      <label class="mdl-textfield__label" for="new-item-quantity">Quantity</label>
  </div>
  </div>

  <div class="mdl-grid center-items">
  <button id="add-item" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
      Add Item
  </button>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The code to handle data collection and saving the data is in the file public/js/src/index.js. The function saveNewItem() on line 28 collects the value from the input elements and saves the item. It is then bound to the click event of the add-item button. Here’s the code:

function saveNewitem() {
  let name = document.getElementById("new-item-name").value;
  let cost = document.getElementById("new-item-cost").value;
  let quantity = document.getElementById("new-item-quantity").value;
  let subTotal = cost * quantity;
  if (name && cost && quantity) {
    hoodie.store.withIdPrefix("item").add({
      name: name,
      cost: cost,
      quantity: quantity,
      subTotal: subTotal
    });
    document.getElementById("new-item-name").value = "";
    document.getElementById("new-item-cost").value = "";
    document.getElementById("new-item-quantity").value = "";
  } else {
    let snackbarContainer = document.querySelector("#toast");
    snackbarContainer.MaterialSnackbar.showSnackbar({
      message: "All fields are required"
    });
  }
}

document.getElementById("add-item").addEventListener("click", saveNewitem);
Enter fullscreen mode Exit fullscreen mode

Switching to Vue

In adopting Vue the first thing is to add a reference to Vue on your page. I added this to index.html as follows:

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
Enter fullscreen mode Exit fullscreen mode

I also added a div element with an id of app to surround every page elements inside the body tag. This is needed because when we will initialise a Vue instance, we need to tell it what section of our app we want it to control. And by doing this I’m telling it to manage everything inside that block. I modified the markup to use some Vue directives. Vue Directives are special attributes with the v- prefix. Below is the updated markup

<form v-on:submit.prevent="onSubmit">
  <div class="mdl-grid center-items">
    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="text" id="new-item-name" v-model="name">
      <label class="mdl-textfield__label" for="new-item-name">Item Name</label>
    </div>
    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="number" id="new-item-cost" v-model.number="cost">
      <label class="mdl-textfield__label" for="new-item-cost">Item Cost</label>
    </div>
    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="number" id="new-item-quantity" v-model.number="quantity">
      <label class="mdl-textfield__label" for="new-item-quantity">Quantity</label>
    </div>
  </div>

  <div class="mdl-grid center-items">
    <button id="add-item" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
      Add Item
    </button>
  </div>
</form>
Enter fullscreen mode Exit fullscreen mode

The v-on directive if used to listen to DOM events. In code above it’s used in the form element to listen to the submit event. It also uses the .prevent modifier which tells the v-on directive to call event.preventDefault() on the triggered event. We’ve used v-model directives on the input elements. It is used to create two-way data bindings on form input. It will automatically pick the correct way to update the element based on the input type. We’ve used the .number modifier for the cost and quantity input elements. What it does is to automatically typecast the value from the input element to a number. This is because even if the type set is type=number the value will always return string. So these modifiers I’ve used here helps short-circuit some extra checks checks we would have had to do.

I created a new file index-vue.js to contain code equivalent of what is in index.js but using Vue. Below is the code in this file, which creates a Vue instance with needed properties to handle the form event and collect data.

const vm = new Vue({
  el: "#app",
  data: {
    name: "",
    cost: "",
    quantity: ""
  },
  methods: {
    onSubmit: function(event) {
      if (this.name && this.cost && this.quantity) {
        hoodie.store.withIdPrefix("item").add({
          name: this.name,
          cost: this.cost,
          quantity: this.quantity,
          subTotal: this.cost * this.quantity
        });

        this.name = "";
        this.cost = "";
        this.quantity = "";
      } else {
        const snackbarContainer = document.querySelector("#toast");
        snackbarContainer.MaterialSnackbar.showSnackbar({
          message: "All fields are required"
        });
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

In the code block above, I created a Vue instance passing it an object that tells Vue how to setup the application. The el property tells it the id of the DOM element that Vue will pick and define its territory. It’s within this territory that it picks Vue directives (and other things related to Vue), and when it is initialised, it sets up bindings and event handlers for the app.

The data property contains the application state. All the properties in the containing object here will be added to Vue’s reactivity system when the Vue instance is initialised. It is this reactivity system that causes the UI to update when one of the values bound to the DOM changes. For example, the name property is bound to the name input element using the v-model="name" directive. That directive sets up a two-way binding between the name and the input element such that when a character is added or removed in the input field, it updates the name property which will cause the value of the input to reflect the current value of name. Same way other elements bound to the name will also change as a user types in value.

The methods property contains functions. The code above defines an onSubmit() function that is bound to the form’s submit event.

Displaying Saved Items

The onSubmit functions saves an item to Hoodie. I want to display the items added in a table in the UI. The Vanilla JS app had the following markup for it:

<div class="mdl-grid center-items">
  <table id="item-table" class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
    <thead>
      <tr>
        <th class="mdl-data-table__cell--non-numeric">Item Name</th>
        <th class="mdl-data-table__cell--non-numeric">Cost</th>
        <th class="mdl-data-table__cell--non-numeric">Quantity</th>
        <th class="mdl-data-table__cell">Sub-total</th>
        <th class="mdl-data-table__cell--non-numeric">
          <button class="mdl-button mdl-js-button mdl-button--icon">
            <i class="material-icons">delete</i>
          </button>
        </th>
      </tr>
    </thead>
    <tbody>

    </tbody>
  </table>
</div>
<div class="mdl-grid center-items">
  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
    <input class="mdl-textfield__input" type="number" id="total-cost" readonly value="0">
    <label class="mdl-textfield__label" for="cost">Total Item Cost</label>
  </div>
</div>

<script id="item-row" type="text/template">
  <tr id='{{row-id}}'>      
    <td class="mdl-data-table__cell--non-numeric">{{name}}</td>
    <td class="mdl-data-table__cell--non-numeric">{{cost}}</td>
    <td class="mdl-data-table__cell--non-numeric">{{quantity}}</td>
    <td class="mdl-data-table__cell">{{subTotal}}</td>
    <td class="mdl-data-table__cell--non-numeric">
          <button class="mdl-button mdl-js-button mdl-button--icon mdl-button--colored"
          onclick="pageEvents.deleteItem('{{item-id}}')">
          <i class="material-icons">remove</i>
          </button>
    </td>
  </tr>
</script>
Enter fullscreen mode Exit fullscreen mode

In the markup above I used micro-template because the table will contain dynamic data and I want a way to replace some placeholder with real data and attach it to the DOM.

Below is the code that displays the items in the UI as it gets added:

function addItemToPage(item) {
  if (document.getElementById(item._id)) return;
  let template = document.querySelector("#item-row").innerHTML;
  template = template.replace("{{name}}", item.name);
  template = template.replace("{{cost}}", item.cost);
  template = template.replace("{{quantity}}", item.quantity);
  template = template.replace("{{subTotal}}", item.subTotal);
  template = template.replace("{{row-id}}", item._id);
  template = template.replace("{{item-id}}", item._id);
  document.getElementById("item-table").tBodies[0].innerHTML += template;

  let totalCost = Number.parseFloat(
    document.getElementById("total-cost").value
  );

  document.getElementById("total-cost").value = totalCost + item.subTotal;
}

hoodie.store.withIdPrefix("item").on("add", addItemToPage);
Enter fullscreen mode Exit fullscreen mode

In the code block above, it gets the script template from the DOM, replaces the placeholders with actual data, then appends it to the DOM. The total cost is also calculated and displayed in the UI.

The Vue Alternative

Transitioning to Vue I removed the script template from the page and updated the table element to use Vue’s v-for directive which will loop through data property that contains the items. Below is the markup

<div class="mdl-grid center-items">
  <table id="item-table" class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
    <thead>
      <tr>
        <th class="mdl-data-table__cell--non-numeric">Item Name</th>
        <th class="mdl-data-table__cell--non-numeric">Cost</th>
        <th class="mdl-data-table__cell--non-numeric">Quantity</th>
        <th class="mdl-data-table__cell">Sub-total</th>
        <th class="mdl-data-table__cell--non-numeric">
          <button class="mdl-button mdl-js-button mdl-button--icon">
            <i class="material-icons">delete</i>
          </button>
        </th>
      </tr>

    </thead>
    <tbody>
      <tr v-for="item in items" :key="item._id">
        <td class="mdl-data-table__cell--non-numeric">{{ item.name}}</td>
        <td class="mdl-data-table__cell--non-numeric">{{ item.cost}}</td>
        <td class="mdl-data-table__cell--non-numeric">{{ item.quantity}}</td>
        <td class="mdl-data-table__cell">{{ item.subTotal}}</td>
        <td class="mdl-data-table__cell--non-numeric">
          <button @click="deleteRow(item._id)" class="mdl-button mdl-js-button mdl-button--icon mdl-button--colored">
            <i class="material-icons">remove</i>
          </button>
        </td>
      </tr>
    </tbody>
  </table>
</div>

<div class="mdl-grid center-items">
  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
    <!-- <input class="mdl-textfield__input" type="number" id="total-cost" readonly value="0">
    <label class="mdl-textfield__label" for="cost">Total Item Cost</label> -->
    <h4>Total Cost: {{ total }}</h4>
  </div>

</div>
Enter fullscreen mode Exit fullscreen mode

There isn’t a big change in the markup. I copied the content from the previous micro-template and used Vue directives and text interpolation. I’m using the v-for directive to render the list of items which will be gotten from a data property called items. The respective columns renders the data using Vue’s text interpolation {{ item.name }}. This is similar to the placeholder we used with the micro-template. Total is displayed on the page using text interpolation.

Updating the JavaScript code in index-vue.js will give us the following:

const vm = new Vue({
  el: "#app",
  data: {
    name: "",
    cost: "",
    quantity: "",
    items: []
  },
  computed: {
    // a computed getter
    total: function() {
      // `this` points to the vm instance
      return this.items.reduce(
        (accumulator, currentValue) => accumulator + currentValue.subTotal,
        0
      );
    }
  },
  methods: {
    .....
  }
});

hoodie.store.withIdPrefix("item").on("add", item => vm.items.push(item));
Enter fullscreen mode Exit fullscreen mode

The Vue adaptation is much shorter and simpler. What I did in the code above was add a data property items, which is what gets used in the v-for directive seen earlier. When an item gets added Hoodie calls the function which runs vm.items.push(item) to update the state and with Vue’s reactive system the UI is automatically updated. To calculate the total there’s no need to track items in the DOM. I used a computed property which runs a reduce function on items. Now with Vue’s reactive system the UI gets updated whenever any of these values change. The good thing here is that I don’t have to worry about DOM manipulation in my code. So in fewer lines of code we achieved what required more code when using vanilla JS (I reckon it would be a similar thing with jQuery).

Save items as a list

After adding items, I want to save them for later reference and be able to add another list of shopping items. I have a button Save List which will gather the items, save them as a group of items with hoodie, and allow the user add new set of items.

The Vanilla JS version had the button bound to a click event. Below is the markup and code that made it work

//index.html
<div class="mdl-grid center-items">
  <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored" onclick="pageEvents.saveList()">
    Save List
  </button>
</div>
Enter fullscreen mode Exit fullscreen mode
//index.js
function saveList() {
  let cost = 0;

  hoodie.store
    .withIdPrefix("item")
    .findAll()
    .then(function(items) {
      for (var item of items) {
        cost += item.subTotal;
      }

      //store the list
      hoodie.store.withIdPrefix("list").add({
        cost: cost,
        items: items
      });

      //delete the items
      hoodie.store
        .withIdPrefix("item")
        .remove(items)
        .then(function() {
          //clear the table
          document.getElementById("item-table").tBodies[0].innerHTML = "";

          //notify the user
          var snackbarContainer = document.querySelector("#toast");
          snackbarContainer.MaterialSnackbar.showSnackbar({
            message: "List saved succesfully"
          });
        })
        .catch(function(error) {
          //notify the user
          var snackbarContainer = document.querySelector("#toast");
          snackbarContainer.MaterialSnackbar.showSnackbar({
            message: error.message
          });
        });
    });
}

window.pageEvents = {
  deleteItem: deleteItem,
  saveList: saveList
  ....
};
Enter fullscreen mode Exit fullscreen mode

Vue Alternative

Switching to Vue didn’t require much of a difference. I still had to bind to a click event and added the event handler method to the methods property in the Vue options object during initialisation.

Below is the markup for it:

<div class="mdl-grid center-items">
  <button @click="saveList" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
    Save List
  </button>
</div>
Enter fullscreen mode Exit fullscreen mode

The @click="saveList" is a shorthand for v-on:click=saveList" which is used to listen to a DOM event. The same saveList function from the Vanilla JS version is added to the methods property of the Vue object.

Navigation Bar

Now that the items can be saved as a list, I want to see a history with the total cost of each list in a period of time. It will be on another page and will look like what’s shown in the image below

This page has its markup in public/history.html and code to control the page in public/js/src/history.js. This page shares some code in common with index.html which is the navigation bar at the top. The navigation bar contains the links to different pages, the Login and Register links which when clicked brings up login or register dialog forms, and the Signout button.

In the version of the app that’s using Vanilla JS I duplicated the same HTML markup in both pages. Below is the markup for the Navigation bar:

<header class="mdl-layout__header">
    <div class="mdl-layout__header-row">
    <!-- Title -->
    <span class="mdl-layout-title">Shopping List</span>
    <!-- Add spacer, to align navigation to the right -->
    <div class="mdl-layout-spacer"></div>
    <!-- Navigation. We hide it in small screens. -->
    <nav class="mdl-navigation mdl-layout--large-screen-only">
        <a class="mdl-navigation__link" href="index.html">Home</a>
        <a class="mdl-navigation__link" href="history.html">History</a>
        <a onclick="pageEvents.showLogin()" style="cursor: pointer" class="mdl-navigation__link login">Login</a>
        <a onclick="pageEvents.showRegister()" style="cursor: pointer" class="mdl-navigation__link register">Register</a>
        <a onclick="pageEvents.signout()" style="cursor: pointer" class="mdl-navigation__link logout">Logout</a>
    </nav>
    </div>
</header>
<div class="mdl-layout__drawer">
    <span class="mdl-layout-title">Shopping List</span>
    <nav class="mdl-navigation">
    <a class="mdl-navigation__link" href="index.html">Home</a>
    <a class="mdl-navigation__link" href="history.html">History</a>
    <a onclick="pageEvents.showLogin()" style="cursor: pointer" class="mdl-navigation__link login">Login</a>
    <a onclick="pageEvents.showRegister()" style="cursor: pointer" class="mdl-navigation__link register">Register</a>
    <a onclick="pageEvents.signout()" style="cursor: pointer" class="mdl-navigation__link logout">Logout</a>
    </nav>
</div>
Enter fullscreen mode Exit fullscreen mode

From the markup you can see that when the links for Login, Register, and Logout are clicked, they call their respective methods. Those page event handlers are defined in index.js

import * as shared from "shared.js";

....

shared.updateDOMLoginStatus();
window.pageEvents = {
  showLogin: shared.showLoginDialog,
  showRegister: shared.showRegisterDialog,
  signout: shared.signOut
};

The actual functions that gets called are defined in `shared.js`. Below are the functions in `shared.js` responsible for the navigation bar:


//register dialog element
let loginDialog = document.querySelector("#login-dialog");
dialogPolyfill.registerDialog(loginDialog);
let registerDialog = document.querySelector("#register-dialog");
dialogPolyfill.registerDialog(registerDialog);

let showLoginDialog = function() {
  loginDialog.showModal();
};

let showRegisterDialog = function() {
  registerDialog.showModal();
};

let showAnonymous = function() {
  document.getElementsByClassName("login")[0].style.display = "inline";
  document.getElementsByClassName("login")[1].style.display = "inline";
  document.getElementsByClassName("register")[0].style.display = "inline";
  document.getElementsByClassName("register")[1].style.display = "inline";
  document.getElementsByClassName("logout")[0].style.display = "none";
  document.getElementsByClassName("logout")[1].style.display = "none";
};

let showLoggedIn = function() {
  document.getElementsByClassName("login")[0].style.display = "none";
  document.getElementsByClassName("login")[1].style.display = "none";
  document.getElementsByClassName("register")[0].style.display = "none";
  document.getElementsByClassName("register")[1].style.display = "none";
  document.getElementsByClassName("logout")[0].style.display = "inline";
  document.getElementsByClassName("logout")[1].style.display = "inline";
};

let updateDOMLoginStatus = () => {
  hoodie.account.get("session").then(function(session) {
    if (!session) {
      // user is singed out
      showAnonymous();
    } else if (session.invalid) {
      // user is signed in, but session is no longer authenticated
      showAnonymous();
    } else {
      // user is signed in
      showLoggedIn();
    }
  });
};

let signOut = function() {
  hoodie.account
    .signOut()
    .then(function() {
      showAnonymous();
      let snackbarContainer = document.querySelector("#toast");
      snackbarContainer.MaterialSnackbar.showSnackbar({
        message: "You logged out"
      });
      location.href = location.origin;
    })
    .catch(function() {
      let snackbarContainer = document.querySelector("#toast");
      snackbarContainer.MaterialSnackbar.showSnackbar({
        message: "Could not logout"
      });
    });
};

export {
  signOut,
  showRegisterDialog,
  showLoginDialog,
  updateDOMLoginStatus
};
Enter fullscreen mode Exit fullscreen mode

This code exports functions which was used in index.js. The showLoginDialog() and showRegisterDialog() functions displays a Modal for login and register respectively. The signout() functions logs the user out and calls showAnonymous() which hides the Logout link and shows only the Register and Login links. The function updateDOMLoginStatus checks if the user is authenticated and displays the appropriate links. This function is called when the page loads.

Achieving a shared navigation bar required duplicating markup and querying DOM elements and applying CSS to show and hide links in the navigation bar. Let’s look at the Vue alternative.

Vue Alternative

Many web applications have portions that are the same across pages, for example navigation headers. These should be abstracted into some sort of container or component. Vue provides what is called a component, which can be used to solve the issue of the navigation bar in this example. Vue components are self-contained and re-usable.

Moving to Vue components I created a new file shared-vue.js. Inside it I defined a Vue component for the navigation bar as follows:

Vue.component("navigation", {
  props: ["isLoggedIn", "toggleLoggedIn"],
  template: `<div>
              <header class="mdl-layout__header">
        <div class="mdl-layout__header-row">
          <!-- Title -->
          <span class="mdl-layout-title">Shopping List</span>
          <!-- Add spacer, to align navigation to the right -->
          <div class="mdl-layout-spacer"></div>
          <!-- Navigation. We hide it in small screens. -->
          <nav class="mdl-navigation mdl-layout--large-screen-only">
            <a class="mdl-navigation__link" href="index.html">Home</a>
            <a class="mdl-navigation__link" href="history.html">History</a>
            <a v-show="!isLoggedIn" @click="showLogin" style="cursor: pointer" class="mdl-navigation__link login">Login</a>
            <a v-show="!isLoggedIn" @click="showRegister" style="cursor: pointer" class="mdl-navigation__link register">Register</a>
            <a v-show="isLoggedIn" @click="logout" style="cursor: pointer" class="mdl-navigation__link logout">Logout</a>
          </nav>
        </div>
      </header>
      <div class="mdl-layout__drawer">
        <span class="mdl-layout-title">Shopping List</span>
        <nav class="mdl-navigation">
          <a class="mdl-navigation__link" href="index.html">Home</a>
          <a class="mdl-navigation__link" href="history.html">History</a>
          <a v-show="!isLoggedIn" @click="showLogin" style="cursor: pointer" class="mdl-navigation__link login">Login</a>
          <a v-show="!isLoggedIn" @click="showRegister" style="cursor: pointer" class="mdl-navigation__link register">Register</a>
          <a v-show="isLoggedIn" @click="logout" style="cursor: pointer" class="mdl-navigation__link logout">Logout</a>
        </nav>
      </div>
            </div>`,
  methods: {
    showLogin: function() {
      const loginDialog = document.querySelector("#login-dialog");
      dialogPolyfill.registerDialog(loginDialog);
      loginDialog.showModal();
    },
    showRegister: function() {
      const registerDialog = document.querySelector("#register-dialog");
      dialogPolyfill.registerDialog(registerDialog);
      registerDialog.showModal();
    },
    logout: function() {
      hoodie.account
        .signOut()
        .then(() => {
          this.toggleLoggedIn();
        })
        .catch(error => {
          alert("Could not logout");
        });
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

In the code above we registered a Vue component named navigation with an options object similar to what we used when creating a Vue instance. The first property is the props. Props are a way to pass data to a component. A component can define its own data, but in cases where a piece of application state needs to be used in different components, props are used. The isLoggedIn props holds a boolean value showing if a user is authenticated or not.

The second property template contains the markup that will be shown in the page. The markup is almost exactly like the vanilla JS alternative in the previous section, except that we’ve used two Vue directives, v-show and @click. The v-show attribute is used for conditional rendering. Here I’m telling it to show the Logout link when isLoggedIn is true, or show Login and Register links when it’s false. Vue also provides v-if and v-else for conditional rendering and you can read more about them here. The @click attribute is a shorthand for v-on:click directive. I’ve set showLogin, showRegister, and logout as event handlers for click events of the respective links.

These functions are defined in the methods property. The logout function after successful signout, calls this.toggleLoggedIn() which is the props passed to this component. This will execute the function passed props, and is expected to change the value of isLoggedIn props which is this component can’t modify. When it changes, Vue’s reactivity system will update the DOM accordingly.

This component is added to index.html like as a custom element. I’ll remove the navigation bar markup from lines 59 to 84 and replace it with the following

<navigation v-bind:is-logged-in="isLoggedIn" v-bind:toggle-logged-in="toggleLoggedIn"></navigation>
Enter fullscreen mode Exit fullscreen mode

In the JavaScript code we declared props isLoggedIn and toggleLoggedIn, but when passing props these values use their kebab-cased equivalents. This is because HTML attributes are case-insensitive. I’ve used the v-bind directive to pass values for these props dynamically. Without this directive, it’ll be passed as a static value and the component will receive the string isLoggedIn instead of a boolean value. We can as well use the shorthand : for v-bind and it can be re-written as <navigation :is-logged-in="isLoggedIn" :toggle-logged-in="toggleLoggedIn"></navigation>

The value isLoggedIn is an application state and toggleLoggedIn is a method declared in the Vue instance in index-vue.js as follows

const vm = new Vue({
  el: "#app",
  data: {
    name: "",
    cost: "",
    quantity: "",
    items: [],
    isLoggedIn: false
  },
  computed: {
    .....//collapsed code
  },
  methods: {
    toggleLoggedIn: function() {
      this.isLoggedIn = !this.isLoggedIn;
    },
    ......//collapsed code
  }
});

.....//collapsed code

hoodie.account.get("session").then(function(session) {
  if (!session) {
    // user is singed out
    vm.isLoggedIn = false;
  } else if (session.invalid) {
    vm.isLoggedIn = false;
  } else {
    // user is signed in
    vm.isLoggedIn = true;
  }
});
Enter fullscreen mode Exit fullscreen mode

With the Vue alternative I’ve eliminated duplicate markup and if in the future I need to make any change for it, I will do it from one location, and this is done using Vue component. I eliminated having to traverse/query the DOM to select which elements to show or hide based on the authentication state.

Login Dialog

The Login and Register links shows a modal which allows a user to enter username and password to get authenticated. The markup for both are duplicated across pages just like the Navigation bar. This can be seen on lines 171 to 244 in index.html and lines 100 to 158 in history.html.

<dialog id="login-dialog" class="mdl-dialog">
  <h4 class="mdl-dialog__title">Login</h4>
  <div class="mdl-dialog__content">
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <input class="mdl-textfield__input" type="text" id="login-username">
        <label class="mdl-textfield__label" for="login-username">Username</label>
      </div>
    </div>
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <input class="mdl-textfield__input" type="password" id="login-password">
        <label class="mdl-textfield__label" for="login-password">Password</label>
      </div>
    </div>
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <span id="login-error"></span>
      </div>
    </div>
  </div>
  <div class="mdl-dialog__actions">
    <button onclick="pageEvents.closeLogin()" type="button" class="mdl-button close">Cancel</button>
    <button onclick="pageEvents.login()" type="button" class="mdl-button">Login</button>
  </div>
</dialog>

<dialog id="register-dialog" class="mdl-dialog">
  <h4 class="mdl-dialog__title">Login</h4>
  <div class="mdl-dialog__content">
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <input class="mdl-textfield__input" type="text" id="register-username">
        <label class="mdl-textfield__label" for="register-username">Username</label>
      </div>
    </div>
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <input class="mdl-textfield__input" type="password" id="register-password">
        <label class="mdl-textfield__label" for="register-password">Password</label>
      </div>
    </div>
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <span id="register-error"></span>
      </div>
    </div>
  </div>
  <div class="mdl-dialog__actions">
    <button onclick="pageEvents.closeRegister()" type="button" class="mdl-button close">Cancel</button>
    <button onclick="pageEvents.register()" type="button" class="mdl-button">Register</button>
  </div>
</dialog>
Enter fullscreen mode Exit fullscreen mode

The code to handle both login and register is defined in shared.js and used in index.js

//shared.js

//register dialog element
let loginDialog = document.querySelector("#login-dialog");
dialogPolyfill.registerDialog(loginDialog);
let registerDialog = document.querySelector("#register-dialog");
dialogPolyfill.registerDialog(registerDialog);

let closeLoginDialog = function() {
  loginDialog.close();
};

let closeRegisterDialog = function() {
  registerDialog.close();
};

let showAnonymous = function() {
  ...
};

let showLoggedIn = function() {
  ....
};

let signOut = function() {
  ....
};

let updateDOMLoginStatus = () => {
  ....
};

let login = function() {
  let username = document.querySelector("#login-username").value;
  let password = document.querySelector("#login-password").value;

  hoodie.account
    .signIn({
      username: username,
      password: password
    })
    .then(function() {
      showLoggedIn();
      closeLoginDialog();

      let snackbarContainer = document.querySelector("#toast");
      snackbarContainer.MaterialSnackbar.showSnackbar({
        message: "You logged in"
      });
    })
    .catch(function(error) {
      console.log(error);
      document.querySelector("#login-error").innerHTML = error.message;
    });
};

let register = function() {
  let username = document.querySelector("#register-username").value;
  let password = document.querySelector("#register-password").value;
  let options = { username: username, password: password };

  hoodie.account
    .signUp(options)
    .then(function(account) {
      return hoodie.account.signIn(options);
    })
    .then(account => {
      showLoggedIn();
      closeRegisterDialog();
      return account;
    })
    .catch(function(error) {
      console.log(error);
      document.querySelector("#register-error").innerHTML = error.message;
    });
};

export {
  register,
  login,
  closeRegisterDialog,
  closeLoginDialog,
  ...
};
Enter fullscreen mode Exit fullscreen mode

index.js

//index.js

window.pageEvents = {
  closeLogin: shared.closeLoginDialog,
  showLogin: shared.showLoginDialog,
  closeRegister: shared.closeRegisterDialog,
  showRegister: shared.showRegisterDialog,
  login: shared.login,
  register: shared.register,
  signout: shared.signOut
};
Enter fullscreen mode Exit fullscreen mode

Vue Alternative

When switching to Vue I used separate component for both login and register component. Below is the component registration for login dialog

Vue.component("login-dialog", {
  data: function() {
    return {
      username: "",
      password: ""
    };
  },
  props: ["toggleLoggedIn"],
  template: `<dialog id="login-dialog" class="mdl-dialog">
      <h4 class="mdl-dialog__title">Login</h4>
      <div class="mdl-dialog__content">
        <div class="mdl-grid center-items">
          <!-- Simple Textfield -->
          <div class="mdl-textfield mdl-js-textfield">
            <input v-model="username" class="mdl-textfield__input" type="text" id="login-username">
            <label class="mdl-textfield__label" for="login-username">Username</label>
          </div>
        </div>
        <div class="mdl-grid center-items">
          <!-- Simple Textfield -->
          <div class="mdl-textfield mdl-js-textfield">
            <input v-model="password" class="mdl-textfield__input" type="password" id="login-password">
            <label class="mdl-textfield__label" for="login-password">Password</label>
          </div>
        </div>
        <div class="mdl-grid center-items">
          <!-- Simple Textfield -->
          <div class="mdl-textfield mdl-js-textfield">
            <span id="login-error"></span>
          </div>
        </div>
      </div>
      <div class="mdl-dialog__actions">
        <button @click="closeLogin" type="button" class="mdl-button close">Cancel</button>
        <button @click="login" type="button" class="mdl-button">Login</button>
      </div>
    </dialog>`,
  methods: {
    closeLogin: function() {
      const loginDialog = document.querySelector("#login-dialog");
      dialogPolyfill.registerDialog(loginDialog);
      loginDialog.close();
    },
    login: function(event) {
      hoodie.account
        .signIn({
          username: this.username,
          password: this.password
        })
        .then(() => {
          this.toggleLoggedIn();
          this.closeLogin();
        })
        .catch(error => {
          console.log(error);
          document.querySelector("#login-error").innerHTML = "Error loggin in";
        });
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

It is registered with data, props, template and methods as properties of the options object passed to Vue.component(). Then on the pages I replace the markup with Vue’s custom element

//index.html
<login-dialog v-bind:toggle-logged-in="toggleLoggedIn"></login-dialog>
Enter fullscreen mode Exit fullscreen mode

Similar steps apply to the register dialog which I’ve skipped.

I also skipped showing some parts of the app in order to avoid showing duplicate Vue syntax. Follow this link if you want to learn how I built the whole app step by step. It also explained concepts such as Service Workers and Push API.

Conclusion

So far I’ve shown you some of the changes I made to my application while moving from Vanilla JS to Vue.js. It’s not complicated to start using Vue if you’ve just learned how to build web apps with HTML, CSS and JavaScript (or jQuery). You don’t need to know ES6 or understand any build step to get started. I achieved less code duplication and better code organisation while using Vue. I only covered the basic things you need to understand to start using Vue, but of course there’s more to Vue. In a future post I’ll cover more concepts as I keep digging into it and hopefully share how to build a fairly complex single page applications with it.

You can find the complete code for the Vanilla JS and Vue.js applications on GitHub using the links below.

Thanks to Andrew Hedges for taking the time to review this.

Oldest comments (0)