How to close Bootstrap 5's modal with back button on Vue project
Jalu Pujo Rumekso
Posted on August 25, 2021
Say you are building a spa using Bootstrap 5 and Vue. And one of its functionality is displaying a list of product through a modal. And you want the modal to get closed when the user press the back button, just like how most mobile app behave.
How do you do that? Let me tell you.
Before we dive into the technical implementation, let's understand the logic first.
The logic is simple. When the user press back button while the modal is open, we want to tell Vue to close the modal instead of navigating back to the previous route.
From that simple logic now we know that we need to know four things:
- First, when the user press back button.
- Second, the Bootstrap modal's state (open/close).
- Third, how to prevent Vue from going away to previous route.
- Finally, how to close Bootstrap's modal programmatically.
Alright, let's get into it.
Project Setup
First, let's open the terminal and run vue create modal-demo
to create a new Vue project. After the setup is complete, install Bootstrap using npm install bootstrap
command. To include Boostrap's css add this line inside the style block of App.vue: @import "~bootstrap/dist/css/bootstrap.min.css"
.
Now let's add a bare bone component to work with. I'll name it ModalDemo
and place it in the views folder since it's a page-level component.
<template>
<button
data-bs-toggle="modal"
data-bs-target="#theModal"
class="btn btn-primary"
>
Open Modal
</button>
<div id="theModal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Close
</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
</template>
export default {
name: 'ModalDemo',
};
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/modal-demo',
name: 'ModalDemo',
component: () => import('@/views/ModalDemo.vue'),
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
As you can see, our component consist of a modal and a button to toggle it. The button has data-bs-toggle
and data-bs-target
to activate the modal on button click.
Then, we registered the component to Vue Router's routes. So now we have three components. The ModalDemo and the Home and About which is included by default when we create our project using vue create
command. Now let's open the App.vue to add a link to our ModalDemo component.
// App.vue
<template>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/modal-demo">Modal Demo</router-link>
</div>
<router-view />
</template>
So here's what we gonna do to test the 'close bootstrap modal on back button' functionality. First we want to visit the home route (/). Then click the 'Modal Demo' link. Then click the button to open up the modal. Then, while the modal is open, we'll click the back button.
As you can see, by default Vue Router will take us to the previous route when we press the back button while the modal is open. Also it leaves the modal overlay open. And those are not what we want, right?
Alright, let's tackle our first task.
Detecting Back Button event on Vue
Well, honestly we can't detect when the user press the back button since there is no such an event. However, thanks to Vue Router's beforeRouteLeave
navigation guard, we can execute code at the time when the user is about to navigate away from the currently rendered component. Whether the user wants to go back, forward, or to completely another route, the beforeRouteLeave
got you covered.
So let's add the beforeRouteLeave
navigation guard to our ModalDemo component definition.
beforeRouteLeave(to, from, next) {
// check modal state and prevent navigation here
next()
}
The beforeRouteLeave
takes three argument: to
, from
, and next
. The first twos give us information about the route that we are going to navigate to and from respectively. The next
is a function that we need to execute to tell Vue whether we are allowing the navigation to occur. And it need to be executed once.
Alright, at this point that's all what we need to know about the role of beforeRouteLeave
. For more information just check the official doc.
Identifying the state of Bootstrap 5's modal.
Lucky for us, Bootstrap 5 offers an out of the box functionality to check the modal's state via the Modal instance.
Here's how you want to instantiate a Bootstrap 5's Modal in Vue's component:
import { Modal } from 'bootstrap';
export default {
...
data() {
return {
theModal: null
}
},
...
mounted() {
this.theModal = new Modal(document.getElementById('theModal'));
}
}
We begin with importing the Modal and instantiate it at the mounted
hook. Why the mounted
? Because at the mounted
we can access the DOM required for Modal instantiation.
Here we instantiate the modal by providing the Modal constructor with a modal element by an id of theModal
(so make sure that at the template definition you have a valid modal markup with an id of 'theModal' or named it anything you want). Then we assign it to theModal
data's prop so now our modal's props and methods is accessible from any place in the component code via this.theModal
.
Note: you can optionally pass the second argument to the Modal constructor, which is modal's option object. Check here for further information about modal's option object.
Alright, now if you check theModal
from Vue devtools we know that it has several properties and the one we're interested with is the _isShown
prop. Its value is true
when the modal is shown and vice versa.
Alright, now let's use it as a condition at our beforeRouteLeave
navigation guard.
beforeRouteLeave(to, from, next) {
if(this.theModal._isShown) {
// cancel navigation here
} else {
// allow to navigate away as usual
next()
}
}
Note: The line of theModal._isShown
will likely cause the eslint to throw 'no-dangle-underscore' error. To fix it we need to add an exception for the _isShown
. We can do so by adding this rule: 'no-underscore-dangle': ["error", { "allow": ["_isShown"] }]
to the rules prop of eslintrc.js
.
// eslintrc.js
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-underscore-dangle': ["error", { "allow": ["_isShown"] }]
},
Alright, now can execute the real route cancelation!
Preventing Vue from Navigating Away
To prevent the app from navigating away we need to pass false
to the next()
method.
beforeRouteLeave(to, from, next) {
// When the modal is shown
if(this.theModal._isShown) {
// cancel navigation
next(false);
} else {
// allow to navigate away as usual
next();
}
}
Next, we just need to close the modal!
Close Bootstrap 5's Modal Programmatically
Now in Bootstrap 5, you want to call the hide()
method on the modal instance to hide the modal.
beforeRouteLeave() {
// When the modal is shown
if(this.theModal._isShown) {
// cancel navigation
next(false);
this.theModal.hide();
} else {
// allow to navigate away as usual
next();
}
}
Alright, now try to open the modal and click the back button. As you can see, the app won't going anywhere except to close down the modal. It works!
That's it! Thanks for reading
Note: Sorry for poor gif. I create it using Peek on Ubuntu laptop. If you know a better way to do this task, feel free to leave your comment out. Thank you.
Posted on August 25, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.