Conditionally attach event listener and handler to Vue component

Viewed 1826

I have a Vue component that is used several places in my app. In some of these cases, I need to handle a click event with a specific function, like:

<div @click="checkNav" />

However, I would only like to attach this handler when needed, so it doesn't fire unnecessarily when it's not needed.

I've tried passing a prop to the component and attaching the handler conditionally, like so:

<div @click="isCheckNeeded ? checkNav : null" />

and then in props, I've specified:

isCheckNeeded {
  type: Boolean,
  required: false,
  default: false,
}

However, my checkNav function never fires, and I've double checked that isCheckNeeded is true in Vue devtools.

Is this kind of conditional check not possible, or not recommended? Is there a better way to conditionally attach event listeners/handlers?

3 Answers

It might help to see how your template is being compiled to understand the cause of the problem...

When v-on receives a method name, vue-template-compiler compiles it into a method lookup, where the resolved method becomes the event handler [1]. For instance, your template <div @click="checkNav" /> is compiled into this render function:

with (this) {
  return _c("div", { on: { click: checkNav } })
}

On the other hand with an inline event handler, <div @click="isCheckNeeded ? checkNav : null" /> is compiled into this:

with (this) {
  return _c("div", {
    on: {
      click: function ($event) {
        isCheckNeeded ? checkNav : null
      },
    },
  })
}

Notice a couple things here:

  1. The expression is wrapped in an anonymous function, which becomes the event handler.
  2. The result of the expression is either a method name (as opposed to a method call) or null. Evaluating a method name is effectively a no-op.

Solution 1: Change method name into method call

This is probably the simplest solution, but it has the disadvantage of the handler always being invoked upon click (although a falsy isCheckNeeded would cause an early return).

<!-- BEFORE: -->
<!--
<div @click="isCheckNeeded ? checkNav : null" />
-->

<!-- AFTER: -->
<div @click="isCheckNeeded ? checkNav() : null" />

<!-- OR: -->
<div @click="isCheckNeeded && checkNav()" />

Solution 2: Use dynamic event

This is slightly more complex, but it has the advantage of registering the event handler only when necessary. The event handler is automatically unregistered when isCheckNeeded is falsy.

<div @[clickEvent]="checkNav" />
...
<script>
export default {
  computed: {
    clickEvent() {
      return this.isCheckNeeded ? 'click' : null
    }
  },
}
</script>

Vue.component('my-component', {
  template: `<div @[clickEvent]="checkNav"><slot/></div>`,
  props: {
    isCheckNeeded: Boolean
  },
  computed: {
    clickEvent() {
      return this.isCheckNeeded ? 'click' : null
    }
  },
  methods: {
    checkNav() {
      console.log('checkNav')
    }
  }
})

new Vue({
  el: '#app',
  data() {
    return {
      isCheckNeeded: false
    }
  }
})
.click-area {
  border: solid 1px;
  padding: 2rem;
  margin: 1rem;
}
<script src="https://unpkg.com/vue@2.6.12"></script>

<div id="app">
  <button @click="isCheckNeeded = !isCheckNeeded">Toggle click handler</button>
  <pre>isCheckNeeded={{isCheckNeeded}}</pre>

  <my-component :is-check-needed="isCheckNeeded">
    <div class="click-area">
      <span v-if="isCheckNeeded">Click me!</span>
      <span v-else>Clicking ignored</span>
    </div>
  </my-component>
</div>

You could use a v-if statement to render the div with or without the click handler.

<div v-if="isCheckedNeeded" @click="checkNav"></div>
<div v-else></div>

Uses @click="enabled && clickHandler($event)".

new Vue ({
  el:'#app',
  data () {
    return {
      enabled: true
    }
  },
  methods: {
    clickHandler: function () {
      console.info('clicked')
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
    <div class="container">
        <a @click="enabled && clickHandler($event)">
        Test: {{enabled}}
        </a>
        <input type="checkbox" v-model="enabled">
    </div>
</div>

Another option is use render function like below:

new Vue ({
  el:'#app',
  render (h) {
    let props = {}
    if (this.enabled) {
      this.$set(props, 'on', {click: this.clickHandler})
    }
    let self = this
    return h('div', [
      h('input', {
        attrs: {type: 'checkbox'}, 
        domProps: {checked: this.enabled}, 
        on: {
          input: function (event) {
            self.enabled = event.target.checked
          }
        }
      }),
      h('a', props, `Test: ${this.enabled}`)
    ])
  },
  data () {
    return {
      enabled: true
    }
  },
  methods: {
    clickHandler: function () {
      console.info('clicked')
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app"></div>

Related