An Easy Way to Detect Clicks Outside an Element in Vue

Taha Shashtari
5 min readDec 27, 2018

--

Most of the web apps we use or create these days are full of elements that open as popups and close when the user clicks outside of them.

And since we have this behavior a lot in the apps we’re building, it’s a good idea to create a reusable solution that allows us to detect clicks outside of these elements.

There are many ways to do it in Vue, but a good, simple one is to create a custom directive.

If you are not familiar with custom directives in Vue, you can read about it in the docs.

Note: I’ve already created a plugin for this directive that you can use right away without implementing it yourself. Check out vue-closable on github.

Let’s first build an example app

Before we create our custom directive, let’s create a page with a button that shows and hides a popup dialog.

You can take a look at what we’re going to build on CodePen:

We’re going to do all of our work in a single App.vue file. We can use Vue CLI 3’s Instant Prototyping to run our project very quickly.

So, create App.vue and run it from the terminal:

vue serve App.vue

Now add this to it:

<template>
<div id="app">
<button
class="toggle-button"
@click="showPopup = !showPopup"
>
TOGGLE
</button>
<div
v-show="showPopup"
class="popup-box"
>
Test Popup Box
</div>
</div>
</template>
<script>
export default {
data () {
return {
showPopup: false
}
}
}
</script>
<style>
#app {
font-family: sans-serif;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 600px;
margin: 50px auto;
position: relative;
}
.toggle-button {
background: #1188FF;
color: #FFF;
border: 0;
border-radius: 2px;
padding: 10px;
font-size: 14px;
cursor: pointer;
outline: none;
transition: 0.2s all;
}
.toggle-button:hover {
background: #1080F8;
}
.popup-box {
position: absolute;
left: 50%;
top: 50px;
transform: translateX(-50%);
background: #F0F8FF;
border-radius: 1px;
box-shadow: 1px 1px 15px 0 rgba(0,0,0,0.2);
padding: 40px;
color: #555585;
}
</style>

Nothing is complex here. We’re displaying a button that toggles the value of showPopup, which we use to show/hide the popup dialog element. (Most of the code here is for styling.)

So, if you run the app and click the button, you should see this:

How it’s going to work

The way we’re going to detect an outside click is by listening to a click event (and touchstart for mobiles) on the whole page (document) and then check if the element we clicked on is not the dialog or part of it.

So if it isn’t the dialog or part of it, we call the outside-click handler.

We also want to check if the clicked element is not the button because the button already toggles the dialog — otherwise we’ll end up calling both the outside-click handler and the button handler, and that will prevent the dialog from opening.

Let’s first see how we’re going to use that directive on the dialog before we create it.

<div
v-show="showPopup"
v-closable="{
exclude: ['button'],
handler: 'onClose'
}"
class="popup-box"
>
Test Popup Box
</div>

Note that I’m calling the directive closable (you can change it to whatever you want).

This directive accepts two values: exclude and handler. We use handler to specify the name of the method that will handle the outside-click event. And in exclude we add an array of reference names of the elements that we don't want to trigger the outside-click event.

Note that I’m using reference names (not class names or ids). This means we have to specify the reference name of the button, like this:

<button
ref="button"
class="toggle-button"
@click="showPopup = !showPopup"
>
TOGGLE
</button>

Before we move on to the next section, let’s define the outside-click handler in methods section.

methods: {
onClose () {
this.showPopup = false
}
}

It just hides the dialog box.

Creating the custom directive

Now it’s time to create the directive.

Let’s create it in the same component we’re working on (but in real projects, it’s better to have a specific place to define all of your directives).

After <script> and before export default { add all the following code (the explanation is included in the comments):

import Vue from 'vue'// This variable will hold the reference to
// document's click handler
let handleOutsideClick
Vue.directive('closable', {
bind (el, binding, vnode) {
// Here's the click/touchstart handler
// (it is registered below)
handleOutsideClick = (e) => {
e.stopPropagation()
// Get the handler method name and the exclude array
// from the object used in v-closable
const { handler, exclude } = binding.value
// This variable indicates if the clicked element is excluded
let clickedOnExcludedEl = false
exclude.forEach(refName => {
// We only run this code if we haven't detected
// any excluded element yet
if (!clickedOnExcludedEl) {
// Get the element using the reference name
const excludedEl = vnode.context.$refs[refName]
// See if this excluded element
// is the same element the user just clicked on
clickedOnExcludedEl = excludedEl.contains(e.target)
}
})
// We check to see if the clicked element is not
// the dialog element and not excluded
if (!el.contains(e.target) && !clickedOnExcludedEl) {
// If the clicked element is outside the dialog
// and not the button, then call the outside-click handler
// from the same component this directive is used in
vnode.context[handler]()
}
}
// Register click/touchstart event listeners on the whole page
document.addEventListener('click', handleOutsideClick)
document.addEventListener('touchstart', handleOutsideClick)
},
unbind () {
// If the element that has v-closable is removed, then
// unbind click/touchstart listeners from the whole page
document.removeEventListener('click', handleOutsideClick)
document.removeEventListener('touchstart', handleOutsideClick)
}
})

Since we’ve already used the directive on our dialog element, outside-click detection should work and the dialog should close when clicking outside of it.

🔗 Let’s stay connected!

--

--