Detect click outside element
Asked Answered
G

42

213

How can I detect a click outside my element? I'm using Vue.js so it's gonna be outside my templates element. I know how to do it in Vanilla JS, but I'm not sure if there's a more proper way to do it, when I'm using Vue.js?

This is the solution for Vanilla JS: Javascript Detect Click event outside of div

I guess I can use a better way to access the element?

Gautious answered 23/3, 2016 at 5:24 Comment(5)
Vue components are isolated. so detecting outside changes is out of question and anti pattern is used.Amylum
Thanks. I'm not sure though how to implement it in a Vue component. There must still be some best practices for the anti-pattern?Gautious
Vue.js component are isolated, thats true, but there are different methods for parent-child communication. So, instead of asking to detect an event outside of an element, you should specify if you want to detect elements inside a component, from the parent component, from some child, or whatever relation is between componentsCanonist
github.com/simplesmiler/vue-clickaway can simplify your jobAmylum
there beautiful plugin there vue click outside -> npmjs.com/package/v-click-outsideCorunna
E
141

Keep in attention that this solution only works with Vue 1.

Can be solved nicely by setting up a custom directive once:

Vue.directive('click-outside', {
  bind () {
      this.event = event => this.vm.$emit(this.expression, event)
      this.el.addEventListener('click', this.stopProp)
      document.body.addEventListener('click', this.event)
  },   
  unbind() {
    this.el.removeEventListener('click', this.stopProp)
    document.body.removeEventListener('click', this.event)
  },

  stopProp(event) { event.stopPropagation() }
})

Usage:

<div v-click-outside="nameOfCustomEventToCall">
  Some content
</div>

In the component:

events: {
  nameOfCustomEventToCall: function (event) {
    // do something - probably hide the dropdown menu / modal etc.
  }
}

Working Demo on JSFiddle with additional info about caveats:

https://jsfiddle.net/Linusborg/yzm8t8jq/

Exhibitioner answered 23/3, 2016 at 14:1 Comment(2)
Using @blur is also an option and makes it way easier giving the same result: <input @blur="hide"> where hide: function() { this.isActive = false; }Jemimah
Tried this on vue 1, it fires the event even if you click on the component itself: I used it to close a dropdown and even when I click to focus, the event is fired and it closes immediately. I know it's been a while but do we have any solution for this?Vauban
G
264

There is the solution I used, which is based on Linus Borg answer and works fine with vue.js 2.0.

Vue.directive('click-outside', {
  bind: function (el, binding, vnode) {
    el.clickOutsideEvent = function (event) {
      // here I check that click was outside the el and his children
      if (!(el == event.target || el.contains(event.target))) {
        // and if it did, call method provided in attribute value
        vnode.context[binding.expression](event);
      }
    };
    document.body.addEventListener('click', el.clickOutsideEvent)
  },
  unbind: function (el) {
    document.body.removeEventListener('click', el.clickOutsideEvent)
  },
});

You bind to it using v-click-outside:

<div v-click-outside="doStuff">

Here's a small demo

You can find some more info about custom directives and what el, binding, vnode means in https://v2.vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments

Gigi answered 22/2, 2017 at 10:56 Comment(17)
Worked, but in Vue 2.0 directives no longer have an instance, so this is undefined. vuejs.org/v2/guide/migration.html#Custom-Directives-simplified . I have no clue why this fiddle works or when this simplification has been done. (To solve, replace "this" with "el" to bind the event to the element)Unmade
It works probably because the window passed as "this". I've fixed the answer. Thank you for pointing out this bug.Gigi
thanks for this response. And what if you want to detect left and right click outside the element? do you add a listener on contextmenu as well ?Warthog
I believe click is both left and right? #2406271Gautious
hi @MadisonTrash. i have implemented your answer in this webpackBin: #36170925 can you help understand why the function clickOutside is being called when clicking on the button, before the component is even mounted?Nympha
Is there a way to exclude specific element outside? For example, I have one button outside which has to open this element and because it triggers both methods nothing happens.Botulin
What is el.event ?Millard
Can you please explain vnode.context[binding.expression](event); ?Millard
This works only partially for me (not sure if it's browser related but i'm using I.E. 11 and I can't test other browsers atm). The problem is that it doesn't work on white-space that is below the closing body tag. So I used vue-clickaway mentioned by @JulienLeCoupanec instead.Underbodice
how to change this so that an expression can be used instead of a method within the v-click-outside is triggered?Sesquicentennial
How can I use this from within a Vue component (I'm using Vue CLI 3)?Suffrage
el == event.target is superfluous. el.contains(event.target) checks whether the given node (event.target) is the node itself (el) or one of its children. Other than that, great solution.Venturesome
This is working perfectly. But when I'm using plugins like vue-datepicker, on selecting a date, the outside click is triggering. Any solution for this?Butterandeggs
If you want a button or something external to the menu to not trigger the directive callback, you can do something like if (event.target.id !== 'myButton' && !(el == event.target || el.contains(event.target))) { ... }Calefacient
@Žilvinas Yeah, just do event.stopPropagation() on the toggle buttonDucktail
It doesn't work for absolute positioned children of the element.Mentally
this derivative code need to be added in main.js fileAbsurd
A
170

Add tabindex attribute to your component so that it can be focused and do the following:

<template>
    <div
        @focus="handleFocus"
        @focusout="handleFocusOut"
        tabindex="0"
    >
      SOME CONTENT HERE
    </div>
</template>

<script>
export default {    
    methods: {
        handleFocus() {
            // do something here
        },
        handleFocusOut() {
            // do something here
        }
    }
}
</script>
Actinoid answered 28/12, 2018 at 10:18 Comment(9)
Just to add onto this, setting a tabindex of -1 will stop the highlight box from appearing when you click on the element, but it will still allow the div to be focusable.Euphrates
For some reason tabindex of -1 doesn't hide the outline for me, so I just added outline: none; on focus for the element.Earflap
how can we apply this to an off-canvas side nav that slides onto the screen? I am unable to give the sidenav focus unless it is clicked,Anacoluthia
you can use tabindex="-1", if you don't want to focus when you press Tab.Bisector
This works, but in my case focusout is only calling when I first trigger @focus. Any solution for that?Claudette
@Claudette when you show the component, you can trigger focus programatically. In my case I had a popup menu as a component, activated by another component on a click. When menu would have it's method show() called, it would also call focus() on it's root div element. This would then make focusout called when user clicks anywhere on the screen outside of the root div element.Shotten
Brilliant. You can add a ref <div ref="menuOptions"> and then give it focus mounted(){this.$refs.menuOptions.focus();} so it will capture when you click outside without requiring to focus on it firstAnatolia
I have a problem with this, in that when I click a child within the focused element, it triggers the focusout eventAbstracted
I found a solution to my own problem. To prevent click on child elements to causing the parent to close, put your login in the event callback within a conditional: if(!event.currentTarget.contains(event.relatedTarget)) { }Abstracted
E
141

Keep in attention that this solution only works with Vue 1.

Can be solved nicely by setting up a custom directive once:

Vue.directive('click-outside', {
  bind () {
      this.event = event => this.vm.$emit(this.expression, event)
      this.el.addEventListener('click', this.stopProp)
      document.body.addEventListener('click', this.event)
  },   
  unbind() {
    this.el.removeEventListener('click', this.stopProp)
    document.body.removeEventListener('click', this.event)
  },

  stopProp(event) { event.stopPropagation() }
})

Usage:

<div v-click-outside="nameOfCustomEventToCall">
  Some content
</div>

In the component:

events: {
  nameOfCustomEventToCall: function (event) {
    // do something - probably hide the dropdown menu / modal etc.
  }
}

Working Demo on JSFiddle with additional info about caveats:

https://jsfiddle.net/Linusborg/yzm8t8jq/

Exhibitioner answered 23/3, 2016 at 14:1 Comment(2)
Using @blur is also an option and makes it way easier giving the same result: <input @blur="hide"> where hide: function() { this.isActive = false; }Jemimah
Tried this on vue 1, it fires the event even if you click on the component itself: I used it to close a dropdown and even when I click to focus, the event is fired and it closes immediately. I know it's been a while but do we have any solution for this?Vauban
F
52

For Vue 3:

This answer is based on MadisonTrash's great answer above but updated to use new Vue 3 syntax.

Vue 3 now uses beforeMount instead of bind, and unmounted instead of unbind (src).

const clickOutside = {
  beforeMount: (el, binding) => {
    el.clickOutsideEvent = event => {
      // here I check that click was outside the el and his children
      if (!(el == event.target || el.contains(event.target))) {
        // and if it did, call method provided in attribute value
        binding.value();
      }
    };
    document.addEventListener("click", el.clickOutsideEvent);
  },
  unmounted: el => {
    document.removeEventListener("click", el.clickOutsideEvent);
  },
};

createApp(App)
  .directive("click-outside", clickOutside)
  .mount("#app");

You bind to it using v-click-outside:

<div v-click-outside="doStuff"></div>
Floorman answered 5/11, 2020 at 13:53 Comment(2)
This works fine! I'm using this directive to close modals and menus, that wasn't mounted initially. And this directive fire "close" event even before the modal was opened, and it doesn't shows up. So I added this code inside modal component to make it work: mounted: function() { setTimeout(() => { this.opened = true; }, 10); }, unmounted: function() { this.opened = false; }, methods: { clickOutside: function() { if (this.opened) { this.$emit("close"); } },}Claudell
I had issues with child elements so I use "mouseup" event instead of "click" event.Linda
G
35

There are three packages available in the community for this task (all are maintained):

Gonzales answered 9/9, 2017 at 23:35 Comment(2)
vue-clickaway package solved my issue perfectly. ThanksSylvanus
What about lots of items? Every item with outside click event will fire event on each click. It's nice when you make dialog and terrible when you create gallery. In non-component era we are listening click from document and check what element was clicked. But now it's a pain.Clop
M
19

I did it a slightly different way using a function within created().

  created() {
      window.addEventListener('click', (e) => {
        if (!this.$el.contains(e.target)){
          this.showMobileNav = false
        }
      })
  },

This way, if someone clicks outside of the element, then in my case, the mobile nav is hidden.

Moyna answered 21/6, 2020 at 21:34 Comment(1)
Note: this solution does NOT unbind, which will present memory leaks and other issues in often non-obvious ways. Choose a solution with unbinding/unmounting for future proofing and stabilising your code.Lolanthe
M
13

If you're specifically looking for a click outside the element but still within the parent, you can use

<div class="parent" @click.self="onParentClick">
  <div class="child"></div>
</div>

I use this for modals.

Mandiemandingo answered 30/6, 2020 at 23:40 Comment(0)
D
10

This Worked for me with Vue.js 2.5.2 :

/**
 * Call a function when a click is detected outside of the
 * current DOM node ( AND its children )
 *
 * Example :
 *
 * <template>
 *   <div v-click-outside="onClickOutside">Hello</div>
 * </template>
 *
 * <script>
 * import clickOutside from '../../../../directives/clickOutside'
 * export default {
 *   directives: {
 *     clickOutside
 *   },
 *   data () {
 *     return {
         showDatePicker: false
 *     }
 *   },
 *   methods: {
 *     onClickOutside (event) {
 *       this.showDatePicker = false
 *     }
 *   }
 * }
 * </script>
 */
export default {
  bind: function (el, binding, vNode) {
    el.__vueClickOutside__ = event => {
      if (!el.contains(event.target)) {
        // call method provided in v-click-outside value
        vNode.context[binding.expression](event)
        event.stopPropagation()
      }
    }
    document.body.addEventListener('click', el.__vueClickOutside__)
  },
  unbind: function (el, binding, vNode) {
    // Remove Event Listeners
    document.body.removeEventListener('click', el.__vueClickOutside__)
    el.__vueClickOutside__ = null
  }
}
Determinable answered 3/1, 2018 at 7:50 Comment(1)
Thank you for this example. Checked this on vue 2.6. There is some fix, in the unbind method you must to fix some issue by this (you forgot the body property in the unbind method): document.body.removeEventListener('click', el.__vueClickOutside__); if not - it will cause the multiple event listeners creation after every component recreation (page refresh);Collaborative
R
10

I have combined all answers (including a line from vue-clickaway) and came up with this solution that works for me:

Vue.directive('click-outside', {
    bind(el, binding, vnode) {
        var vm = vnode.context;
        var callback = binding.value;

        el.clickOutsideEvent = function (event) {
            if (!(el == event.target || el.contains(event.target))) {
                return callback.call(vm, event);
            }
        };
        document.body.addEventListener('click', el.clickOutsideEvent);
    },
    unbind(el) {
        document.body.removeEventListener('click', el.clickOutsideEvent);
    }
});

Use in component:

<li v-click-outside="closeSearch">
  <!-- your component here -->
</li>
Radbun answered 4/2, 2019 at 21:28 Comment(0)
K
8

Vue 3 has breaking changes in directives, all of <Vue3 methods were changed/updated. If you wonder, how to do it in Vue 3, Here's the snippet. For information please go through this link

<div v-click-outside="methodToInvoke"></div>

click-outside.js

export default {
  beforeMount: function (el, binding, vnode) {
    binding.event = function (event) {
      if (!(el === event.target || el.contains(event.target))) {
        if (binding.value instanceof Function) {
          binding.value(event)
        }
      }
    }
    document.body.addEventListener('click', binding.event)
  },
  unmounted: function (el, binding, vnode) {
    document.body.removeEventListener('click', binding.event)
  }
}

and In main.js add the following

// Directives
import ClickOutside from './click-outside'

createApp(App)
 .directive('click-outside', ClickOutside)
 .use(IfAnyModules)
 .mount('#app')
Kenyakenyatta answered 16/10, 2020 at 14:49 Comment(0)
M
8

Complete case for vue 3

This is a complete solution based on MadisonTrash answer, and benrwb and fredrivett tweaks for safari compatibility and vue 3 api changes.

Edit:

The solution proposed below is still useful, and the how to use is still valid, but I changed it to use document.elementsFromPoint instead of event.contains because it doesn't recognise as children some elements like the <path> tags inside svgs. So the right directive is this one:

export default {
    beforeMount: (el, binding) => {
        el.eventSetDrag = () => {
            el.setAttribute("data-dragging", "yes");
        };
        el.eventClearDrag = () => {
            el.removeAttribute("data-dragging");
        };
        el.eventOnClick = event => {
            const dragging = el.getAttribute("data-dragging");
            // Check that the click was outside the el and its children, and wasn't a drag
            console.log(document.elementsFromPoint(event.clientX, event.clientY))
            if (!document.elementsFromPoint(event.clientX, event.clientY).includes(el) && !dragging) {
                // call method provided in attribute value
                binding.value(event);
            }
        };
        document.addEventListener("touchstart", el.eventClearDrag);
        document.addEventListener("touchmove", el.eventSetDrag);
        document.addEventListener("click", el.eventOnClick);
        document.addEventListener("touchend", el.eventOnClick);
    },
    unmounted: el => {
        document.removeEventListener("touchstart", el.eventClearDrag);
        document.removeEventListener("touchmove", el.eventSetDrag);
        document.removeEventListener("click", el.eventOnClick);
        document.removeEventListener("touchend", el.eventOnClick);
        el.removeAttribute("data-dragging");
    },
};

Old answer:

Directive

const clickOutside = {
    beforeMount: (el, binding) => {
        el.eventSetDrag = () => {
            el.setAttribute("data-dragging", "yes");
        };
        el.eventClearDrag = () => {
            el.removeAttribute("data-dragging");
        };
        el.eventOnClick = event => {
            const dragging = el.getAttribute("data-dragging");  
            // Check that the click was outside the el and its children, and wasn't a drag
            if (!(el == event.target || el.contains(event.target)) && !dragging) {
                // call method provided in attribute value
                binding.value(event);
            }
        };
        document.addEventListener("touchstart", el.eventClearDrag);
        document.addEventListener("touchmove", el.eventSetDrag);
        document.addEventListener("click", el.eventOnClick);
        document.addEventListener("touchend", el.eventOnClick);
    },
    unmounted: el => {
        document.removeEventListener("touchstart", el.eventClearDrag);
        document.removeEventListener("touchmove", el.eventSetDrag);
        document.removeEventListener("click", el.eventOnClick);
        document.removeEventListener("touchend", el.eventOnClick);
        el.removeAttribute("data-dragging");
    },
}

createApp(App)
  .directive("click-outside", clickOutside)
  .mount("#app");

This solution watch the element and the element's children of the component where the directive is applied to check if the event.target element is also a child. If that's the case it will not trigger, because it's inside the component.

How to use it

You only have to use as any directive, with a method reference to handle the trigger:

<template>
    <div v-click-outside="myMethod">
        <div class="handle" @click="doAnotherThing($event)">
            <div>Any content</div>
        </div>
    </div>
</template>
Motorman answered 15/1, 2021 at 11:54 Comment(0)
S
7

I have updated MadisonTrash's answer to support Mobile Safari (which does not have click event, touchend must be used instead). This also incorporates a check so that the event isn't triggered by dragging on mobile devices.

Vue.directive('click-outside', {
    bind: function (el, binding, vnode) {
        el.eventSetDrag = function () {
            el.setAttribute('data-dragging', 'yes');
        }
        el.eventClearDrag = function () {
            el.removeAttribute('data-dragging');
        }
        el.eventOnClick = function (event) {
            var dragging = el.getAttribute('data-dragging');
            // Check that the click was outside the el and its children, and wasn't a drag
            if (!(el == event.target || el.contains(event.target)) && !dragging) {
                // call method provided in attribute value
                vnode.context[binding.expression](event);
            }
        };
        document.addEventListener('touchstart', el.eventClearDrag);
        document.addEventListener('touchmove', el.eventSetDrag);
        document.addEventListener('click', el.eventOnClick);
        document.addEventListener('touchend', el.eventOnClick);
    }, unbind: function (el) {
        document.removeEventListener('touchstart', el.eventClearDrag);
        document.removeEventListener('touchmove', el.eventSetDrag);
        document.removeEventListener('click', el.eventOnClick);
        document.removeEventListener('touchend', el.eventOnClick);
        el.removeAttribute('data-dragging');
    },
});
Stonge answered 22/5, 2018 at 10:34 Comment(0)
T
7
export default {
  bind: function (el, binding, vNode) {
    // Provided expression must evaluate to a function.
    if (typeof binding.value !== 'function') {
      const compName = vNode.context.name
      let warn = `[Vue-click-outside:] provided expression '${binding.expression}' is not a function, but has to be`
      if (compName) { warn += `Found in component '${compName}'` }

      console.warn(warn)
    }
    // Define Handler and cache it on the element
    const bubble = binding.modifiers.bubble
    const handler = (e) => {
      if (bubble || (!el.contains(e.target) && el !== e.target)) {
        binding.value(e)
      }
    }
    el.__vueClickOutside__ = handler

    // add Event Listeners
    document.addEventListener('click', handler)
  },

  unbind: function (el, binding) {
    // Remove Event Listeners
    document.removeEventListener('click', el.__vueClickOutside__)
    el.__vueClickOutside__ = null

  }
}
Tad answered 6/6, 2018 at 8:0 Comment(0)
L
6

I create a div at the end of the body like that:

<div v-if="isPopup" class="outside" v-on:click="away()"></div>

Where .outside is :

.outside {
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0px;
  left: 0px;
}

And away() is a method in Vue instance :

away() {
 this.isPopup = false;
}
Lupus answered 21/6, 2019 at 15:7 Comment(0)
V
4

I use this code:

show-hide button

 <a @click.stop="visualSwitch()"> show hide </a>

show-hide element

<div class="dialog-popup" v-if="visualState" @click.stop=""></div>

script

data () { return {
    visualState: false,
}},
methods: {
    visualSwitch() {
        this.visualState = !this.visualState;
        if (this.visualState)
            document.addEventListener('click', this.visualState);
        else
            document.removeEventListener('click', this.visualState);
    },
},

Update: remove watch; add stop propagation

Valet answered 17/5, 2018 at 9:52 Comment(0)
M
4

There are already many answers to this question, and most of them are based on the similar custom directive idea. The problem with this approach is that one have to pass a method function to the directive, and cannot directly write code as in other events.

I created a new package vue-on-clickout that is different. Check it out at:

It allows one to write v-on:clickout just like any other events. For example, you can write

<div v-on:clickout="myField=value" v-on:click="myField=otherValue">...</div>

and it works.

Update

vue-on-clickout now supports Vue 3!

Update 2

vue-on-clickout is now replaced by a new package Clickout-Event which works for any front-end framework (or vanilla)!

Mombasa answered 1/8, 2019 at 13:40 Comment(5)
Just a question on how to implement this; do I need to load the Javascript file (<script src="clickout-event.js"></script>) completely or can I use import?Treenware
Clickout-Event does not really export anything, so I didn't design it in a way that can be imported as modules. You directly add the script to your page, preferably in the <head> section. Is there a particular reason why you want to use import instead?Mombasa
I don't need it on each page and because it's in the node_modules directory, it's not available in the public directory by default, so I still need to copy the file manuallyTreenware
It's not, but an import would be nice. especially when the package is updated it makes life easier.Treenware
You can now use import 'clickout-event'; to add it to your projectMombasa
S
4

I hate additional functions so... here is an awesome vue solution without an additional vue methods, only var

  1. create html element, set controls and directive
    <p @click="popup = !popup" v-out="popup">

    <div v-if="popup">
       My awesome popup
    </div>
  1. create a var in data like
data:{
   popup: false,
}
  1. add vue directive. its
Vue.directive('out', {

    bind: function (el, binding, vNode) {
        const handler = (e) => {
            if (!el.contains(e.target) && el !== e.target) {
                //and here is you toggle var. thats it
                vNode.context[binding.expression] = false
            }
        }
        el.out = handler
        document.addEventListener('click', handler)
    },

    unbind: function (el, binding) {
        document.removeEventListener('click', el.out)
        el.out = null
    }
})
Stephie answered 9/9, 2019 at 12:35 Comment(0)
M
4

For those using Vue 3.

Vue3 has changed the syntax for Directive Hooks:

  • Bind -> beforeMount
  • Unbind -> unmounted

To detect a click outside an element in Vue 3:

click-outside.js

export default function directive(app) {
  // you can name the directive whatever you want. -> click-outside
  app.directive('click-outside', {
    beforeMount(el, binding) {
      el.clickOutsideEvent = (evt) => {
        evt.stopPropagation();
        if (!(el === evt.target || el.contains(evt.target))) {
          binding.value(evt, el);
        }
      };
      window.requestAnimationFrame(() => {
        document.addEventListener("click", el.clickOutsideEvent);
      });
    },
    unmounted(el) {
      document.removeEventListener("click", el.clickOutsideEvent);
    },
  })
}

Register directive:

main.js

import { createApp } from "vue";
import App from "./App.vue";

// Import your directive, in order to register it.
import clickOutside from "./directives/click-outside.js"

createApp(App).use(clickOutside).mount("#app");

Usage:

<template>
  <div class="dropdown" v-click-outside="() => hideDropdown()"></div>
</template>
<script setup>
  function hideDropdown() {
    console.log("close dropdown")
  }
</script>

### OR 

<script>
  methods: {
    hideDropdown() {
      console.log("close dropdown")
    }
  }
</script> 
Mckelvey answered 27/3, 2022 at 17:4 Comment(0)
T
4

If you're using vue 3 with script setup, you can do this:

<script setup>
import {ref} from 'vue'

const elementRef = ref(null)
    
window.addEventListener('click', (event) => {
    if (!elementRef.value.contains(event.target)){
        console.log('click outside element')
    }
})

</script>


<template>
  <div ref="elementRef">your element</div>
</template>

or if you don't want to use event listeners at all, here's an example where the child closes if you click on the parent while not hovering the child:

<script setup>
import { reactive } from 'vue'

const state = reactive({
  show: false,
  hover: null
})

const closeModal = () => {
  if (!state.hover) {
    state.show = false
  }
}
</script>

<div class="parent w-20 h-20 bg-gray-400" v-if="state.show" @click=closeModal>
  <div class="child w-10 h-10 bg-red-400" @mouseenter=state.hover = true @mouseleave=state.hover = false>hello world</div>
</div>
Tolbutamide answered 26/7, 2022 at 13:40 Comment(0)
G
3

You can register two event listeners for click event like this

document.getElementById("some-area")
        .addEventListener("click", function(e){
        alert("You clicked on the area!");
        e.stopPropagation();// this will stop propagation of this event to upper level
     }
);

document.body.addEventListener("click", 
   function(e) {
           alert("You clicked outside the area!");
         }
);
Greet answered 23/3, 2016 at 5:48 Comment(0)
D
3

The short answer: This should be done with Custom Directives.

There are a lot of great answers here that also say this, but most of the answers I have seen break down when you start using outside-click extensively (especially layered or with multiple excludes). I have written an article on medium talking about the nuances of Custom Directives and specifically implementation of this one. It may not cover all edge cases but it has covered everything I have thought up.

This will account for multiple bindings, multiple levels of other element exclusions and allow your handler to only manage the "business logic".

Here's the code for at least the definition portion of it, check out the article for full explanation.

var handleOutsideClick={}
const OutsideClick = {
  // this directive is run on the bind and unbind hooks
  bind (el, binding, vnode) {
    // Define the function to be called on click, filter the excludes and call the handler
    handleOutsideClick[el.id] = e => {
      e.stopPropagation()
      // extract the handler and exclude from the binding value
      const { handler, exclude } = binding.value
      // set variable to keep track of if the clicked element is in the exclude list
      let clickedOnExcludedEl = false
      // if the target element has no classes, it won't be in the exclude list skip the check
      if (e.target._prevClass !== undefined) {
        // for each exclude name check if it matches any of the target element's classes
        for (const className of exclude) {
          clickedOnExcludedEl = e.target._prevClass.includes(className)
          if (clickedOnExcludedEl) {
            break // once we have found one match, stop looking
          }
        }
      }
      // don't call the handler if our directive element contains the target element
      // or if the element was in the exclude list
      if (!(el.contains(e.target) || clickedOnExcludedEl)) {
        handler()
      }
    }
    // Register our outsideClick handler on the click/touchstart listeners
    document.addEventListener('click', handleOutsideClick[el.id])
    document.addEventListener('touchstart', handleOutsideClick[el.id])
    document.onkeydown = e => {
      //this is an option but may not work right with multiple handlers
      if (e.keyCode === 27) {
        // TODO: there are minor issues when escape is clicked right after open keeping the old target
        handleOutsideClick[el.id](e)
      }
    }
  },
  unbind () {
    // If the element that has v-outside-click is removed, unbind it from listeners
    document.removeEventListener('click', handleOutsideClick[el.id])
    document.removeEventListener('touchstart', handleOutsideClick[el.id])
    document.onkeydown = null //Note that this may not work with multiple listeners
  }
}
export default OutsideClick
Depressant answered 4/2, 2020 at 17:22 Comment(0)
E
2

You can emit custom native javascript event from a directive. Create a directive that dispatches an event from the node, using node.dispatchEvent

let handleOutsideClick;
Vue.directive('out-click', {
    bind (el, binding, vnode) {

        handleOutsideClick = (e) => {
            e.stopPropagation()
            const handler = binding.value

            if (el.contains(e.target)) {
                el.dispatchEvent(new Event('out-click')) <-- HERE
            }
        }

        document.addEventListener('click', handleOutsideClick)
        document.addEventListener('touchstart', handleOutsideClick)
    },
    unbind () {
        document.removeEventListener('click', handleOutsideClick)
        document.removeEventListener('touchstart', handleOutsideClick)
    }
})

Which can be used like this

h3( v-out-click @click="$emit('show')" @out-click="$emit('hide')" )
Elite answered 19/3, 2019 at 21:11 Comment(0)
S
2

If you have a component with multiple elements inside of the root element you can use this It just works™ solution with a boolean.

<template>
  <div @click="clickInside"></div>
<template>
<script>
export default {
  name: "MyComponent",
  methods: {
    clickInside() {
      this.inside = true;
      setTimeout(() => (this.inside = false), 0);
    },
    clickOutside() {
      if (this.inside) return;
      // handle outside state from here
    }
  },
  created() {
    this.__handlerRef__ = this.clickOutside.bind(this);
    document.body.addEventListener("click", this.__handlerRef__);
  },
  destroyed() {
    document.body.removeEventListener("click", this.__handlerRef__);
  },
};
</script>
Simmons answered 14/7, 2019 at 17:39 Comment(0)
F
2
  <button 
    class="dropdown"
    @click.prevent="toggle"
    ref="toggle"
    :class="{'is-active': isActiveEl}"
  >
    Click me
  </button>

  data() {
   return {
     isActiveEl: false
   }
  }, 
  created() {
    window.addEventListener('click', this.close);
  },
  beforeDestroy() {
    window.removeEventListener('click', this.close);
  },
  methods: {
    toggle: function() {
      this.isActiveEl = !this.isActiveEl;
    },
    close(e) {
      if (!this.$refs.toggle.contains(e.target)) {
        this.isActiveEl = false;
      }
    },
  },
Fireguard answered 19/7, 2019 at 16:8 Comment(0)
P
2

You can achieve it by using few lines of script. I am using nuxt3 for this but I think it should also work on vue3

const showDropdown = ref( false )

onMounted( () => {
    window.addEventListener( 'click', ( e ) => {
        if ( document.getElementById( 'dropdown' ).contains( e.target ) ) {
            showDropdown.value = !showDropdown.value
        }
        else {
            showDropdown.value = false
        }
    } )
} )

In my template.

<div>
    <div
        class="flex gap-2 items-center relative px-5"
        id="dropdown"
    >
        <div class="w-5 h-5">
            <IconsCog />
        </div>
        <div class="relative">Settings</div>
        <div
            v-if="showDropdown"
            class="absolute bg-white w-full top-[40px] left-0 shadow-md rounded"
        >
            <ul>
                <li class="px-4 text-sm py-3">Account</li>
                <li class="px-4 text-sm pb-3">Logout</li>
            </ul>
        </div>
    </div>
</div>

Stackblitz example https://stackblitz.com/edit/nuxt-starter-cnn3qc?file=app.vue

Palestine answered 21/12, 2022 at 23:42 Comment(0)
J
1

Just if anyone is looking how to hide modal when clicking outside the modal. Since modal usually has its wrapper with class of modal-wrap or anything you named it, you can put @click="closeModal" on the wrapper. Using event handling stated in vuejs documentation, you can check if the clicked target is either on the wrapper or on the modal.

methods: {
  closeModal(e) {
    this.event = function(event) {
      if (event.target.className == 'modal-wrap') {
        // close modal here
        this.$store.commit("catalog/hideModal");
        document.body.removeEventListener("click", this.event);
      }
    }.bind(this);
    document.body.addEventListener("click", this.event);
  },
}
<div class="modal-wrap" @click="closeModal">
  <div class="modal">
    ...
  </div>
<div>
Justen answered 13/11, 2018 at 2:57 Comment(0)
S
0

Frequently people want to know if user leave root component (works with any level components)

Vue({
  data: {},
  methods: {
    unfocused : function() {
      alert('good bye');
    }
  }
})
<template>
  <div tabindex="1" @blur="unfocused">Content inside</div>
</template>
Sheetfed answered 13/3, 2017 at 14:40 Comment(0)
J
0

I am using this package : https://www.npmjs.com/package/vue-click-outside

It works fine for me

HTML :

<div class="__card-content" v-click-outside="hide" v-if="cardContentVisible">
    <div class="card-header">
        <input class="subject-input" placeholder="Subject" name=""/>
    </div>
    <div class="card-body">
        <textarea class="conversation-textarea" placeholder="Start a conversation"></textarea>
    </div>
</div>

My script codes :

import ClickOutside from 'vue-click-outside'
export default
{
    data(){
        return {
            cardContentVisible:false
        }
    },
    created()
    {
    },
    methods:
        {
            openCardContent()
            {
                this.cardContentVisible = true;
            }, hide () {
            this.cardContentVisible = false
                }
        },
    directives: {
            ClickOutside
    }
}
Jilljillana answered 30/11, 2018 at 19:26 Comment(0)
S
0

@Denis Danilenko solutions works for me, here's what I did: By the way I'm using VueJS CLI3 and NuxtJS here and with Bootstrap4, but it will work on VueJS without NuxtJS also:

<div
    class="dropdown ml-auto"
    :class="showDropdown ? null : 'show'">
    <a 
        href="#" 
        class="nav-link" 
        role="button" 
        id="dropdownMenuLink" 
        data-toggle="dropdown" 
        aria-haspopup="true" 
        aria-expanded="false"
        @click="showDropdown = !showDropdown"
        @blur="unfocused">
        <i class="fas fa-bars"></i>
    </a>
    <div 
        class="dropdown-menu dropdown-menu-right" 
        aria-labelledby="dropdownMenuLink"
        :class="showDropdown ? null : 'show'">
        <nuxt-link class="dropdown-item" to="/contact">Contact</nuxt-link>
        <nuxt-link class="dropdown-item" to="/faq">FAQ</nuxt-link>
    </div>
</div>
export default {
    data() {
        return {
            showDropdown: true
        }
    },
    methods: {
    unfocused() {
        this.showDropdown = !this.showDropdown;
    }
  }
}
Singultus answered 19/2, 2019 at 2:59 Comment(0)
W
0

Use this package vue-click-outside

It's simple and reliable, currently used by many other packages. You can also reduce your javascript bundle size by calling the package only in the required components (see example below).

npm install vue-click-outside

Usage :

<template>
  <div>
    <div v-click-outside="hide" @click="toggle">Toggle</div>
    <div v-show="opened">Popup item</div>
  </div>
</template>

<script>
import ClickOutside from 'vue-click-outside'

export default {
  data () {
    return {
      opened: false
    }
  },

  methods: {
    toggle () {
      this.opened = true
    },

    hide () {
      this.opened = false
    }
  },

  mounted () {
    // prevent click outside event with popupItem.
    this.popupItem = this.$el
  },

  // do not forget this section
  directives: {
    ClickOutside
  }
}
</script>
Wolffish answered 15/7, 2019 at 7:4 Comment(0)
R
0

Don't reinvent the wheel, use this package v-click-outside

Radiophotograph answered 1/4, 2020 at 9:22 Comment(0)
E
0

You can create new component which handle outside click

Vue.component('click-outside', {
  created: function () {
    document.body.addEventListener('click', (e) => {
       if (!this.$el.contains(e.target)) {
            this.$emit('clickOutside');
           
        })
  },
  template: `
    <template>
        <div>
            <slot/>
        </div>
    </template>
`
})

And use this component:

<template>
    <click-outside @clickOutside="console.log('Click outside Worked!')">
      <div> Your code...</div>
    </click-outside>
</template>
Extragalactic answered 25/6, 2020 at 15:10 Comment(0)
F
0

I'm not sure if someone will ever see this answer but here it is. The idea here is to simply detect if any click was done outside the element itself.

I first start by giving an id to the main div of my "dropdown".

<template>
  <div class="relative" id="dropdown">
    <div @click="openDropdown = !openDropdown" class="cursor-pointer">
      <slot name="trigger" />
    </div>

    <div
      class="absolute mt-2 w-48 origin-top-right right-0 text-red  bg-tertiary text-sm text-black"
      v-show="openDropdown"
      @click="openDropdown = false"
    >
      <slot name="content" />
    </div>
  </div>
</template>

And then I just loop thru the path of the mouse event and see if the div with my id "dropdown" is there. If it is, then we good, if it is no, then we close the dropdown.

<script>
export default {
  data() {
    return {
      openDropdown: false,
    };
  },
  created() {
    document.addEventListener("click", (e) => {
      let me = false;
      for (let index = 0; index < e.path.length; index++) {
        const element = e.path[index];

        if (element.id == "dropdown") {
          me = true;
          return;
        }
      }

      if (!me) this.openDropdown = false;
    });
  }
};
</script>

I'm pretty sure this can bring performance issues if you have many nested elements, but I found this as the most lazy-friendly way of doing it.

Formulaic answered 25/2, 2021 at 0:27 Comment(0)
A
0

A lot of these answers seem to be over-complicating this, which is making me wonder if I'm missing something. But the solution seems simple: just compare this.$el (or one of its children) to e.target. No non-Vue event listeners needed — just the Vue @click listener.

In this example, I have a Vue component that renders a modal. I want to close the modal if someone clicks on the .modal element (which is the overlay behind the modal), but not if they click on any of its children (i.e., the contents of the modal). Here's the code to do that:

// MyComponent.js
export default {
  methods: {
    handleClickModal(e) {
      // Prevent clicks on child elements (e.g., `.modal-dialog`) from
      // closing the modal.
      if (this.$el === e.target) {
        closeMyModal()
      }
    },
  },

  template: `
    <div class="modal" style="display:initial" @click="handleClickModal">
      <div class="modal-dialog">
        <!-- modal content here... -->
      </div>
    </div>
  `,
}

It Just Works™.

Arlo answered 13/7, 2022 at 1:23 Comment(0)
A
0

Full Implementation (all steps)

Despite having so many answers under this thread, I still struggled to get the full implementation of the click outside event. You can follow the steps below to see the complete implementation process.

main.js -- register a global directive (just copy and paste)


import Vue from 'vue';

Vue.directive('click-outside', {
    bind(el, binding) {
        const handler = (e) => {
            if ((!el.contains(e.target) && el !== e.target)) {
                binding.value(e);
            }
        }

        el.__vueClickOutside__ = handler;
        document.addEventListener('click', handler);
    },
    unbind(el) {
        document.removeEventListener('click', el.__vueClickOutside__);
        el.__vueClickOutside__ = null;
    }
});

YourComponent.vue -- the use of the directive


<template>

<div>

  <button @click.stop="showCalloutMenu()">Callout</button>

  <section v-if="callout" v-click-outside="hideCalloutMenu">
    Your callout menu content
  </section>

</div>

<script>

export default {
  data:() => ({
    callout: false,
  }),
  methods: {
    showCalloutMenu() {
        this.callout = true;
        // or if you prefer toggle effect, use 
        // this.callout = !this.callout;
    },
    hideCalloutMenu() {
        this.callout = false;
    },
  }
};

Caveats

Make sure to use .stop event modifier on your callout element, otherwise your callout menu will not show up.

@click.stop="showCalloutMenu()"

Aeroplane answered 11/8, 2022 at 11:3 Comment(0)
F
0

If anyone would like to accomplish this using Vue 3 composables, you can use this. If you'd like to code it yourself, see below:

event.js

import { onMounted, onUnmounted } from 'vue';

export function useEventListener(target, event, callback) {
  onMounted(() => target.addEventListener(event, callback));
  onUnmounted(() => target.removeEventListener(event, callback));
}

click-outside.js

import { unref } from 'vue';
import { useEventListener } from './event';

export function useClickOutside(ref, cb) {
  let target = null;

  const listener = (event) => {
    if (!target) target = unref(ref);
    if (!(target === event.target || target.contains(event.target))) cb();
  };

  useEventListener(document.body, 'click', listener);
}

Your Component

<script setup>
import { ref } from 'vue';
import { useClickOutside } from 'path/to/click-outside';

const myCallback = () => {
  console.log('You clicked outside the container!');
};

const container = ref(null);
useClickOutside(container, myCallback);
</script>

<template>
  <div ref="container"></div>
</template>

Note that useEventListener is a common pattern and can be used independent of useClickOutside.

Flitter answered 22/8, 2022 at 14:6 Comment(0)
W
0

In addition to all the great answers, I just want to mention that you can also use VueUse composable onClickOutside.

Here is an example usage as a directive

<template>
    <component v-if="showDialog" v-on-click-outside="() =>showDialog = false"/>
</template>

<script>
import { vOnClickOutside } from '@vueuse/components'
const showDialog = ref(true)
</script>

You can also use it directly as a component method or as a renderless component.

Wrinkly answered 9/11, 2022 at 8:29 Comment(0)
S
0

https://jsbin.com/yomimudahi/edit?html,css,js,output

document.addEventListener("click", printMousePos);

function printMousePos(event) {

  const list = document.getElementsByTagName("nav")[0];
  
  var ssdd = list.classList.contains("test2");

    if (ssdd === false && (event.clientX > list.clientWidth || event.clientY > list.clientHeight)) {
        list.classList.add("test2");
    }   
}


function openNav() {
  const list = document.getElementsByTagName("nav")[0];
  
  list.classList.remove("test2");
}
@media only screen and (min-width: 701px) {

  main {
    display: grid;
    width: 100%;
    grid-template-areas: "nav main";
    grid-template-columns: 150px 1fr;
    position: relative;
  }

  main > nav {
    grid-area: nav;
    background-color: #ffa08c;
  }

  main > section {
    grid-area: main;
    background-color: #ffff64;
  }

  main > section > aside {
    grid-area: head;
    background-color: #8ca0ff;
  }
  
  .test {
     display: none;  
  }
}



@media only screen and (max-width: 700px) {
  
  main {
    display: grid;
    width: 100%;
    grid-template-areas: "main";
    grid-template-columns: 1fr;
    position: relative;
  }

  main > nav {
    height: 300px;  
    background-color: coral;
    position: fixed;  
  }

  main > section {
    grid-area: main;
    background-color: #ffff64;
  }

  main > section > aside {
    grid-area: head;
    background-color: #8ca0ff;
  }
  
  .test2 {
     display: none;  
  }  

}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <main>
    <nav class="test2">
      <header>        
        <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTCPQ5XLyAjzq-NHitpSKVIsF8LorhPxewY_vERnurZLA&s" width="50" height="50" />
      </header>
      <div>      
        Navigation
       </div>
    </nav>
    <section>
      <aside>
        Aside
        <div class="test" onclick="openNav()">
          <img src="https://cdn-icons-png.flaticon.com/512/1752/1752735.png" width="50" height="50" />
        </div>
      </aside>
      <article>
        Content<br/>
        <br/>
        <br/>
        <br/>
        <br/>
        <br/>
        <br/>
        <br/>
        <br/>
        <br/>
        <br/>
        <br/>
        <br/>
        <br/>
        <br/>
        <br/>
      </article>
    </section>
  </main>
</body>
</html>
Shorthand answered 30/12, 2022 at 15:52 Comment(0)
A
0

I created mixin for Vue 2:

Mixin file:

export const detectClickOutside = {
   data() {
      return {
         statusElement: false,
         nameElement: undefined,
         ref: undefined,
      };
   },
   watch: {
      statusElement(isShow) {
         if (isShow) document.addEventListener('mousedown', this.handleClickOutside);
         else this.stopEventListener();
      },
   },
   beforeDestroy() {
      this.stopEventListener();
   },
   methods: {
      stopEventListener() {
         this.statusElement = false;
         this.nameElement = undefined;
         this.ref = undefined;
         document.removeEventListener('mousedown', this.handleClickOutside);
      },
      setStatusShowingElement(status, name, refElement) {
         this.statusElement = status;
         this.nameElement = name;
         this.ref = refElement;
      },
      handleClickOutside({ target }) {
         if (this.ref && !this.ref.contains(target)) this[this.nameElement] = false;
      },
   },
};

And how to use it in component:

<button @click="openModal = !openModal">Click</button> // button for open modal

component which will be open:

<ExampleComponent v-show="openModal" ref="modal" />

and watcher to send data to mixin:

watch: {
  openModal(newValue) {
     this.setStatusShowingElement(
        newValue,
        'openModal',
        this.$refs.modal.$el,
     );
  },
},
Auric answered 26/1, 2023 at 9:29 Comment(0)
S
0

Add a supplement to @Julien Le Coupanec's answer.

If feasible, consider incorporating a widely-used package, such as https://vueuse.org/core/onClickOutside/.

However, it's important to be aware of the drawbacks associated with utilizing a niche npm package, which include:

  • Limited Documentation: Niche packages often lack sufficient documentation.
  • Lack of Support: They may lack a strong user community or active maintainers.
  • Stability Issues: Niche packages may offer lower stability compared to popular alternatives.
  • Compatibility Issues: Risk of incompatibility with other packages or technologies.
  • Security Risks: Potential security concerns if not regularly maintained.
  • Discontinued Maintenance: Risk of discontinuation if the author stops maintaining the package.
  • Fewer Features: Niche packages may lack extensive features found in popular alternatives.
Sachiko answered 16/1 at 7:14 Comment(0)
D
-1

I have a solution for handling toggle dropdown menu:

export default {
data() {
  return {
    dropdownOpen: false,
  }
},
methods: {
      showDropdown() {
        console.log('clicked...')
        this.dropdownOpen = !this.dropdownOpen
        // this will control show or hide the menu
        $(document).one('click.status', (e)=> {
          this.dropdownOpen = false
        })
      },
}
Dallon answered 27/6, 2017 at 21:22 Comment(0)
C
-1

Now you should use the vue-click-outside plugin for this.

  1. you can run an event when you click outside that's div.

    NPM Plugin: https://www.npmjs.com/package/v-click-outside

Corunna answered 8/10, 2021 at 2:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.