Vue Directive - Click Outside
disgusting-dev
Posted on March 14, 2021
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" />
At the end of the text there will be 'Refactoring' section with extension of the logic for more usage ways
References used
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);
}
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;
}
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 })
)
Here - no invention, just going to Vue doc and seeing about Custom Directives
Basically, we need only three lifecycle stages there:
- bind - to assign directive logic to element and create listeners
- 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);
}
(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
);
}
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);
}
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;
'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')
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
}
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),
};
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);
}
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!
Posted on March 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 11, 2024