89

In my scenario I have mouseover and mouseout events that I want to bind to conditionally (e.g only if user is on a device that has a mouse).

I realize I can have the condition in the event handler itself but that would still be allocating the memory for the event handlers which is unnecessary.

Is there a way to make the event binding itself conditional?

(to be clear, what I'd like is to be able to short-circuit the event subscription so the underlying addEventListener operation never happens if the condition is false)

5
  • 2
    You could use v-if hasMouse and have one branch with the events and the other without. Probably use scoped slots to insert the common code. Commented Dec 31, 2017 at 13:17
  • That's OK for very simple components but if your template is substantial (I'm working with a complex SVG in this case) it becomes a pain Commented Jan 2, 2018 at 10:10
  • The computed property looks to be a great answer. Is there a reason you don't want to type it up and mark it as the accepted answer? Commented Jan 2, 2018 at 13:05
  • 3
    Could not get any of these solutions working reliably when using event modifiers (e.g. .stop) Commented Jul 14, 2021 at 15:07
  • @user9645 you are correct! Had the same issue. See my answer for implementing .stop. Commented Jan 22, 2022 at 16:19

13 Answers 13

142

Update (February 2021)

Several solutions seem to be accepted on Github, whereas my original answer is not. I'm rounding them up here for convenience:

Solution 1a (see Engin Yapici's answer below):

v-on="enableClick ? { click: clickHandler } : {}"

Solution 1b (see Heals Legodi's answer below):

v-on="enableClick ? { click: () => clickHandler(params) } : {}"

Solution 2a (see rvy's answer below and this working demo)

@[eventName]="clickHandler"

Solution 2b (from coyotte508's comment; 2a without the computed property):

@[isClickable&&`click`]="clickHandler"

Solution 3 (mentioned here; seems to be compatible with event modifiers):

@click="enableClick && clickHandler"

Original answer

This works as of Vue 2.6:

<div
  @mouseover="enableMouseover ? mouseoverHandler : null"
  @click="enableClick ? clickHandler : null"
  ...
>

While an event resolves to null, the binding will be removed.

https://github.com/vuejs/vue/issues/7349#issuecomment-458405684

Comments on original answer

It seems to be something I came up with accidentally by misunderstanding that Github thread. But I know from my own testing that it definitely worked back when I posted it. And people continued to upvote it right up to when I made this edit, implying the solution still had some merit. So give it a try if you like.

But after a downvote and a couple of negative comments, I felt the need to improve my answer to save future readers potential headaches. Note that pretty much all the answers to this question refer to the same Github thread, so if you've got time, you might want to go right to the source to learn more. There are many options discussed there, and I suspect more will continue to be posted over time.

Sign up to request clarification or add additional context in comments.

6 Comments

Binding @click to null, now returns a warning from Vue: [Vue warn]: Invalid handler for event "click": got null.
The syntax in this example doesn't seem to work. Not sure why it's so upvoted. Other answers are good. You need to use v-on="condition ? { click: handler } : {}" or actual dynamic name of event that resolves to null (but it probably need to be a computed field)
This is wrong. The linked issue on github shows the proper solution.
I added a comment on @rve's answer - maybe you can add it as well? It's a version without needing to create a computed for the event name
“compatible with event modifiers” is not really true. Maybe it is for some, but not for .stop. For modifiers that don't work, just implement them yourself like this.
|
31

Following this discussion it appears the best way to achieve this is to bind v-on to a specification object containing the events you are interested in subscribing to and place your conditionals there like so:

<div v-on="{ mouseover: condition ? handler : null, click: ... }">

Some notes:

  • Passing null for a handler means the underlying addEventLisetener will not happen - which is what we want
  • This means grouping all the event subscriptions into one v-on attribute rather then splitting it into separate and explicit bindings (<div @mouseover='...' @click='...'/>)

  • If this is a long living component and the underlying data changes frequently (leading to rebinding) you should be paying attention to the disposal of the subscriptions (i.e the corresponding removeEventListener) as subscriptions made in one bind pass will not be disposed of on subsequent ones. Evaluate as per your use case...

1 Comment

The new way to do conditional event binding: v-on="condition ? { mouseover: handler } : {}" from the convesation linked in the answer
26

This is what I'm using:

v-on="condition ? { click: handler } : {}" (Reference)

I get Invalid handler for event "click": got null with v-on="{ click: condition ? handler : null }"

1 Comment

I am trying this on a text-field but it isn't working for me yet it has worked for me before on a card. This time the event isn't added at all
9

If you get the 'Invalid handler for event "click": got null' error and your handler function expects some parameters then you should wrap your handler in a function.

So this >

v-on="condition ? { blur: () => handler(params) } : {}"

Instead of

v-on="condition ? { click: handler(params) } : {}"

2 Comments

This worked for me in Vue 2.
That's the best solution :)
6

I am unable to comment on answers so I am adding my own.

Solution 3 from the approved answer does not work - parentheses are missing. The correct syntax in this case would be:

@click="enableClick && clickHandler()"

Here is working example

1 Comment

Beware : nothing will happen if enableClick is false but a listener will be registered anyway. If the attribute is used in a loop, all elements added to the DOM will get independent click event registered (not sure Vue is smart enough to use the same handler internally). So a handler will always be created and invoked but once again call to clickHandler will only happen if enableClick is truthy.
5

Even more simpler would be to use render functions for that. You won't need to be manually removing the listeners and taking care of them. Also uses simple JS syntax with no mixins.

new Vue({
  el: "#app",
  data: () => ({
    counter: 0
  }),
  methods: {
    handleClick() {
      this.counter++;
    }
  },
  render(h) {
    return h(
      "div",
      IS_MOBILE_DEVICE
        ? {}
        : {
            on: { click: this.handleClick }
          },
      this.counter
    );
  }
});

Full example: https://codesandbox.io/s/nw6vyo6knj

1 Comment

Render functions would work well for a very simple component but in some cases (I'm working with a complex SVG) are unmanageable
4

If you want to do something like that you could just apply the event listener manually by adding a ref on the element you want to apply the event to, then using that to bind the event listener in the mounted hook if the condition is met:

Markup

<button ref="button">
  Mouse Over Me
</button>

Vue Instance

new Vue({
  el: '#app',
  mounted() {
    let hasMouse = true;

    // If the user has a mouse, add the event listeners
    if (hasMouse) {
      let button = this.$refs.button

      button.addEventListener('mouseover', e => {
        this.mouseover = true
      })

      button.addEventListener('mouseout', e => {
        this.mouseover = false
      })
    }

  },
  data: {
    mouseover: false
  }
})

Here's a JSFiddle for that: https://jsfiddle.net/0fderek6/

If you don't like that approach, you could also use a directive and place the conditional in there, you could then place that in a mixin to make it reusable:

Mixin

const mouseEvents = {
  directives: {
    mouseEvents: {
      bind(el, binding, vnode) {
        let hasMouse = true;

        if (hasMouse) {
          el.addEventListener('mouseover', e => {
            vnode.context.mouseover = true
          })

          el.addEventListener('mouseout', e => {
            vnode.context.mouseover = false
          })
        }
      }
    }
  },
  data: {
    mouseover: false
  }
}

Vue Instance

new Vue({
  el: '#app',
  mixins: [mouseEvents]
})

Markup

<button v-mouse-events>
  Mouse Over Me
</button>

Here's the JSFiddle for that: https://jsfiddle.net/nq6x5qeq/

EDIT

If you like the directive approach, all you need to do is add an unbind hook to remove the listener, you can then have the binding arg be the event type and the binding value be the handler:

Vue.directive('mouse', {
  bind(el, binding) {
      if (hasMouse) {
        console.log(binding.arg + ' added')
          // bind the event listener to the element
        el.addEventListener(binding.arg, binding.value)
      }
    },
    unbind(el, binding) {
      if (hasMouse) {
        console.log(binding.arg + ' removed')
        el.removeEventListener(binding.arg, binding.value)
      }
    }
});

Now all you need to do is add each listener exactly like you would with v-bind:

<div v-mouse:mouseover="mouseOverFunction"></div>

Here's the JSFiddle to show you how that works: https://jsfiddle.net/59ym6hdb/

7 Comments

This has one downside: it doesn't do a full removeEventListener in the beforeDestroy lifecycle event. On a lot of un-mounts -> mounts may perform even poorer than moving the condition into the event itself, it will just keep adding event listeners without ever removing them!
@AndreiGlingeanu I think we've both overcomplicated this, you would have to unbind the listeners in the directive (which would mean sharing state across directive hooks) and render functions aren't really maintainable. I would actually go for the solution provided by @RoyJ which is much simpler.
True. But, sometimes you really have to _really_skip an event conditionally on the same node. That may happen when you have a lot of logic attached to a single DOM node (like a lot of mouse* listeners that implement a simple drag-n-drop and you just need to skip the click event, for example). Of course we can debate on that forever, I just gave a simpler and a more low-level alternative solution.
@AndreiGlingeanu Yes, I thought your answer was a good answer. I think between us we've managed to provide pretty much all the options available. Happy new year!
Happy new year!
|
4

If you need to use a .stop modifier, then none of the listed solutions will work. But it is actually easy, just implement whatever the modifier does in the event handler itself.

In the template: @click="clickHandler" (yes, always attached!)

Then for your handler:

function clickHandler(event) {
  if (whateverCondition) {
    event.stopPropagation()
    …
  }
}

Comments

3

Refer to this by Evan You

<div v-on="{ mouseover: condition ? handler : null }">

should work and be clean.

Comments

3

None of the above solutions work, when you want to make the handler dependent on a condition, that uses props passed to the slot by the child.

In that case the below syntax can be used:

:[enableClick?`onClick`:``]="clickHandler"

where enableClick is a prop received from the children as described here: https://vuejs.org/guide/components/slots.html#scoped-slots and onClick is the event name (click) prefixed with on in camelCase e.g.:

<MyComponent v-slot="{ enableClick }">
  <OtherComponent :[enableClick?`onClick`:``]="clickHandler" />
</MyComponent>

and inside MyComponent template:

<div>
  <slot :enableClick="isClickEnabled"></slot>
</div>

Comments

2

Conditional event binding works as follows:

<div @[event]="handler" />

While event resolves to null, the binding will be removed.

(directly from https://github.com/vuejs/vue/issues/7349#issuecomment-458405684 )

Example:

<template>
  ...
  <span @[mayclick]="onClick">...</span>
  ...
</template>

<script>
export default {
  ...
  computed: {
    mayclick: function() {
      return this.isClickable ? "click" : null;
    }
  },
  methods: {
    onClick: function (message) {
      ...
    }
  }
}

1 Comment

This also works: <span @[isClickable&&`click`]="onClick">...</span>
2

I needed to conditionally bind multiple events so I managed by doing this: v-on="isEnabled ? {mouseenter:open, mouseleave:close, touchend:toggle} : null"

Comments

0

For me this solution is the best to understand and 100% compatible with any VUE JS version

<div v-on:click="click()">...</div>

methods : {
    click() {
        if (yourCondition)
            yourhandler();
    },
},

If yourCondition evaluates to false, then click() method behaves such as no operation (a.k.a. noop)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.