[javascript] Vue v-on:click does not work on component

I'm trying to use the on click directive inside a component but it does not seem to work. When I click the component nothings happens when I should get a 'test clicked' in the console. I don't see any errors in the console, so I don't know what am I doing wrong.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>vuetest</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

App.vue

<template>
  <div id="app">
    <test v-on:click="testFunction"></test>
  </div>
</template>

<script>
import Test from './components/Test'

export default {
  name: 'app',
  methods: {
    testFunction: function (event) {
      console.log('test clicked')
    }
  },
  components: {
    Test
  }
}
</script>

Test.vue (the component)

<template>
  <div>
    click here
  </div>
</template>

<script>
export default {
  name: 'test',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  }
}
</script>

This question is related to javascript vue-component vuejs2 vue.js

The answer is


It's the @Neps' answer but with details.


Note: @Saurabh's answer is more suitable if you don't want to modify your component or don't have access to it.


Why can't @click just work?

Components are complicated. One component can be a small fancy button wrapper, and another one can be an entire table with bunch of logic inside. Vue doesn't know what exactly you expect when bind v-model or use v-on so all of that should be processed by component's creator.

How to handle click event

According to Vue docs, $emit passes events to parent. Example from docs:

Main file

<blog-post
  @enlarge-text="onEnlargeText"
/>

Component

<button @click="$emit('enlarge-text')">
  Enlarge text
</button>

(@ is the v-on shorthand)

Component handles native click event and emits parent's @enlarge-text="..."

enlarge-text can be replaced with click to make it look like we're handling a native click event:

<blog-post
  @click="onEnlargeText"
></blog-post>
<button @click="$emit('click')">
  Enlarge text
</button>

But that's not all. $emit allows to pass a specific value with an event. In the case of native click, the value is MouseEvent (JS event that has nothing to do with Vue).

Vue stores that event in a $event variable. So, it'd the best to emit $event with an event to create the impression of native event usage:

<button v-on:click="$emit('click', $event)">
  Enlarge text
</button>

A bit verbose but this is how I do it:

@click="$emit('click', $event)"

UPDATE: Example added by @sparkyspider

<div-container @click="doSomething"></div-container>

In div-container component...

<template>
  <div @click="$emit('click', $event);">The inner div</div>
</template>

From the documentation:

Due to limitations in JavaScript, Vue cannot detect the following changes to an array:

  1. When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
  2. When you modify the length of the array, e.g. vm.items.length = newLength

In my case i stumbled on this problem when migrating from Angular to VUE. Fix was quite easy, but really difficult to find:

setValue(index) {
    Vue.set(this.arr, index, !this.arr[index]);
    this.$forceUpdate(); // Needed to force view rerendering
}

I think the $emit function works better for what I think you're asking for. It keeps your component separated from the Vue instance so that it is reusable in many contexts.

// Child component
<template>
  <div id="app">
    <test @click="$emit('test-click')"></test>
  </div>
</template>

Use it in HTML

// Parent component
<test @test-click="testFunction">

Native events of components aren't directly accessible from parent elements. Instead you should try v-on:click.native="testFunction", or you can emit an event from Test component as well. Like v-on:click="$emit('click')".


As mentioned by Chris Fritz (Vue.js Core Team Emeriti) in VueCONF US 2019

if we had Kia enter .native and then the root element of the base input changed from an input to a label suddenly this component is broken and it's not obvious and in fact, you might not even catch it right away unless you have a really good test. Instead by avoiding the use of the .native modifier which I currently consider an anti-pattern will be removed in Vue 3 you'll be able to explicitly define that the parent might care about which element listeners are added to...

With Vue 2

Using $listeners:

So, if you are using Vue 2 a better option to resolve this issue would be to use a fully transparent wrapper logic. For this Vue provides a $listeners property containing an object of listeners being used on the component. For example:

{
  focus: function (event) { /* ... */ }
  input: function (value) { /* ... */ },
}

and then we just need to add v-on="$listeners" to the test component like:

Test.vue (child component)

<template>
  <div v-on="$listeners">
    click here
  </div>
</template>

Now the <test> component is a fully transparent wrapper, meaning it can be used exactly like a normal <div> element: all the listeners will work, without the .native modifier.

Demo:

_x000D_
_x000D_
Vue.component('test', {_x000D_
  template: `_x000D_
    <div class="child" v-on="$listeners">_x000D_
      Click here_x000D_
    </div>`_x000D_
})_x000D_
_x000D_
new Vue({_x000D_
  el: "#myApp",_x000D_
  data: {},_x000D_
  methods: {_x000D_
    testFunction: function(event) {_x000D_
      console.log('test clicked')_x000D_
    }_x000D_
  }_x000D_
})
_x000D_
div.child{border:5px dotted orange; padding:20px;}
_x000D_
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>_x000D_
<div id="myApp">_x000D_
  <test @click="testFunction"></test>_x000D_
</div>
_x000D_
_x000D_
_x000D_

Using $emit method:

We can also use $emit method for this purpose, which helps us to listen to child components events in parent component. For this, we first need to emit a custom event from child component like:

Test.vue (child component)

<test @click="$emit('my-event')"></test>

Important: Always use kebab-case for event names. For more information and demo regading this point please check out this answer: VueJS passing computed value from component to parent.

Now, we just need to listen to this emitted custom event in parent component like:

App.vue

<test @my-event="testFunction"></test>

So, basically instead of v-on:click or the shorthand @click we will simply use v-on:my-event or just @my-event.

Demo:

_x000D_
_x000D_
Vue.component('test', {_x000D_
  template: `_x000D_
    <div class="child" @click="$emit('my-event')">_x000D_
      Click here_x000D_
    </div>`_x000D_
})_x000D_
_x000D_
new Vue({_x000D_
  el: "#myApp",_x000D_
  data: {},_x000D_
  methods: {_x000D_
    testFunction: function(event) {_x000D_
      console.log('test clicked')_x000D_
    }_x000D_
  }_x000D_
})
_x000D_
div.child{border:5px dotted orange; padding:20px;}
_x000D_
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>_x000D_
<div id="myApp">_x000D_
  <test @my-event="testFunction"></test>_x000D_
</div>
_x000D_
_x000D_
_x000D_


With Vue 3

Using v-bind="$attrs":

Vue 3 is going to make our life much easier in many ways. One of the examples for it is that it will help us to create a simpler transparent wrapper with very less config by just using v-bind="$attrs". By using this on child components not only our listener will work directly from the parent but also any other attribute will also work just like it a normal <div> only.

So, with respect to this question, we will not need to update anything in Vue 3 and your code will still work fine as <div> is the root element here and it will automatically listen to all child events.

Demo #1:

_x000D_
_x000D_
const { createApp } = Vue;_x000D_
_x000D_
const Test = {_x000D_
  template: `_x000D_
    <div class="child">_x000D_
      Click here_x000D_
    </div>`_x000D_
};_x000D_
_x000D_
const App = {_x000D_
  components: { Test },_x000D_
  setup() {_x000D_
    const testFunction = event => {_x000D_
      console.log("test clicked");_x000D_
    };_x000D_
    return { testFunction };_x000D_
  }_x000D_
};_x000D_
_x000D_
createApp(App).mount("#myApp");
_x000D_
div.child{border:5px dotted orange; padding:20px;}
_x000D_
<script src="//unpkg.com/vue@next"></script>_x000D_
<div id="myApp">_x000D_
  <test v-on:click="testFunction"></test>_x000D_
</div>
_x000D_
_x000D_
_x000D_

But for complex components with nested elements where we need to apply attributes and events to main <input /> instead of the parent label we can simply use v-bind="$attrs"

Demo #2:

_x000D_
_x000D_
const { createApp } = Vue;_x000D_
_x000D_
const BaseInput = {_x000D_
  props: ['label', 'value'],_x000D_
  template: `_x000D_
    <label>_x000D_
      {{ label }}_x000D_
      <input v-bind="$attrs">_x000D_
    </label>`_x000D_
};_x000D_
_x000D_
const App = {_x000D_
  components: { BaseInput },_x000D_
  setup() {_x000D_
    const search = event => {_x000D_
      console.clear();_x000D_
      console.log("Searching...", event.target.value);_x000D_
    };_x000D_
    return { search };_x000D_
  }_x000D_
};_x000D_
_x000D_
createApp(App).mount("#myApp");
_x000D_
input{padding:8px;}
_x000D_
<script src="//unpkg.com/vue@next"></script>_x000D_
<div id="myApp">_x000D_
  <base-input _x000D_
    label="Search: "_x000D_
    placeholder="Search"_x000D_
    @keyup="search">_x000D_
  </base-input><br/>_x000D_
</div>
_x000D_
_x000D_
_x000D_


Examples related to javascript

need to add a class to an element How to make a variable accessible outside a function? Hide Signs that Meteor.js was Used How to create a showdown.js markdown extension Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Summing radio input values How to execute an action before close metro app WinJS javascript, for loop defines a dynamic variable name Getting all files in directory with ajax

Examples related to vue-component

Vue 'export default' vs 'new Vue' Vuex - Computed property "name" was assigned to but it has no setter How to add external JS scripts to VueJS Components How to listen for 'props' changes How can I set selected option selected in vue.js 2? How do I format currencies in a Vue component? [Vue warn]: Property or method is not defined on the instance but referenced during render VueJs get url query Vue.js - How to properly watch for nested data Vue template or render function not defined yet I am using neither?

Examples related to vuejs2

How can I go back/route-back on vue-router? Change the default base url for axios How to change port number in vue-cli project How to solve 'Redirect has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header'? vuetify center items into v-flex Vuejs: Event on route change Vuex - Computed property "name" was assigned to but it has no setter Vuex - passing multiple parameters to mutation How to listen to the window scroll event in a VueJS component? How to acces external json file objects in vue.js app

Examples related to vue.js

How to fix 'Unchecked runtime.lastError: The message port closed before a response was received' chrome issue? Center content vertically on Vuetify Vue.js get selected option on @change Using Environment Variables with Vue.js did you register the component correctly? For recursive components, make sure to provide the "name" option Vue 'export default' vs 'new Vue' How can I go back/route-back on vue-router? Change the default base url for axios How to reference static assets within vue javascript How to change port number in vue-cli project