Vue Directive - Click Outside
Tags: vue, javascript
Published: 2021-03-14 13:18:38

##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:

```html
<app-datepicker v-click-outside="someFunc" />
```

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

##References used
1. [Buefy](https://github.com/buefy/buefy/blob/dev/src/directives/clickOutside.js)
2. [Vuetify](https://github.com/vuetifyjs/vuetify/tree/master/packages/vuetify/src/directives/click-outside)

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:

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

Now, if the flag is true, we need to call some handler:
```javascript
function onClickOutside(event, el, handler) {
  const isClickOutside =
    event.target !== el
    && !el.contains(event.target);

  return isClickOutside ? handler(event, el) : null;
}
```

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:
```javascript
document.addEventListener(
  'click',
  (event) => onClickOutside({ event })
)
```

Here - no invention, just going to Vue doc and seeing about [Custom Directives](https://vuejs.org/v2/guide/custom-directive.html)

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:

```javascript
function toggleEventListeners(action, eventHandler) {
  document[`${action}EventListener`]('click', eventHandler, true);
}
```
(The 'true' third param I used for calling the handler on capturing phase, a bit earlier than in bubbling)

bind function will look like:
```javascript
function bind(el, { value: handler }) {
  const eventHandler = event => onClickOutside({ el, event, handler});

  toggleEventListeners('add', eventHandler);

  instances.set(
    el,
    eventHandler
  );
}
```

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

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

  toggleEventListeners('remove', eventHandler);

  instances.delete(el);
}
```

At the end, we just need to export this directive properly and connect with our Vue instance in 'main.js' file:
```javascript
const directive = {
  bind,
  unbind,
};

export default directive;
```
'main.js':
```javascript
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')
```
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
```javascript
//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
}
```
###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
```javascript
return {
  handler: isFunction ? value : value.handler,
  middleware: value.middleware || (() => true),
};
```
Extend logic of clickOutside function
```javascript
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);
}
```
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!