listen to events from dynamic vue components - vue.js

How would you listen to an event emitted by a dynamically created component instance?
In the example, the top component is added in the DOM, while the second is dynamically created in javascript.
Vue.component("button-counter", {
data: function() {
return {
count: this.initial_count
}
},
props: ['initial_count'],
methods: {
add: function() {
this.count++
this.$emit('myevent', this.count)
}
},
template: '<button v-on:click="add">You clicked me {{ count }} times.</button>'
})
let app = new Vue({
el: "#app",
data() {
return {
initial_count: 10,
}
},
mounted: function() {
let initial_count = this.initial_count
let ButtonCounterComponentClass = Vue.extend({
data: function() {
return {}
},
render(h) {
return h("button-counter", {
props: {
initial_count: initial_count
}
})
}
})
let button_counter_instance = new ButtonCounterComponentClass()
button_counter_instance.$mount()
button_counter_instance.$on('myevent', function(count) {
console.log('listened!')
this.say(count)
})
this.$refs.container.appendChild(button_counter_instance.$el)
},
methods: {
say: function(message) {
alert(message)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<button-counter initial_count=20 v-on:myevent="say"></button-counter>
<div ref='container'></div>
</div>

If I've understood what you want then you just need to listen for the event on the inner component and pass it on.
I've used arrow functions in a couple of places to avoid problems with this bindings. Otherwise I've tried to leave your code unchanged as much as possible. Changes marked with ****.
Vue.component("button-counter", {
data: function() {
return {
count: this.initial_count
}
},
props: ['initial_count'],
methods: {
add: function() {
this.count++
this.$emit('myevent', this.count)
}
},
template: '<button v-on:click="add">You clicked me {{ count }} times.</button>'
})
let app = new Vue({
el: "#app",
data() {
return {
initial_count: 10,
}
},
mounted: function() {
let initial_count = this.initial_count
let ButtonCounterComponentClass = Vue.extend({
data: function() {
return {}
},
render(h) {
return h("button-counter", {
props: {
initial_count: initial_count
},
// **** Added this ****
on: {
myevent: count => {
this.$emit('myevent', count);
}
}
// ****
})
}
})
let button_counter_instance = new ButtonCounterComponentClass()
button_counter_instance.$mount()
// **** Changed the next line ****
button_counter_instance.$on('myevent', count => {
console.log('listened!')
this.say(count)
})
this.$refs.container.appendChild(button_counter_instance.$el)
},
methods: {
say: function(message) {
alert(message)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<button-counter initial_count=20 v-on:myevent="say"></button-counter>
<div ref='container'></div>
</div>
It's important to understand that button_counter_instance is not an instance of your button-counter component. You've wrapped it in another component, albeit a component that doesn't add any extra DOM nodes. So listening on the wrapper component is not the same as listening on button-counter.
Docs for what you can pass to h: https://v2.vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth

Related

I need fresh eyes to fix VUE props not working

This is a silly task in VUE.JS... but I'm missing it.
I have a sub parent component having:
<teamPlayers :teamId="team.id_team"></teamPlayers>
The value teamId is sent to the child component and it works: I can see the value in child template <h2>{{teamId}}</h2> properly.
But in same child component I got undefined inside the methods using this.teamId.
Here the whole child code:
export default {
props: ['teamId'],
methods: {
getJokess: function () {
console.log(this.teamId);
},
},
created() {
this.getJokess();
}
}
The console should return the correct value but it returns undefined instead of the {{teamId}} is render perfectly.
All that I can think of is that teams may not be declared in your data() function. If it isn't it won't be reactive. Consider the example below:
const teamPlayers = {
props: ["teamId"],
methods: {
getJokess() {
console.log(this.teamId);
}
},
created() {
this.getJokess();
},
template: "<h2>{{teamId}}</h2>"
};
const app = new Vue({
el: "#app",
components: {
"team-players": teamPlayers
},
data() {
return {
teams: []
};
},
mounted() {
setTimeout(() => {
this.teams = [{
id_team: "fizz"
},
{
id_team: "buzz"
},
{
id_team: "foo"
},
{
id_team: "bar"
}
]
}, 1000);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="team of teams">
<team-players :team-id="team"></team-players>
</div>
</div>

Updating a prop inside a child component so it updates on the parent container too

So I have a simple template like so:
<resume-index>
<div v-for="resume in resumes">
<resume-update inline-template :resume.sync="resume" v-cloak>
//...my forms etc
<resume-update>
</div>
<resume-index>
Now, inside the resume-updatecomponent I am trying to update the prop on the inside so on the outside it doesn't get overwritten, my code is like so;
import Multiselect from "vue-multiselect";
import __ from 'lodash';
export default {
name: 'resume-update',
props: ['resume'],
components: {
Multiselect
},
data: () => ({
form: {
name: '',
level: '',
salary: '',
experience: '',
education: [],
employment: []
},
submitted: {
form: false,
destroy: false,
restore: false
},
errors: []
}),
methods: {
update(e) {
this.submitted.form = true;
axios.put(e.target.action, this.form).then(response => {
this.resume = response.data.data
this.submitted.form = false;
}).catch(error => {
if (error.response) {
this.errors = error.response.data.errors;
}
this.submitted.form = false;
});
},
destroy() {
this.submitted.destroy = true;
axios.delete(this.resume.routes.destroy).then(response => {
this.resume = response.data.data;
this.submitted.destroy = false;
}).catch(error => {
this.submitted.destroy = false;
})
},
restore() {
this.submitted.restore = true;
axios.post(this.resume.routes.restore).then(response => {
this.resume = response.data.data;
this.submitted.restore = false;
}).catch(error => {
this.submitted.restore = false;
})
},
reset() {
for (const prop of Object.getOwnPropertyNames(this.form)) {
delete this.form[prop];
}
}
},
watch: {
resume: function() {
this.form = this.resume;
},
},
created() {
this.form = __.cloneDeep(this.resume);
}
}
When I submit the form and update the this.resume I get the following:
[Vue warn]: Avoid mutating a prop directly since the value will be
overwritten whenever the parent component re-renders. Instead, use a
data or computed property based on the prop's value. Prop being
mutated: "resume"
I have tried adding computed to my file, but that didn't seem to work:
computed: {
resume: function() {
return this.resume
}
}
So, how can I go about updating the prop?
One solution:
simulate v-model
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So the steps:
create one prop = value which you'd like to sync to parent component
inside the child component, create one data porperty=internalValue, then uses Watcher to sync latest prop=value to data property=intervalValue
if intervalValue change, emit one input event to notice parent component
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('container', {
template: `<div>
<p><button #click="changeData()">{{value}}</button></p>
</div>`,
data() {
return {
internalValue: ''
}
},
props: ['value'],
mounted: function () {
this.internalValue = this.value
},
watch: {
value: function (newVal) {
this.internalValue = newVal
}
},
methods: {
changeData: function () {
this.internalValue += '#'
this.$emit('input', this.internalValue)
}
}
})
new Vue({
el: '#app',
data () {
return {
items: ['a', 'b', 'c']
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
<p>{{items}}
<container v-for="(item, index) in items" :key="index" v-model="items[index]">
</container>
</div>
</div>
or use other prop name instead of value (below demo use prop name=item):
Also you can use other event name instead of event name=input.
other steps are similar, but you have to $on the event then implement you own handler like below demo.
Vue.config.productionTip = false
Vue.component('container', {
template: `<div>
<p><button #click="changeData()">{{item}}</button></p>
</div>`,
data() {
return {
internalValue: ''
}
},
props: ['item'],
mounted: function () {
this.internalValue = this.item
},
watch: {
item: function (newVal) {
this.internalValue = newVal
}
},
methods: {
changeData: function () {
this.internalValue += '#'
this.$emit('input', this.internalValue)
this.$emit('test-input', this.internalValue)
}
}
})
new Vue({
el: '#app',
data () {
return {
items: ['a', 'b', 'c']
}
},
methods: {
syncChanged: function (target, index, newData) {
this.$set(target, index, newData)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
Event Name=input
<p>{{items}}</p>
<container v-for="(item, index) in items" :key="index" :item="item" #input="syncChanged(items, index,$event)">
</container>
</div>
<hr> Event Name=test-input
<container v-for="(item, index) in items" :key="index" :item="item" #test-input="syncChanged(items, index,$event)">
</container>
</div>
I usually use vuex to manage variables that I will be using in multiple components and like the error says, load them in the various components using the computed properties. Then use the mutations property of the store object to handle changes
In component files
computed: {
newProfile: {
get() {
return this.$store.state.newProfile;
},
set(value) {
this.$store.commit('updateNewProfile', value);
}
},
In the vuex store
state: {
newProfile: {
Name: '',
Website: '',
LoginId: -1,
AccountId: ''
}
},
mutations: {
updateNewProfile(state, profile) {
state.newProfile = profile;
}
}

VueJs Nested props coming through undefined

I am trying to access an array which is part of a prop (event) passed into a component, but when in created() or mounted() the array part of the event prop (the rest is fine) comes through as undefined.
As can be seen below, when I inspect the props in the vue chrome plugin, the registration_fields are there.
I can add a watcher to the event prop and can access the registration_fields that way, but this seems very awkward to have to do this to access already passed in data.
This is from the Chrome vue inspector:
event:Object
address1_field:"Some Address 1"
address2_field:"Some Address 2"
approved:true
registration_fields:Array[1]
This is what part of my vue file looks like:
export default {
props: ['event'],
data() {
return {
regFields: []
}
},
created() {
this.regFields = this.event.registration_fields // Undefined here!
},
watch: {
event() {
this.regFields = this.event.registration_fields //Can access it here
});
}
}
}
I am using Vue 2.4.4
This is how the component is called:
<template>
<tickets v-if="event" :event="event"></tickets>
</template>
<script>
import tickets from './main_booking/tickets.vue'
export default {
created() {
var self = this;
this.$http.get('events/123').then(response => {
self.event = response.data
}).catch(e => {
alert('Error here!');
})
},
data: function () {
return {event: {}}
},
components: {
tickets: tickets
}
}
</script>
Thank you
It actually works fine without the watcher.
new Vue({
el: '#app',
data: {
event: undefined
},
components: {
subC: {
props: ['event'],
data() {
return {
regFields: []
}
},
created() {
this.regFields = this.event.registration_fields // Undefined here!
}
}
},
mounted() {
setTimeout(() => {
this.event = {
registration_fields: [1, 3]
};
}, 800);
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<sub-c v-if="event" :event="event" inline-template>
<div>
{{regFields}}
</div>
</sub-c>
</div>
If, as Belmin Bedak suggests in the comment below, event is populated asynchronously, it comes in as undefined because it's undefined. In that case, you need a watcher, or, somewhat more elegantly, use a computed:
new Vue({
el: '#app',
data: {
event: {}
},
components: {
subC: {
props: ['event'],
computed: {
regFields() {
return this.event.registration_fields;
}
}
}
},
// delay proper population
mounted() {
setTimeout(() => { this.event = {registration_fields: [1,2,3]}; }, 800);
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<sub-c :event="event" inline-template>
<div>
{{regFields}}
</div>
</sub-c>
</div>

VueJS Simple Alert Appear

Can someone help me make this simple alert work? What I wanted was if I click the trigger button, the text would appear but then the component should do its part.
I'm new and learning VueJS and now is in the components part but I haven't fully grasped it yet.
Here's the link: JSBIN
Snippet of the JS Script
Vue.component('alert', {
template: '#alert',
props: {
errors:false
},
data: function() {
return {
message:""
}
},
methods: {
appear: function (status) {
if(status=="yes") {
errors = true;
message = "Appeared";
}
}
}
});
var myapp = new Vue({
components: 'alert',
el: '#app',
data: {
},
methods: {
trigger: function() {
this.$alert.appear("yes");
}
}
});
To make your code work the way I think you are trying to get it to work I made a few changes.
Template
<div id=app>
<button #click="trigger">Trigger</button>
<alert ref="alert"></alert>
</div>
Code
Vue.component('alert', {
template: '#alert',
data: function() {
return {
message:"",
errors: false
}
},
methods: {
appear: function (status) {
if(status=="yes") {
this.errors = true;
this.message = "Appeared";
}
}
}
});
var myapp = new Vue({
components: 'alert',
el: '#app',
data: {
},
methods: {
trigger: function() {
this.$refs.alert.appear("yes");
}
}
})
Here is the updated bin.
This is a pretty atypical way to do this kind of thing though. Here is an example of a more idiomatic alert.
Vue.component('alert', {
props:["message","show"],
template: '#alert',
});
var myapp = new Vue({
el: '#app',
data: {
errors: false,
errorMessage: null
},
methods: {
trigger: function() {
this.errors = true
this.errorMessage = "Whoops!"
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id=app>
<button #click="trigger">Trigger</button>
<alert :message="errorMessage" :show="errors"></alert>
</div>
<template id="alert">
<div v-show="show">
<div>{{ message }}</div>
</div>
</template>
In this second example, the information to show in the alert is passed down to the alert component via properties and the alert is also triggered via a property. This is how you would typically do it in Vue.

Where I should place handler of emit?

I have child component and want to pass some data to it's parent.
My child component looks like:
// <button #click="sendClick($event)">Send</button>
// ...
data: function (){
return {
mycode: ""
}
},
methods: {
sendClick(e)
{
bus.$emit('change', this.mycode);
}
}
My parent component looks:
var app = new Vue({
el: '#app',
data: {
currentView: 'past-form',
mycode: ''
},
methods:
{
changeView()
{
this.currentView = 'past-form'
console.log(this.mycode);
},
},
created()
{
bus.$on('change', function(mycode){
this.mycode = mycode;
});
}
})
I haven't found a better place for placing bus.$on (bus is declared globally) than in created(), but the docs state that created() is for stuff that should be initialized after the page is loaded. The created() block works; I checked it by placing in it console.log(this.mycode), but should I move emit handler somewhere else?
It's look like my code does not execute mycode: '', because console.log(this.mycode); does not print anything.
As I mentioned in the comment, if your component is a direct child of your Vue, then there is no need for a bus.
That said, the created handler is fine for adding your bus event handler.
I expect the issue you have is a this issue. Try changing your handler to
bus.$on('change', mycode => this.mycode = mycode)
See How to access the correct this inside a callback?
Here is an example.
console.clear()
const bus = new Vue()
Vue.component("child", {
template: `<button #click="sendClick($event)">Send</button>`,
data: function() {
return {
mycode: "something"
}
},
methods: {
sendClick(e) {
bus.$emit('change', this.mycode);
}
}
})
var app = new Vue({
el: '#app',
data: {
currentView: 'past-form',
mycode: ''
},
methods: {
changeView() {
this.currentView = 'past-form'
console.log(this.mycode);
},
},
created() {
bus.$on('change', mycode => {
this.mycode = mycode
this.changeView()
})
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.js"></script>
<div id="app">
<child></child>
Parent mycode: {{mycode}}
</div>