How to pass all events to far ancestor component in vuejs? - vue.js

The question is similar to this one, except, the emit event is not going to the grand parent, but a further one.
How to pass all events to parent in VueJS
The way I am trying to emit all events up the stack is this way:
<View_5 /> <!-- does an emit event -->
<View_4 v-on="$attrs" /> <!-- pass all events to parent -->
<View_3 v-on="$attrs" /> <!-- pass all events to parent. But it breaks here. -->
At View_3, it doesnt pass the events to its parents. What I'm i doing wrong?
[EDIT] - Here is a link to a sample project on stackblitz
Click the black square, and you can see the text changes. This works because it bubbled to the a "go" event from components D -> to C -> to B -> to A, using the old fashion way.
Now how do i make it so that components C and B do NOT specifically look for the "go" event, but simply pass all events up to component A?

Personally, I'm not a big fan of emitting the events up the stack if the event is not emitted to a direct parent and should go way up, exactly for the reasons you mentioned: it may be hard to follow where exactly things break. But that's just my opinion. What I do like to do in such cases is to use EventBus.
Essentially, an event bus is a Vue.js instance that can emit events in one component, and then listen and react to the emitted event in another component directly — without the help of a parent component.
First create an eventBus.js file (I like to store mine in a utils directory):
import Vue from 'vue'
const EventBus = new Vue()
export default EventBus
In your child component:
import EventBus from '#/utils/eventBus
export default {
//rest of your setup
methods: {
myMethodHandler() {
EventBus.$emit('myEvent')
}
}
}
And then in the grand parent components (the component that has to receive the event):
import EventBus from '#/utils/eventBus
export default {
//rest of your setup
created() {
EventBus.$on('myEvent', () => {
// your business logic here
})
}
}
Of course you can give the events whatever name that you like and then listen to the same event. And you can pass payload if needed - just pass it in the emitted event right after the event name and receive them in the EventBus callback function:
EventBus.$emit('myEvent', someString, someObject)
//...
EventBus.$on('myEvent', (someStringPayload, someObjectPayload) => {
// do your thing
})
The examples above are for Vue2. For Vue3, according to the official doc, you can use a third party library, such as mitt or tiny-emitter.

v-on="$attrs" should be v-bind="$attrs".
$attrs contains key-value pairs of attributes and their values. For #go="handler", $attrs would be { onGo: handler }, where the on-prefix is automatically to the key.
v-on="obj" creates event handlers for the key-value pairs in obj. For instance, v-on="{ foo: handler } creates a listener for the foo event that runs handler().
Given the above, v-on="$attrs" in your case would incorrectly create a listener for the onGo event (when it really should be for the go event). Further, each v-on="$attr" in the nested components would prepend on to the name at each nested level, leading to onOnOnGo in DD.vue.
Solution
Use v-bind="$attrs" to correctly forward the v-on directive:
<!-- AA.vue -->
<BB #click="onClick" />
<!-- BB.vue -->
<CC v-bind="$attrs" />
<!-- CC.vue -->
<DD v-bind="$attrs" />
<!-- DD.vue -->
<button v-bind="$attrs" />
demo

Related

Child components not rendering when referenced dynamically in composition API

I'm converting some components from vue 3's option API to the composition API. In this particular component I have two nested child components:
<script lang="ts" setup>
import ShiftOperation from "#/components/transformation-widgets/ShiftOperation.vue";
import RawJolt from "#/components/transformation-widgets/RawJolt.vue";
console.log([ShiftOperation, RawJolt])
...
From what I understand, if you're using the setup attribute in the script tag then all you have to do is import the component into a variable like I'm doing above and it should be available for the template without having to do anything else, like it's not like the old options api where you had to inject those components into the parent component.
Both components are imported successfully (confirmed by the console log:
When I'm rendering out this parent component I'm using the two child components to render out an array of data where I reference the children dynamically in the template based on information in each block of data that I'm iterating over:
<template>
<div class="renderer-wrapper">
<component
v-for="(block, index) in store.specBlocks"
v-bind:key="index"
:block="block"
:index="index"
:is="determineBlockComponent(block)"
#block-operation-updated="updateBlock"
>
</component>
</div>
</template>
// logic for determining the component to use:
export const determineBlockComponent = (block: JoltOperation) => {
switch (block.renderComponent) {
case 'shift':
return 'ShiftOperation'
default:
return 'RawJolt'
}
}
This worked fine in the options api version of it, but for some reason the components don't actually render. They show up in the elements tab:
But they don't show up in the view. I also added a created lifecycle hook into the child components that just console.log's out saying "created X", but those hooks don't fire.
Business logic wise nothing has changed, it's just been going from option api to composition api, so I'm assuming I'm missing some key detail.
Any ideas?
Your determineBlockComponent function should not return the string but the object of the component. Replace return 'ShiftOperation' with return ShiftOperation

Cannot get DOM events on component in host component

I have a Vue component that contains a list of objects named lines. I build a table from those lines using different components based on the line type. This works perfectly. Here's a stripped down version of the component:
<template>
<table>
<tr v-for="line in lines"
:key="line.key"
:is="componentForType[line.eventType] || 'LogLine'"
v-bind="line"
/>
</table>
</template>
<script>
export default {
name: 'DebugLog',
components: {
LogLine,
FormattedLogLine,
UserDebug,
Limits
},
data () {
return {
lines: [],
selectedKey: null,
componentForType: {
'USER_DEBUG' : 'UserDebug',
'LIMIT_USAGE_FOR_NS' : 'Limits',
'EXCEPTION_THROWN' : 'FormattedLogLine',
'FATAL_ERROR' : 'FormattedLogLine'
}
}
},
mounted() {
// code that loads this.lines
}
}
</script>
Now I want to be able to click any row of the table, and have the row become "selected", meaning that I want store line.key in this.selectedKey and use CSS to render that line differently. But I can't get the events working. Here's the updated <template>; nothing else is changed:
<template>
<table>
<tr v-for="line in lines"
:key="line.key"
:is="componentForType[line.eventType] || 'LogLine'"
v-bind="line"
:class="{selected: line.key == selectedKey}"
#click.capture="selectedKey = line.key"
/>
</table>
</template>
I've added the last 2 properties on the tr element - a dynamic class binding and a click event handler to set this.selectedKey to the active line's key. But it isn't working. I replaced the #click handler code with console.log(line.key) and nothing is logged, which tells me that my #click handler is never firing. I originally wrote it with out the .capture modifier, but tried adding the modifier when the original didn't work.
Is vue.js stopping propagation from the child component to the parent? Can I not bind the click event on the tr since it :is another vue component? Or is there something else going on? The examples I've found in the docs are much simpler and I'm not sure they correspond to my situation. The various child components are not binding any click events. I'd prefer to handle the event entirely in the parent as shown, since I will have a number of types of child component, and I don't want to have to implement click handlers in each.
Update: Looking at my child components, I note that each contains a tr tag that must effectively replace the tr in the parent template. For example, my most basic component is LogLine, shown here:
<template>
<tr>
<td>{{timeStamp}}</td>
<td>{{eventType}}</td>
<td>{{lineNumber}}</td>
<td>{{lineData}}</td>
</tr>
</template>
<script>
export default {
name: 'LogLine',
props: ['timeStamp', 'eventType', 'lineData', 'lineNumber'],
data: function () {
return {}
}
}
</script>
So I'm guessing that the binding in the parent isn't actually binding on the tr in the DOM; it's just binding on the Vue component, listening for a click event to be sent from the child with $emit; and that each child component will need to bind #click on its tr and emit it to the parent. Assuming I'm right, is there any shortcut I can use from the parent template to have vue forward the DOM events? Any other option I'm missing besides binding click in every child component?
Piggy-backing off of Jacob's answer here. Since you're essentially attaching an event listener to a dynamic component it expects a custom click event. So you have two options here:
Listen for the native DOM click event within that component (by attaching a click event listener to a normal DOM element within the component) and emit a custom click event to the parent.
Use the .native modifier to listen for the native DOM click event instead of a custom one directly in the parent.
Since you are using an :is prop, it's considered a dynamic Vue component, not a DOM element.
Events listener on a Vue component won't be passed down to its DOM element by default. You have to do it manually by going into the component template and add v-on="$listeners".
demo: https://jsfiddle.net/jacobgoh101/am59ojwx/7/
e.g. <div v-on="$listeners"> ... </div>
#Jacob Goh's use of v-on="$listeners" is simple and allows forwarding of all DOM events in one action, but I wanted to document an approach I tried on my own for completeness. I will be switching to Jacob's solution in my component. I am now using Husam's .native modifier in the parent as it is more suitable to my particular use case.
I was able to make my component work by editing each child component, capturing the click event and re-emitting it. For example:
<template>
<tr #click="$emit('click')">
<td>{{timeStamp}}</td>
<td>{{eventType}}</td>
<td>{{lineNumber}}</td>
<td>{{lineData}}</td>
</tr>
</template>
<script>
export default {
name: 'LogLine',
props: ['timeStamp', 'eventType', 'lineData', 'lineNumber'],
data: function () {
return {}
}
}
</script>

is it correct global component communication in vue?

i make modal popup components myPopup.vue for global.
and import that in App.vue and main.js
i use this for global, define some object Vue.prototype
make about popup method in Vue.prototype
like, "show" or "hide", any other.
but i think this is maybe anti pattern..
i want to find more best practice.
in App.vue
<div id="app>
<my-popup-component></my-popup-conponent>
<content></content>
</div>
main.js
...
Vue.prototype.$bus = new Vue(); // global event bus
Vue.prototype.$popup = {
show(params) {
Vue.prototype.$bus.$emit('showPopup', params);
},
hide() {
Vue.prototype.$bus.$emit('hidePopup');
}
}
Vue.component('my-popup-component', { ... });
...
myPopup.vue
....
export default {
...
created() {
this.$bus.$on('showPopup', this.myShow);
this.$bus.$on('hidePopup', this.myHide);
}
...
need-popup-component.vue
methods: {
showPopup() {
this.$popup.show({
title: 'title',
content: 'content',
callback: this.okcallback
});
}
}
It seems to be works well, but i don't know is this correct.
Is there any other way?
I was very surprised while reading your solution, but if you feel it simple and working, why not?
I would do this:
Add a boolean property in the state (or any data needed for showing popup), reflecting the display of the popup
use mapState in App.vue to bring the reactive boolean in the component
use v-if or show in App.vue template, on the popup declaration
create a 'showPopup' mutation that take a boolean and update the state accordingly
call the mutation from anywhere, anytime I needed to show/hide the popup
That will follow the vue pattern. Anything in state, ui components reflect the state, mutations mutates the state.
Your solution works, ok, but it doesn't follow vue framework, for exemple vue debug tools will be useless in your case. I consider better to have the minimum of number of patterns in one app, for maintenance, giving it to other people and so on.
You somehow try to create global component, which you might want to consume in your different projects.
Here is how I think I would do this -
How do I reuse the modal dialog, instead of creating 3 separate dialogs
Make a separate modal component, let say - commonModal.vue.
Now in your commonModal.vue, accept single prop, let say data: {}.
Now in the html section of commonModal
<div class="modal">
<!-- Use your received data here which get received from parent -->
<your modal code />
</div>
Now import the commonModal to the consuming/parent component. Create data property in the parent component, let say - isVisible: false and a computed property for the data you want to show in modal let say modalContent.
Now use it like this
<main class="foo">
<commonModal v-show="isVisible" :data="data" />
<!-- Your further code -->
</main>
The above will help you re-use modal and you just need to send the data from parent component.
How do I know which modal dialog has been triggered?
Just verify isVisible property to check if modal is open or not. If isVisible = false then your modal is not visible and vice-versa
How my global dialog component will inform it's parent component about its current state
Now, You might think how will you close your modal and let the parent component know about it.
On click of button trigger closeModal for that
Create a method - closeModal and inside commonModal component and emit an event.
closeModal() {
this.$emit('close-modal')
}
Now this will emit a custom event which can be listen by the consuming component.
So in you parent component just use this custom event like following and close your modal
<main class="foo">
<commonModal v-show="isVisible" :data="data" #close- modal="isVisible = false"/>
<!-- Your further code -->
</main>

How do you and should you move event bus functionality to Vuex store?

Right now I'm using an event bus to call methods of certain Vue components from other non-related Vue components.
I have a functioning Vuex store, so I'm trying to get rid of the event bus and move this functionality to Vuex store.
Questions
Should I move event bus functionality to Vuex store or should I use both?
What's the best way to implement event bus functionality in a Vuex store?
Could you please give an actual example of how to call a method inside another non-related component using Vuex:
First.vue
methods: {
test1 () {
console.log('test1 was called')
}
}
Second.vue
methods: {
callMethodInsideFirstComponent () {
...
}
}
If you have a need for an event bus, there's no harm in using one. The main problems people have with them are 1) that they pollute the global event space (obviously), 2) if relied upon too heavily can become cumbersome to track, and 3) risk collisions with event names or unintended side effects.
Vuex is a shared reactive state accessible anywhere throughout your application. Key word being reactive, don't think you will be calling methods between components, i.e. component A calls a method defined in component B. Instead, component A will mutate a given property in the state tree which component B is observing (typically in a computed property or watcher).
For example:
// First.vue
<template>
<div>{{ myStoreProp }}</div>
</template>
...
computed: {
myStoreProp () {
return this.$store.getters['myModule/myStoreProp']
}
}
// Second.vue
<template>
<button #click="updateMyStoreProp('Hello from Second.vue')">Click Me</button>
</template>
...
methods: {
updateMyStoreProp (value) {
this.$store.commit('myModule/myStoreProp', value)
}
}
Now whenever Second.vue calls it's updateMyStoreProp function, the value committed to the store will reflect in First.vue's template, in this case printing "Hello from Second.vue".
You can use event bus to emit and listen event, but you should use vuex when your app become complex. Use vuex API subscribe to implement event bus functionality using vuex. https://vuex.vuejs.org/api/#subscribe
Just simple step, you commit mutation in component A, and use subscribe to listen mutation in component B.

Vue two way prop binding

Below is my current structure (which doesn't work).
Parent component:
<template>
<field-input ref="title" :field.sync="title" />
</template>
<script>
import Field from './input/Field'
export default {
components: {
'field-input': Field
},
data() {
return {
title: {
value: '',
warn: false
}
}
}
}
</script>
Child component:
<template>
<div>
<input type="text" v-model="field.value">
<p v-bind:class="{ 'is-invisible' : !field.warn }">Some text</p>
</div>
</template>
<script>
export default {
props: ['field']
}
</script>
The requirements are:
If parent's data title.warn value changes in parent, the child's class bind should be updated (field.warn).
If the child's <input> is updated (field.value), then the parent's title.value should be updated.
What's the cleanest working solution to achieve this?
Don't bind the child component's <input> to the parent's title.value (like <input type="text" v-model="field.value">). This is a known bad practice, capable of making your app's data flow much harder to understand.
The requirements are:
If parent's data title.warn value changes in parent, the child's class bind should be updated (field.warn).
This is simple, just create a warn prop and pass it from parent to child.
Parent (passing the prop to the child):
<field-input ref="title" :warn="title.warn" />
Child/template (using the prop -- reading, only):
<p v-bind:class="{ 'is-invisible' : !warn }">Some text</p>
Child/JavaScript (declaring the prop and its expected type):
export default {
props: {warn: Boolean}
}
Notice that in the template it is !warn, not !title.warn. Also, you should declare warn as a Boolean prop because if you don't the parent may use a string (e.g. <field-input warn="false" />) which would yield unexpected results (!"false" is actually false, not true).
If the child's <input> is updated (field.value), then the parent's title.value should be updated.
You have a couple of possible options here (like using .sync in a prop), but I'd argue the cleanest solution in this case is to create a value prop and use v-model on the parent.
Parent (binding the prop using v-model):
<field-input ref="title" v-model="title.value" />
Child/template (using the prop as initial value and emitting input events when it changes):
<input type="text" :value="value" #input="$emit('input', $event.target.value)">
Child/JavaScript (declaring the prop and its expected type):
export default {
props: {value: String}
}
Click here for a working DEMO of those two solutions together.
There are several ways of doing it, and some are mentioned in other answers:
Use props on components
Use v-model attribute
Use the sync modifier (for Vue 2.0)
Use v-model arguments (for Vue 3.0)
Use Pinia
Here are some details to the methods that are available:
1.) Use props on components
Props should ideally only be used to pass data down into a component and events should pass data back up. This is the way the system was intended. (Use either v-model or sync modifier as "shorthands")
Props and events are easy to use and are the ideal way to solve most common problems.
Using props for two-way binding is not usually advised but possible, by passing an object or array you can change a property of that object and it will be observed in both child and parent without Vue printing a warning in the console.
Because of how Vue observes changes all properties need to be available on an object or they will not be reactive.
If any properties are added after Vue has finished making them observable 'set' will have to be used.
//Normal usage
Vue.set(aVariable, 'aNewProp', 42);
//This is how to use it in Nuxt
this.$set(this.historyEntry, 'date', new Date());
The object will be reactive for both component and the parent:
I you pass an object/array as a prop, it's two-way syncing automatically - change data in the
child, it is changed in the parent.
If you pass simple values (strings, numbers)
via props, you have to explicitly use the .sync modifier
As quoted from --> https://stackoverflow.com/a/35723888/1087372
2.) Use v-model attribute
The v-model attribute is syntactic sugar that enables easy two-way binding between parent and child. It does the same thing as the sync modifier does only it uses a specific prop and a specific event for the binding
This:
<input v-model="searchText">
is the same as this:
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
Where the prop must be value and the event must be input
3.) Use the sync modifier (for Vue 2.0)
The sync modifier is also syntactic sugar and does the same as v-model, just that the prop and event names are set by whatever is being used.
In the parent it can be used as follows:
<text-document v-bind:title.sync="doc.title"></text-document>
From the child an event can be emitted to notify the parent of any changes:
this.$emit('update:title', newTitle)
4.) Use v-model arguments (for Vue 3.0)
In Vue 3.x the sync modifier was removed.
Instead you can use v-model arguments which solve the same problem
<ChildComponent v-model:title="pageTitle" />
<!-- would be shorthand for: -->
<ChildComponent :title="pageTitle" #update:title="pageTitle = $event" />
5.) Use Pinia (or Vuex)
As of now Pinia is the official recommended state manager/data store
Pinia is a store library for Vue, it allows you to share a state across components/pages.
By using the Pinia store it is easier to see the flow of data mutations and they are explicitly defined. By using the vue developer tools it is easy to debug and rollback changes that were made.
This approach needs a bit more boilerplate, but if used throughout a project it becomes a much cleaner way to define how changes are made and from where.
Take a look at their getting started section
**In case of legacy projects** :
If your project already uses Vuex, you can keep on using it.
Vuex 3 and 4 will still be maintained. However, it's unlikely to add new functionalities to it. Vuex and Pinia can be installed in the same project. If you're migrating existing Vuex app to Pinia, it might be a suitable option. However, if you're planning to start a new project, we highly recommend using Pinia instead.