just like the title i need to dynamically emit an event to parent component's methods, i have a component structured like this
<TableComponent
:actionmenus="actionmenus"
#edit="parenteditmethod"
#delete="parentdeletemethod">
</TableComponent>
here is actionmenus object
actionmenus: [
{title: 'Edit', emitevent: 'edit'},
{title: 'Delete', emitevent: 'delete'}
]
and then here is snippet of my tablecomponent
...
<ul>
<li v-for="menu in actionmenus"><a #click="$emit(menu.emitevent)" class="text-menu">{{ menu.title }}</a></li>
</ul>
...
i know this should be easily done by $emit('edit') or $emit('delete') without using actionmenus object but the $emit() part should be dynamic based on the passed array actionmenus so that the tablecomponent can be re-used on different case. how should i approaching this? is there any way?
From what I understand, you would like to emit an event from the child component to the parent, and pass some data with the emit (sorry if thats not the case).
As you know, you can emit events in the child component like this :
$emit("EVENT");
And Catch it in the parent like this :
<childTag v-on:EVENT="parentFunction"></childTag>
You can also pass data to the parent from the child like this :
$emit("EVENT",DATA);
And catch the data in the parent function like this
<childTag v-on:EVENT="parentFunction"></childTag>
...
methods{
parentFunction(DATA){
//Handle the DATA object from the child
}
}
Hope this helps and best of luck!
#codincat is right, that it all works. It's true also for Vue3
const rootComponent = {
el: "#app",
data: function () {
return {
actionmenus: [
{ title: "Edit", emitevent: "edit" },
{ title: "Delete", emitevent: "delete" }
]
};
},
methods: {
parenteditmethod() {
console.log("edit");
},
parentdeletemethod() {
console.log("delete");
}
}
};
const app = Vue.createApp(rootComponent);
app.component("table-component", {
props: { actionmenus: Array },
template: `
<ul>
<li v-for="menu in actionmenus">
<a #click="$emit(menu.emitevent)" class="text-menu">{{ menu.title }}</a>
</li>
</ul>`
});
const rootComponentInstance = app.mount("#app");
<!-- https://stackoverflow.com/questions/43750969/vuejs2-how-to-dynamically-emit-event-to-parent-component -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.2.29/vue.global.min.js"></script>
<div id="app">
<table-component :actionmenus="actionmenus" #edit="parenteditmethod" #delete="parentdeletemethod">
</table-component>
</div>
Related
I have a list of child components that dynamically gets filled with component instances
data: function () {
return {
child_components: []
}
}
I would like them to be rendered in the template as the list changes, this doesn't work:
<li v-for="c in child_components">
{{c.$el}}
</li>
Is it possible to render the actual component instances? Because in the component templates there are inputs that change their data, and I would like to access that data from the child_components list.
Thank you
I think you need something. child_components is key of object, so we can't use it like that.
data: {
child_components: function () {
return [
{ el: 'example' },
{ el2: 'example2' },
];
}
}
<li v-for="c in child_components">
{{c.el}}
</li>
I have a model from a backend where the property that normally contains an array of elements can be nullable. When this happens I'll init it to an empty array. However when doing that it seems to break my ability to update the array in the child component. Sample code can be found below.
Vue.component('Notes', {
template: '<div>{{items}}<ul><li v-for="item in items">{{ item.text }}</li></ul><button #click="add">Add</button></div>',
props: {
items: Array,
},
methods: {
add() {
console.log('added');
this.items.push({ text: "new item" });
}
}
});
new Vue({
el: "#demo",
data() { return { model: { } }},
created() { if(!this.model.items) this.model.items = []; }
});
<script src="https://unpkg.com/vue"></script>
<div id="demo">
<Notes :items="model.items" />
</div>
If data in the main component is model: { items : [] } everything works fine. But I don't have control over the backend data to guarantee that.
In your Notes component, you declare a model in the data, then, just underneath, you add an items[] if one doesn't exist already. This is not a good practice, and could be the cause of your problems. Vue needs to know about all the properties on objects it's watching. They need to be there when Vue first processes the object, or you need to add them with Vue.set().
You should emit an event to update the prop in the parent component
in child component :
this.$emit('add-item',{
text: "new item"
});
in parent template add a handler for the emitted event :
<Notes :items="model.items" #add-item="AddItem" />
Vue.component('Notes', {
template: '<div>{{items}}<ul><li v-for="item in items">{{ item.text }}</li></ul><button #click="add">Add</button></div>',
props: {
items: Array,
},
methods: {
add() {
console.log('added');
this.$emit('add-item', {
text: "new item"
});
}
}
});
new Vue({
el: "#demo",
data() {
return {
model: {
items: [] //init it here in order to avoid reactivity issues
}
}
},
methods: {
AddItem(item) {
this.model.items.push(item)
}
},
});
<script src="https://unpkg.com/vue"></script>
<div id="demo">
<Notes :items="model.items" #add-item="AddItem" />
</div>
I have the following component
<template>
<li v-for="(item, i) in this.menu" :key="i" #click="item.action()"> //trying to call the method in the component
{{menu.title}}
<li>
</template>
<script>
export default {
data: () => ({
menu: [
{title: 'Start Preparing', action: this.startPrepare}, //how do I reference the method here?
{title: 'Cancel Order'},
],
}),
methods: {
startPrepare: function (orderId) {
console.log("start")
}
}
}
</script>
As you can see in the commented sections, I have a menu in the data section, it has a title and action properties in it. So in the template, I want to invoke whatever function we have specified when someone clicks on that particular item.
So how do I refer a method in the same component in the data section of that component? as of now, I am getting start prepare is undefined error.
Let me know if any further clarifications are needed
The main problem here I think is that you're using an arrow function for your data, which can't be bound to the Vue instance. You need to use a normal function instead ..
export default {
data() {
return {
menu: [{
title: 'Start Preparing',
action: this.startPrepare
}, //how do I reference the method here?
{
title: 'Cancel Order'
},
],
}
},
methods: {
startPrepare: function(orderId) {
console.log("start")
}
}
}
<template>
<li v-for="(item, i) in this.menu" :key="i" #click="item.action()"> //trying to call the method in the component
{{menu.title}}
<li>
</template>
Try to add the method name as a string like action value and in the template access it like #click="handleAction(item.action)":
<template>
<li v-for="(item, i) in menu" :key="i" #click="handleAction(item.action)">
{{menu.title}}
<li>
</template>
<script>
export default {
data: () => ({
menu: [
{title: 'Start Preparing', action:'startPrepare'}, //how do I reference the method here?
{title: 'Cancel Order'},
],
}),
methods: {
handleAction(actionName){
this[actionName]();
}
startPrepare: function (orderId) {
console.log("start")
}
}
}
</script>
I'm building a small vue application where among other things it is possible to delete an entry of a music collection. So at this point I have a list of music albums and next to the entry I have a "delete" button. When I do the following:
<li v-for="cd in cds">
<span>{{cd.artist}} - {{cd.album}}</span> <button v-on:click="deleteAlbum(cd.ID)">Delete</button>
</li>
and then in my methods do:
deleteAlbum(id){
this.$http.delete('/api/cds/delete/'+id)
.then(function(response){
this.fetchAll()
// });
},
this works fine so far, but to make it more nice, I want the delete functionality to appear in a modal/popup, so I made the following changes:
<li v-for="cd in cds">
<div class="cd-wrap">
<span>{{cd.artist}} - {{cd.album}}</span>
<button #click="showDeleteModal({id: cd.ID, artist: cd.artist, album: cd.album})" class="btn">Delete</button>
</div>
<delete-modal v-if="showDelete" #close="showDelete = false" #showDeleteModal="cd.ID = $event"></delete-modal>
</li>
so, as seen above I created a <delete-modal>-component. When I click on the delete button I want to pass the data from the entry to <delete-modal> component with the help of an eventbus. For that, inside my methods I did this:
showDeleteModal(item) {
this.showDelete = true
eventBus.$emit('showDeleteModal', {item: item})
}
Then, in the <delete-modal>, inside the created()-lifecycle I did this:
created(){
eventBus.$on('showDeleteModal', (item) => {
console.log('bus data: ', item)
})
}
this gives me plenty of empty opened popups/modals!!??
Can someone tell me what I'm doing wrong here?
** EDIT **
After a good suggestion I dumped the eventBus method and pass the data as props to the <delete-modal> so now it looks like this:
<delete-modal :id="cd.ID" :artist="cd.artist" :album="cd.album"></delete-modal>
and the delete-modal component:
export default {
props: ['id', 'artist', 'album'],
data() {
return {
isOpen: false
}
},
created(){
this.isOpen = true
}
}
Only issue I have now, is that it tries to open a modal for each entry, how can I detect the correct ID/entry?
I am going to show you how to do it with props since it is a parent-child relation.I will show you a simple way of doing it.You need to modify or add some code of course in order to work in your app.
Parent component
<template>
<div>
<li v-for="cd in cds" :key="cd.ID">
<div class="cd-wrap">
<span>{{cd.artist}} - {{cd.album}}</span>
<button
#click="showDeleteModal({id: cd.ID, artist: cd.artist, album: cd.album})"
class="btn"
>
Delete
</button>
</div>
<delete-modal v-if="showDelete" :modal.sync="showDelte" :passedObject="objectToPass"></delete-modal>
</li>
</div>
</template>
<script>
import Child from 'Child'
export default {
components: {
'delete-modal': Child
},
data() {
return {
showDelete: false,
objectToPass: null,
//here put your other properties
}
},
methods: {
showDeleteModal(item) {
this.showDelete = true
this.objectToPass = item
}
}
}
</script>
Child Component
<template>
/* Here put your logic component */
</template>
<script>
export default {
props: {
modal:{
default:false
},
passedObject: {
type: Object
}
},
methods: {
closeModal() { //the method to close the modal
this.$emit('update:modal')
}
}
//here put your other vue.js code
}
</script>
When you use the .sync modifier to pass a prop in child component then,there (in child cmp) you have to emit an event like:
this.$emit('update:modal')
And with that the modal will close and open.Also using props we have passed to child component the object that contains the id and other stuff.
If you want to learn more about props, click here
I'm currently trying to get a simple Tabs/Tab component up and running.
It seems like something in the event handling mechanism has changed, therefore I can't get it to work.
Current implementation:
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<li class="tab" v-for="tab in tabs" #click="activateTab(tab)">{{ tab.header }}</li>
</ul>
<slot></slot>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: [],
data() {
return {
tabs: []
}
},
created() {
this.$on('tabcreated', this.registerTab)
},
methods: {
registerTab(tab) {
this.tabs.push(tab);
},
activateTab(tab) {
}
}
}
</script>
Tab.vue
<template>
<div class="tab-pane" v-show="active">
<slot></slot>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: {
'header': String
},
data() {
return {
active: false
}
},
mounted() {
this.$emit('tabcreated', this);
}
}
</script>
eventhub.js
import Vue from 'vue';
export default new Vue();
View
<tabs>
<tab header="Test">
First Tab
</tab>
<tab header="Test2">
Second Tab
</tab>
<tab header="Test3">
Third Tab
</tab>
</tabs>
I've tried the following things:
use a Timeout for the $emit to test if it's a timing issue (it is
not)
use #tabcreated in the root element of the Tabs components
template
It works if...
... I use the suggested "eventhub" feature (replacing this.$on and
this.$emit with hub.$on and hub.$emit)
but this is not suitable for me, as I want to use the Tabs component multiple times on the same page, and doing it with the "eventhub" feature wouldn't allow that.
... I use this.$parent.$emit
but this just feels weird and wrong.
The documentation states that it IS possible to listen for events triggered by $emit on direct child components
https://v2.vuejs.org/v2/guide/migration.html#dispatch-and-broadcast-replaced
Does anyone have an Idea?
You're right, in vue 2, there is no more $dispatch. $emit could work for a single component but it will be scoped to himself (this). The recommended solution is to use a global event manager, the eventhub.
the eventhub can be stored in the window object to be used anywhere without import, I like to declare in my main.js file like this:
window.bus = new Vue()
and then in whatever component:
bus.$emit(...)
bus.$on(...)
It works just the same as this.$root.$emit / this.$root.$on. You said it works when you call this.$parent.$emit, but this code, simulate a scoped emit in the parent component but fired from the child, not good.
What I understand in your code is that you want to have an array of created tabs, but to do what with them ?
Instead of storing the tab instance in the parent and then activate from the parent, you should think about a more functional way.
The activateTab method should be declared on the tab component and manage the instanciation through the data, something like:
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<tab v-for="tab in tabs" :header="tab.header"></tab>
</ul>
</div>
</template>
<script>
import hub from '../eventhub';
import Tab from 'path/to/Tab.vue';
export default {
components: [Tab],
props: [],
data() {
return {
tabs: ['First Tab', 'Second Tab', 'Third Tab']
}
}
}
</script>
Tab.vue
<template>
<div class="tab tab-pane" #click:activeTab()>
<span v-show="active">Activated</span>
<span>{{ header }}</span>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: {
'header': String
},
data() {
return {
active: false
}
},
methods: {
activeTab () {
this.active = true
}
}
}
</script>
This way, your Tab is more independant. For parent/child communication keep this in mind :
parent to child > via props
child to parent > via $emit (global bus)
If you need a more complexe state management you definitely should take a look at vuex.
Edit
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<tab v-for="tabData in tabs" :custom="tabData"></tab>
</ul>
</div>
</template>
<script>
import Tab from 'path/to/Tab.vue';
export default {
components: [Tab],
props: [],
data() {
return {
tabs: [
{foo: "foo 1"},
{foo: "foo 2"}
{foo: "foo 3"}
]
}
}
}
</script>
Tab.vue
<template>
<div class="tab tab-pane" #click:activeTab()>
<span v-show="active">Activated</span>
<span>{{ custom.foo }}</span>
</div>
</template>
<script>
export default {
props: ['custom'],
data() {
return {
active: false
}
},
methods: {
activeTab () {
this.active = true
}
}
}
</script>
This is what I don't like about VueJS (2), there is no convenient way of catching events emitted from child components to the parent component.
Anyways an alternative to this is if you do not want to use the eventhub approach, specially if you are only going to have an event communication between related components ( child and parent ) and not with non-related components, then you can do these steps.
reference your parent vue component on its data property (very important, you can't just pass this to the child component)
pass that parent vue component reference as an attribute to the child component ( make sure to bind it)
trigger the appropriate event of the parent component inside the child component whenever a desired event is emitted
Pseudo code
// Parent vue component
Vue.component( 'parent_component' , {
// various codes here ...
data : {
parent_component_ref : this // reference to the parent component
},
methods : {
custom_event_cb : function() {
// custom method to execute when child component emits 'custom_event'
}
}
// various codes here ...
} );
// Parent component template
<div id="parent_component">
<child_component :parent_component_ref="parent_component_ref"></child_component>
</div>
// Child component
Vue.component( 'child_component' , {
// various codes here ...
props : [ 'parent_component_ref' ],
mounted : function() {
this.$on( 'custom_event' , this.parent_component_ref.custom_event_cb );
this.$emit( 'custom_event' );
},
// You can also, of course, emit the event on events inside the child component, ex. button click, etc..
} );
Hope this helps anyone.
Use v-on="$listeners", which is available since Vue v2.4.0. You can then subscribe to any event you want on the parent, see fiddle.
Credit to BogdanL from Vue Support # Discord.