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>
If you want to listen to a native event on the root element of a component, you have to use the .native modifier for v-on
, like following:
<template>
<div id="app">
<test v-on:click.native="testFunction"></test>
</div>
</template>
or in shorthand, as suggested in comment, you can as well do:
<template>
<div id="app">
<test @click.native="testFunction"></test>
</div>
</template>
Reference to read more about native event
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">
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>
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, and 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:
Vue.component('test', {
template: `
<div class="child" v-on="$listeners">
Click here
</div>`
})
new Vue({
el: "#myApp",
data: {},
methods: {
testFunction: function(event) {
console.log('test clicked')
}
}
})
div.child{border:5px dotted orange; padding:20px;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="myApp">
<test @click="testFunction"></test>
</div>
Using $emit
method:
We can also use the $emit
method for this purpose, which helps us to listen to a child component's events in the parent component. For this, we first need to emit a custom event from a 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 a 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 the 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:
Vue.component('test', {
template: `
<div class="child" @click="$emit('my-event')">
Click here
</div>`
})
new Vue({
el: "#myApp",
data: {},
methods: {
testFunction: function(event) {
console.log('test clicked')
}
}
})
div.child{border:5px dotted orange; padding:20px;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="myApp">
<test @my-event="testFunction"></test>
</div>
With Vue 3
Using v-bind="$attrs"
:
Vue 3 is going to make our life much easier in many ways. One example is that it will help us create a simpler transparent wrapper with less config, by just using v-bind="$attrs"
. By using this on child components, not only will our listener work directly from the parent, but also any other attributes will also work just like they would with a normal <div>
.
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:
const { createApp } = Vue;
const Test = {
template: `
<div class="child">
Click here
</div>`
};
const App = {
components: { Test },
setup() {
const testFunction = event => {
console.log("test clicked");
};
return { testFunction };
}
};
createApp(App).mount("#myApp");
div.child{border:5px dotted orange; padding:20px;}
<script src="//unpkg.com/vue@next"></script>
<div id="myApp">
<test v-on:click="testFunction"></test>
</div>
But, for complex components with nested elements where we need to apply attributes and events to the <input />
instead of the parent label we can simply use v-bind="$attrs"
Demo #2:
const { createApp } = Vue;
const BaseInput = {
props: ['label', 'value'],
template: `
<label>
{{ label }}
<input v-bind="$attrs">
</label>`
};
const App = {
components: { BaseInput },
setup() {
const search = event => {
console.clear();
console.log("Searching...", event.target.value);
};
return { search };
}
};
createApp(App).mount("#myApp");
input{padding:8px;}
<script src="//unpkg.com/vue@next"></script>
<div id="myApp">
<base-input
label="Search: "
placeholder="Search"
@keyup="search">
</base-input><br/>
</div>