Notify parallel Components about changes - vue.js

i try to notify parallel components (rendered with v-for) about changes.
This is my markup:
<div v-for="item in items" :key="item.ID">
<my-component :item="item"></my-component>
</div>
And now, if for example something in the first instance of "my-component" changes all remaining "my-component"s should be notified about his change. How can i achieve that?
(At the moment i use this.$root.$emit and this.$root.$on)
Thanks in advance for your help.

There are a few ways you can achieve this:
1. Use props
VueJS' data-flow patterns recommend passing props down and bubbling events up. You could use this pattern by having a prop that is passed down that notifies the child components about the change.
Parent
<div v-for="item in items" :key="item.ID">
<my-component :item="item" :last-modified="lastModified"></my-component>
</div>
Child
<script>
export default {
...
watch: {
lastModified (v) {
// Do something
}
}
}
</script>
2. Use Vuex
Another way to achieve this is to use Vuex for global state management. This is the recommended option particularly if you're already using Vuex to manage state.
Parent
<div v-for="item in items" :key="item.ID">
<my-component :item="item"></my-component>
</div>
Child
<script>
import { mapGetters } from 'vuex'
export default {
...
computed: {
mapGetters([
'lastModified'
]),
...
},
watch: {
lastModified (v) {
// Do something
}
}
}
</script>
2. Use $refs
Another simple way to achieve this on a small-scale is to use ref.
Parent
<template>
<div v-for="item in items" :key="item.ID">
<my-component :item="item" :last-modified="lastModified" ref="childComponents"></my-component>
</div>
</template>
<script>
export default {
...
watch: {
lastModified () {
for (const child of this.$refs.childComponents) {
child.onModified()
}
}
}
}
</script>
Child
export default {
...
methods: {
onModified () {
// Do something
}
}
}
4. Use an event bus
This is a very handy way of achieving event propagation, but is not recommended. For this approach, we will create a third component, that is a simple vue instance that will be used for events.
bus.vue
import Vue from 'vue'
export default new Vue()
Parent
<template>
<div v-for="item in items" :key="item.ID">
<my-component :item="item"></my-component>
</div>
</template>
<script>
import Bus from 'bus'
export default {
...
watch: {
lastModified () {
Bus.$emit('modified')
}
}
}
</script>
Child
<script>
import Bus from 'bus'
export default {
...
methods: {
onModified () {
// Do something
}
},
beforeMount () {
Bus.$on('modified', this.onModified)
},
beforeDestroy () {
Bus.$off('modified', this.onModified)
}
}
</script>

Related

Accessing components data from slot in vue.js (without declare template in outskirts)

Being create Menu components use to frameless windows. but I don't know how to bind events into slot. I want to build structure like this
in WindowBar.vue
<template>
<div>
<app-menu-bar>
<app-menu label="File"></app-menu>
</app-menu-bar>
</div>
</temaplte>
in AppMenuBar.vue (name is app-menu-bar)
<template>
<section>
<slot
v-bind:onSelected="onSelected"
v-on:doClick="doClick"
>
</slot>
</section>
</template>
<script>
export default {
name: 'app-menu-bar',
data () {
return {
onSelected: ''
}
}
}
</script>
in AppMenu.vue (name is app-menu)
<template>
<section class="menu">
<button
v-on:click="doClick"
v-bind:class="isSelected"
>
{{ label }}
</button>
</section>
</template>
<script>
export default {
name: 'app-menu',
props: {
label: String
},
computed: {
isSelected () {
// This is binding data from AppMenuBar
return this.onSelected == this.label ? 'selected' : ''
}
},
methods: {
doClick () {
this.$emit('doClick', this.label)
}
},
mounted () {
console.log(this.onSelected) // it is undefined
}
}
</script>
how do i binding data into slot, like this.onSelected?
You can't listen to events on <slot>. The more detailed answer you can read here https://github.com/vuejs/vue/issues/4332.
In your case, I will suggest putting all business logic in the parent component (which is WindowBar.vue). After that, you can have control of your child's component events and props. Or, if there is some specific situation, and you want to pass events through nested components you can use the event bus for it. More about the event bus you can read here: https://www.digitalocean.com/community/tutorials/vuejs-global-event-bus

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

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

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.

Share an object with another Vue component

I know this has been asked several times before, but as a Vue.js beginner I had trouble interpreting some of the other discussions and applying them to my situation. Using this CodeSandbox example, how would one pass the indicated object from "Hello" to "Goodbye" when the corresponding button is pressed? I'm unsure if I should be trying to use props, a global event bus, a plugin, vuex, or simply some sort of global variable.
Edit:
Here is the code for App.vue, Hello.vue and Goodbye.vue (from the previously linked CodeSandbox example).
App.vue
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "app"
};
</script>
Hello.vue:
<template>
<div class="hello">
<h1>This is Hello</h1>
<div v-for="(obj, index) in objects" :key="index">
<router-link class="button" :to="{ path: '/goodbye'}">Share obj[{{ index }}] with Goodbye</router-link>
</div>
</div>
</template>
<script>
export default {
name: "hello",
data() {
return {
objects: [0, 1, 2, 3]
};
}
};
</script>
Goodbye.vue:
<template>
<div class="goodbye">
<h1>This is Goodbye</h1>
<p>Obj = "???"</p>
<router-link class="button" :to="{ path: '/hello'}">Hello</router-link>
</div>
</template>
<script>
export default {
name: "goodbye"
};
</script>
Props are used to share data with child components. Since the components never exist at the same time, this is not useful for you. Similarly, events are not very useful to you here. You can send an event on a global bus, but since the other component does not exist yet, it cannot listen for the event.
I am not sure what you would want to do with a plugin in this case. You should never use a global variable, unless you have a very good reason to (e.g. you use Google Analytics, which happens to use a global variable, or you want to expose something within Vue in development mode for debugging purposes). In your case, you likely want to change some global app state, which is exactly what Vuex was made for. Call a Vuex mutator or action either when clicking, or in a router hook such as router.beforeEach to save the information in a structured manner so you can then retrieve it with a mapped getter. Keep in mind that you want to structure your vuex store, so don't use a state variable thingsIWantToShareWithGoodbye, but instead split it up in previousPage, lastClickOffset and numberOfClicks.
For example:
// store/index.js
import Vuex from "vuex";
import Vue from "vue";
Vue.use(Vuex);
const state = {
button: null
};
const getters = {
button(state) {
return state.button;
}
};
const mutations = {
setButton(state, payload) {
state.button = payload;
}
};
export default new Vuex.Store({
state,
getters,
mutations
});
// Hello.vue
<template>
<div class="hello">
<h1>This is Hello</h1>
<div v-for="(obj, index) in objects" :key="index">
<router-link #click.native="setButtonState(obj)" class="button" :to="{ path: '/goodbye'}">Share obj[{{ index }}] with Goodbye</router-link>
</div>
</div>
</template>
<script>
export default {
name: "hello",
data() {
return {
objects: [0, 1, 2, 3]
};
},
methods: {
setButtonState (obj) {
this.$store.commit('setButton', obj)
}
}
};
</script>
// Goodbye.vue
<template>
<div class="goodbye">
<h1>This is Goodbye</h1>
<p>Obj = {{ button }}</p>
<router-link class="button" :to="{ path: '/hello'}">Hello</router-link>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: "goodbye",
computed: {
...mapGetters({
button: 'button'
})
}
};
</script>

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>