Vue.js - Emit from grandchild slot component didn't bubble $emit event to parent - vue.js

I planned and made a modal and then created a button to close the modal window.
I wanted to change the value of isHomeDialog using $emit as an event of the button.
However, $emit's event was not delivered to "Home.vue"
Home.vue
<template>
<div>
<ReviewDialog
:is-open="isHomeDialog"
#close="closeEvent()/>
</div>
</template>
<script>
import ReviewDialog from '#/components/popup/dialog/ReviewDialog';
</script>
export default {
name: 'Home',
components: {
ReviewDialog
},
data () {
return {
isHomeDialog: true
};
},
methods: {
closeEvent () {
console.log('close');
isHomeDialog = false;
}
}
};
BaseDialog.vue
<template>
<div v-show="isOpen">
<div class="mask"> </div>
<div class="content">
<button
class="close"
#click="$emit('close')"> </button>
<slot/>
</div>
</div>
</template>
<script>
export default {
props: {
isOpen: {
type: Boolean,
required: true
}
}
};
Reviewdialog.vue
<template>
<BaseDialog
url="#/components/popup/dialog/BaseDialog"
id="review-dialog"
:is-open="isOpen"
:header-text="'REVIEW'">
<div class="warp">
<p>
test
</p>
</div>
</BaseDialog>
</template>
<script>
import BaseDialog from '#/components/popup/dialog/BaseDialog';
export default {
components: {
BaseDialog
},
props: {
isOpen: {
type: Boolean,
default: false
}
}
</script>
Home
└ BaseDialog
 └ ReviewDialog
In the above structure, I tried to send a request to BaseDialog and ReviewDialog with $emit, but it was not delivered to Home.
Is there any way to send $ emit to its parent component other than requesting it with $root.$emit?

BaseDialog emits the close event to its parent, ReviewDialog, which is not listening for it. So you need to listen in ReviewDialog, and re-emit the event to Home:
<BaseDialog
url="#/components/popup/dialog/BaseDialog"
id="review-dialog"
:is-open="isOpen"
:header-text="'REVIEW'"
#close="$emit('close')"> <-- This line is new -->
Another way to do this is to have ReviewDialog pass all its listeners down:
<BaseDialog
url="#/components/popup/dialog/BaseDialog"
id="review-dialog"
:is-open="isOpen"
:header-text="'REVIEW'"
v-on="$listeners"> <-- This line is new -->

Since two-way binding is deprecated in Vue2 + child cannot mutate props directly
Likely another approach custom component with v-model
I put reference below:
vuejs update parent data from child component

Related

Vue.JS - Avoid re-rendering of slots when parent re-renders

I'm struggling on Vue.JS with a component that has as children/slots some "complex" components with canvas data (e.g. Maps).
I want to avoid that when the parent component re-renders, their inner slots re-render. Because of how this components work (or any other scenario), every time it re-renders it needs to do all of it's "loading" steps. Even when saving their real-time state.
For example:
Component.vue
<template>
<div>
<span>{{value}}</span>
<slot></slot>
</div>
</template>
View
<Component v-model="value">
<Map :latitude="0" :longitude="0"/>
</Component>
<script>
this.value = "Hello";
setTimeout(()=>{
this.value="Hello world!";
},1000);
</script>
Is there any way to prevent from slots from re-rendering when their parent re-renders?
Thanks in advance.
Hello use props for a child component, and this child is not will rerender
App
<template>
<div id="app">
<HelloWorld msg="Hello Vue in CodeSandbox!" :somedata="key">
slot information key: {{ key }}
</HelloWorld>
<button #click="key++">key++</button>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: "App",
components: {
HelloWorld,
},
data() {
return {
key: 0,
};
},
};
</script>
Child
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h3>somedata from props: {{ somedata }}</h3>
<hr />
<slot></slot>
<hr />
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: {
type: String,
default: "",
},
somedata: {
type: Number,
default: 999,
},
},
created() {
console.log("renred");
},
};
</script>
How you can see I used console.log("renred"); in the child component, you can check the console, the information will be shown only one time.
https://codesandbox.io/s/naughty-platform-ib68g?file=/src/components/HelloWorld.vue

Opening (and Closing) vue material dialog on different components

I'm trying to open a vue material dialog on a child component. I want the trigger button to remain on the parent and the dialog box template on a child component so that it would be easier to maintain.
The dialog box opens correctly, but when I click on the close button, the dialog box closes, but I'm unable to open it again with the trigger button. I'm using a prop to pass variables from the parent to the child.
How do I close the dialog box and be able to open it up again?
app.vue:
<template>
<div>
<button #click="showContextMenu = true">
<span>Show Context Menu</span>
</button>
<contextMenu :showContextMenu="showContextMenu"></contextMenu>
</div>
</template>
<script>
import contextMenu from "contextMenu.vue";
export default {
data() {
return {
showContextMenu: false,
};
},
components: {
contextMenu,
},
};
</script>
contextMenu.vue:
<template>
<md-dialog :md-active.sync="showContextMenu">
<md-dialog-title>Preferences</md-dialog-title>Dialog
<md-dialog-actions>
<md-button class="md-primary" #click="showContextMenu = false">Close</md-button>
</md-dialog-actions>
</md-dialog>
</template>
<script>
export default {
data() {
return {};
},
props: ["showContextMenu"],
};
</script>
Passing props to child components creates a one way binding, thus you cannot use them to pass state from child to its parent. You can have you contextMenu emit an event, on which you change the value of the showContextMenu variable in parent. Remember the following: props are used for passing state from parent to child components and events for doing the reverse.
The code would look like this:
app.vue:
<template>
<div>
<button #click="showContextMenu = true">
<span>Show Context Menu</span>
</button>
<contextMenu :showContextMenu="showContextMenu" #close="showContextMenu = false"></contextMenu>
</div>
</template>
<script>
import contextMenu from "contextMenu.vue";
export default {
data() {
return {
showContextMenu: false,
};
},
components: {
contextMenu,
},
};
</script>
contextMenu.vue:
<template>
<md-dialog :md-active.sync="showContextMenu">
<md-dialog-title>Preferences</md-dialog-title>Dialog
<md-dialog-actions>
<md-button class="md-primary" #click="$emit('close')">Close</md-button>
</md-dialog-actions>
</md-dialog>
</template>
<script>
export default {
data() {
return {};
},
props: ["showContextMenu"],
};
</script>
You are actually trying to set a property present in app, but you are toggling it false in contextMenu.
Mutating prop is never recommended.
Do this instead
App.vue
<template>
<div>
<button #click="showContextMenu = true">
<span>Show Context Menu</span>
</button>
<contextMenu v-if="showContextMenu"
:showContextMenu="showContextMenu"
#close-context="showContextMenu = false"> //capturing event close-context
</contextMenu>
</div>
</template>
Then in contextMenu, emit an event to parent.
contextMenu.vue
<template>
<md-dialog :md-active.sync="showContextMenu">
<md-dialog-title>Preferences</md-dialog-title>Dialog
<md-dialog-actions>
<md-button class="md-primary" #click="closeContext">Close</md-button>
</md-dialog-actions>
</md-dialog>
</template>
<script>
export default {
data() {
return {};
},
methods: {
closeContext() {
this.$emit('close-context'); // emitting to parent
},
props: ["showContextMenu"],
};
</script>

Vuex: Child component wait for parent component dispatch action

Parent component (Dashboard):
<template>
<div id="dashboard">
<Header />
<b-container>
<div>
<b-row>
<b-col>
<Overview />
</b-col>
</b-row>
</div>
</b-container>
</div>
</template>
<script>
import Header from '#/components/common/Header';
import Overview from '#/components/dashboard/Overview';
import { mapGetters } from 'vuex';
export default {
name: 'dashboard',
components: {
Header,
Overview
},
mounted() {
const sessionId = this.$cookie.get('connect.sid');
this.$store.dispatch('user/getUser', sessionId).then((userData) => {
this.$store.dispatch('project/getProject', userData.data.user);
});
},
computed: {
...mapGetters('user', {
user: 'getUser'
})
}
}
</script>
Child component (Overview):
<template>
<div class="overview">
<div class="overview__title">
<h1>
Welcome {{user.cn[0]}} // Works
</h1>
</div>
<div class="overview__project">
<p v-for="project in runningprojects" :key="project._id">
{{project.name}} // Does not work at refresh
</p>
</div>
</div>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
name: 'dashboard-overview',
data() {
return {
runningprojects: []
}
},
computed: {
...mapGetters('user', {
user: 'getUser'
}),
...mapGetters('project', {
projects: 'getProjects',
allProjects: 'getAllProjects'
})
},
mounted() {
console.log("mounted this.projects", this.projects);
// add something from this.projects to this.runningprojects
},
methods: {
calcReq() {
...
},
...
}
</script>
In my Dashboard component (parent) I fetch the user data with a vuex action dispatch('user/getUser) and after that I fetch the projects of this user dispatch('project/getProject).
In my Overview component (child) I want to show the project information of this user. I call my mapGetters and I have a component variable runningprojects inside data(). In my mounted() lifecycle I want to push data from my getters to this data array.
The following problem is given:
When I refresh my application, the console.log from my child component mounted() is called before the dispatch jobs are finished in the parent component (dashboard).
It only works if change something in my local files and vue-cli does a live reload.
Because of the page lifecycle of the vue app. when component renders mounted is called after created and it wont wait for the ajax or any async calls.
One solution would be to not render the child component until the async return
<template>
<div id="dashboard">
<Header />
<b-container>
<div>
<b-row>
<b-col>
<Overview v-if="finished"/>
</b-col>
</b-row>
</div>
</b-container>
</div>
</template>
<script>
import Header from '#/components/common/Header';
import Overview from '#/components/dashboard/Overview';
import { mapGetters } from 'vuex';
export default {
name: 'dashboard',
data() {
return {
finished: false,
}
},
components: {
Header,
Overview
},
mounted() {
const sessionId = this.$cookie.get('connect.sid');
this.$store.dispatch('user/getUser', sessionId).then((userData) => {
this.$store.dispatch('project/getProject', userData.data.user);
this.finished = true;
});
},
computed: {
...mapGetters('user', {
user: 'getUser'
})
}
}
</script>
Just add a v-if in the child component and when dispatch has return then set the value to true which will render the child component and the then the mounted will have the values you want
Other solution would be.
Use updated function instead of mounted and which will be called when ever there is a change in the state.

vuejs passing method to child dynamically

I have a 3rd party component <third-party-component /> which accepts following event:
onAdd, onRemove, onUpdate
I want to create a wrapper component around it and want to pass these events dynamically so that can handle the response in wrapper component, something like
wrapper.js
<template>
<div class="my-wrapper">
<third-party-component />
</div>
<template>
using-wrapper.js
<template>
<div>
...
<wrapper #onAdd="add" #onRemove="remove"></wrapper>
...
</div>
</template>
<script>
export default {
methods: {
onAdd() {
console.log('on add in using-wrapper.js');
},
onRemove() {
console.log('on remove in using-wrapper.js');
}
}
}
</script>
You can pass all attributes and listeners by binding them using v-bind and v-on
You also need to set inheritAttrs to false
https://v2.vuejs.org/v2/api/#inheritAttrs
<template>
<div class="my-wrapper">
<third-party-component v-bind="$attrs" v-on="$listeners"/>
</div>
<template>
using-wrapper.js
<template>
<div>
...
<wrapper #onAdd="add" #onRemove="remove"></wrapper>
...
</div>
</template>
<script>
export default {
inheritAttrs: false,
methods: {
onAdd() {
console.log('on add in using-wrapper.js');
},
onRemove() {
console.log('on remove in using-wrapper.js');
}
}
}
</script>
Well on "wrapper" you will need to create 3 methods that listen and get triggered on "third-party-component".
<template>
<div class="my-wrapper">
<third-party-component #onAdd="wrapperAdd" #onRemove="wrapperRemove" #onUpdate="wrapperUpdate" />
</div>
<script>
export default {
methods:{
wrapperAdd(){
this.$emit("onAdd",{obj that will get to the parent});
}
}
}
I made only 1 method because the other 2 are similar.

vue reload child component

I'm using vue, version 2.5.8
I want to reload child component's, or reload parent and then force children components to reload.
I was trying to use this.$forceUpdate() but this is not working.
Do You have any idea how to do this?
Use a :key for the component and reset the key.
See https://michaelnthiessen.com/force-re-render/
Add key to child component, then update the key in parent. Child component will be re-created.
<childComponent :key="childKey"/>
If the children are dynamically created by a v-for or something, you could clear the array and re-assign it, and the children would all be re-created.
To simply have existing components respond to a signal, you want to pass an event bus as a prop, then emit an event to which they will respond. The normal direction of events is up, but it is sometimes appropriate to have them go down.
new Vue({
el: '#app',
data: {
bus: new Vue()
},
components: {
child: {
template: '#child-template',
props: ['bus'],
data() {
return {
value: 0
};
},
methods: {
increment() {
this.value += 1;
},
reset() {
this.value = 0;
}
},
created() {
this.bus.$on('reset', this.reset);
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<child :bus="bus">
</child>
<child :bus="bus">
</child>
<child :bus="bus">
</child>
<button #click="() => bus.$emit('reset')">Reset</button>
</div>
<template id="child-template">
<div>
{{value}} <button #click="increment">More!</button>
</div>
</template>
I'm using directive v-if which is responsible for conditional rendering. It only affects reloading HTML <template> part. Sections created(), computed are not reloaded. As I understand after framework load components reloading it is not possible. We can only re render a <template>.
Rerender example.
I have a Child.vue component code:
<template>
<div v-if="show">
Child content to render
{{ callDuringReRender() }}
</div>
</template>
<script>
export default {
data() {
return {
show: true
}
}
,
methods: {
showComponent() {
this.show = true
},
hideComponent() {
this.show = false
},
callDuringReRender() {
console.log("function recall during rendering")
}
}
}
</script>
In my Parent.vue component I can call child methods and using it's v-if to force the child rerender
<template>
<div>
<input type="button"
value="ReRender the child"
#click="reRenderChildComponent"/>
<child ref="childComponent"></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
methods: {
reRenderChildComponent(){
this.$refs.childComponent.hideComponent();
this.$refs.childComponent.showComponent()
}
},
components: {
Child
}
}
</script>
After clicking a button in console You will notice message "function recall during rendering" informing You that component was rerendered.
This example is from the link that #sureshvv shared
import Vue from 'vue';
Vue.forceUpdate();
// Using the component instance
export default {
methods: {
methodThatForcesUpdate() {
// ...
this.$forceUpdate(); // Notice we have to use a $ here
// ...
}
}
}
I've found that when you want the child component to refresh you either need the passed property to be output in the template somewhere or be accessed by a computed property.
<!-- ParentComponent -->
<template>
<child-component :user="userFromParent"></child-component>
</template>
<!-- ChildComponent -->
<template>
<!-- 'updated' fires if property 'user' is used in the child template -->
<p>user: {{ user.name }}
</template>
<script>
export default {
props: {'user'},
data() { return {}; }
computed: {
// Or use a computed property if only accessing it in the script
getUser() {
return this.user;
}
}
}
</script>