1

I am using a custom click-outside directive from this post:

Detect click outside element

This is my element:

<div class="datepicker panel panel-default" v-click-outside="close">

The custom directive:

module.exports = {
    bind(el, binding, vnode) {
        el.event = (event) => {
            // Check that click was outside the el and his children.
            if (!(el === event.target || el.contains(event.target))) {
                console.log('Clicked outside');

                // Call the method provided as the attribute value.
                vnode.context[binding.expression](event);
            }
        };

        document.body.addEventListener('click', el.event);
    },

    unbind(el) {
        document.body.removeEventListener('click', el.event);
    }
};

It works and to my knowledge the bind takes place on render of the element. Because I only want the click event to be registered when my datepicker is in view I wrapped it with a v-if.

The problem is that when I use a button to toggle the display of the v-if on the datepicker, the close method from the directive immediately fires.

It seems that the event within bind takes places before the element is even shown, therefore it closes immediately and nothing is shown at all.

This looks like pretty strange behavior singe the button is responsible for showing the datepicker and I would expect the bind to take place when the datepicker has rendered. Not at the same time or before.

Now it seems to take place even before the element has fully rendered. This causes my display button to cause a v-click-outside event.

What is causing this?

Edit:

Made a Jsfiddle to demonstrate this problem(open console):

https://jsfiddle.net/stephanv/qqjnngdz/2/

Stephan-v
  • 19,255
  • 31
  • 115
  • 201

1 Answers1

1

Well it sounds absolutely logical. Here's what happens:

  1. You click on "show date picker", which is actually a button
  2. show method is called, because on the button you have @click="show"
  3. The the visible property is changed to true.

  4. The directive is evaluated, because of the click event

  5. The if-else gets in place, stating that el (div with a directive) is different than event.target (button)
  6. The binding expression (hide) is called because of the if-else just before

  7. hide function is called, visible property is set to false, therefore the component is hidden!

What you are missing here, compared to the samples, is that you open the datepicker from another target (button in your case). Which means by default that you ARE clicking outside of the element in order to display it.

You have two options - one is to check the current state of the datepicker - add another property (justOpened), set it to true inside show, and when you check if it's clicked outside or not, if the property is true, set it to false and return. This way you "skip" one event of that kind.

All of that is not needed if you can simply do this:

<button type="button" @click.stop="show">Show datepicker</button>

This .stop modifier will actually call stopImmediatePropagation() on the native click event, and therefore you directive won't be called at all when you click on this button. It works like a charm, but beware that if you open the datepicker and then click on the same button again, the datepicker will not be hidden (as it won't know that you've clicked outside)!

Hope that helps :)

Andrey Popov
  • 7,362
  • 4
  • 38
  • 58
  • How can number 4 happen when the element has not rendered yet though? At this point the bind should not yet have taken place because there is no rendered element. How can the event be called when the binding has not taken place yet? There should be no event unless the datepicker is open and the bind has registered because the element has been rendered. – Stephan-v Jul 19 '17 at 13:44
  • The answer lies in the lifecycle diagram: https://vuejs.org/v2/guide/instance.html#Lifecycle-Diagram – Andrey Popov Jul 19 '17 at 13:50
  • There's a lot of stuff happening before the actual `mount` (rendering). I cannot explicitly state why binding occurs before the native click bubbles up, but it seems it's synchronous! Or they bubble it up again, I don't really know. – Andrey Popov Jul 19 '17 at 13:51
  • I am pretty interested in why they chose to do it this way. This seems kind of strange. Still I understand if the components events are set up before the create, but it is strange to have a directive that is `part of the HTML of the element` fire before the HTML has even rendered. This seems like a big `what the fuck`. – Stephan-v Jul 19 '17 at 13:54
  • Actually it does not fire before that. I've added logs, and as it's described in those [Hook Functions](https://vuejs.org/v2/guide/custom-directive.html#Hook-Functions) actually it is first binded, then inserted, then the event fires. That's why I think it's not synchronous but they re-fire (bubble it up) again after something's changed. Why they do that is a mystery indeed! You can file an issue in their repo - I would love to follow it up! – Andrey Popov Jul 19 '17 at 13:59
  • I have posted on the Vue.js forums I will try and find it what is going on here, perhaps create an issue. – Stephan-v Jul 19 '17 at 14:10
  • Posted on the Vue.js forums here: https://forum.vuejs.org/t/unbind-event-fires-too-soon/14542 I think it boils down to the click event still bubbling up while the elements get rendered straight away with its `bind` event. – Stephan-v Jul 20 '17 at 09:12
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/149720/discussion-between-andrey-popov-and-stephan-v). – Andrey Popov Jul 20 '17 at 13:18