How to close Bootstrap 5's modal with back button on Vue project
Tags: vue, javascript, webdev, ux
Published: 2021-08-25 03:27:48

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. 

```html
<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>
```

```js
export default {
  name: 'ModalDemo',
};
```

```js
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.

```html
// 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. 
 
![bootstrap-modal-back-button-vue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b1rd7cfp0yreb0stk4gn.gif)

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. 

```js
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:

```js
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. 

![bootstrap-5-modal-instance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lpp7m4s338lxc4xlo8cm.png)
 
Alright, now let's use it as a condition at our `beforeRouteLeave` navigation guard.

```js
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`.

```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. 

```js
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.

```js
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!

![close-bootstrap-modal-with-browser-back-buttton](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3do84gim1redm3qngqje.gif)

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.