Vue.js Login Component with Spinner
About
The code is available in a Github repository
What we will be creating
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
- Vue CLI for rapid Vue.js development
- sass-loader node-sass style-loader for SCSS Support
- Axios for making Promise based HTTP requests (to the RESTful API)
- Vuex for state management
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
toLogin
- 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
- Replace the import in the script from
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 tofalse
- Set the
loginError
property to the parameter passed to the Mutation (null
or a STRING) - Set the
loginSuccessful
property totrue
if there was no error,false
if there was
- Set the
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 (withnull
as parameter because no error occurred) - If the request was successful, the
loginStop
Mutation will be committed (witherror.response.data.error
as parameter, to pass the error from the backend)
- If the request was successful, the
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 notnull
) - 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>
- The
loading.gif
file can be found in the Github repository
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 thedoLogin
action with bothemail
and thepassword
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.