Luke Evers

Crafting a Login Modal with Vuejs

Posted on 17 August 2015

Vue

A few weeks ago I started working on a sub project called LineLevel (more info to come when it's closer to alpha release), and I decided to use the Vue JavaScript framework. I've seen a lot of positive feedback on twitter and decided to give it a try.

Since Vue is still a fairly new framework there aren't nearly as many tutorials out there as there are on another framework--say Angular, for example.

Let's see the full product now, and then we'll get down to it. Click the links below to play with the modal.

Tutorial

Disclaimer: I haven't been using Vue for that long, and it's possible that there's a better way to do any of what I'm about to show you.

Now that you've seen it and played with it, let's break it down.

What exactly is happening here? What we've got is two buttons that do the same thing--but load a different section of the modal. The very first thing we want to do--before we even create the modal--is create a click event for the modal. In the user's mind we see two buttons that will take us either to a Register page or a Login page. What we're doing here is loading the content in a modal instead of redirecting the user to a different page.

I've wrapped the links in a div element called #fake-nav to correspond with the element that we will use later in our first Vue model.

<div id="fake-nav">  
  <a href="#register" v-on="click: open('register', $event)">Register</a>
  <a href="#login" v-on="click: open('login', $event)">Login</a>
</div>  

If you're unfamiliar with v-on that's because it's a Vue directive. Vue's guide has a lot of information; if you're just getting started with Vue, and I recommend you checkout their examples as well.

Before we get into how our click events work, let's take a look at our basic corresponding Vue model that goes along with our #fake-nav we created.

var nav = new Vue({  
  el: '#fake-nav',
  methods: {
    open: function(which, e) {
      // Prevents clicking the link from doing anything
      e.preventDefault();
    }
  }
});

Taking a look at our model, the above #fake-nav should be easier to understand. When a user clicks Register, the function open is ran with the parameters 'register' and $event. The second parameter, $event, is the actual click event that occurs when the user clicks down. If we were not sending our function the parameter 'register' (or 'login' in the other link) then we could have made our links look like this instead:

<... v-on="click: open">  

By default, the $event parameter is passed to the function, but if we override the default parameters we must include $event if we want the event as a parameter.

Before we can do anything else in our nav Vue model, we need to create our modal. The HTML is pretty straightforward for our modal. Here's the top-level structure for our modal:

<div class="user-modal-container" id="login-modal" v-on="click: close"></div>  

Our modal has the id #login-modal which will be used in our corresponding Vue model. Let's add some basic styles to our modal. I usually use less as my css preprocessor, but for this I just wrote plain CSS. You can probably make this better if you do something like this for your own application!

.user-modal-container * {
  box-sizing: border-box;
}

First of all, we're going to set box-sizing to border-box on everything inside our modal. I prefer this compared to the others, and so do others. Feel free to alter any of this CSS though if you use it! I'm not forcing you to use my CSS habits, I'm just showing a way how to make a nice login modal with Vue.

.user-modal-container {
  position: fixed;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  opacity: 0;
  visibility: hidden;
  cursor: pointer;
  overflow-y: auto;
  z-index: 3;
  font-family: 'Lato', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif';
  font-size: 14px;
  background-color: rgba(17,17,17,.9);
  -webkit-transition: all 0.25s linear;
  -moz-transition: all 0.25s linear;
  -o-transition: all 0.25s linear;
  -ms-transition: all 0.25s linear;
  transition: all 0.25s linear;
}

.user-modal-container.active {
  opacity: 1;
  visibility: visible;
}

There's a lot of style-specifics in that CSS block. Font family, size, background color, and transitions aren't exactly needed--although without background-color on our modal background it might be hard to tell there's a modal in the room.

As you can see by the CSS above, our modal is currently invisible. We've got opacity set to 0, and visibility set to hidden. By using these two CSS properties--along with our transitions--we can make our modal fade in. If we didn't want the modal to fade in, we could just set display to none and change it to block when we want to show it--but that wouldn't be any fun! CSS transitions are awesome!

Now that we've got a semi-black screen (assuming you kept my rgba background color) when our modal is visible, let's make that modal toggle-able! Going back to our nav Vue model, all we have to do to show our modal is edit our open function and have it add the active class to our modal.

open: function(which, e) {  
  e.preventDefault();
  $('#login-modal').addClass('active');
}

JSFiddle - What we've done so far

I'm using jQuery in this tutorial because I already have it in my blog theme, but I've heard vanillajs works great for all of your JavaScript needs.

Now that we can make our modal appear, let's make it disappear. First we're going to create our Vue model, and give it a close function. In our HTML above, we've set a v-on directive to run the function close on click.

var modal = new Vue({  
  el: '#login-modal',
  methods: {
    close: function(e) {
      e.preventDefault();
      $('#login-modal').removeClass('active');
    }
  }
});

This modal Vue model is pretty similar to our nav model. By removing the class active, our opacity and visibility are being reset, and the transition helps it fade out.

There's a problem with our above code though. If we click on any child element on our modal it's going to fade it out--and that's not going to help at all for our users. We want to make sure that our function will only run if the element we click is the background around the rest of the modal--and not the box in the center itself.

close: function(e) {  
  e.preventDefault();
  if (e.target === this.$el) {
    $('#login-modal').removeClass('active');
  }
}

Since we've defined el in our model as #login-modal and that's the element that we are fading, we can use the $el instance property to compare with the target element's DOM element.

JSFiddle - What we've done so far

Next we want to add some more content to our modal. Again, let's just start with the next top-level HTML.

<div class="user-modal"></div>  
.user-modal-container .user-modal {
  position: relative;
  margin: 50px auto;
  width: 90%;
  max-width: 500px;
  background-color: #f6f6f6;
  cursor: initial;
}

We need to set cursor to initial unless we want to see that pointer on every single element inside of our modal since we added it on the parent. And inside our user-modal we've got our form switcher.

<ul class="form-switcher">  
  <li v-on="click: flip('register', $event)"><a href="" id="register-form">Register</a></li>
  <li v-on="click: flip('login', $event)"><a href="" id="login-form">Login</a></li>
</ul>  

Flip! Looks like we've got another function to add to our object of methods in our modal model. We're sending a custom parameter like in the other example, so we need to send that $event as well.

flip: function(which, e) {  
  e.preventDefault();
  // TODO
}

Before we can add anything else to this function we need to finish setting everything up so we can see it all working.

.user-modal-container ul.form-switcher {
  margin: 0;
  padding: 0;
}

.user-modal-container ul.form-switcher li {
  list-style: none;
  display: inline-block;
  width: 50%;
  float: left;
  margin: 0;
}

.user-modal-container ul.form-switcher li a {
  width: 100%;
  display: block;
  height: 50px;
  line-height: 50px;
  color: #666666;
  background-color: #dddddd;
  text-align: center;
}

.user-modal-container ul.form-switcher li a.active {
  color: #000000;
  background-color: #f6f6f6;
}

We're also going to go ahead and add the rest of our top level HTML to our user-model:

<div class="form-register" id="form-register"></div>  
<div class="form-login" id="form-login"></div>  
<div class="form-password" id="form-password"></div>  
.user-modal-container .form-login,
.user-modal-container .form-register,
.user-modal-container .form-password {
  padding: 75px 25px 25px;
  display: none;
}

.user-modal-container .form-login.active,
.user-modal-container .form-register.active,
.user-modal-container .form-password.active {
  display: block;
}

Now that we've got that setup, let's go back to our flip function.

flip: function(which, e) {  
  e.preventDefault();
  if (which !== this.active) {
    $('#form-' + this.active).removeClass('active');
    $('#form-' + which).addClass('active');
    $('#'+which+'-form').addClass('active');
    $('#'+this.active+'-form').removeClass('active');

    this.active = which;
  }
}

We also need to add some data to our model so we can use this.active. We want to add an object called data to our Vue model, which should look something like this:

var modal = new Vue({  
  el: '#login-modal',
  data: {
    active: null,
  },
  methods: {
    // ...
  }
});

Now that we're saving which form is currently active, we can use that in our nav Vue model's open function.

open: function(which, e) {  
  e.preventDefault();
  if (modal.active !== null) {
      $('#form-'+modal.active).removeClass('active');
      $('#'+modal.active+'-form').removeClass('active');
  }

  $('#login-modal').addClass('active');
  $('#form-'+which).addClass('active');
  $('#'+which+'-form').addClass('active');
  modal.active = which;
}

First we want to check to see if modal.active has been used or if it's still null. If it's not, then we want to remove the active class from the current sections of the modal that are active so we don't have multiple sections being active.

After that, we add the active class to the elements that need it, and then update which modal is active.

JSFiddle - What we've done so far

Time to add in the bulk of our HTML. Here's our form register fields:

<div class="error-message" v-text="registerError"></div>  
<input type="text" name="name" placeholder="Name" v-model="registerName" v-on="keyup: submit('register', $event) | key 'enter'">  
<input type="email" name="email" placeholder="Email" v-model="registerEmail" v-on="keyup: submit('register', $event) | key 'enter'">  
<input type="password" name="password" placeholder="Password" v-model="registerPassword" v-on="keyup: submit('register', $event) | key 'enter'">  
<input type="submit" v-on="click: submit('register', $event)" v-model="registerSubmit" id="registerSubmit">  
<div class="links">  
  <a href="" v-on="click: flip('login', $event)">Already have an account?</a>
</div>  

You'll notice we have a button that asks if you already have an account--which uses our flip function from before! We don't have to do anything else to get that form switching working.

Our form login fields:

<div class="error-message" v-text="loginError"></div>  
<input type="text" name="user" placeholder="Email or Username" v-model="loginUser" v-on="keyup: submit('login', $event) | key 'enter'">  
<input type="password" name="password" placeholder="Password" v-model="loginPassword" v-on="keyup: submit('login', $event) | key 'enter'">  
<input type="submit" v-on="click: submit('login', $event)" v-model="loginSubmit"  id="loginSubmit">  
<div class="links">  
  <a href="" v-on="click: flip('password', $event)">Forgot your password?</a>
</div>  

And lastly our form password reset fields:

<div class="error-message" v-text="passwordError"></div>  
<input type="text" name="email" placeholder="Email" v-model="passwordEmail" v-on="keyup: submit('password', $event) | key 'enter'">  
<input type="submit" v-on="click: submit('password', $event)" v-model="passwordSubmit" id="passwordSubmit">  

There's a lot of stuff going on here! The first thing is that we've used the keyup filter. Both pressing the enter key on any of these input, or clicking the submit button will fire off our submit function which we have not yet defined.

Before we write the submit function there's one other Vue directive in there we should talk about: v-model. The directive v-model uses the data object in our Vue model. When we update it in the object, Vue updates it on the DOM for us!

var modal_submit_register = 'Register';  
var modal_submit_password = 'Reset Password';  
var modal_submit_login    = 'Login';

var modal = new Vue({  
  el: '#login-modal',
  data: {
    // Submit button text
    registerSubmit: modal_submit_register,
    passwordSubmit: modal_submit_password,
    loginSubmit: modal_submit_login,

    // Modal text fields
    registerName:     '',
    registerEmail:    '',
    registerPassword: '',
    loginUser:        '',
    loginPassword:    '',
    passwordEmail:    '',

    // Modal error messages
    registerError: '',
    loginError:    '',
    passwordError: '',

    // ...
  },
  methods: {
    // ...
  }
});

We're storing the default submit button information in variables because we're going to be updating the text when we hit submit to let the user know we're currently submitting, and then resetting the text. We also have error messages, which we can update as well. The other data fields will contain the form data, which is updated when the user types in new information.

Before we do anything else, let's add the rest of our CSS for our inputs:

.user-modal-container input {
  width: 100%;
  padding: 10px;
  margin-bottom: 10px;
  border: 1px solid #eeeeee;
}

.user-modal-container input[type="submit"] {
  color: #f6f6f6;
  border: 0;
  margin-bottom: 0;
  background-color: #3fb67b;
  cursor: pointer;
}

.user-modal-container input[type="submit"]:hover {
  background-color: #3aa771;
}

.user-modal-container input[type="submit"]:active {
  background-color: #379d6b;
}

.user-modal-container .links {
  text-align: center;
  padding-top: 25px;
}

.user-modal-container .links a {
  color: #3fb67b;
}

JSFiddle - What we've done so far

Now let's get down to that famous submit function we're seeing everywhere:

submit: function(which, e) {  
  e.preventDefault();
  var data = { form: which };

  switch(which) {
    case 'register':
        data.name = this.registerName;
        data.email = this.registerEmail;
        data.password = this.registerPassword;
        this.$set('registerSubmit', 'Registering...');
        break;
    case 'login':
        data.user = this.loginUser;
        data.password = this.loginPassword;
        this.$set('loginSubmit', 'Logging In...');
        break;
    case 'password':
        data.email = this.passwordEmail;
        this.$set('passwordSubmit', 'Resetting Password...')
        break;
  }

  // TODO: submit our `data` variable
}

So far what we've got should make a lot of sense. We're creating a variable called data where we'll store everything. We're using variables like this.registerName which is our v-model for the user's the following input that we added earlier:

<input type="text" name="name" v-model="registerName" ...>  

We update models by using the this.$set function. If we just wanted to update the data in the model, and not update it on the DOM we could just update it with this.registerName = 'new thing', but that wouldn't update it on the DOM.

So now we're changing our text in the submit button to say that whatever type of request that is supposed to happen, is currently happening. When we want to reset it, we can easily reset it since we saved our text in a variable.

modal.$set('registerSubmit', modal_submit_register);  

Let's also change the color of our submit buttons. In our submit function we'll add a class called disabled to the submit button that we're currently using.

$('#'+which+'Submit').addClass('disabled');
.user-modal-container input[type="submit"].disabled {
  background-color: #98d6b7;
}

JSFiddle - What we've done so far

Once we're done that, we want to actually submit the data. I'm not actually submitting data in this tutorial--if you've read this far you probably already know how to submit data with JavaScript (but it's okay if you don't!).

You can submit your data however you want. I'm also trying out vue-resource for the project I've been working on--I'll save that for another post though. You could also use jQuery.ajax(), XMLHttpRequest, or whatever else you like.

Since we're not actually submitting any data, you'll have to use your imagination for some other parts here. If we sent a POST request, you'd want to parse the response and see if you got an error or not. If you get an error, you'll want to use those error messages we set up earlier:

// Yes you can call the var name instead of `this`
modal.$set('loginError', 'Wrong input!');  

Going back to our flip function we want to remove all error messages when we switch to other types of forms. We don't have to do this, but it's nice for the user to be able to clear the error messages.

// Remove error messages
this.$set('registerError', '');  
this.$set('passwordError', '');  
this.$set('loginError', '');  

Another extra feature we definitely want to add in is adding a submit lock--if the user clicks submit 30 times while the first request is already submitting, we don't want to submit every single time. The way everything is right now, that would happen. Let's prevent that.

In our modal model we're going to add a new variable into our data:

data: {  
  lock: false
},

And in the top of our submit function we want to check our lock before we do anything else:

if (this.lock === true) return;  
// If we get this far, now we lock it!
this.lock = true;  

If our lock is set to true then we'll exit right there, but if it's set to false then we'll set it to true ASAP. When we're done receiving the response from our server we'll want to set our lock back to false so the user can make more requests.

Conclusion

Although I've explained how everything works, you might want a link to the script running on this page. Feel free to check it out, take/change/edit any of that code, and do whatever you please with it.

I wrote this tutorial for people that would like to use Vue but are unsure where to start that want more of a real-world example.

I hope that this post helped someone in any way! If anyone has been using Vue longer than I have, and see a better way to do anything that I've done, don't hesitate to leave a comment and let me know.

comments powered by Disqus