Invoke actions when clicking outside of components

Currently, I have a HeaderSubmenu component that is designed to show/hide a drop-down menu when a specific button is clicked. However, I am now trying to implement a solution where if the user clicks anywhere else in the application other than on this drop-down menu, it should automatically hide.

For my setup, I am utilizing Vue 2.3.3 along with Vuex and VueRouter.

This is the entry point of my App:

'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)
  }
})

Here is the template for the HeaderSubmenu component:

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

The goal is to call the toggleMenu() method of this component whenever the user clicks outside the <ul class="submenu-list">.

I have been considering using a global event bus mechanism where the drop-down menu would be 'registered' to detect any click events within the entire application. If the registered menu is not the element clicked, then its toggleMenu() method would be triggered. Ideally, other elements with similar behavior could also be registered in this manner.

However, I currently lack a clear understanding of Vue's event system and how to determine if an event was triggered outside of a specific element. Can anyone provide guidance or assistance? Thank you!

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

After consulting with Bert Evans, I implemented a custom directive as follows:

// directive-clickoutside.js
export default {
  bind(el, binding, vnode) {
    el.event = function (event) {
      // Check if the click occurred outside the element and its children
      if (!(el == event.target || el.contains(event.target))) {
        // If true, call the method provided in the 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);

Incorporating this directive into my component template:

// HeaderSubmenu component
<template>
  <li class="header-submenu">
    <!-- Element in the header used to trigger the submenu -->
    <header-link v-bind="$props" :href="to ? false : '#'" @click.native="toggle()">
      <slot name="item"></slot>
    </header-link>
    <!-- Submenu content -->
    <ul class="submenu-list" :class="{'open': open, 'animated': animated}" @animationend="displaynone()" v-clickout="hide()">
      <slot></slot>
    </ul>
  </li>
</template>

However, upon testing, I encountered the following error when clicking anywhere on the page:

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

What could be causing this issue?

Answer №1

The problem lies right here.

v-clickout="hide()"

Essentially, what you're doing is assigning the result of hide() to v-clickout. Instead, simply provide it with the hide function.

v-clickout="hide"

Generally speaking in Vue, when working with templates, if you just want the template to execute a function without any specific handling, just pass the name of the function.

Answer №2

Follow these steps to implement the click-outside functionality in Vue 3:

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

  // hook function for unbinding
  unmounted(el) {
    document.body.removeEventListener('click', el.clickOutsideEvent)
  },
})

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The URL may change, but the component remains constant when navigating back

My application consists of two primary components: the Project component and MainContainer. The MainContainer component regularly fetches data using a fetchData method. When moving forward, both the URL and component can change dynamically. However, when m ...

Run the .map() method at regular intervals of 'x' seconds

I have a certain function in mind: function fetchDesign (items) { items.map(item => item.classList.add('selected')) // additional code here } Is there a way to trigger item.classList.add('selected') every 2 seconds, while ensu ...

What causes vue.js to be unable to function within HTML documents?

I was trying to integrate Vue.js into my Spring Boot application. Even though the build seemed successful, I am encountering issues with getting the Vue component to work properly. Here is a code snippet of my simple component, MenuBar.vue: <template> ...

What is the best way to enhance the class following this condition in JavaScript?

Initially, the original script worked perfectly with just one class being added: function check_btn_charge() { if (parseInt(jQuery(".total-change-price").text()) >= 0) { jQuery(".btn-action-save-charge"+"&nbsp;"+"btn-danger" ...

Integrating TypeScript into an established create-react-app project

Struggling to integrate TypeScript into an existing create-react-app? I've always added it at the beginning of a project using create-react-app my-app --scripts-version=react-scripts-ts, but that's not working this time. The only "solution" I co ...

I desire a smooth fade-in transition for the background image when the addClass function is

If the background color is set to header-fixed-position, the color will fade in. However, if the background is an image, the image will not fade in. Apologies for my lack of proficiency in English. Please refer to the sample code below. Try removing the c ...

Issue with AJAX MVC HTML: jQuery value is not blocking API call

Upon clicking a button, I am triggering a web API call using AJAX. My form is utilizing JqueryVal for form validations based on my viewmodel data annotations. The issue I am facing is that when I click the "Listo" button in my form, it executes the API ca ...

Learn how to retrieve the value on keyup in Laravel 5 when using Vue.js for editing purposes

Within our application, we have incorporated validation into the code. When working on the Edit section, how can I verify if a particular code already exists? The following snippet demonstrates my attempt: Edit Vue <label>Code</label> <inp ...

"The JavaScript code that functions perfectly in the browser console, but fails to execute when running in the actual

I'm encountering an issue with a simple piece of JavaScript code that seems to only work when executed in the browser console: <script> $(".hopscotch-close").click(function () { alert("Hi"); Cookies.set("tourState", "closed" ...

What is the difference in performance between using named functions versus anonymous functions in Node.js?

Currently, I am working on a Node.js app and was initially using anonymous functions for callbacks. However, after referring to the website callbackhell.com, I discovered that using named functions is considered best practice for coding. Despite switching ...

Struggling to manage texbox with Reactjs

Currently working in Reactjs with Nextjs and encountering a problem with the "text box". When I use the "value" attribute in the textbox, I am unable to input anything, but when I use the "defaultValue" attribute, I receive a validation message saying "Ple ...

Loading slides in bxSlider using ajax

Is there a method to dynamically insert a slide into bxSlider via ajax without affecting its smooth transition? I am looking to initially display the contents of only one slide upon page load, and then have subsequent slides loaded when the next or previo ...

A guide to accessing OS environment variables using Vue 2.7

Setting environment variables in my OS has been straightforward: However, I am facing an issue when trying to access these variables in my Vue app. Here is my .env file content: VUE_APP_GATEWAY_ADDRESS=localhost:8083 VUE_APP_GATEWAY_ADDRESS_TEST_TWO=gwat ...

Tips for preserving the status of a sidebar

As I work on developing my first web application, I am faced with a navigation challenge involving two menu options: Navbar Sidebar When using the navbar to navigate within my application, I tend to hide the sidebar. However, every ti ...

Permit the use of the "&" character in mailto href links

When including an email mailto href link and using a & character in the subject, it can cause issues with code rendering. For example, if the subject is "Oil & Gas," only "Oil" may show up. In most cases, you could simply replace the & with th ...

"Enable a smooth transition and switch the visibility of a div element when clicked

How to create a dropdown form with a transition effect that triggers on click of an element? Once triggered, the transition works smoothly but clicking again only hides the div element without triggering the transition. See the demo here: Check out the fu ...

Tips for resolving the issue of "Warning: useLayoutEffect does not have any effect on the server" when working with Material UI and reactDOMServer

Encountering an issue with ReactDOMServer and Material UI Theme Provider. Everything seems to be functioning properly, but a persistent error keeps appearing in the console: Warning: useLayoutEffect does nothing on the server, because its effect cannot be ...

Unexpected resizing of a div due to Vue animation

I'm currently working on a quiz and I'm trying to incorporate some animation effects when users navigate between questions. I've been experimenting with a fade in and out effect, but there seems to be a glitch. Whenever I click the button, t ...

Tips for incorporating a dynamic basePath into your NextJS project

For my new app, I am trying to include the groupID in the URL before the actual path, such as 'https://web-site.com/1/tasks'. The NextJs docs mention a basePath that is set at build time and cannot be modified. With numerous routes in my current ...

Guide on incorporating dynamic markers into MapBox using Vue/Nuxt

After setting up my MapBox, I wanted to dynamically add markers using Vue/Nuxt. However, the markers are not appearing on the map and I'm unsure of what the issue could be. The code snippet below shows that console.log() prints the correct coordinate ...