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
Top comments (3)
Well, that was so impressive that made me login to comment! Keep up the good work :)
Cheers!!
Works like magic, thanks!
Thanks for this :) It worked like a charm!