Vuejs $emit doesnt work in some part of a function, and in other part works - vue.js

I am using vuejs3 and trying to emit event from a child component.
child Component
<input type="button" v-if="edition_mode" #click="cancel()" class="btn btn-primary" value="Annuler">
[...]
cancel(){
if(this.new_sav){
this.$emit('test')
}else{
console.log('else')
this.$emit('test')
}
},
Parent Component
<div v-if="creation_form">
<h4>Ajout Nouveau Sav</h4>
<sav-form
:initial_data="null"
:li_product="li_product"
:new_sav="true"
:customer_id="data.customer.id"
#action="form_action"
#test="test()"/>
</div>
[...]
test(){
console.log('test emit works')
}
When cancel() is executed, in the if case $emit() works correctly, but in the else case, only 'else' is printed and $emit is not executed. What I am doing wrong here ?
I also have several buttons in child component, in the same div, that all call differents function but some function 'can' emit event and other can't.

I am not sure what issue you are facing but it is working fine in the below code snippet. Please have a look and let me know if any further clarification/discussion required.
Demo :
Vue.component('child', {
template: '<div><button #click="cancel()">Trigger emit event from child!</button></div>',
data() {
return {
isValid: true
}
},
methods: {
cancel: function() {
this.isValid = !this.isValid;
if (this.isValid) {
console.log('valid');
this.$emit('test');
} else {
console.log('not valid');
this.$emit('test')
}
}
}
});
var vm = new Vue({
el: '#app',
methods: {
getTestEvent() {
console.log('Emit event from child triggered!');
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<child #test="getTestEvent"></child>
</div>

I was facing the same issue.
I use Vuex to store authenticated status of the user and my mistake was to use v-if="isAuth" attribute in parent component and the child component set isAuth to false through the store so this.$emit() doesn't work anymore.
Parent Component:
<login-form #onLogin="onLoginHandler" v-if="!isAuth" />
Child Component:
methods: {
example() {
[...]
const response = await axios.post("/login", data);
if (response.status === 200) {
// under the hood isAuth is set to True;
this.$store.dispatch("SET_AUTH", true);
}
// Doesn't work anymore because the parent component v-if is now False.
this.$emit("onLogin", response.data);
}
}

Related

Call method when modal closes in Vue

I have a Vue app (and I'm relatively new to Vue), anyway I have a generic error modal which is displayed when any of my axios calls fail.
On the modal, I want to be able to retry the failed process when the 'Retry' button is clicked but I'm struggling a bit on how to achieve this. I don't think props will help me as the modal is triggered by
VueEvent.$emit('show-error-modal')
I have managed in my catch to pass the function which has failed by using
VueEvent.$emit('show-error-modal', (this.test));
Then in my modal, I have access to it using
created() {
VueEvent.$on('show-error-modal', (processFailed) => {
console.log('processFailed', processFailed)
this.processFailed = processFailed;
$('#errorModal').modal('show').on('shown.bs.modal', this.focus);
});
}
Using 'F12' it gives
test: function test() {
var _this2 = this;
var page = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
alert('boo');
this.loading = true;
var order_by = {
'ho_manufacturers.name': 4,
'services.servicename': 4
};
axios.post('/test/url', {
id: this.selectedManufacturers,
page: page,
order_by: order_by
}).then(function (response) {
var paginator = response.data.paginator;
_this2.$store.dispatch('paginateProducts', paginator);
_this2.$store.dispatch('setProductPaginator', paginator);
_this2.loading = false;
})["catch"](function (error) {
var processFailed = 'fetchProducts';
_this2.loading = false;
VueEvent.$emit('show-error-modal', _this2.test);
console.log(error);
});
},
I don't think this is the right way of doing it though as all this. are replaced with _this2 as shown above. I need the modal to be generic so I can reuse it but I can't figure it out how to retry the process when clicking the button.
The modals are registered in my app.vue file as
<template>
<div>
<!-- Other code here -->
<app-error-modal />
</div>
</template>
<script>
// Other code here
import ErrorModal from './components/ErrorModal';
export default {
name: 'App',
components: {
// Other code here
appErrorModal: ErrorModal
}
// Other code here
</script>
Modal button HTML
<button ref="retryButton" class="btn btn-success col-lg-2" type="submit" data-dismiss="modal" #click="retryProcess">Retry</button>
Modal script code
<script>
export default {
name: "ErrorModal",
created() {
VueEvent.$on('show-error-modal', (processFailed) => {
console.log('processFailed', processFailed)
this.processFailed = processFailed;
$('#errorModal').modal('show').on('shown.bs.modal', this.focus);
});
},
methods: {
focus() {
this.$refs.retryButton.focus();
},
retryProcess() {
//this.$parent.test(); TRIED THIS BUT DIDN'T WORK
}
}
}
</script>
I'd rather not have to the store.
Use custom event on your component
<div id="app">
<error-modal v-if="isError" v-on:retry-clicked="retry"></error-modal>
<button #click="isError = true">Make Error</button>
</div>
const errorModal = {
template : "<button #click=\"$emit('retry-clicked')\">Retry</button>"
}
new Vue({
el : "#app",
data : {
isError : false
},
components : {
errorModal
},
methods : {
retry : function(){
this.isError = false;
console.log("child component has called parent when retry clicked")
}
}
})
Custom event on component - VUEJS DOC
Everything you have there looks correct. You are passing the function to retry ("test") as "processFailed" - I would call that something different such as retryFn.
Then in your error modal component you just need:
<button #click="processFailed">Retry</button>
Don't worry about what the browser shows you in F12, that is the transpiled Javascript, it will work fine.

VUEJS 2: Events. Parent to trigger an method found in a child component [duplicate]

Context
In Vue 2.0 the documentation and others clearly indicate that communication from parent to child happens via props.
Question
How does a parent tell its child an event has happened via props?
Should I just watch a prop called event? That doesn't feel right, nor do alternatives ($emit/$on is for child to parent, and a hub model is for distant elements).
Example
I have a parent container and it needs to tell its child container that it's okay to engage certain actions on an API. I need to be able to trigger functions.
Vue 3 Composition API
Create a ref for the child component, assign it in the template, and use the <ref>.value to call the child component directly.
<script setup>
import {ref} from 'vue';
const childComponentRef = ref(null);
function click() {
// `childComponentRef.value` accesses the component instance
childComponentRef.value.doSomething(2.0);
}
</script>
<template>
<div>
<child-component ref="childComponentRef" />
<button #click="click">Click me</button>
</div>
</template>
Couple things to note-
If your child component is using <script setup>, you'll need to declare public methods (e.g. doSomething above) using defineExpose.
If you're using Typescript, details of how to type annotate this are here.
Vue 3 Options API / Vue 2
Give the child component a ref and use $refs to call a method on the child component directly.
html:
<div id="app">
<child-component ref="childComponent"></child-component>
<button #click="click">Click</button>
</div>
javascript:
var ChildComponent = {
template: '<div>{{value}}</div>',
data: function () {
return {
value: 0
};
},
methods: {
setValue: function(value) {
this.value = value;
}
}
}
new Vue({
el: '#app',
components: {
'child-component': ChildComponent
},
methods: {
click: function() {
this.$refs.childComponent.setValue(2.0);
}
}
})
For more info, see Vue 3 docs on component refs or Vue 2 documentation on refs.
What you are describing is a change of state in the parent. You pass that to the child via a prop. As you suggested, you would watch that prop. When the child takes action, it notifies the parent via an emit, and the parent might then change the state again.
var Child = {
template: '<div>{{counter}}</div>',
props: ['canI'],
data: function () {
return {
counter: 0
};
},
watch: {
canI: function () {
if (this.canI) {
++this.counter;
this.$emit('increment');
}
}
}
}
new Vue({
el: '#app',
components: {
'my-component': Child
},
data: {
childState: false
},
methods: {
permitChild: function () {
this.childState = true;
},
lockChild: function () {
this.childState = false;
}
}
})
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.1/vue.js"></script>
<div id="app">
<my-component :can-I="childState" v-on:increment="lockChild"></my-component>
<button #click="permitChild">Go</button>
</div>
If you truly want to pass events to a child, you can do that by creating a bus (which is just a Vue instance) and passing it to the child as a prop.
You can use $emit and $on. Using #RoyJ code:
html:
<div id="app">
<my-component></my-component>
<button #click="click">Click</button>
</div>
javascript:
var Child = {
template: '<div>{{value}}</div>',
data: function () {
return {
value: 0
};
},
methods: {
setValue: function(value) {
this.value = value;
}
},
created: function() {
this.$parent.$on('update', this.setValue);
}
}
new Vue({
el: '#app',
components: {
'my-component': Child
},
methods: {
click: function() {
this.$emit('update', 7);
}
}
})
Running example: https://jsfiddle.net/rjurado/m2spy60r/1/
A simple decoupled way to call methods on child components is by emitting a handler from the child and then invoking it from parent.
var Child = {
template: '<div>{{value}}</div>',
data: function () {
return {
value: 0
};
},
methods: {
setValue(value) {
this.value = value;
}
},
created() {
this.$emit('handler', this.setValue);
}
}
new Vue({
el: '#app',
components: {
'my-component': Child
},
methods: {
setValueHandler(fn) {
this.setter = fn
},
click() {
this.setter(70)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id="app">
<my-component #handler="setValueHandler"></my-component>
<button #click="click">Click</button>
</div>
The parent keeps track of the child handler functions and calls whenever necessary.
Did not like the event-bus approach using $on bindings in the child during create. Why? Subsequent create calls (I'm using vue-router) bind the message handler more than once--leading to multiple responses per message.
The orthodox solution of passing props down from parent to child and putting a property watcher in the child worked a little better. Only problem being that the child can only act on a value transition. Passing the same message multiple times needs some kind of bookkeeping to force a transition so the child can pick up the change.
I've found that if I wrap the message in an array, it will always trigger the child watcher--even if the value remains the same.
Parent:
{
data: function() {
msgChild: null,
},
methods: {
mMessageDoIt: function() {
this.msgChild = ['doIt'];
}
}
...
}
Child:
{
props: ['msgChild'],
watch: {
'msgChild': function(arMsg) {
console.log(arMsg[0]);
}
}
}
HTML:
<parent>
<child v-bind="{ 'msgChild': msgChild }"></child>
</parent>
The below example is self explainatory. where refs and events can be used to call function from and to parent and child.
// PARENT
<template>
<parent>
<child
#onChange="childCallBack"
ref="childRef"
:data="moduleData"
/>
<button #click="callChild">Call Method in child</button>
</parent>
</template>
<script>
export default {
methods: {
callChild() {
this.$refs.childRef.childMethod('Hi from parent');
},
childCallBack(message) {
console.log('message from child', message);
}
}
};
</script>
// CHILD
<template>
<child>
<button #click="callParent">Call Parent</button>
</child>
</template>
<script>
export default {
methods: {
callParent() {
this.$emit('onChange', 'hi from child');
},
childMethod(message) {
console.log('message from parent', message);
}
}
}
</script>
If you have time, use Vuex store for watching variables (aka state) or trigger (aka dispatch) an action directly.
Calling child component in parent
<component :is="my_component" ref="my_comp"></component>
<v-btn #click="$refs.my_comp.alertme"></v-btn>
in Child component
mycomp.vue
methods:{
alertme(){
alert("alert")
}
}
I think we should to have a consideration about the necessity of parent to use the child’s methods.In fact,parents needn’t to concern the method of child,but can treat the child component as a FSA(finite state machine).Parents component to control the state of child component.So the solution to watch the status change or just use the compute function is enough
you can use key to reload child component using key
<component :is="child1" :filter="filter" :key="componentKey"></component>
If you want to reload component with new filter, if button click filter the child component
reloadData() {
this.filter = ['filter1','filter2']
this.componentKey += 1;
},
and use the filter to trigger the function
You can simulate sending event to child by toggling a boolean prop in parent.
Parent code :
...
<child :event="event">
...
export default {
data() {
event: false
},
methods: {
simulateEmitEventToChild() {
this.event = !this.event;
},
handleExample() {
this.simulateEmitEventToChild();
}
}
}
Child code :
export default {
props: {
event: {
type: Boolean
}
},
watch: {
event: function(value) {
console.log("parent event");
}
}
}

Vue directive not triggering method

I have a custom vue directive.
Vue.directive('click-outside', {
bind: function (el, binding, vnode) {
document.addEventListener(clickHandler, (event) => {
const clickedInsideDropdown = el.contains(event.target);
if (!clickedInsideDropdown && el.classList.contains(openClass)) {
vnode.context.$emit(binding.expression);
}
});
}
});
I then initialize it with the dropdown template:
<template>
<div class="dropdown" :class="{ '-is-open': open }" v-click-outside="close">
<span #click="toggle">
<slot name="toggle"></slot>
</span>
<slot name="menu"></slot>
</div>
</template>
The supporting logic is functioning as expected as well:
<script>
export default {
data: function () {
return {
open: false
}
},
methods: {
close: function () {
this.open = false;
console.log('close');
},
toggle: function () {
this.open = !this.open;
console.log('toggle');
}
}
}
</script>
The Problem
The event should fire when the current dropdown _is open and none of the items inside of it are clicked - which is does (console logging confirms this). However, the $emit is not triggering the close method for some reason.
The event is being emitted in the Vue devtools as expected.
Vue version 2.5.3
Credits to Linus Borg who answered my question for me on the forum. Was just understanding the purpose of events incorrectly.
Events are usually used to communicate from a child component to a parent component, so triggering an event ‘close’ in a componet will not run a method of that name in that component.
If you want that, you have to actually register a listener to that event:
created () {
this.$on('close', this.close /*the name of the method to call */)
}
However, this isn’t really necessary in your case. you are already passing the close method to the directive, so you can run it directly:
Vue.directive('click-outside', {
bind: function (el, binding, vnode) {
document.addEventListener(clickHandler, (event) => {
const clickedInsideDropdown = el.contains(event.target);
if (!clickedInsideDropdown && el.classList.contains(openClass)) {
binding.value()
// alternartively, you could also call the method directly on the instance, no need for an event:
vnode.context.[expression]()
// but that wouldn't really be elegant, agreed?
}
});
}
});

Can a vue component know if a listener is listening?

Say I have a modal dialogue as a Vue component. Sometimes I want OK and Cancel. Sometimes, I just want OK. The cleanest way I can think to do this would be for my component to only display Cancel when it's caller is listening for cancel events. Can this be done?
jsfiddle
markup
<div id="vue-root">
<confirm-modal v-on:ok="handleOk"></confirm-modal>
<confirm-modal v-on:ok="handleOk" v-on:cancel="handleCancel"></confirm-modal>
</div>
code
Vue.component('confirm-modal',{
template : `
<div class="confirm-modal">
Are you sure<br>
<button v-on:click="$emit('ok',{})">OK</button>
<button v-if="'HOW DO I TEST IF CANCEL WILL BE CAPTURED???'" v-on:click="$emit('cancel',{})">Cancel</button
</div>
`,
})
vm = new Vue({
el : '#vue-root',
methods : {
handleOk : function(){
alert('OK already');
},
handleCancel : function(){
}
}
})
First you can emit an event without value and the parent will catch it. You dont need this empty Object.
What i understood from your question is this:
If you to track in confirm-modal component if the cancel button is clicked ?
Then do this.
Vue.component('confirm-modal',{
template : `
<div class="confirm-modal">
Are you sure<br>
<button v-on:click="$emit('ok',{})">OK</button>
<button v-if="isCaptured" v-on:click="cancelClick">Cancel</button
</div>
`,
data: function () {
return {
isCaptured: false,
};
},
methods: {
cancelClick: function() {
this.isCaptured = true;
// or this.$emit('cancel'); then pass prop from parent
},
}
})
vm = new Vue({
el : '#vue-root',
methods : {
handleOk : function(){
alert('OK already');
},
handleCancel : function(){
}
}
})
The easy way to do this, you can pass props from parent to child component when you want to show ok and cancel button.
Here you have some more about props
https://v2.vuejs.org/v2/guide/components.html#Passing-Data-with-Props

How to use a global event bus in vue.js 1.0.x?

I am trying to create a event bus with an empty new Vue instance. The app is large enough to be split into multiple files for components. As an example my app is structured as :
main.js
import Vue from vue;
window.bus = new Vue();
Vue.component('update-user', require('./components/update-user');
Vue.component('users-list', require('./components/users-list');
Vue.component('edit-user', require('./components/edit-user');
Vue.component('user-address', require('./components/user-address');
new Vue({
el:'body',
ready(){
}
});
components/update-user.js
export default{
template: require('./update-user.template.html'),
ready(){
bus.$emit('test-event', 'This is a test event from update-user');
}
}
components/users-list.js
export default{
template:require('./users-list.template.html'),
ready(){
bus.$on('test-event', (msg) => { console.log('The event message is: '+msg)});
//outputs The event message is: This is a test event
}
components/edit-user.js
export default{
template:require('./edit-user.template.html'),
ready(){
bus.$on('test-event', (msg) => {console.log('Event message: '+msg)});
//doesn't output anything
console.log(bus) //output shows vue instance with _events containing 'test-event'
}
}
components/user-address.js
export default{
template:require('./user-address.template.html'),
ready(){
bus.$on('test-event', () => {console.log('Event message: ' +msg)});
//doesn't output anything
console.log(bus) //output shows vue instance with _events containing 'test-event'
}
}
index.html
...
<body>
<update-user>
<users-list></users-list>
<edit-user>
<user-address></user-address>
</edit-user>
</update-user>
</body>
...
My question is that why does bus.$on work in the first child component only? Even if I remove the listener from <users-list>, none of the other components are able to listen to the event i.e console.log() with bus.$on doesn't work in any component below/after <users-list> i.e. the immediate child component.
Am I missing something or where am I doing wrong?
How to get this working so that any child component at any depth can listen to an event emitted from even the root component or any where higher up in the hierarchy and vice-versa?
I figured it out and got it working. Posting here to be of help to someone else who hits this question.
Actually there's nothing wrong with the implementation I have mentioned above in the question. I was trying to listen to the event in a component which was not yet rendered (v-if condition was false) when the event was fired. So a second later (after the event was fired) when the component was rendered it could not listen for the event - this is intended behavior in Vue (I got a reply on laracasts forum).
However, I finally implemented it slightly differently (based on a suggestion from Cody Mercer as below:
import Vue from vue;
var bus = new Vue({});
Object.defineProperty(Vue.prototype, $bus, {
get(){
return this.$root.bus;
}
});
Vue.component('update-user', require('./components/update-user');
Vue.component('users-list', require('./components/users-list');
Vue.component('edit-user', require('./components/edit-user');
Vue.component('user-address', require('./components/user-address');
new Vue({
el:'body',
ready(){
},
data:{
bus:bus
}
});
Now to access the event bus from any component I can use this.$bus as
this.$bus.$emit('custom-event', {message:'This is a custom event'});
And I can listen for this event from any other component like
this.$bus.$on('custom-event', event => {
console.log(event.message);
//or I can assign the message to component's data property
this.message = event.message;
//if this event is intended to be handled in other components as well
//then as we normally do we need to return true from here
return true;
});
Event propagation stops when a listener is triggered. If you want the event to continue on, just return true from your listener!
https://vuejs.org/api/#vm-dispatch
bus.$on('test-event', () => {
console.log('Event message: ' +msg);
return true;
});
$emit dispatches the event within the scope of the instance — it doesn't propagate to parents/children. $broadcast will propagate to child components. As mentioned in #Jeff's answer, the intermediate component event callbacks have to return true to allow the event to continue cascading to [their] children.
var child = Vue.extend({
template: '#child-template',
data: function (){
return {
notified: false
}
},
events: {
'global.event': function ( ){
this.notified = true;
return true;
}
}
});
var child_of_child = Vue.extend({
data: function (){
return {
notified: false
}
},
template: '#child-of-child-template',
events: {
'global.event': function ( ){
this.notified = true;
}
}
});
Vue.component( 'child', child );
Vue.component( 'child-of-child', child_of_child );
var parent = new Vue({
el: '#wrapper',
data: {
notified: false
},
methods: {
broadcast: function (){
this.$broadcast( 'global.event' );
},
emit: function (){
this.$emit( 'global.event' );
}
},
events: {
'global.event': function (){
this.notified = true;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<div id="wrapper">
<h2>Parent notified: {{ notified }}</h2>
<child></child>
<button #click="broadcast">$broadcast</button>
<button #click="emit">$emit</button>
</div>
<template id="child-template">
<h5>Child Component notified: {{ notified }}</h5>
<child-of-child></child-of-child>
</template>
<template id="child-of-child-template">
<h5>Child of Child Component notified: {{ notified }}</h5>
</template>