Vue.js + Vuex - User Registration and Login Tutorial & Example
Tutorial built with Vue.js 2.6.10 + Vuex 3.1.2 and Webpack 4.41
Other versions available:
- Vue: Vue 3 + Pinia
- Angular: Angular 15/16, 14, 10, 9, 8, 7, 6, 2/5
- React: React 18 + Redux, React 17 + Recoil, React 16 Hooks + Redux, React 16 + Redux
- Next.js: Next.js 13 (App Router) + MongoDB, Next.js 13 (Pages Router) + MongoDB, Next.js 13 (Pages Router) + MySQL, Next.js 11
- AngularJS: AngularJS
- .NET: Blazor WebAssembly
In this tutorial we'll go through an example of how to build a simple user registration and login system using Vue.js + Vuex and webpack 4.16.
The tutorial project is available on GitHub at https://github.com/cornflourblue/vue-vuex-registration-login-example.
Here it is in action:(See on StackBlitz at https://stackblitz.com/edit/vue-vuex-registration-login-example)
Update History:
- 11 Jan 2020 - Added instructions on how to deploy Vue app to AWS and Azure
- 28 May 2019 - Updated tutorial to Vue.js 2.6.10
- 14 Jul 2018 - Built tutorial with Vue.js 2.5.16
Running the Vue.js + Vuex Tutorial Example Locally
- Install NodeJS and NPM from https://nodejs.org/en/download/.
- Download or clone the project source code from https://github.com/cornflourblue/vue-vuex-registration-login-example
- Install all required npm packages by running
npm install
from the command line in the project root folder (where the package.json is located). - Start the application by running
npm start
from the command line in the project root folder, this will launch a browser displaying the application.
For more info on setting up a Vue.js development environment see Vue - Setup Development Environment.
Running the Tutorial Example with a Real Backend API
The boilerplate code uses a fake / mock backend by default that uses browser local storage for managing application data, to switch to a real backend api you just have to remove a couple of lines of code from the main vue entry file /src/index.js
below the comment // setup fake backend
.
You can build your own backend api or start with one of the below options:
- To run the vue auth example with a real backend API built with NodeJS and MongoDB follow the instructions at NodeJS + MongoDB - Simple API for Authentication, Registration and User Management
- For an API built with NodeJS and MySQL follow the instructions at Node.js + MySQL - Simple API for Authentication, Registration and User Management
- For a real backend API built with ASP.NET Core 3.1 follow the instructions at ASP.NET Core 3.1 - Simple API for Authentication, Registration and User Management
Deploying the Vue.js App to AWS
This video shows how to setup a production ready web server from scratch on AWS, then deploy the example Vue.js + Vuex app and configure it to run with a real Node.js + MongoDB backend api. The tutorial used in the video is available at Vue.js + Node.js on AWS - How to Deploy a MEVN Stack App to Amazon EC2.
Deploying the Vue.js App to Microsoft Azure
For instructions on how to deploy the Vue.js app to Azure with a real backend api built with ASP.NET Core and SQL Server see Vue.js + ASP.NET Core + SQL on Azure - How to Deploy a Full Stack App to Microsoft Azure.
Vue.js + Vuex Tutorial Project Structure
All source code for the Vue.js + Vuex tutorial app is located in the /src
folder. Inside the src folder there is a folder per feature (app, home, login, register) and a few folders for non-feature code that can be shared across different parts of the app (_helpers, _services, _store).
I prefixed non-feature folders with an underscore "_" to group them together and make it easy to distinguish between features and non-features, it also keeps the project folder structure shallow so it's quick to see everything at a glance from the top level and to navigate around the project.
Click any of the below links to jump down to a description of each file along with it's code:
Vue + Vuex Helpers Folder
The helpers folder contains all the bits and pieces that don't fit into other folders but don't justify having a folder of their own.
Vue Auth Header
Auth header is a helper function that returns an HTTP Authorization header containing the JSON Web Token (JWT) of the currently logged in user from local storage. If the user isn't logged in an empty object is returned.
The auth header is used to make authenticated HTTP requests to the server api using JWT authentication.
export function authHeader() {
// return authorization header with jwt token
let user = JSON.parse(localStorage.getItem('user'));
if (user && user.token) {
return { 'Authorization': 'Bearer ' + user.token };
} else {
return {};
}
}
Vue Fake / Mock Backend
The fake backend is used for running the tutorial example without a server api (backend-less). It monkey patches the fetch()
function to intercept certain api requests and mimic the behaviour of a real api. Any requests that aren't intercepted get passed through to the real fetch()
function.
I created it so I could focus the tutorial on the Vue + Vuex code and not worry about the backend, and also to make it work on StackBlitz.
// array in local storage for registered users
let users = JSON.parse(localStorage.getItem('users')) || [];
export function configureFakeBackend() {
let realFetch = window.fetch;
window.fetch = function (url, opts) {
return new Promise((resolve, reject) => {
// wrap in timeout to simulate server api call
setTimeout(() => {
// authenticate
if (url.endsWith('/users/authenticate') && opts.method === 'POST') {
// get parameters from post request
let params = JSON.parse(opts.body);
// find if any user matches login credentials
let filteredUsers = users.filter(user => {
return user.username === params.username && user.password === params.password;
});
if (filteredUsers.length) {
// if login details are valid return user details and fake jwt token
let user = filteredUsers[0];
let responseJson = {
id: user.id,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
token: 'fake-jwt-token'
};
resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(responseJson)) });
} else {
// else return error
reject('Username or password is incorrect');
}
return;
}
// get users
if (url.endsWith('/users') && opts.method === 'GET') {
// check for fake auth token in header and return users if valid, this security is implemented server side in a real application
if (opts.headers && opts.headers.Authorization === 'Bearer fake-jwt-token') {
resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(users))});
} else {
// return 401 not authorised if token is null or invalid
reject('Unauthorised');
}
return;
}
// get user by id
if (url.match(/\/users\/\d+$/) && opts.method === 'GET') {
// check for fake auth token in header and return user if valid, this security is implemented server side in a real application
if (opts.headers && opts.headers.Authorization === 'Bearer fake-jwt-token') {
// find user by id in users array
let urlParts = url.split('/');
let id = parseInt(urlParts[urlParts.length - 1]);
let matchedUsers = users.filter(user => { return user.id === id; });
let user = matchedUsers.length ? matchedUsers[0] : null;
// respond 200 OK with user
resolve({ ok: true, text: () => JSON.stringify(user)});
} else {
// return 401 not authorised if token is null or invalid
reject('Unauthorised');
}
return;
}
// register user
if (url.endsWith('/users/register') && opts.method === 'POST') {
// get new user object from post body
let newUser = JSON.parse(opts.body);
// validation
let duplicateUser = users.filter(user => { return user.username === newUser.username; }).length;
if (duplicateUser) {
reject('Username "' + newUser.username + '" is already taken');
return;
}
// save new user
newUser.id = users.length ? Math.max(...users.map(user => user.id)) + 1 : 1;
users.push(newUser);
localStorage.setItem('users', JSON.stringify(users));
// respond 200 OK
resolve({ ok: true, text: () => Promise.resolve() });
return;
}
// delete user
if (url.match(/\/users\/\d+$/) && opts.method === 'DELETE') {
// check for fake auth token in header and return user if valid, this security is implemented server side in a real application
if (opts.headers && opts.headers.Authorization === 'Bearer fake-jwt-token') {
// find user by id in users array
let urlParts = url.split('/');
let id = parseInt(urlParts[urlParts.length - 1]);
for (let i = 0; i < users.length; i++) {
let user = users[i];
if (user.id === id) {
// delete user
users.splice(i, 1);
localStorage.setItem('users', JSON.stringify(users));
break;
}
}
// respond 200 OK
resolve({ ok: true, text: () => Promise.resolve() });
} else {
// return 401 not authorised if token is null or invalid
reject('Unauthorised');
}
return;
}
// pass through any requests not handled above
realFetch(url, opts).then(response => resolve(response));
}, 500);
});
}
}
Vue Router
The vue router defines all of the routes for the application, and contains a function that runs before each route change to prevent unauthenticated users from accessing restricted routes.
import Vue from 'vue';
import Router from 'vue-router';
import HomePage from '../home/HomePage'
import LoginPage from '../login/LoginPage'
import RegisterPage from '../register/RegisterPage'
Vue.use(Router);
export const router = new Router({
mode: 'history',
routes: [
{ path: '/', component: HomePage },
{ path: '/login', component: LoginPage },
{ path: '/register', component: RegisterPage },
// otherwise redirect to home
{ path: '*', redirect: '/' }
]
});
router.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/login', '/register'];
const authRequired = !publicPages.includes(to.path);
const loggedIn = localStorage.getItem('user');
if (authRequired && !loggedIn) {
return next('/login');
}
next();
})
Vue Helpers Index
The helpers index file groups all helper exports together so they can be imported in other parts of the app using only the folder path, and enables importing multiple helpers in a single statement (e.g. import { helper1, helper2, ... } from '../_helpers'
).
export * from './fake-backend';
export * from './router';
export * from './auth-header';
Vue Services Folder
The services layer handles all http communication with backend apis for the application, each service encapsulates the api calls for a content type (e.g. users) and exposes methods for performing various operations (e.g. CRUD operations). Services can also have methods that don't wrap http calls, for example the userService.logout()
method just removes an item from local storage.
I like wrapping http calls and implementation details in a services layer, it provides a clean separation of concerns and simplifies the vuex modules that use the services.
Vue User Service
The user service encapsulates all backend api calls for performing CRUD operations on user data, as well as logging and out of the example application. The service methods are exported via the userService
object at the top of the file, and the implementation of each method is located in the functions below.
In the handleResponse method the service checks if the http response from the api is 401 Unauthorized and automatically logs the user out. This handles if the JWT token expires or is no longer valid for any reason.
import config from 'config';
import { authHeader } from '../_helpers';
export const userService = {
login,
logout,
register,
getAll,
getById,
update,
delete: _delete
};
function login(username, password) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
};
return fetch(`${config.apiUrl}/users/authenticate`, requestOptions)
.then(handleResponse)
.then(user => {
// login successful if there's a jwt token in the response
if (user.token) {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify(user));
}
return user;
});
}
function logout() {
// remove user from local storage to log user out
localStorage.removeItem('user');
}
function register(user) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
};
return fetch(`${config.apiUrl}/users/register`, requestOptions).then(handleResponse);
}
function getAll() {
const requestOptions = {
method: 'GET',
headers: authHeader()
};
return fetch(`${config.apiUrl}/users`, requestOptions).then(handleResponse);
}
function getById(id) {
const requestOptions = {
method: 'GET',
headers: authHeader()
};
return fetch(`${config.apiUrl}/users/${id}`, requestOptions).then(handleResponse);
}
function update(user) {
const requestOptions = {
method: 'PUT',
headers: { ...authHeader(), 'Content-Type': 'application/json' },
body: JSON.stringify(user)
};
return fetch(`${config.apiUrl}/users/${user.id}`, requestOptions).then(handleResponse);
}
// prefixed function name with underscore because delete is a reserved word in javascript
function _delete(id) {
const requestOptions = {
method: 'DELETE',
headers: authHeader()
};
return fetch(`${config.apiUrl}/users/${id}`, requestOptions).then(handleResponse);
}
function handleResponse(response) {
return response.text().then(text => {
const data = text && JSON.parse(text);
if (!response.ok) {
if (response.status === 401) {
// auto logout if 401 response returned from api
logout();
location.reload(true);
}
const error = (data && data.message) || response.statusText;
return Promise.reject(error);
}
return data;
});
}
Vue Services Index
The services index file groups all service exports together so they can be imported in other parts of the app using only the folder path, and enables importing multiple services in a single statement (e.g. import { service1, service2, ... } from '../_services'
).
export * from './user.service';
Vuex Store Folder
The vuex store folder contains all vuex modules and everything relating to the vuex store, if you're new to vuex you can find out all about it at https://vuex.vuejs.org/.
In a nutshell: Vuex manages a centralised state store for the tutorial application that can be accessed from any component, mutations are committed to update sections of the state, and actions are dispatched to perform more complex operations that can include async calls and multiple mutations.
Vuex Account Module
The vuex account module is in charge of the account
section of the centralised state store. It contains actions for registering a new user, logging in and logging out of the tutorial application, and contains mutations for each of the lower level state changes involved for each account action.
The initial logged in state of the user is set by checking if the user is saved in local storage, which keeps the user logged in if the browser is refreshed and between browser sessions.
import { userService } from '../_services';
import { router } from '../_helpers';
const user = JSON.parse(localStorage.getItem('user'));
const state = user
? { status: { loggedIn: true }, user }
: { status: {}, user: null };
const actions = {
login({ dispatch, commit }, { username, password }) {
commit('loginRequest', { username });
userService.login(username, password)
.then(
user => {
commit('loginSuccess', user);
router.push('/');
},
error => {
commit('loginFailure', error);
dispatch('alert/error', error, { root: true });
}
);
},
logout({ commit }) {
userService.logout();
commit('logout');
},
register({ dispatch, commit }, user) {
commit('registerRequest', user);
userService.register(user)
.then(
user => {
commit('registerSuccess', user);
router.push('/login');
setTimeout(() => {
// display success message after route change completes
dispatch('alert/success', 'Registration successful', { root: true });
})
},
error => {
commit('registerFailure', error);
dispatch('alert/error', error, { root: true });
}
);
}
};
const mutations = {
loginRequest(state, user) {
state.status = { loggingIn: true };
state.user = user;
},
loginSuccess(state, user) {
state.status = { loggedIn: true };
state.user = user;
},
loginFailure(state) {
state.status = {};
state.user = null;
},
logout(state) {
state.status = {};
state.user = null;
},
registerRequest(state, user) {
state.status = { registering: true };
},
registerSuccess(state, user) {
state.status = {};
},
registerFailure(state, error) {
state.status = {};
}
};
export const account = {
namespaced: true,
state,
actions,
mutations
};
Vuex Alert Module
The vuex alert module is in charge of the alert
section of the centralised state store, it contains actions and mutations for setting a success or error alert message, and for clearing the alert.
In this module each alert action just commits a single mutation so it would be possible to commit the mutations directly from your vue components and get rid of the actions. However I prefer to dispatch actions from everywhere for consistency rather than dispatching actions for some things and committing mutations for others. This way also provides a bit more flexibility if you decide to expand an action to do more than just committing a mutation.
const state = {
type: null,
message: null
};
const actions = {
success({ commit }, message) {
commit('success', message);
},
error({ commit }, message) {
commit('error', message);
},
clear({ commit }) {
commit('clear');
}
};
const mutations = {
success(state, message) {
state.type = 'alert-success';
state.message = message;
},
error(state, message) {
state.type = 'alert-danger';
state.message = message;
},
clear(state) {
state.type = null;
state.message = null;
}
};
export const alert = {
namespaced: true,
state,
actions,
mutations
};
Vuex Users Module
The vuex users module is in charge of the users
section of the centralised state store. It contains actions for fetching all users from the api and deleting a user, and contains mutations for each of the lower level state changes involved in each action.
import { userService } from '../_services';
const state = {
all: {}
};
const actions = {
getAll({ commit }) {
commit('getAllRequest');
userService.getAll()
.then(
users => commit('getAllSuccess', users),
error => commit('getAllFailure', error)
);
},
delete({ commit }, id) {
commit('deleteRequest', id);
userService.delete(id)
.then(
user => commit('deleteSuccess', id),
error => commit('deleteFailure', { id, error: error.toString() })
);
}
};
const mutations = {
getAllRequest(state) {
state.all = { loading: true };
},
getAllSuccess(state, users) {
state.all = { items: users };
},
getAllFailure(state, error) {
state.all = { error };
},
deleteRequest(state, id) {
// add 'deleting:true' property to user being deleted
state.all.items = state.all.items.map(user =>
user.id === id
? { ...user, deleting: true }
: user
)
},
deleteSuccess(state, id) {
// remove deleted user from state
state.all.items = state.all.items.filter(user => user.id !== id)
},
deleteFailure(state, { id, error }) {
// remove 'deleting:true' property and add 'deleteError:[error]' property to user
state.all.items = state.items.map(user => {
if (user.id === id) {
// make copy of user without 'deleting:true' property
const { deleting, ...userCopy } = user;
// return copy of user with 'deleteError:[error]' property
return { ...userCopy, deleteError: error };
}
return user;
})
}
};
export const users = {
namespaced: true,
state,
actions,
mutations
};
Vuex Store
This is the main vuex store file that configures the store with all of the above vuex modules.
import Vue from 'vue';
import Vuex from 'vuex';
import { alert } from './alert.module';
import { account } from './account.module';
import { users } from './users.module';
Vue.use(Vuex);
export const store = new Vuex.Store({
modules: {
alert,
account,
users
}
});
Vue App Feature Folder
The app folder is for vue components and other code that is used only by the app component in the tutorial application.
Vue App Component
The app component is the root component for the vue tutorial application, it contains the outer html, routes and global alert notification for the tutorial app.
<template>
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-sm-6 offset-sm-3">
<div v-if="alert.message" :class="`alert ${alert.type}`">{{alert.message}}</div>
<router-view></router-view>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
name: 'app',
computed: {
...mapState({
alert: state => state.alert
})
},
methods: {
...mapActions({
clearAlert: 'alert/clear'
})
},
watch: {
$route (to, from){
// clear alert on location change
this.clearAlert();
}
}
};
</script>
Vue Home Feature Folder
The home folder is for vue components and other code that is used only by the home page component in the tutorial application.
Vue Home Page Component
The home page component is displayed after signing in to the application, it shows the signed in user's name plus a list of all users in the tutorial app. The users are loaded into the vuex state by calling this.getAllUsers();
from the created()
vue lifecycle hook, the getAllUsers()
method is mapped to the vuex action 'users/getAll'
with the help of the vuex mapActions()
function.
Users can also be deleted from the user list, when the delete link is clicked it dispatches the vuex action 'users/delete'
which is mapped to the deleteUser()
method of the home page component.
<template>
<div>
<h1>Hi {{account.user.firstName}}!</h1>
<p>You're logged in with Vue + Vuex & JWT!!</p>
<h3>Users from secure api end point:</h3>
<em v-if="users.loading">Loading users...</em>
<span v-if="users.error" class="text-danger">ERROR: {{users.error}}</span>
<ul v-if="users.items">
<li v-for="user in users.items" :key="user.id">
{{user.firstName + ' ' + user.lastName}}
<span v-if="user.deleting"><em> - Deleting...</em></span>
<span v-else-if="user.deleteError" class="text-danger"> - ERROR: {{user.deleteError}}</span>
<span v-else> - <a @click="deleteUser(user.id)" class="text-danger">Delete</a></span>
</li>
</ul>
<p>
<router-link to="/login">Logout</router-link>
</p>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
computed: {
...mapState({
account: state => state.account,
users: state => state.users.all
})
},
created () {
this.getAllUsers();
},
methods: {
...mapActions('users', {
getAllUsers: 'getAll',
deleteUser: 'delete'
})
}
};
</script>
Vue Login Feature Folder
The login folder is for vue components and other code that is used only by the login page component in the tutorial application.
Vue Login Page Component
The login page component renders a login form with username and password fields. It displays validation messages for invalid fields when the user attempts to submit the form. If the form is valid, submitting it causes this.login({ username, password })
to be called which is mapped to the 'account/login'
vuex action.
In the created()
function the 'account/logout'
vuex action is dispatched via the logout()
method which logs the user out if they're logged in, this enables the login page to also be used as the logout page.
Form validation is implemented with the VeeValidate library.
<template>
<div>
<h2>Login</h2>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="username">Username</label>
<input type="text" v-model="username" name="username" class="form-control" :class="{ 'is-invalid': submitted && !username }" />
<div v-show="submitted && !username" class="invalid-feedback">Username is required</div>
</div>
<div class="form-group">
<label htmlFor="password">Password</label>
<input type="password" v-model="password" name="password" class="form-control" :class="{ 'is-invalid': submitted && !password }" />
<div v-show="submitted && !password" class="invalid-feedback">Password is required</div>
</div>
<div class="form-group">
<button class="btn btn-primary" :disabled="status.loggingIn">Login</button>
<img v-show="status.loggingIn" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />
<router-link to="/register" class="btn btn-link">Register</router-link>
</div>
</form>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
data () {
return {
username: '',
password: '',
submitted: false
}
},
computed: {
...mapState('account', ['status'])
},
created () {
// reset login status
this.logout();
},
methods: {
...mapActions('account', ['login', 'logout']),
handleSubmit (e) {
this.submitted = true;
const { username, password } = this;
if (username && password) {
this.login({ username, password })
}
}
}
};
</script>
Vue Register Feature Folder
The register folder is for vue components and other code that is used only by the register page component in the tutorial application.
Vue Register Page Component
The register page component renders a simple registration form with first name, last name, username and password fields. It displays validation messages for invalid fields when the user attempts to submit the form. If the form is valid, submitting it causes the 'account/register'
vuex action to be dispatched along with the data entered into the form.
Form validation is implemented with the VeeValidate library.
<template>
<div>
<h2>Register</h2>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" v-model="user.firstName" v-validate="'required'" name="firstName" class="form-control" :class="{ 'is-invalid': submitted && errors.has('firstName') }" />
<div v-if="submitted && errors.has('firstName')" class="invalid-feedback">{{ errors.first('firstName') }}</div>
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" v-model="user.lastName" v-validate="'required'" name="lastName" class="form-control" :class="{ 'is-invalid': submitted && errors.has('lastName') }" />
<div v-if="submitted && errors.has('lastName')" class="invalid-feedback">{{ errors.first('lastName') }}</div>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" v-model="user.username" v-validate="'required'" name="username" class="form-control" :class="{ 'is-invalid': submitted && errors.has('username') }" />
<div v-if="submitted && errors.has('username')" class="invalid-feedback">{{ errors.first('username') }}</div>
</div>
<div class="form-group">
<label htmlFor="password">Password</label>
<input type="password" v-model="user.password" v-validate="{ required: true, min: 6 }" name="password" class="form-control" :class="{ 'is-invalid': submitted && errors.has('password') }" />
<div v-if="submitted && errors.has('password')" class="invalid-feedback">{{ errors.first('password') }}</div>
</div>
<div class="form-group">
<button class="btn btn-primary" :disabled="status.registering">Register</button>
<img v-show="status.registering" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />
<router-link to="/login" class="btn btn-link">Cancel</router-link>
</div>
</form>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
data () {
return {
user: {
firstName: '',
lastName: '',
username: '',
password: ''
},
submitted: false
}
},
computed: {
...mapState('account', ['status'])
},
methods: {
...mapActions('account', ['register']),
handleSubmit(e) {
this.submitted = true;
this.$validator.validate().then(valid => {
if (valid) {
this.register(this.user);
}
});
}
}
};
</script>
Vue Main Index HTML
The main index html file contains the outer html for the whole tutorial application. When the app is started with npm start
, Webpack bundles up all of the vue + vuex code into a single javascript file and injects it into the body of the page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue + Vuex - User Registration and Login Example & Tutorial</title>
<link href="//netdna.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" />
<style>
a { cursor: pointer; }
</style>
</head>
<body>
<div id="app"></div>
</body>
</html>
Vue App Entrypoint
The root index.js file bootstraps the vue + vuex tutorial application by rendering the App
component into the #app
div element defined in the main index html file above.
The tutorial app uses a fake / mock backend that stores data in browser local storage, to switch to a real backend api simply remove the fake backend code below the comment // setup fake backend
.
The VeeValidate library is used for form validation on the login and registration pages.
import Vue from 'vue';
import VeeValidate from 'vee-validate';
import { store } from './_store';
import { router } from './_helpers';
import App from './app/App';
Vue.use(VeeValidate);
// setup fake backend
import { configureFakeBackend } from './_helpers';
configureFakeBackend();
new Vue({
el: '#app',
router,
store,
render: h => h(App)
});
Babel Config / Run Commands File
The babel config file defines the presets used by babel to transpile the ES6 code. The babel transpiler is run by webpack via the babel-loader
module configured in the webpack.config.js file below.
{
"presets": [
"env",
"stage-0"
]
}
npm package.json
The package.json file contains project configuration information including package dependencies which get installed when you run npm install
. Full documentation is available on the npm docs website.
{
"name": "vue-vuex-registration-login-example",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "https://github.com/cornflourblue/vue-vuex-registration-login-example.git"
},
"license": "MIT",
"scripts": {
"start": "webpack-dev-server --open",
"build": "webpack --mode production"
},
"dependencies": {
"vee-validate": "^2.2.8",
"vue": "^2.6.10",
"vue-router": "^3.1.3",
"vuex": "^3.1.2"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.5",
"babel-preset-env": "^1.6.1",
"babel-preset-stage-0": "^6.24.1",
"babel-preset-vue": "^2.0.2",
"css-loader": "^3.3.2",
"html-webpack-plugin": "^3.2.0",
"path": "^0.12.7",
"vue-loader": "^14.2.3",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0"
}
}
Vue Webpack Config
Webpack is used to compile and bundle all the project files so they're ready to be loaded into a browser, it does this with the help of loaders and plugins that are configured in the webpack.config.js file. For more info about webpack check out the webpack docs.
The webpack config file also defines a global config object for the application using the externals
property, you can use this to define different config variables for your development and production environments.
var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
resolve: {
extensions: ['.js', '.vue']
},
module: {
rules: [
{
test: /\.vue?$/,
exclude: /(node_modules)/,
use: 'vue-loader'
},
{
test: /\.js?$/,
exclude: /(node_modules)/,
use: 'babel-loader'
}
]
},
plugins: [new HtmlWebpackPlugin({
template: './src/index.html'
})],
devServer: {
historyApiFallback: true
},
externals: {
// global app config object
config: JSON.stringify({
apiUrl: 'http://localhost:4000'
})
}
}
Need Some Vue Help?
Search fiverr for freelance Vue developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!