DEV Community

disgusting-dev
disgusting-dev

Posted on • Updated on

Vue Directive - Click Outside

The problem I wanna solve is:

I need to catch moments when I click outside of some element

Why?

It might be useful for UI components such as dropdowns, datepickers, modal windows - to assign some logic for this certain behaviour

As a starter, I will say that the accepted value for directive will be just a function and in the code it will look like:

<app-datepicker v-click-outside="someFunc" />
Enter fullscreen mode Exit fullscreen mode

At the end of the text there will be 'Refactoring' section with extension of the logic for more usage ways

References used

  1. Buefy
  2. Vuetify

The text and code in the article is a result of open source analysis and going through existing solutions written above

Solution

I am gonna use Vue as a UI framework here. Why? I just use Vue as my current business framework, so for me this would be a nice chance to dive in it deeper.

First of all, let's just define a function that catches outside clicks, without any wrappers, almost pseudo-code

Define, when we can tell that we clicked outside of an element

For that, we need to know, where we clicked, and what's our element with assigned listener, so the function will start like that:

function onClickOutside(event, el) {
  const isClickOutside =
    event.target !== el
    && !el.contains(event.target);
}
Enter fullscreen mode Exit fullscreen mode

Now, if the flag is true, we need to call some handler:

function onClickOutside(event, el, handler) {
  const isClickOutside =
    event.target !== el
    && !el.contains(event.target);

  return isClickOutside ? handler(event, el) : null;
}
Enter fullscreen mode Exit fullscreen mode

For me it looks a bit hard only that I have to follow the arguments order, so i gonna use one param of object instead;
function onClickOutside({ event, el, handler })

Start listening the function

Logically, we need to find a place, where we can use this:

document.addEventListener(
  'click',
  (event) => onClickOutside({ event })
)
Enter fullscreen mode Exit fullscreen mode

Here - no invention, just going to Vue doc and seeing about Custom Directives

Basically, we need only three lifecycle stages there:

  1. bind - to assign directive logic to element and create listeners
  2. unbind - when element is not in DOM anymore and we need to remove our listeners

To be able to catch listeners binded to the element, I'm gonna create a Map of those - for storing and fast achieving them:

const instances = new Map();

Before writing the hooks themself, I'm gonna write a function for reusing the code - there I will manipulate my eventListeners:

function toggleEventListeners(action, eventHandler) {
  document[`${action}EventListener`]('click', eventHandler, true);
}
Enter fullscreen mode Exit fullscreen mode

(The 'true' third param I used for calling the handler on capturing phase, a bit earlier than in bubbling)

bind function will look like:

function bind(el, { value: handler }) {
  const eventHandler = event => onClickOutside({ el, event, handler});

  toggleEventListeners('add', eventHandler);

  instances.set(
    el,
    eventHandler
  );
}
Enter fullscreen mode Exit fullscreen mode

Unbind function will do simple logic for remove our listeners from the system:

function unbind(el) {
  const eventHandler = instances.get(el);

  toggleEventListeners('remove', eventHandler);

  instances.delete(el);
}
Enter fullscreen mode Exit fullscreen mode

At the end, we just need to export this directive properly and connect with our Vue instance in 'main.js' file:

const directive = {
  bind,
  unbind,
};

export default directive;
Enter fullscreen mode Exit fullscreen mode

'main.js':

import Vue from 'vue'
import App from './App.vue'

import clickOutside from './directives/clickOutside';

Vue.config.productionTip = false

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

new Vue({
  render: h => h(App),
}).$mount('#app')
Enter fullscreen mode Exit fullscreen mode

That's it as a minimum, now goes next section

Refactoring

I'd like to handle not only function as value, but also an object

//Validator function
function processArgs(value) {
  const isFunction = typeof value === 'function';

  if (!isFunction && typeof value !== 'object') {
      throw new Error(`v-click-outside: Binding value should be a function or an object, ${typeof bindingValue} given`)
  }

  return {
    handler: isFunction ? value : value.handler,
  }
}

//Handler goes from the processing function
function bind(el, { value }) {
  const { handler } = processArgs(value);

  //...function body
}
Enter fullscreen mode Exit fullscreen mode

I wanna add a middleware function to define conditions when I want or don't want to invoke my handler

Extend the processing result with middleware method

return {
  handler: isFunction ? value : value.handler,
  middleware: value.middleware || (() => true),
};
Enter fullscreen mode Exit fullscreen mode

Extend logic of clickOutside function

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

  if (!isClickOutside || !middleware(event, el)) {
    return null;
  }

  return handler(event, el);
}
Enter fullscreen mode Exit fullscreen mode

Then just everywhere you were using handler, don't forget also to destructure middleware and add as parameters to bind and adapter functions

Well, that's it, the full example might be found here in my gists - https://gist.github.com/disgusting-dev/8d45aebff8a536af1cba39b0fcd203e3
Thank you for reading, further there will be more and more, so feel free to subscribe to not miss any analysis!

Top comments (3)

Collapse
 
leandrodelimac profile image
Leandro de Lima

Well, that was so impressive that made me login to comment! Keep up the good work :)
Cheers!!

Collapse
 
tgilany profile image
Tamir Gilany

Works like magic, thanks!

Collapse
 
rahulonsong profile image
rahulonsong • Edited

Thanks for this :) It worked like a charm!