2

References

Add vue directive on condition

Detect click outside element

I am writing a custom directive for 'click-outside senario' for a list of elements.

Basically when a button is clicked on a item in the list it goes into selected mode . Now if a click occurs anywhere else I need to cancel selection mode . For that I need to detect click outside . I figured out the directive for it from For that I have come up with

  const clickOutside = {
  bind: function (el, binding, vnode) {
    console.log('bind called')

    document.body.addEventListener('click', (event) => {
      // check that click was outside the el and his childrens
      if (!(el == event.target || el.contains(event.target))) {
        // and if it did, call handle method provided in attribute value
        console.log('directive working')
        vnode.context[binding.expression](event);
      }
    })
  },
  unbind: function (el) {
    document.body.removeEventListener('click', el.event)
    console.log('unbind called')
  }
}
export {
  clickOutside
}

from the reference sited above

Now I only want each list item to listen for outside clicks when that item is in selected mode .

So I need to accomplish something like

<div id="list-item"  v-on-click-outside="outSideClickHandler" //trigger only when selected>
</div>

<script>
export default{
data:{
selectedState:false;
},
methods:{
outSideClickHandler:{//......}
}
</script>
Sainath S.R
  • 3,074
  • 9
  • 41
  • 72

2 Answers2

4

Why don't you just do the selected check inside the click outside handler? You'll also need a way of passing the clicked item to the handler.

<div id="list-item" v-on-click-outside="outSideClickHandler(item)"></div>
outSideClickHandler(item) {
  return event => {
    if (item.selected) {
      // Handle the click outside
    }
  };
}

Call the handler in the directive like this:

binding.value(event);

You do not get the automatic "expression/statement" binding for custom directives like do you with v-on which is why you need to curry the handler function when you want to pass extra arguments to it.

By this I mean the argument passed to v-on can be an expression or a statement, such as:

@click="handler"        - handler is an expression (the function itself)
@click="handler(item)"  - handler(item) is a statement

But with custom directives you can only pass expressions; the second line above is not possible unless handler() returns another function.


I think there is some confusion because it seems what you want is to have a custom directive which is used only in this specific situation with your list items, but my solution above is more about writing a general "click outside" directive which you can use in any situation.

Also I think you do not want the directive to register any event listeners if the list item is not selected (for performance reasons?). If that's the case, then you can use event delegation instead.

There is no way to conditionally enable/disable a directive, you would have to do something like Morty's answer, both of which is kind of messy.

This seems workable but the whole point of using custom directives is to write reusable dom manipulation code

Are you against writing DOM manipulation code outside of directives? Angular 1 had this philosophy. Unless you want to reuse the directive in different situations then it may be overkill to write a directive for this situation just so that "DOM manipulation code does not pollute my component". If I'm going to write a directive, then I would want it to be as general as possible so that I can use it in many different situations.

I don't even need to pass the item in that case. Cause I have a component inside a v-for and not a div and I bind the custom directive on that component of which the handler is a method

Then I'm not sure why you'd want to implement this as a directive, which is rarely needed anyway. You can just register the body click event in the mounted hook and remove it in the destroyed hook. All of the click-outside logic can be contained within the component itself.


If your main concern is not having to register a body click event listener for each list item, you can do something like this:

const handlers = new Map();

document.addEventListener('click', e => {
    for (const handler of handlers.values()) {
        handler(e);
    }
});

Vue.directive('click-outside', {
    bind(el, binding) {
        const handler = e => {
            if (el !== e.target && !el.contains(e.target)) {
                binding.value(e);
            }
        };

        handlers.set(el, handler);
    },

    unbind(el) {
        handlers.delete(el);
    },
});

You can go one step further and automatically add the body click event listener whenever handlers is nonempty and remove the listener when handlers is empty. It's up to you.

Decade Moon
  • 32,968
  • 8
  • 81
  • 101
  • It seems that `outSideClickHandler(item)` will evaluate right at directive bind time. I think it should be `v-on-click-outside="() => outSideClickHandler(item)"` – Morty Choi May 11 '18 at 09:15
  • @MortyChoi Yes it will, but it [returns another function](https://en.wikipedia.org/wiki/Partial_application). – Decade Moon May 11 '18 at 09:22
  • OK. I see that now. Thanks. – Morty Choi May 11 '18 at 09:23
  • This seems workable but the whole point of using custom directives is to write reusable dom manipulation code , I though of keeping it more generic by passing something like a flag on when to activate the directive but in that case I need to listen when the value of that flag changes – Sainath S.R May 11 '18 at 09:31
  • @SainathS.R My answer is not much different from what you would do if you wanted to use `v-on:click` instead of a custom directive, so I'm not sure how you think this is not reusable DOM manipulation code – it doesn't make any assumptions about the situations in which it should be enabled. Are you saying you want the enable-if-selected behavior a part of the directive? That's even less generic. – Decade Moon May 11 '18 at 09:37
  • Oh okay ,I thought of something like what the below answer suggests, additional directive param . But what you suggest seems neater,but then again I don't even need to pass the item in that case. Cause I have a component inside a v-for and not a div and I bind the custom directive on that component of which the handler is a method . Still all the list items will be listening to the outside click event , I wanted a solution where only the selected list items listen to the event in the first place – Sainath S.R May 11 '18 at 09:56
  • Also please elaborate what you mean by _You do not get the automatic "expression/statement" binding for custom directives like do you with v-on which is why you need to curry the handler function when you want to pass extra arguments to it._ – Sainath S.R May 11 '18 at 09:57
  • Thanks I'll think about the solution using delegation techniques too . Also the reason I still wanna write a directive here is ,when I use the same component for different routes with just the id parameters changing vue doesn't not even fire the mount and unmounted, destroyed hooks etc whereas the bind/unbind hooks do fire , if I had to proceed without a directive I would have to watch different hooks like update in the component lifecycle instead and implement a lot of custom logic, vue recommends using custom directives for these cases cause they neatly encapsulate dom manipulation code . – Sainath S.R May 11 '18 at 11:01
1

Currently there is no easy way to do conditional directive binding. You might considered using v-if.

   <div v-for='item of list'>
        <div
          v-if='item.selected'
          id="list-item" 
          v-on-click-outside="outSideClickHandler"
        />
        <div v-else
          id="list-item" 
          v-on-click-outside="outSideClickHandler"
        />
    </div>

Another approach would be modifying you directive implementation, so that it accepts another active boolean flag to opt out inside the eventListener.

<div id="list-item"  v-on-click-outside="{handler: outSideClickHandler, active: item.selected}"  />
Morty Choi
  • 2,466
  • 1
  • 18
  • 26