When a Vue.js Directive is inserted or bound, it actually takes precedence over the click event

Imagine having multiple dropdowns or elements on the page that all utilize a directive called "closable". This directive triggers an expression if the element clicked is outside of the element using the directive.

The intended behavior is that when clicking on another element on the page, such as a different dropdown with the same directive, it should compare the click event path to the existing one. If they do not match or are not contained within the element, it should close it.

However, what actually occurs is that the click event is never registered; instead, it initializes another directive and for some reason, the click event is lost.

The only scenario where the click event gets registered is when clicking on something without the directive.

Vue.directive('closable', {
    inserted: (el, binding, vnode) => {
        // assign event to the element
        el.clickOutsideEvent = function(event) {
            console.log({el, event});
            // here we check if the click event is outside the element and its children
            if (!(el == event.path[0] || el.contains(event.path[0]))) {
                // if clicked outside, call the provided method
                vnode.context[binding.expression](event);
            }
        };
        // register click and touch events
        document.body.addEventListener('click', el.clickOutsideEvent);
        document.body.addEventListener('touchstart', el.clickOutsideEvent);
    },
    unbind: function(el) {
        // unregister click and touch events before the element is unmounted
        document.body.removeEventListener('click', el.clickOutsideEvent);
        document.body.removeEventListener('touchstart', el.clickOutsideEvent);
    },
    stopProp(event) {
        event.stopPropagation();
    },
});

Answer №1

UPDATE

Check out this alternative approach for a custom v-click-outside directive that can be implemented directly within your Vue component:

  directives:
    {
      clickOutside:
        {
          bind(elem, binding, vnode)
          {
            elem.clickOutsideEvent = function(evt)
            {
              if (elem !== evt.target && !elem.contains(evt.target)) vnode.context[binding.expression](evt);
            };
            document.body.addEventListener('click', elem.clickOutsideEvent);
          },
          unbind(elem)
          {
            document.body.removeEventListener('click', elem.clickOutsideEvent);
          }
        }
    },

Feel free to test out this implementation in your project:

import Vue from 'vue'

// Define constants and variables
const HAS_WINDOWS = typeof window !== 'undefined';
const HAS_NAVIGATOR = typeof navigator !== 'undefined';
const IS_TOUCH = HAS_WINDOWS && ('ontouchstart' in window || (HAS_NAVIGATOR && navigator.msMaxTouchPoints > 0));
const EVENTS = IS_TOUCH ? ['touchstart'] : ['click'];
const IDENTITY = (item) => item;

// Initialize the directive object
const directive = {
  instances: [],
};

// Function to process directive arguments
function processDirectiveArguments(bindingValue)
{
  const isFunction = typeof bindingValue === 'function';
  if (!isFunction && typeof bindingValue !== 'object')
  {
    throw new Error('v-click-outside: Binding value must be a function or an object')
  }

  return {
    handler: isFunction ? bindingValue : bindingValue.handler,
    middleware: bindingValue.middleware || IDENTITY,
    events: bindingValue.events || EVENTS,
    isActive: !(bindingValue.isActive === false),
  }
}

// Event handler function
function onEvent({ el, event, handler, middleware })
{
  const isClickOutside = event.target !== el && !el.contains(event.target);

  if (!isClickOutside)
  {
    return
  }

  if (middleware(event, el))
  {
    handler(event, el)
  }
}

// Create a new instance of the directive
function createInstance({ el, events, handler, middleware })
{
  return {
    el,
    eventHandlers: events.map((eventName) => ({
      event: eventName,
      handler: (event) => onEvent({
        event,
        el,
        handler,
        middleware
      }),
    })),
  }
}

// Remove an existing instance of the directive
function removeInstance(el)
{
  const instanceIndex = directive.instances.findIndex((instance) => instance.el === el);
  if (instanceIndex === -1)
  {
    // Note: This can happen when active status changes from false to false
    return
  }

  const instance = directive.instances[instanceIndex];

  instance.eventHandlers.forEach(({ event, handler }) =>
    document.removeEventListener(event, handler)
  );

  directive.instances.splice(instanceIndex, 1)
}

// Bind function for the directive
function bind(el, { value })
{
  const { events, handler, middleware, isActive } = processDirectiveArguments(value);

  if (!isActive)
  {
    return
  }

  const instance = createInstance({
    el,
    events,
    handler,
    middleware
  });

  instance.eventHandlers.forEach(({ event, handler }) =>
    setTimeout(() => document.addEventListener(event, handler), 0)
  );
  directive.instances.push(instance)
}

// Update function for the directive
function update(el, { value, oldValue })
{
  if (JSON.stringify(value) === JSON.stringify(oldValue))
  {
    return
  }

  const { events, handler, middleware, isActive } = processDirectiveArguments(value);

  if (!isActive)
  {
    removeInstance(el);
    return
  }

  let instance = directive.instances.find((instance) => instance.el === el);

  if (instance)
  {
    instance.eventHandlers.forEach(({ event, handler }) =>
      document.removeEventListener(event, handler)
    );
    instance.eventHandlers = events.map((eventName) => ({
      event: eventName,
      handler: (event) => onEvent({
        event,
        el,
        handler,
        middleware
      }),
    }))
  }
  else
  {
    instance = createInstance({
      el,
      events,
      handler,
      middleware
    });
    directive.instances.push(instance)
  }

  instance.eventHandlers.forEach(({ event, handler }) =>
    setTimeout(() => document.addEventListener(event, handler), 0)
  )
}

// Set up the directive in Vue
directive.bind = bind;
directive.update = update;
directive.unbind = removeInstance;

Vue.directive('click-outside', directive); 

Answer №2

After numerous attempts to make the events register properly, I decided to take a different approach.

Whenever a closable directive is added, it triggers any previous expressions that were open before and then adds the new expression handler to a variable named prevNodes so that the next time a closable directive is inserted, it will call that expression.

let prevNodes = [];
Vue.directive ( 'closable', {
    inserted: ( el, binding, vnode ) => {

        console.log ( {prevNodes} );

        prevNodes.forEach ( item => {
            //console.log ( item );
            const {vnode, binding} = item;
            vnode.context[binding.expression] ();
        } );


        // assign event to the element
        el.clickOutsideEvent = function ( event ) {
            // here we check if the click event is outside the element and it's children
            if ( !( el == event.path[0] || el.contains ( event.path[0] ) ) ) {
                // if clicked outside, call the provided method
                vnode.context[binding.expression] ( event );
            }
        };

        prevNodes.push ( {vnode, binding} );

        // register click and touch events
        document.body.addEventListener ( 'click', el.clickOutsideEvent );
        document.body.addEventListener ( 'touchstart', el.clickOutsideEvent );
    },
    unbind: function ( el, binding, vnode ) {
        const removeIndex = prevNodes.findIndex ( item => item.vnode.elm === vnode.elm );
        prevNodes.splice ( removeIndex, 1 );
        // unregister click and touch events before the element is unmounted
        document.body.removeEventListener ( 'click', el.clickOutsideEvent );
        document.body.removeEventListener ( 'touchstart', el.clickOutsideEvent );
    },
    stopProp ( event ) {
        event.stopPropagation ();
    },
} );

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

Tips for asynchronously modifying data array elements by adding and slicing

I am facing an issue in my vuejs application where I need to modify an array of items after the app has finished loading. My current setup looks like this: var n = 100; var myData = []; function loadMovies(n){ // async ajax requests // add items to ...

Display only the offcanvas button

Having trouble with Bootstrap 5 offcanvas? The offcanvas doesn't hide when I click the button again. <button data-bs-toggle="offcanvas" role="button">Add to Cart</button> Every time I click the button again, the offcan ...

Material-UI icons refusing to show up on the display

I've been working on creating a sidebar component and importing various icons to enhance the UI, but for some reason they are not displaying on the screen. I even tried other suggested solutions without success. <SidebarOption Icon = {InsertComment ...

The form is functioning properly on mobile devices but is currently experiencing issues on the server

Everything runs smoothly when accessing the website and using the form on localhost. However, once it's uploaded to a server, the form only functions correctly on desktop devices. On mobile, the form fails to filter and displays all professionals inst ...

What is the possible reason behind the Vue warning message: "The instance is referencing the 'msg' property or method during rendering, even though it is not defined"?

Although the situation seems straightforward, the reason behind it is not clear to me. I am attempting to create a Vue component for a project with older ES5 code. The Vue library version I am using is 2.6x (I also tried 2.5x). Here is the Vue component I ...

Integrating Dialogflow with a Heroku JavaScript application

After extensive research, I delved into the realm of integrating DialogFlow requests with a webhook hosted on platforms like Heroku. With both Heroku and nodeJS impeccably installed on my system, I diligently followed the heroku tutorial to kickstart the p ...

Troubleshooting a 400 Bad Request Error in jQuery Ajax for WordPress Widgets

I am attempting to send information to admin-ajax.php in order to save it as a $_POST variable for handling on the cart page. However, my .ajax function keeps failing. var sendJsonA = {'value':'Data coming from JSON'} var ajaxurl = $ ...

Having trouble with state not updating correctly after making a fetch request within a useEffect hook in

In my React app with an Express backend, I am facing a challenge in updating the component state using the useEffect hook to trigger once when the component renders. Inside the useEffect, I fetch data from the Express server. const Favorites = ({ user }) = ...

construct a table utilizing JSON information

If I have data returned from an ajax call that needs to be processed, a table like the following needs to be created: ID NAME Object Type ============================================== 1 SWT-F1-S32-RTR-1 Network Switch 2 ...

What is the best way to dynamically convert a lodash object (specifically a filter object) into jQuery's listview component?

After referencing the answer to this topic as my first step, which can be found here, my next goal is to utilize a filter function to retrieve all matching entries from a JSON file based on a search term. The objective is to loop through each match and con ...

Can someone please share a straightforward method for dynamically rendering views in Vue.js?

Let's pause and consider the scenario: You're creating a modular interface, where any module that implements it must be able to 'render itself' into the application using a slot and state. How can you achieve this in Vue? An Example S ...

Next auth does not provide authentication functionality for Firebase

I've implemented next-auth with a firebase adapter, and while everything seems to be functioning properly in terms of saving users in the database, I'm encountering some issues with authentication. import NextAuth from "next-auth" impo ...

Guide to correctly selecting <i> tags within a <p> tag using jQuery

I'm attempting to retrieve the text from the i (italic) elements within a paragraph using this code: $('p').each(function(j, element){ if($(element).is("i")){ console.log("The value is: "+$(element).text()); } }); However, ...

Using JavaScript to set the value of an input field based on the selected option value

I'm attempting to display the value of the selected OPTION in a separate INPUT FIELD, but for some reason it's not working and I can't figure out what's causing the issue. Here’s the code snippet: <!doctype html> <html lan ...

Locate and extract the JSON object using the specified key-value pair

Trying to extract a specific object from a complex json file, noting the key, and then returning a new json poses a challenge. I am using expressjs as the backend for this task. Sample.json '[{"ServID":"Rai 1","Parametri" ...

Is it possible for browsers to handle PUT requests using multipart/form data?

Is it common for HTML forms to not support HTTP PUT requests when submitted from certain browsers like Google Chrome? <form id="#main-form" action="http://localhost:8080/resource/1" method="put" enctype=" ...

Do we need to have the 'input type file' field as

I am currently working on a PHP form that includes mandatory fields for saving the data. For example: <td><input class="text_area required" type="text" name="new field" This form also has Javascript code with file upload functionality, which I ...

Error: Unable to locate module: Unable to resolve './Page.module.css' in Next.js version 13

When I run npm run build on Vercel and Heroku, I encounter an error that does not occur on my local computer: The error message is Module not found: Can't resolve './Page.module.css' I am trying to import this file from app/page.tsx, and b ...

Executing callback functions after numerous asynchronous calls have been completed

Is there a method to delay the execution of a specific line of code until multiple API calls have all returned? Typically, I follow this pattern: APIService.call(parameter).then(function(response) { // Do something callBack(); }); This approach wo ...

Tips for toggling the visibility of a revolution slider based on current time using JavaScript

Currently, I am integrating the revolution slider into my WordPress website. My goal is to have the slider change according to the standard time of day. For instance, I want slider1 to display in the morning, slider2 at noon, slider3 in the evening, and sl ...