In this tutorial, we’ll walk through how to build a reusable card modal using Vue.js and Tailwind CSS. Modal windows are a popular UI component and are useful for many different scenarios. You might use one for alerting a user, showing a form, or even popping up a login form. The uses are limitless.
The component will use Vue.js slots, so you can change the contents of the modal wherever it is used while retaining the open/close functionality and the wrapper design.
We’ll be starting with a brand-new Laravel 5.8 project. The only additional setup we need to perform is setting up Tailwind, but I won’t be going into detail on how to setup Vue and Tailwind in this tutorial.
Getting started with the modalTo begin, let’s create a CardModal
Vue component and register it in the resources/js/app.js
file.
// resources/assets/js/components/CardModal.vue
<template>
<div>
The modal will go here.
</div>
</template>
<script>
export default {
//
}
</script>
// resources/js/app.js
Vue.component('card-modal', require('./components/CardModal.vue').default);
const app = new Vue({
el: '#app',
});
To start using the component, we need to update the resources/views/welcome.blade.php
view to the following. Note the .relative
class on the body tag.
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ config('app.name', 'Laravel') }}</title>
<script src="{{ asset('js/app.js') }}" defer></script>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body class="relative font-sans p-8">
<div id="app">
<h1 class="font-bold text-2xl text-gray-900">Example Project</h1>
<p class="mb-6">This is just a example text for my tutorial.</p>
<card-modal></card-modal>
</div>
</body>
</html>
Making the modal appear
Right now, the text inside the modal will always show. Let’s start by making the component accept a prop to show or hide the contents.
Update the component to accept a showing
prop and add a v-if
directive to the div
in the template to show/hide the contents when the showing
prop changes.
<template>
<div v-if="showing">
The modal will go here.
</div>
</template>
<script>
export default {
props: {
showing: {
required: true,
type: Boolean
}
}
}
</script>
We’ll also need to add a data property to our Vue instance so we can show or hide the modal from outside the CardModal
component. We’ll default the property to false
so the modal will be hidden when the page loads.
const app = new Vue({
el: '#app',
data: {
exampleModalShowing: false,
},
});
Then, we need to pass the exampleModalShowing
prop to the CardModal
in our welcome
view. We’ll also need a button to show the modal.
<div id="app">
<h1 class="font-bold text-2xl text-gray-900 ">Example Project</h1>
<p class="mb-6">This is just a example text for my tutorial.</p>
<button
class="bg-blue-600 text-white px-4 py-2 text-sm uppercase tracking-wide font-bold rounded-lg"
@click="exampleModalShowing = true"
>
Show Modal
</button>
<card-modal :showing="exampleModalShowing"></card-modal>
</div>
Styling the modal
Next, let’s add some styling to the modal. We’ll need a card surrounding the contents and a semi-transparent background around the card. The background will also need to be position fixed so it can take up the full screen without moving any of the other contents on the page. Let’s start by adding the background and centering the contents. For the transparent background, we will need to add a semi-75
color to our Tailwind configuration.
<template>
<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
>
The modal will go here.
</div>
</template>
To add the semi-75
color so the bg-semi-75
class works, we will extend the colors configuration in our tailwind.config.js
file.
module.exports = {
theme: {
extend: {
colors: {
'bg-semi-75': 'rgba(0, 0, 0, 0.75)'
}
}
}
};
Now, we need to set a max width, background color, shadow, rounded edges, and padding for the card. We’ll add a div
to wrap the content inside the modal and add these classes to it.
<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
>
<div class="w-full max-w-2xl bg-white shadow-lg rounded-lg p-8">
The modal will go here.
</div>
</div>
Using slots for the content
Now that we have the basic styling finished, let’s update the component to use a slot so the content of the modal can be configured where the component is used instead of inside the component. This will make the component much more reusable.
First, we need to replace the content inside the component with a <slot>
. If you’re not familiar with Vue.js slots, essentially, they allow you to pass html into a component and it will be rendered wherever you specify the <slot>
tags.
<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
>
<div class="w-full max-w-2xl bg-white shadow-lg rounded-lg p-8">
<slot />
</div>
</div>
Second, in the welcome view, we just place the html we want to show inside the modal between the <card-modal>
and </card-modal>
tags.
<card-modal :showing="exampleModalShowing">
<h2>Example modal</h2>
<p>This is example text passed through to the modal via a slot.</p>
</card-modal>
Closing the modal
The component is getting close to finished, but we have one little problem. We haven’t made a way to close the modal yet. I’d like to add a few different ways to close the modal. First, we’ll add a simple close x at the top right of the card. We need to add a button to the template that calls a close
method inside the component. Be sure to add the .relative
class to the card div
.
<template>
<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
>
<div class="relative w-full max-w-2xl bg-white shadow-lg rounded-lg p-8">
<button
aria-label="close"
class="absolute top-0 right-0 text-xl text-gray-500 my-2 mx-4"
@click.prevent="close"
>
×
</button>
<slot />
</div>
</div>
</template>
<script>
export default {
props: {
showing: {
required: true,
type: Boolean
}
},
methods: {
close() {
this.$emit('close');
}
}
};
</script>
You’ll see that the close
method emits a close
event. We’ll need to listen for the event outside the component and update the exampleModalShowing
property to false
. In the welcome view, we can listen for the event by adding a @close
listener on the <card-modal>
tag.
<card-modal :showing="exampleModalShowing" @close="exampleModalShowing = false">
<h2 class="text-xl font-bold text-gray-900">Example modal</h2>
<p>This is example text passed through to the modal via a slot.</p>
</card-modal>
To close the modal from outside the component, we can add a button that sets exampleModalShowing
to false
as well.
<card-modal :showing="exampleModalShowing" @close="exampleModalShowing = false">
<h2 class="text-xl font-bold text-gray-900">Example modal</h2>
<p class="mb-6">This is example text passed through to the modal via a slot.</p>
<button
class="bg-blue-600 text-white px-4 py-2 text-sm uppercase tracking-wide font-bold rounded-lg"
@click="exampleModalShowing = false"
>
Close
</button>
</card-modal>
Now when we click the “Show Modal” button, the modal should appear. When we click the close button or the x inside the modal, the modal should disappear.
I’d also like the modal to close when the background behind the card is clicked. Using Vue.js, it’s pretty easy to add that functionality. We can just add @click.self="close"
to the background div
and Vue will handle the rest. The .self
modifier will make it so the listener is only triggered when the background itself is clicked. Without that modifier, the modal would close whenever anything inside the card is clicked as well, which is not what we want.
<template>
<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
@click.self="close"
>
<div class="relative w-full max-w-2xl bg-white shadow-lg rounded-lg p-8">
<button
aria-label="close"
class="absolute top-0 right-0 text-xl text-gray-500 my-2 mx-4"
@click.prevent="close"
>
×
</button>
<slot />
</div>
</div>
</template>
Adding a transition
To make the component feel smoother, let’s wrap the component in a transition so the modal fades in. Once again, Vue makes this pretty easy with <Transition>
components. We just need to wrap the background div
in a <Transition>
tag and add a few CSSclasses to the bottom of the component.
<template>
<Transition name="fade">
<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
@click.self="close"
>
<div class="relative w-full max-w-2xl bg-white shadow-lg rounded-lg p-8">
<button
aria-label="close"
class="absolute top-0 right-0 text-xl text-gray-500 my-2 mx-4"
@click.prevent="close"
>
×
</button>
<slot />
</div>
</div>
</Transition>
</template>
// script...
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: all 0.4s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
Fixing scroll issues
Overall, the component is working pretty well. We can open/close the modal, it fades in nicely, and is really reusable. If you add the component to a page with a lot of content though, you might notice one issue. While the modal is open, if you try to scroll the page, the background is allowed to scroll. This is usually not desirable, so I’ll show you how to fix that issue. We can add a Vue watcher to the showing
prop. When the showing
prop is set to true
, we need to add overflow: hidden
to the body
element of our page. When it is set to false
, we need to remove that style. We can use the .overflow-hidden
class provided by Tailwind.
<script>
export default {
props: {
showing: {
required: true,
type: Boolean
}
},
watch: {
showing(value) {
if (value) {
return document.querySelector('body').classList.add('overflow-hidden');
}
document.querySelector('body').classList.remove('overflow-hidden');
}
},
methods: {
close() {
this.$emit('close');
}
}
};
</script>
Conclusion
Now that our component is complete, you’re free to use it as you wish, in multiple places with different content in each place. It’s a really useful component for showing small forms, getting user confirmations, and other use cases. I’d love to hear how you end up using the component!
Thanks for reading ❤
If you liked this post, share it with all of your programming buddies!
Follow us on Facebook | Twitter
☞ Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)
☞ Nuxt.js - Vue.js on Steroids
☞ Build Web Apps with Vue JS 2 & Firebase
☞ Build a Progressive Web App In VueJs
☞ Build a Basic CRUD App with Vue.js and Node
☞ Best JavaScript Frameworks, Libraries and Tools to Use in 2019
Vue.js is an extensively popular JavaScript framework with which you can create powerful as well as interactive interfaces. Vue.js is the best framework when it comes to building a single web and mobile apps.
We, at HireFullStackDeveloperIndia, implement the right strategic approach to offer a wide variety through customized Vue.js development services to suit your requirements at most competitive prices.
Vue.js is an open-source JavaScript framework that is incredibly progressive and adoptive and majorly used to build a breathtaking user interface. Vue.js is efficient to create advanced web page applications.
Vue.js gets its strength from the flexible JavaScript library to build an enthralling user interface. As the core of Vue.js is concentrated which provides a variety of interactive components for the web and gives real-time implementation. It gives freedom to developers by giving fluidity and eases the integration process with existing projects and other libraries that enables to structure of a highly customizable application.
Vue.js is a scalable framework with a robust in-build stack that can extend itself to operate apps of any proportion. Moreover, vue.js is the best framework to seamlessly create astonishing single-page applications.
Our Vue.js developers have gained tremendous expertise by delivering services to clients worldwide over multiple industries in the area of front-end development. Our adept developers are experts in Vue development and can provide the best value-added user interfaces and web apps.
We assure our clients to have a prime user interface that reaches end-users and target the audience with the exceptional user experience across a variety of devices and platforms. Our expert team of developers serves your business to move ahead on the path of success, where your enterprise can have an advantage over others.
Here are some key benefits that you can avail when you decide to hire vue.js developers in USA from HireFullStackDeveloperIndia:
If you are looking to hire React Native developers in USA, then choosing HireFullStackDeveloperIndia would be the best as we offer some of the best talents when it comes to Vue.js.
How To Publish Your Vue.js Component On NPM. You’ve made an awesome component with Vue.js that you think other developers could use in their projects. How can you share it with them?
In this tutorial, we’re gonna build a Vue.js with Vuex and Vue Router Application that supports JWT Authentication
In this tutorial, we’re gonna build a Vue.js with Vuex and Vue Router Application that supports JWT Authentication. I will show you:
Let’s explore together.
Contents
We will build a Vue application in that:
– Signup Page:
– Login Page & Profile Page (for successful Login):
– Navigation Bar for Admin account:
This is full Vue JWT Authentication App demo (with form validation, check signup username/email duplicates, test authorization with 3 roles: Admin, Moderator, User). In the video, we use Spring Boot for back-end REST APIs.
Flow for User Registration and User LoginFor JWT Authentication, we’re gonna call 2 endpoints:
api/auth/signup
for User Registrationapi/auth/signin
for User LoginYou can take a look at following flow to have an overview of Requests and Responses Vue Client will make or receive.
Vue Client must add a JWT to HTTP Authorization Header before sending request to protected resources.
Vue App Component Diagram with Vuex & Vue RouterNow look at the diagram below.
Let’s think about it.
– The App
component is a container with Router
. It gets app state from Vuex store/auth
. Then the navbar now can display based on the state. App
component also passes state to its child components.
– Login
& Register
components have form for submission data (with support of vee-validate
). We call Vuex store dispatch()
function to make login/register actions.
– Our Vuex actions call auth.service
methods which use axios
to make HTTP requests. We also store or get JWT from Browser Local Storage inside these methods.
– Home
component is public for all visitor.
– Profile
component get user
data from its parent component and display user information.
– BoardUser
, BoardModerator
, BoardAdmin
components will be displayed by Vuex state user.roles
. In these components, we use user.service
to get protected resources from API.
– user.service
uses auth-header()
helper function to add JWT to HTTP Authorization header. auth-header()
returns an object containing the JWT of the currently logged in user from Local Storage.
We will use these modules:
This is folders & files structure for our Vue application:
With the explaination in diagram above, you can understand the project structure easily.
Setup Vue App modulesRun following command to install neccessary modules:
npm install vue-router
npm install vuex
npm install [email protected]
npm install axios
npm install bootstrap jquery popper.js
npm install @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome
After the installation is done, you can check dependencies
in package.json file.
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/vue-fontawesome": "^0.1.7",
"axios": "^0.19.0",
"bootstrap": "^4.3.1",
"core-js": "^2.6.5",
"jquery": "^3.4.1",
"popper.js": "^1.15.0",
"vee-validate": "^2.2.15",
"vue": "^2.6.10",
"vue-router": "^3.0.3",
"vuex": "^3.0.1"
},
Open src/main.js, add code below:
import Vue from 'vue';
import App from './App.vue';
import { router } from './router';
import store from './store';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import VeeValidate from 'vee-validate';
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import {
faHome,
faUser,
faUserPlus,
faSignInAlt,
faSignOutAlt
} from '@fortawesome/free-solid-svg-icons';
library.add(faHome, faUser, faUserPlus, faSignInAlt, faSignOutAlt);
Vue.config.productionTip = false;
Vue.use(VeeValidate);
Vue.component('font-awesome-icon', FontAwesomeIcon);
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
You can see that we import and apply in Vue
object:
– store
for Vuex (implemented later in src/store)
– router
for Vue Router (implemented later in src/router.js)
– bootstrap
with CSS
– vee-validate
– vue-fontawesome
for icons (used later in nav
)
We create two services in src/services folder:
services
auth-header.js
auth.service.js (Authentication service)
user.service.js (Data service)
The service provides three important methods with the help of axios for HTTP requests & reponses:
login()
: POST {username, password} & save JWT
to Local Storagelogout()
: remove JWT
from Local Storageregister()
: POST {username, email, password}import axios from 'axios';
const API_URL = 'http://localhost:8080/api/auth/';
class AuthService {
login(user) {
return axios
.post(API_URL + 'signin', {
username: user.username,
password: user.password
})
.then(this.handleResponse)
.then(response => {
if (response.data.accessToken) {
localStorage.setItem('user', JSON.stringify(response.data));
}
return response.data;
});
}
logout() {
localStorage.removeItem('user');
}
register(user) {
return axios.post(API_URL + 'signup', {
username: user.username,
email: user.email,
password: user.password
});
}
handleResponse(response) {
if (response.status === 401) {
this.logout();
location.reload(true);
const error = response.data && response.data.message;
return Promise.reject(error);
}
return Promise.resolve(response);
}
}
export default new AuthService();
If login
request returns 401 status (Unauthorized), that means, JWT was expired or no longer valid, we will logout the user (remove JWT from Local Storage).
We also have methods for retrieving data from server. In the case we access protected resources, the HTTP request needs Authorization header.
Let’s create a helper function called authHeader()
inside auth-header.js:
export default function authHeader() {
let user = JSON.parse(localStorage.getItem('user'));
if (user && user.accessToken) {
return { Authorization: 'Bearer ' + user.accessToken };
} else {
return {};
}
}
It checks Local Storage for user
item.
If there is a logged in user
with accessToken
(JWT), return HTTP Authorization header. Otherwise, return an empty object.
Now we define a service for accessing data in user.service.js:
import axios from 'axios';
import authHeader from './auth-header';
const API_URL = 'http://localhost:8080/api/test/';
class UserService {
getPublicContent() {
return axios.get(API_URL + 'all');
}
getUserBoard() {
return axios.get(API_URL + 'user', { headers: authHeader() });
}
getModeratorBoard() {
return axios.get(API_URL + 'mod', { headers: authHeader() });
}
getAdminBoard() {
return axios.get(API_URL + 'admin', { headers: authHeader() });
}
}
export default new UserService();
You can see that we add a HTTP header with the help of authHeader()
function when requesting authorized resource.
We put Vuex module for authentication in src/store folder.
store
auth.module.js (authentication module)
index.js (Vuex Store that contains also modules)
Now open index.js file, import auth.module
to main Vuex Store here.
import Vue from 'vue';
import Vuex from 'vuex';
import { auth } from './auth.module';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
auth
}
});
Then we start to define Vuex Authentication module that contains:
We use AuthService
which is defined above to make authentication requests.
auth.module.js
import AuthService from '../services/auth.service';
const user = JSON.parse(localStorage.getItem('user'));
const initialState = user
? { status: { loggedIn: true }, user }
: { status: {}, user: null };
export const auth = {
namespaced: true,
state: initialState,
actions: {
login({ commit }, user) {
return AuthService.login(user).then(
user => {
commit('loginSuccess', user);
return Promise.resolve(user);
},
error => {
commit('loginFailure');
return Promise.reject(error.response.data);
}
);
},
logout({ commit }) {
AuthService.logout();
commit('logout');
},
register({ commit }, user) {
return AuthService.register(user).then(
response => {
commit('registerSuccess');
return Promise.resolve(response.data);
},
error => {
commit('registerFailure');
return Promise.reject(error.response.data);
}
);
}
},
mutations: {
loginSuccess(state, user) {
state.status = { loggedIn: true };
state.user = user;
},
loginFailure(state) {
state.status = {};
state.user = null;
},
logout(state) {
state.status = {};
state.user = null;
},
registerSuccess(state) {
state.status = {};
},
registerFailure(state) {
state.status = {};
}
}
};
You can find more details about Vuex at Vuex Guide.
Create Vue Authentication ComponentsTo make code clear and easy to read, we define the User
model first.
Under src/models folder, create user.js like this.
export default class User {
constructor(username, email, password) {
this.username = username;
this.email = email;
this.password = password;
}
}
Let’s continue with Authentication Components.
Instead of using axios or AuthService
directly, these Components should work with Vuex Store:
– getting status with this.$store.state.auth
– making request by dispatching an action: this.$store.dispatch()
views
Login.vue
Register.vue
Profile.vue
In src/views folder, create Login.vue file with following code:
<template>
<div class="col-md-12">
<div class="card card-container">
<img
id="profile-img"
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
class="profile-img-card"
/>
<form name="form" @submit.prevent="handleLogin">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
class="form-control"
name="username"
v-model="user.username"
v-validate="'required'"
/>
<div
class="alert alert-danger"
role="alert"
v-if="errors.has('username')"
>Username is required!</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
class="form-control"
name="password"
v-model="user.password"
v-validate="'required'"
/>
<div
class="alert alert-danger"
role="alert"
v-if="errors.has('password')"
>Password is required!</div>
</div>
<div class="form-group">
<button class="btn btn-primary btn-block" :disabled="loading">
<span class="spinner-border spinner-border-sm" v-show="loading"></span>
<span>Login</span>
</button>
</div>
<div class="form-group">
<div class="alert alert-danger" role="alert" v-if="message">{{message}}</div>
</div>
</form>
</div>
</div>
</template>
<script>
import User from '../models/user';
export default {
name: 'login',
computed: {
loggedIn() {
return this.$store.state.auth.status.loggedIn;
}
},
data() {
return {
user: new User('', ''),
loading: false,
message: ''
};
},
mounted() {
if (this.loggedIn) {
this.$router.push('/profile');
}
},
methods: {
handleLogin() {
this.loading = true;
this.$validator.validateAll();
if (this.errors.any()) {
this.loading = false;
return;
}
if (this.user.username && this.user.password) {
this.$store.dispatch('auth/login', this.user).then(
() => {
this.$router.push('/profile');
},
error => {
this.loading = false;
this.message = error.message;
}
);
}
}
}
};
</script>
<style scoped>
label {
display: block;
margin-top: 10px;
}
.card-container.card {
max-width: 350px !important;
padding: 40px 40px;
}
.card {
background-color: #f7f7f7;
padding: 20px 25px 30px;
margin: 0 auto 25px;
margin-top: 50px;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}
.profile-img-card {
width: 96px;
height: 96px;
margin: 0 auto 10px;
display: block;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
}
</style>
This page has a Form with username
& password
. We use [VeeValidate 2.x](http://<a href=) to validate input before submitting the form. If there is an invalid field, we show the error message.
We check user logged in status using Vuex Store: this.$store.state.auth.status.loggedIn
. If the status is true
, we use Vue Router to direct user to Profile Page:
created() {
if (this.loggedIn) {
this.$router.push('/profile');
}
},
In the handleLogin()
function, we dispatch 'auth/login'
Action to Vuex Store. If the login is successful, go to Profile Page, otherwise, show error message.
This page is similar to Login Page.
For form validation, we have some more details:
username
: required|min:3|max:20email
: required|email|max:50password
: required|min:6|max:40For form submission, we dispatch 'auth/register'
Vuex Action.
src/views/Register.vue
<template>
<div class="col-md-12">
<div class="card card-container">
<img
id="profile-img"
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
class="profile-img-card"
/>
<form name="form" @submit.prevent="handleRegister">
<div v-if="!successful">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
class="form-control"
name="username"
v-model="user.username"
v-validate="'required|min:3|max:20'"
/>
<div
class="alert-danger"
v-if="submitted && errors.has('username')"
>{{errors.first('username')}}</div>
</div>
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
class="form-control"
name="email"
v-model="user.email"
v-validate="'required|email|max:50'"
/>
<div
class="alert-danger"
v-if="submitted && errors.has('email')"
>{{errors.first('email')}}</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
class="form-control"
name="password"
v-model="user.password"
v-validate="'required|min:6|max:40'"
/>
<div
class="alert-danger"
v-if="submitted && errors.has('password')"
>{{errors.first('password')}}</div>
</div>
<div class="form-group">
<button class="btn btn-primary btn-block">Sign Up</button>
</div>
</div>
</form>
<div
class="alert"
:class="successful ? 'alert-success' : 'alert-danger'"
v-if="message"
>{{message}}</div>
</div>
</div>
</template>
<script>
import User from '../models/user';
export default {
name: 'register',
computed: {
loggedIn() {
return this.$store.state.auth.status.loggedIn;
}
},
data() {
return {
user: new User('', '', ''),
submitted: false,
successful: false,
message: ''
};
},
mounted() {
if (this.loggedIn) {
this.$router.push('/profile');
}
},
methods: {
handleRegister() {
this.message = '';
this.submitted = true;
this.$validator.validate().then(valid => {
if (valid) {
this.$store.dispatch('auth/register', this.user).then(
data => {
this.message = data.message;
this.successful = true;
},
error => {
this.message = error.message;
this.successful = false;
}
);
}
});
}
}
};
</script>
<style scoped>
label {
display: block;
margin-top: 10px;
}
.card-container.card {
max-width: 350px !important;
padding: 40px 40px;
}
.card {
background-color: #f7f7f7;
padding: 20px 25px 30px;
margin: 0 auto 25px;
margin-top: 50px;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}
.profile-img-card {
width: 96px;
height: 96px;
margin: 0 auto 10px;
display: block;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
}
</style>
This page gets current User from Vuex Store and show information. If the User is not logged in, it directs to Login Page.
src/views/Profile.vue
<template>
<div class="container">
<header class="jumbotron">
<h3>
<strong>{{currentUser.username}}</strong> Profile
</h3>
</header>
<p>
<strong>Token:</strong>
{{currentUser.accessToken.substring(0, 20)}} ... {{currentUser.accessToken.substr(currentUser.accessToken.length - 20)}}
</p>
<p>
<strong>Id:</strong>
{{currentUser.id}}
</p>
<p>
<strong>Email:</strong>
{{currentUser.email}}
</p>
<strong>Authorities:</strong>
<ul>
<li v-for="(role,index) in currentUser.roles" :key="index">{{role}}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'profile',
computed: {
currentUser() {
return this.$store.state.auth.user;
}
},
mounted() {
if (!this.currentUser) {
this.$router.push('/login');
}
}
};
</script>
Create Vue Components for accessing Resources
These components will use UserService
to request data.
views
Home.vue
BoardAdmin.vue
BoardModerator.vue
BoardUser.vue
This is a public page.
src/views/Home.vue
<template>
<div class="container">
<header class="jumbotron">
<h3>{{content}}</h3>
</header>
</div>
</template>
<script>
import UserService from '../services/user.service';
export default {
name: 'home',
data() {
return {
content: ''
};
},
mounted() {
UserService.getPublicContent().then(
response => {
this.content = response.data;
},
error => {
this.content = error.response.data.message;
}
);
}
};
</script>
We have 3 pages for accessing protected data:
UserService.getUserBoard()
UserService.getModeratorBoard()
UserService.getAdminBoard()
This is an example, other Page are similar to this Page.
src/views/BoardUser.vue
<template>
<div class="container">
<header class="jumbotron">
<h3>{{content}}</h3>
</header>
</div>
</template>
<script>
import UserService from '../services/user.service';
export default {
name: 'user',
data() {
return {
content: ''
};
},
mounted() {
UserService.getUserBoard().then(
response => {
this.content = response.data;
},
error => {
this.content = error.response.data.message;
}
);
}
};
</script>
Define Routes for Vue Router
Now we define all routes for our Vue Application.
src/router.js
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import Login from './views/Login.vue';
import Register from './views/Register.vue';
Vue.use(Router);
export const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/home',
component: Home
},
{
path: '/login',
component: Login
},
{
path: '/register',
component: Register
},
{
path: '/profile',
name: 'profile',
// lazy-loaded
component: () => import('./views/Profile.vue')
},
{
path: '/admin',
name: 'admin',
// lazy-loaded
component: () => import('./views/BoardAdmin.vue')
},
{
path: '/mod',
name: 'moderator',
// lazy-loaded
component: () => import('./views/BoardModerator.vue')
},
{
path: '/user',
name: 'user',
// lazy-loaded
component: () => import('./views/BoardUser.vue')
}
]
});
Add Navigation Bar to Vue App
This is the root container for our application that contains navigation bar. We will add router-view
here.
src/App.vue
<template>
<div id="app">
<nav class="navbar navbar-expand navbar-dark bg-dark">
<a href="#" class="navbar-brand">bezKoder</a>
<div class="navbar-nav mr-auto">
<li class="nav-item">
<a href="/home" class="nav-link">
<font-awesome-icon icon="home" /> Home
</a>
</li>
<li class="nav-item" v-if="showAdminBoard">
<a href="/admin" class="nav-link">Admin Board</a>
</li>
<li class="nav-item" v-if="showModeratorBoard">
<a href="/mod" class="nav-link">Moderator Board</a>
</li>
<li class="nav-item">
<a href="/user" class="nav-link" v-if="currentUser">User</a>
</li>
</div>
<div class="navbar-nav ml-auto" v-if="!currentUser">
<li class="nav-item">
<a href="/register" class="nav-link">
<font-awesome-icon icon="user-plus" /> Sign Up
</a>
</li>
<li class="nav-item">
<a href="/login" class="nav-link">
<font-awesome-icon icon="sign-in-alt" /> Login
</a>
</li>
</div>
<div class="navbar-nav ml-auto" v-if="currentUser">
<li class="nav-item">
<a href="/profile" class="nav-link">
<font-awesome-icon icon="user" />
{{currentUser.username}}
</a>
</li>
<li class="nav-item">
<a href class="nav-link" @click="logOut">
<font-awesome-icon icon="sign-out-alt" /> LogOut
</a>
</li>
</div>
</nav>
<div class="container">
<router-view />
</div>
</div>
</template>
<script>
export default {
computed: {
currentUser() {
return this.$store.state.auth.user;
},
showAdminBoard() {
if (this.currentUser) {
return this.currentUser.roles.includes('ROLE_ADMIN');
}
return false;
},
showModeratorBoard() {
if (this.currentUser) {
return this.currentUser.roles.includes('ROLE_MODERATOR');
}
return false;
}
},
methods: {
logOut() {
this.$store.dispatch('auth/logout');
this.$router.push('/login');
}
}
};
</script>
Our navbar looks more professional when using font-awesome-icon
.
We also make the navbar dynamically change by current User’s roles
which are retrieved from Vuex Store state
.
If you want to check Authorized status everytime a navigating action is trigger, just add router.beforeEach()
at the end of src/router.js like this:
router.beforeEach((to, from, next) => {
const publicPages = ['/login', '/home'];
const authRequired = !publicPages.includes(to.path);
const loggedIn = localStorage.getItem('user');
// try to access a restricted page + not logged in
if (authRequired && !loggedIn) {
return next('/login');
}
next();
});
Conclusion
Congratulation!
Today we’ve done so many interesting things. I hope you understand the overall layers of our Vue application, and apply it in your project at ease. Now you can build a front-end app that supports JWT Authentication with Vue.js, Vuex and Vue Router.
Happy learning, see you again!