WebDevChallenges Logo
WebDevChallenges

Vue.js Login Component with Spinner

Updated June 5, 21
Login component with error and success message and a spinner. Built with Vue CLI 3, SCSS, Axios and Vuex

About

The code is available in a Github repository

What we will be creating

image

In this post I will show you, how to create a Login Component in Vue.js.

To simulate a RESTful API to connect to, I will use reqres (more specific the POST /api/login route).

The Component we create should do the following

  • Let us log in by entering an E-Mail and a Password followed by submitting the form
  • While the POST request is sent to the backend, a spinner should be overlaying the form
  • If the POST request was successful (Statuscode 2XX), a success message should be displayed
  • If the POST request was not successful (Statuscode 4XX), the error message from the backend (response) should be displayed

How will we accomplish this

To accomplish this, a variety of technologies working together will be used, such as

Install the Vue CLI

If you didn’t install the Vue CLI yet, install it

sudo npm install -g @vue/cli

Create a new Project

Create a new Project and serve it

vue create <project-name>
cd <project-name>
npm run serve

Add additional packages

Add SCSS support and axios by installing them via npm

npm install sass-loader node-sass style-loader --save-dev
npm install axios --save

Implement Vuex into the Application by using the Vue CLI (Version 3) which supports Vuex as a plugin.

vue add vuex

This will install the Vuex npm package aswell as configure the application (src/main.js) correctly to use Vuex and create a Store file in src/store.js

Adjust App and add Login Component

  • Add the Login Component src/components/Login.vue
  • Remove the HelloWorld Component in src/components/
  • Adjust the src/App.vue file
    • Replace the import in the script from HelloWorld to Login
    • Replace the template usage of <HelloWorld /> with <Login /> in the template
    • Remove the <img /> tag in the template
    • Remove the margin-top style in the styles

The App Component should look like this now

<template>
  <div id="app">
    <Login />
  </div>
</template>

<script>
import Login from './components/Login.vue'

export default {
  name: 'app',
  components: {
    Login
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
</style>

The Store

Before we edit the Login Component, I want to create the Vuex Store first.

State

  • We need a property to determine, if we are currently logging in (to display the spinner). This will be a BOOLEAN
  • We need a property for the Error Message. This will be a STRING (if the property is null, no Error Message will be displayed)
  • We need a property to determine, if the login was successful (to display the Success Message). This will be a BOOLEAN

state: {
  loggingIn: false,
  loginError: null,
  loginSuccessful: false
}

Getters

We don’t need to generate Getters for accessing unmodified state properties. ...mapState can be used instead in the Components. (I will come back to this later).

Mutations

  • We need a mutation for when we start logging in, to set the loggingIn property of the state accordingly.
  • We need a mutation for when the login request has finished. This Mutation will
    • Set the loggingIn property to false
    • Set the loginError property to the parameter passed to the Mutation (null or a STRING)
    • Set the loginSuccessful property to true if there was no error, false if there was

mutations: {
  loginStart: state => state.loggingIn = true,
  loginStop: (state, errorMessage) => {
    state.loggingIn = false;
    state.loginError = errorMessage;
    state.loginSuccessful = !errorMessage;
  }
}

Actions

We need one Action which will

  • First of all commit the loginStart Mutation
  • Send an axios.post (import axios from 'axios') request with the data passed to the action as body to the API Endpoint
    • If the request was successful, the loginStop Mutation will be committed (with null as parameter because no error occurred)
    • If the request was successful, the loginStop Mutation will be committed (with error.response.data.error as parameter, to pass the error from the backend)

By using the Spread Operator you can merge the key-value pairs of one object into another (this is an ES6 feature). In this case you could also omit it by using loginData as the body but I wanted to mention this option.

actions: {
  doLogin({ commit }, loginData) {
    commit('loginStart');

    axios.post('https://reqres.in/api/login', {
      ...loginData
    })
    .then(() => {
      commit('loginStop', null)
    })
    .catch(error => {
      commit('loginStop', error.response.data.error)
    })
  }
}

Complete Store

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    loggingIn: false,
    loginError: null,
    loginSuccessful: false
  },
  mutations: {
    loginStart: state => state.loggingIn = true,
    loginStop: (state, errorMessage) => {
      state.loggingIn = false;
      state.loginError = errorMessage;
      state.loginSuccessful = !errorMessage;
    }
  },
  actions: {
    doLogin({ commit }, loginData) {
      commit('loginStart');

      axios.post('https://reqres.in/api/login', {
        ...loginData
      })
      .then(() => {
        commit('loginStop', null)
      })
      .catch(error => {
        commit('loginStop', error.response.data.error)
      })
    }
  }
})

Login Markup

  • We need a form holding two inputs (for E-Mail and Password both with v-model binding) and a button
  • We need an element to display error messages (if loginError is not null)
  • We need an element to display the success message (if loginSuccessful)
  • We need an element to overlay and display a spinner (if loggingIn)

The advantage of using a form with ` @submit.prevent ` (instead of a click listener on the button) is, that you can submit the form either by clicking the button or by pressing ENTER when an input field is focussed.

Submitting the form will trigger a component method called loginSubmit, which will be defined in the js code of this Component.

<template>
  <div class="login">
    <div v-if="loggingIn" class="container-loading">
      <img src="/loading.gif" alt="Loading Icon">
    </div>
    <p v-if="loginError">{{ loginError }}</p>
    <p v-if="loginSuccessful">Login Successful</p>
    <form @submit.prevent="loginSubmit">
      <input type="email" placeholder="E-Mail" v-model="email">
      <input type="password" placeholder="Password" v-model="password">
      <button type="submit">Login</button>
    </form>
  </div>
</template>

Login Styling

There is not much to say about the styling. We installed all necessary packages to support scss. You can of course use css aswell.

I scoped the styles to this Component so they won’t affect the rest of the application.

<style scoped lang="scss">
  .login {
    border: 1px solid black;
    border-radius: 5px;
    padding: 1.5rem;
    width: 300px;
    margin-left: auto;
    margin-right: auto;
    position: relative;
    overflow: hidden;
    .container-loading {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      background-color: rgba(0,0,0,.3);
      img {
        width: 2rem;
        height: 2rem;
      }
    }
    form {
      display: flex;
      flex-flow: column;
      *:not(:last-child) {
        margin-bottom: 1rem;
      }
      input {
        padding: .5rem;
      }
      button {
        padding: .5rem;
        background-color: lightgray;
        border: 1px solid gray;
        border-radius: 3px;
        cursor: pointer;
        &:hover {
          background-color: lightslategray;
        }
      }
    }
  }
</style>

Login Javascript

Data

We need one property called email and one called password to bind to the input fields. They should be empty per default.

data() {
  return {
    email: '',
    password: ''
  }
}

Computed

As mentioned in the Getters section of the Store, we can use ...mapState (import { mapState } from 'vuex';) to get access to the properties of the state we need.

computed: {
  ...mapState([
    'loggingIn',
    'loginError',
    'loginSuccessful'
  ])
}

Methods

  • We need to get access to the doLogin action to dispatch it. We can use ...mapActions (import { mapActions } from 'vuex';) for this
  • We need to define the loginSubmit method as used in the template. This method should dispatch the doLogin action with both email and the password in a javascript object

methods: {
  ...mapActions([
    'doLogin'
  ]),
  loginSubmit() {
    this.doLogin({
      email: this.email,
      password: this.password
    })
  }
}

Complete Javascript

<script>
  import { mapState, mapActions } from 'vuex';

  export default {
    data() {
      return {
        email: '',
        password: ''
      }
    },
    computed: {
      ...mapState([
        'loggingIn',
        'loginError',
        'loginSuccessful'
      ])
    },
    methods: {
      ...mapActions([
        'doLogin'
      ]),
      loginSubmit() {
        this.doLogin({
          email: this.email,
          password: this.password
        })
      }
    }
  }
</script>

Test it

The reqres API Endpoint we use, will return successfully if you type any E-Mail and any Password but will fail if you leave one or both blank.

I hope this helped you. If it did, make sure to follow me on social media for upcoming posts.

Also make sure to check out my next post about persisting the token, we receive from the backend upon logging in which builds on top of the code discussed here.