4

I've a HeaderSubmenu component which can show/hide a drop-down menu if a button is clicked. Okay, but now I try to find a good solution to make that if the user clicks anywhere in the app but not on this drop-down menu, it hides.

I'm using Vue 2.3.3 with Vuex and VueRouter.

my App entry point :

'use strict';

import Vue from 'vue';
import VueRouter from 'vue-router';
import Vuex from 'vuex';

Vue.use(VueRouter);
Vue.use(Vuex);

import store_data from 'store';
import {router} from 'routes';

import App from 'app.vue';

var store = new Vuex.Store(store_data);

new Vue({
  el: '#app',
  store,
  router: router,
  render: function (createElement) {
    return createElement(App)
  }
})

template of HeaderSubmenu component :

<template>
  <li class="header-submenu">
    <!-- button to show/hide the drop-down menu -->
    <header-link v-bind="$props" :href="to ? false : '#'" @click.native="toggleMenu()">
      <slot name="item"></slot>
    </header-link>
    <!-- drop-down menu -->
    <ul class="submenu-list" :class="{'open': open, 'animated': animated}" @animationend="displaynone()">
      <slot></slot>
    </ul>
  </li>
</template>

This component is somewhere in my app, and I want to call his toggleMenu() method when the users clicks elsewhere than on the <ul class="submenu-list">.

I thought of a global event bus where my drop-down menu should be "registred" and detects on a global @click event on my App. If the registered menu is not the element clicked we'd call his toggleMenu() method if not. (Ideally I could register other elements which have the same behaviour.)

... But I don't master vue event system at all. How can check if an event is not on some element ? I've no idea of how to achieve this. Can you help me ? Thank you !

====== EDIT ======

With the help of Bert Evans I used this custom directive :

// directive-clickoutside.js
export default {
  bind(el, binding, vnode) {
    el.event = function (event) {
      // here I check that click was outside the el and his childrens
      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.event)
  },
  unbind(el) {
    document.body.removeEventListener('click', el.event)
  },
};

// main.js
import clickout from 'utils/directive-clickoutside';
Vue.directive('clickout', clickout);

I used this in my component template :

// HeaderSubmenu component
<template>
  <li class="header-submenu">
    <!-- élément du header servant à ouvrir le sous-menu -->
    <header-link v-bind="$props" :href="to ? false : '#'" @click.native="toggle()">
      <slot name="item"></slot>
    </header-link>
    <!-- sous-menu -->
    <ul class="submenu-list" :class="{'open': open, 'animated': animated}" @animationend="displaynone()" v-clickout="hide()">
      <slot></slot>
    </ul>
  </li>
</template>

But this code fails when I click anywhere on the page :

Uncaught TypeError: n.context[e.expression] is not a function
    at HTMLBodyElement.t.event (directive-clickoutside.js:7)

Where is the problem ?

Thaledric
  • 539
  • 2
  • 8
  • 17
  • 1
    Possible duplicate of [Detect click outside element](https://stackoverflow.com/questions/36170425/detect-click-outside-element) – Bert Jun 17 '17 at 13:49
  • Thanks, I tried to use a custom directive, but it ends with an infinite update loop. I edited the post to show you that. – Thaledric Jun 17 '17 at 19:58
  • What does `toggleMenu` do? – Bert Jun 17 '17 at 20:13
  • toogleMenu invert the state of appearance of the submenu (show/hide). It certainly was the origin of infinite loop. I changed to use `toogle()` when click on the link, and juste `hide()` when click away from the submenu. But a new problem occurs in the custom directive (post edited). – Thaledric Jun 17 '17 at 20:24

2 Answers2

10

The issue is here.

v-clickout="hide()"

Effectively what you are doing is setting v-clickout to the result of hide(). Instead, just pass it the hide method.

v-clickout="hide"

In general with Vue, in your template, if you are just wanting the template to call a function and not do anything special, just pass it the name of the function.

Bert
  • 80,741
  • 17
  • 199
  • 164
  • Oh yes, so it's not like event directives ! I added the `stop` modifier on the click event. Thank you ! I've a last question : if I have two Submenus, when I open a first one and open the other one, the first one doesn't close (certainly because of the stop modifier of the toggle button). How can I achieve this ? – Thaledric Jun 17 '17 at 21:03
  • @Thaledric First, event directives are the same. You can just set them equal to the name without parentheses. I'm having trouble imagining the situation in the second case. Are these true submenus (children of a menu)? Maybe a new question so you can give all the details. – Bert Jun 17 '17 at 21:08
  • Thank you ! I've posted a new question here : https://stackoverflow.com/questions/44613980/close-all-popups-when-the-user-click-to-open-a-new-one – Thaledric Jun 18 '17 at 10:24
  • You're a superstar dude – Joao Alves Marrucho Apr 07 '22 at 22:59
0

Here's how to create the click-outside directive that works with Vue 3:

app.directive('click-outside', {
  // bind
  beforeMount(el, binding) {
    el.clickOutsideEvent = function (event) {
      if (!(el == event.target || el.contains(event.target))) {
        binding.value(event)
      }
    }
    document.body.addEventListener('click', el.clickOutsideEvent)
  },

  // unbind
  unmounted(el) {
    document.body.removeEventListener('click', el.clickOutsideEvent)
  },
})