In Vue - how can I trigger a broadcast to pick up event in a different control? - vuejs2

I have a component for steps, and I need to trigger an event in my steps component when I click on the next button.
This event should be picked up by a different component, that is representing the content of the page in the current step.
This is what I have tried:
Steps component template:
<template>
<div class="steps">
<div class="steps-content">
<section class="steps-panel" v-for="(stepPage, index) in steps">
<header class="posA wrap pTs">{{$t(title)}}</header>
<component :is="stepPage">
<!-- Summary component is injected here -->
</component>
</section>
</div>
<div role="navigation">
<ol class="fixed nav nav--block steps-nav w100">
<li class="steps-label txtCap" v-for="(stepPage, index) in steps">
{{$t(stepPage.name)}}
</li>
</ol>
<button class="steps-button" type="button">
<i class="pf pf-arrow-left"></i>
<span class="steps-button-label">{{$tc('previous')}}</span>
</button>
<button class="steps-button" type="button" v-on:click="next">
<span class="steps-button-label">{{$tc('next')}}</span>
<i class="pf pf-arrow-right"></i>
</button>
</div>
</div>
</template>
Steps component method:
methods: {
next(event) {
console.log('emit stepnext')
this.$emit('stepnext')
}
}
I call this with v-on:click="next" in the steps template (on the 'next' button)
From the console.log, I see that the click event is executed, and the $emit call does not trigger any error, so it seems to work fine at this point.
Summary component
The summary component is one of the components in `steps', and is loaded by this entry in the Steps template:
<component :is="stepPage"></component>
In the Summary component that knows what to do when this is clicked, I try to pick up on this event by having this in the template:
<div class="wrap" v-on:stepnext="stepfinish">
... content ...
</div>
... and a method in the summary component named stepfinish that does the action, but it seems like the emitted event never reaches my summary component.
How can I solve this?

A simple solution when using $root:
// trigger
this.$root.$emit('my-event');
// listen
this.$root.$on('my-event');

I think you are looking for the event bus
Here is some snippet from the official docs:
var bus = new Vue()
// in component A's method
bus.$emit('id-selected', 1)
// in component B's created hook
bus.$on('id-selected', function (id) {
// ...
})
link: https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication
Here is a blog on the same: https://alligator.io/vuejs/global-event-bus/

If you totally want to decouple your components, or you want to communicate across Vue "apps" and/or different frameworks (React, ...), then you can also use the DOM Native events:
document.dispatchEvent(
new CustomEvent("myapp:someClick", {
detail: {
id: 1,
someMoreCustomPayload: "bar",
},
})
)
And on the receiver's side:
document.addEventListener("myapp:someClick", (ev) => {
const { id, someMoreCustomPayload } = ev.detail
...
})
IE11 can also handle CustomEvents but apparently has no support for detail, but can be polyfilled.

Related

Vue3: Check if event listener is bound to component instance

I have a reusable Badge component. I want to be able to add a close/delete button when an onDelete event listener is present on the component instance.
<template>
<div class="flex inline-flex items-center px-2.5 py-0.5 text-xs font-medium select-none" :class="[square ? '' : 'rounded-full']">
<slot />
<button class="cursor-pointer ml-2" #click="$emit('onDelete')">
<XIcon class="flex-shrink-0 h-3.5 w-3.5 text-gray-400 hover:text-gray-500" aria-hidden="true" />
</button>
</div>
</template>
<script>
import { XIcon } from '#heroicons/vue/solid';
export default {
props: {
color: { type: String },
square: { type: Boolean, default: false },
},
components: {
XIcon,
},
emits: ['onDelete'],
};
</script>
If I add a v-if statement to the button, the emit event is executed immediately
<button v-if="$emit('onDelete')" class="cursor-pointer ml-2" #click="$emit('onDelete')">
I'm using Vue 3
UPDATE: If your component is using the new emits option in Vue3, which is the recommended best practice from the Vue3 docs, the event listeners will not be apart of the $attrs. An issue will be submitted to the Vue team for clarification and guidance on why this behaves this way.
I have simplified your example above in StackBlitz to isolate the functionality you are after.
Important note, I am using Vue 3.2.26.
In Vue3 $listeners were removed.
Event listeners are now part of $attrs. However simply console logging this.$attrs in Badge won't display the key you are looking for, they are within targets but accessible by prepending on to the bound event name. In your case in the Badge component you will use onOnDelete.
Complete working example with Two Badges. One will display because it has a bound event onDelete, the other will not due to the fact that the bound onDelete is not present.
https://stackblitz.com/edit/vue-8b6exq?devtoolsheight=33&file=src/components/Badge.vue

unexpected vue warn on a declared prop

I'm learning vuex. I'm facing a strange issue after I've migrated some methods to vuex actions.
I get this error in a component that has worked fine until I've migrated some things to vuex and I've implemented inside the component ...mapGetters and ...mapActions
the error is [Vue warn]: Property or method "isVisible" is not defined on the instance but referenced during render
but in my data I've declared the prop
data() {
return {
id: state.userInfo.id,
endCursor: state.userInfo.end_cursor,
nextPageLoaded: false,
isVisible: false,
isVideo: null,
url: null
}
}
<div class="modal fade show" tabindex="-1" role="dialog" v-if="isVisible">
<div class="modal-dialog">
<div class="modal-content h-100 rounded-0">
<div class="modal-header">
<button type="button" class="close mb-3 float-right" #click.prevent="closeZoomModal()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<!-- display image -->
<img class="img-fluid w-100 h-100 img-zoom" :src="url" v-if="!isVideo">
<!-- display video -->
<div class="embed-responsive embed-responsive-4by3 h-100" v-else>
<iframe class="embed-responsive-item h-100" :src="url" title=""></iframe>
</div>
</div>
</div>
</div>
</div>
This happen after the user click on the home component to search for ome data and the result component where the error is fires, is loaded.
How I can fix it? can the error be caused from ...mapGetters or ...mapActions ?
state is not available in data. According to docs, you can pass the instance as first param of the data function
data: vm => ({
isVisible: vm.$store.state.isVisible
})
... but I personally haven't used this (it doesn't work with Typescript and the component is still in an early lifecycle stage and a lot of things are missing from it). Besides, this is merely an assignment (it only runs once - it's not a getter - so if the state changes after data has been set, data won't react to it. You'd have to modify the data prop itself).
So what you need to do is move all store related component properties from data into computed by using either ...mapState() (if they're vuex state props), ...mapGetters() (if they're vuex getters) or use explicit computed syntax:
computed: {
isVisible() {
return this.$store.state.isVisible; // if store state prop
// return this.$store.getters['isVisible'] // if store getter
}
}
If you also want to be able to assign to it (as you would to a data property), you have to replace the above computed syntax (only getter) with a getter + setter syntax:
computed: {
isVisible: {
get() {
return this.$store.state.isVisible;
},
set(value) {
this.$store.dispatch('setVisibility', value);
// you can also commit mutations `this.$store.commit()` from here
}
}
}
If you're still having trouble, please create a minimal reproducible example on codesandbox.io and I'll help sort it out.

How do you pass a prop inside a function inside a template in vue.js?

I am trying to pass a prop inside a function inside a template for a to-do test site I'm making. Basically I want to have a list item which includes the todo item with a button next to it that deletes the same item.
Vue.component("todo-item", {
props: ["todotext"],
template: "<li>{{todotext.text}} <button v-on:click='removeThisItem({{todotext}})'>X</button></li>",
})
var next_id = 3
var app = new Vue ({
el: "#app",
data: {
message: "",
todos: [
{id: 0, text: "Do assignment"},
]
},
methods: {
addTodoItem: function () {
this.todos.push({id: next_id, text: this.message})
next_id += 1
},
removeThisItem: function removeThisItem (item) {
this.todos.splice(this.todos.indexOf(item))
}
}
})
and the HTML
<div id="app">
<input type="text" name="" v-model="message">
<button type="button" name="button" v-on:click="addTodoItem">Add Todo Item</button>
<ul>
<todo-item
v-for="todo in todos"
v-bind:todotext="todo"
v-bind:key="todo.id">
</todo-item>
</ul>
</div>
However I get the error
invalid expression: Unexpected token '{' in removeThisItem({{todotext}})
Is there a way to pass the prop as an argument inside this function inside this template to be able to delete this list item?
Edit: Here is the JSFiddle: https://jsfiddle.net/f6sn52w8/
Thanks!
Well, trying to solve the issue in your jsfiddle I got the error
[Vue warn]: Property or method "outerHTML" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option. (found in <TodoItem>)
Anyway, I figure it out what is happening with your code and why it isn't working.
You have the parent component where you are using your todo-item component:
<!-- parent component -->
<div id="app">
<input type="text" name="" v-model="message">
<button type="button" name="button" v-on:click="addTodoItem">
Add Todo Item
</button>
<ul>
<todo-item
v-for="todo in todos"
v-bind:todotext="todo"
v-bind:key="todo.id">
</todo-item>
</ul>
</div>
The method removeThisItem is declared in this component, so it isn't available in the child component <todo-item>, that's why you see the error in the console.
So the way to handle the click to remove the item is by listening for an event in the parent component and emitting the event from the child component:
Note about shorthand: v-bind:todotext="todo" is the same as :todotext="todo", and v-on:click is the same as #click
<!-- parent component -->
<div id="app">
<input type="text" name="" v-model="message">
<button type="button" name="button" v-on:click="addTodoItem">
Add Todo Item
</button>
<ul>
<todo-item
v-for="todo in todos"
:todotext="todo"
:key="todo.id"
#removeItem="removeThisItem"> <!-- listen for the removeItem event and run the removeThisItem method when it's triggered -->
</todo-item>
</ul>
</div>
Now the child component template must be updated:
Vue.component("todo-item", {
props: ["todotext"],
template:
`<li>
{{todotext.text}}
<button #click="$emit('removeItem', todotext)">X</button>
</li>`,
})
The todo-item component will emit the event removeItem when the button is clicked, and will send todotext prop as parameter to the function that will run on the parent (removeThisItem).
An alternative way to explain better this behavior:
Vue.component("todo-item", {
props: ["todotext"],
template:
`<li>
{{todotext.text}}
<button #click="emitEventRemoveItem">X</button>
</li>`,
methods: {
emitEventRemoveItem() {
// this.$emit will emit an event to the parent
// the first parameter is the event name, the second parameter
// is the argument that is expected in the parent method that
// will run when the event is triggered, removeThisItem in this case
this.$emit('removeItem', this.todotext);
}
}
})
Try to run this in your editor, in jsfiddle I got an error. Anyway, the issue is that you're trying to run a method that is declared in the parent component from the child component.
Let me know if it works or if you get any error.

Conditional wrapper rendering in vue

I'm making a link/button component which either can have a button or an anchor wrapper, a text and an optional icon. My template code below is currently rendering either an anchor or a button (with the exact same content) based on an if statement on the wrapper element, resulting in duplicate code.
<template>
<a v-if="link" v-bind:href="url" class="btn" :class="modifier" :id="id" role="button" :disabled="disabled">
{{buttonText}}
<svg class="icon" v-if="icon" :class="iconModifier">
<use v-bind="{ 'xlink:href':'#sprite-' + icon }"></use>
</svg>
</a>
<button v-else type="button" class="btn" :class="modifier" :id="id" :disabled="disabled">
{{buttonText}}
<svg class="icon" v-if="icon" :class="iconModifier">
<use v-bind="{ 'xlink:href':'#sprite-' + icon }"></use>
</svg>
</button>
</template>
Is there a more clean way for wrapping my buttonText and icon inside either an anchor or button?
I've solved my issue by intensive Google-ing! Found this issue regarding Vue on Github which pointed me in the right direction.
Small piece of backstory
I'm using Vue in combination with Storybook to build a component library in which a button can either be a button or an anchor. All buttons look alike (apart from color) and can be used for submitting or linking. To keep my folder structure ordered, I would like a solution that generates a multiple buttons types (with or without link) from one single file.
Solution
Using computed properties I'm able to "calculate" the necessary tag, based on the url property of my component. When a url is passed, I know that my button has to link to another page. If there is no url property it should submit something or preform a custom click handler (not in the sample code below).
I've created the returnComponentTag computed property to avoid placing any complex or bulky logic (like my original solution) in my template. This returns either an a or a button tag based on the existence of the url property.
Next, as suggested by ajobi, using the :is attribute I'm able to define the component tag based on the result of my computed property. Below a stripped sample of my final (and working) solution:
<template>
<component :is="returnComponentTag" v-bind:href="url ? url : ''" class="btn" :class="modifier" :id="id">
{{buttonText}}
</component>
</template>
<script>
export default {
name: "Button",
props: {
id: {
type: Number
},
buttonText: {
type: String,
required: true,
default: "Button"
},
modifier: {
type: String,
default: "btn-cta-01"
},
url: {
type: String,
default: ""
}
},
computed: {
returnComponentTag() {
return this.url ? "a" : "button"
}
}
};
</script>
You could extract the wrapping element into a dedicated component.
<template>
<a v-if="link" v-bind:href="url" class="btn" :class="modifier" :id="id" role="button" :disabled="disabled">
<slot></slot>
</a>
<button v-else type="button" class="btn" :class="modifier" :id="id" :disabled="disabled">
<slot></slot>
</button>
</template>
// You would use it like this
<SomeComponent /* your props here */ >
{{buttonText}}
<svg class="icon" v-if="icon" :class="iconModifier">
<use v-bind="{ 'xlink:href':'#sprite-' + icon }"></use>
</svg>
</SomeComponent>
There are multiple ways of doing this. Two examples would be the following based on the point of view:
You are defining two different components (Button or Anchor) and want to use a wrapper to render either one of them.
You could seperate the Wrapper Content into two components so that the wrapper only decides on which of the components to render (either the Button or the Anchor).
The problem with this approach could be you will have doubled code for methods and styling for the button and anchor component.
You are defining the content as a component and use the wrapper to define what to wrap the content in.
See Answer of https://stackoverflow.com/a/60052780/11930769
It would be great to know, why you would want to achive this. Maybe there are better solutions for your usecase. Cheers!

All dynamically generated components are changing to the same value in VUEJS

we are building a chat application in Vuejs, now every chat message is component in our application, now whenever we are changing the value of one chat message, the value of all chat messages changes
What is happening
source code
App Component
const App = new Vue({
el: '#myApp',
data: {
children: [
MyCmp
],
m1: '',
m2: '',
m3: 'Hello world',
m4: 'How are you'
},
methods: {
sendMessage (event) {
if(event.key == "Enter") {
this.m2= this.m3;
this.children.push(MyCmp);
}
},
}
});
component code
let MyCmp = {
props: ['myMessage'],
template: `
<li class="self">
<div class="avatar"><img src="" draggable="false"/></div>
<div class="msg">
<p>{{ myMessage }}</p>
</div>
</li>
`
};
** view where components are generating **
<ol class="chat">
<template v-for="(child, index) in children">
<component :is="child" :key="child.name" v-bind="{myMessage: m3}"></component>
</template>
</ol>
Even though you are creating new components by pushing them into the children array, they are still getting bound to the same data via the line v-bind="{myMessage: m3}". Whenever you change m3, it will be passed down to all the child components and they will all render the same message.
This is an odd way of creating custom components since you could easily do so using the templating syntax or render function provided by Vue.
Solution 1 - Recommended
Change your approach - push message strings instead of card component definitions into children and use MyCmp inside v-for to render the message cards.
So the new template could be refactored as :-
<ol class="chat">
<template v-for="(message, index) in children">
<my-cmp :key="index" :my-message="message"></my-cmp>
</template>
</ol>
And inside App component, you can replace this.children.push(MyCmp) with this.children.push(messageVariable); where messageVariable contains the message that you receive from the input box.
Why is the recommended? This is a standard approach where component lists are rendered based on an array of data. It will be easier to scale and maintain.
Solution 2
You can use the v-once directive to bind the message one-time as static text. The binding won't be updated even if m3 changes on the parent.
Then MyCmp template will become :-
<li class="self">
<div class="avatar"><img src="" draggable="false"/></div>
<div class="msg">
<p v-once>{{ myMessage }}</p>
</div>
</li>
You bind myMessage of all your components instances with one variable m3. So, when m3 is changed myMessage in all instances changes respectively. Use another variable (e.g. msg) for rendering the message and then use myMessage property only for the initialisation of msg, like this:
let MyCmp = {
props: ['myMessage'],
data: function () {
return {
msg: this.myMessage
}
},
template: `
<li class="self">
<div class="avatar"><img src="" draggable="false"/></div>
<div class="msg">
<p>{{ msg }}</p>
</div>
</li>
`
};