Vue 2 and child component events with multiple components - events

I've read this question / answer, and whilst that works when the parent contains a specific component, in my scenario the parent contains this:
<component :is="currentView"></component>
and the value of currentView determines which component is 'loaded' at any particular time. So, using v-on:event="handler" in the component tag here means all child components must use the same event name :(. Is it possible in the created() function of the parent to set up specific handlers which will be called regardless of which component is currently 'in view', and when they each might send different events? For example, I have a login component which sends a 'login' event, and a data view component which sends an 'update' event. In the parent, I want something like this:
this.$on('login', doLogin)
this.$on('update', doUpdate)
However, that's listening to events from itself, not its children. I also tried giving the component a ref:
<component ref="main" :is="currentView"></component>
and using this.$refs.main.$on('login', doLogin), but the $refs object isn't created until the initial render, so that can't go in the mounted() method (too early), and in the updated() method it will be repeatedly called which I'm sure isn't a good idea...

you could set a global event and then pass the name of the action as a part of the payload, i.e
const Login = {
template: `<h1>Login</h1>`,
mounted() {
this.$emit('global-event', {
action: 'login',
data: {
user: 'foo',
},
})
},
}
const Update = {
template: `<h1>Update</h1>`,
mounted() {
this.$emit('global-event', {
action: 'update',
data: {
user: 'foo bar',
},
})
},
}
new Vue({
el: '#app',
data: {
called: {
update: 0,
login: 0,
},
currentView: 'login',
},
components: {
Login,
Update,
},
methods: {
// within doSomething you would process the various events based on their payload.action
doSomething (payload) {
this.called[payload.action]++
},
toggleView () {
this.currentView = this.currentView === 'login' ? 'update' : 'login'
},
},
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<component #global-event="doSomething" :is="currentView"></component>
<button #click="toggleView">toggle view</button>
<pre>{{ called }}</pre>
</div>

Gah! After spending ages searching, finding nothing, then posting here, I of course immediately stumble on the answer.
But this only applies if you have control over the child components (I'm building all of them). In the child, you simply:
this.$parent.$emit('login', {some: 'data'})
And then set up the event listeners with this.$on('login', doLogin) in the parent as normal in the created() method.
I'm sure there will be a scenario where I'm using a third party component that simply calls this.$emit(), and at the moment, I can't see any alternative to v-on:eventName="handler" in the parent.

Related

Append components inside other component in Vue JS

I'm trying to make a simple "Load More" function for posts using Vue JS but when I try to append new posts, the previous ones are removed.
This is my PostWallComponent, which is supposed to hold all posts (<post-item-component>).
I fetch first 4 posts from the DB, store them in this.posts and then I send them using the v-for loop to <post-item-component>.
Then when someone clicks on the "More" button I call getPosts() function where I fetch another 4 posts from the DB. Here comes my problem - I store these new posts inside this.posts and I try to append them to the post container. They do append but the previous 4 get deleted from the container.
I think I know what is wrong - at line this.posts = response.data I replace old posts with new ones but I don't know how to append new ones without removing old ones. I tried to push() new posts to the array but that turned into a big mess (repetitive posts in the container).
<template>
<div class="container">
<div class="post_container">
<post-item-component v-for="post in this.posts"
v-bind:cuid="cuid"
v-bind:auid="auid"
v-bind:post="post"
v-bind:key="post.id">
</post-item-component>
<button type="button" #click="getPosts">More</button>
</div>
</div>
</template>
<script>
import PostItemComponent from "./PostItemComponent";
export default {
props: ['init_place', 'init_type', 'current_user_id', 'active_user'],
components: {
PostItemComponent
},
data() {
return {
place: this.init_place,
type: this.init_type,
cuid: this.current_user_id,
auid: this.active_user,
limit: 4,
offset: 0,
posts: [],
};
},
mounted() {
console.log('Component mounted.');
this.getPosts();
},
methods: {
getPosts() {
console.log('post');
axios.get('/p/fetch', {
params: {
place: this.place,
type: this.type,
cuid: this.cuid,
auid: this.auid,
offset: this.offset,
limit: this.limit,
}
})
.then((response) => {
console.log(response);
this.posts = response.data;
this.offset = this.limit;
this.limit += 4;
})
.catch(function (error) {
//currentObj.output = error;
});
}
}
}
</script>
In case someone wonders:
cuid is current user id = ID of user whose profile I opened
auid is active user ID = logged in user ID
<post-item-component> is just couple of divs displaying post header, body etc.
you could also use this.posts = this.posts.concat(response.data)
the problem is that the Array.push() method does not work with vue reactivity. For that you need to replace the whole array. As one proposed solution, you could use the spread operator to achieve this as so:
this.posts = [...this.posts, ...response.data];
This is replacing the whole array with a new array that is combining the old items with the fetched ones by spreading each of the array elements into the new array.
You can see an example here:
codesandbox example

create dynamic events for a vue component

I have a problem with vue routes. I am using laravel 6 with vue#2.6.10
I want to create the actions button in the header dynamically (the actions are different which depends on the component). This AppHeader component is on every component and on the current component I want to create in the header the events for the current component.
For example the component CategoryDetails I want to have two actions in the header (save and exit).
The route foe the category is this:
path: '/',
redirect: 'dashboard',
component: DashboardLayout,
children: [
{
path: '/categories',
component: Category,
name: 'category',
meta: {
requiresAuth: true
}
},
{
path: '/categories/:CategoryID',
component: CategoryDetails,
name: 'category-details',
props: true,
meta: {
requiresAuth: true
}
},
]
In the component CategoryDetails:
<template>
<div>
<app-header :actions="actions"></app-header>
// other code
</div>
</template>
<script>
import AppHeader from "../../layout/AppHeader";
export default {
name: "CategoryDetails",
components: {AppHeader},
data() {
actions: [{label: 'Save', event: 'category.save'}, {label: 'Exit', event: 'category.exit'}],
},
mounted() {
const vm = this;
Event.$on('category.save', function(){
alert('Save Category!');
});
Event.$on('category.exit', function(){
vm.$router.push({name: 'category'});
});
}
}
</script>
I crated the action object which tells the header component what events to emit and listen to them in this component.
In the AppHeader component:
<template>
<div v-if="typeof(actions) !== 'undefined'" class="col-lg-6 col-sm-5 text-right">
{{ btn.label }}
</div>
</template>
<script>
export default {
name: "AppHeader",
props: [
'actions'
],
methods: {
onActionClick(event) {
Event.$emit(event);
}
}
}
</script>
The Event is the "bus event" defined in the app.js
/**
* Global Event Listener
*/
window.Event = new Vue();
So... let`s tested :)
I am in the category component. Click on the category details ... the actions are in the header (save and exit). Click on exit...we area pushed back to the category component... click again to go in the category details and click save ... the alert appears TWICE.
Exit and enter again ... the alert "Save Category!" appears 3 times.....and so on ...
Why ?
I think the issue is not with your routes. I don't know but try testing with your event locally (not globally) in the component of interest. There may be duplicate action (CategoryDetails).
According to this post: https://forum.vuejs.org/t/component-not-destroying/25008/10
I have to destroy the "zombie effect"
destroyed() {
Event.$off('category.save');
Event.$off('category.exit');
}

Vue.JS not update data into nested Component

I'm working with 3 VUE nested components (main, parent and child) and I'm getting trouble passing data.
The main component useget a simple API data based on input request: the result is used to get other info in other component.
For example first API return the regione "DE", the first component is populated then try to get the "recipes" from region "DE" but something goes wrong: The debug comments in console are in bad order and the variable used results empty in the second request (step3):
app.js:2878 Step_1: DE
app.js:3114 Step_3: 0
app.js:2890 Step_2: DE
This is the parent (included in main component) code:
parent template:
<template>
<div>
<recipes :region="region"/>
</div>
</template>
parent code:
data: function () {
return {
region: null,
}
},
beforeRouteEnter(to, from, next) {
getData(to.params.e_title, (err, data) => {
console.log("Step_1: "+data.region); // return Step_1: DE
// here I ned to update the region value to "DE"
next(vm => vm.setRegionData(err, data));
});
},
methods: {
setRegionData(err, data) {
if (err) {
this.error = err.toString();
} else {
console.log("Step_2: " + data.region); // return DE
this.region = data.region;
}
}
},
child template:
<template>
<div v-if="recipes" class="content">
<div class="row">
<recipe-comp v-for="(recipe, index) in recipes" :key="index" :title="recipe.title" :vote="recipe.votes">
</recipe-comp>
</div>
</div>
</template>
child code:
props: ['region'],
....
beforeMount () {
console.log("Step_3 "+this.region); // Return null!!
this.fetchData()
},
The issue should be into parent beforeRouteEnter hook I think.
Important debug notes:
1) It looks like the child code works properly because if I replace the default value in parent data to 'IT' instead of null the child component returns the correct recipes from second API request. This confirms the default data is updated too late and not when it got results from first API request.
data: function () {
return {
region: 'IT',
}
},
2) If I use {{region}} in child template it shows the correct (and updated) data: 'DE'!
I need fresh eyes to fix it. Can you help me?
Instead of using the beforeMount hook inside of the child component, you should be able to accomplish this using the watch property. I believe this is happening because the beforeMount hook is fired before the parent is able to set that property.
More on the Vue lifecycle can be found here
More on the beforeMount lifecycle hook can be found here
In short, you can try changing this:
props: ['region'],
....
beforeMount () {
console.log("Step_3 "+this.region); // Return null!!
this.fetchData()
},
To something like this:
props: ['region'],
....
watch: {
region() {
console.log("Step_3 "+this.region); // Return null!!
this.fetchData()
}
},
Cheers!!

How to get a variable to bind from xhr call response in VueJS?

Ok I'm a beginner at VueJS and I'm just trying to do a simple XHR call and bind the json data response to my variable...
I have a component App.vue and this part of the template I want to show the results of the json. bpi is the name of the variable
<div id="simulationPoints">
<h2 className="logTitle">Full Log:</h2>
{{ bpi }}
</div>
then my script
export default {
name: 'App',
data: () => ({
bpi: []
}),
mounted: () => {
axios.get(`https://api.coindesk.com/v1/bpi/historical/close.jsonp?start=2011-01-01&end=2018-02-01`)
.then(response => {
this.bpi = response.data.bpi
})
.catch(e => {
this.errors.push(e)
})
}
}
This doesn't seem to work. I'm using Axiom to fetch the data and assign the response, and this is how all the examples I found online did it, but the array object I have is still empty and it doesn't render on the page. I don't know whats the issue here? A Vue expert please help :)
There are sorts of problem in your code.
First, don't use arrow function on options property or callback since arrow functions are bound to the parent context, this will not be the Vue instance as you’d expect.
Second, use return statement in your data function.
Third, use created hook for inserting data after instance is created. mounted hook is called for mutation after DOM is rendered.
export default {
name: 'App',
data: function() {
return {
bpi: []
}
},
created() {
axios.get(`https://api.coindesk.com/v1/bpi/historical/close.jsonp?start=2011-01-01&end=2018-02-01`)
.then(response => {
this.bpi = response.data.bpi
})
.catch(e => {
this.errors.push(e)
})
}
}

Call mutation before every action Vuex

Vuex allows us to write plugins that do something whenever a mutation is committed. Is there any way to have a similar functionality, but with actions?
I notice you can "enhance" actions like the vuexfire library does, is this the best way to do so?
My goal is to have some way to track if/how many ajax calls are currently pending, and automatically show some kind of animation based on that number.
Edit: To clarify, I am wondering if there is a way to do this using just Vuex, without pulling into additional libraries.
As of Vuex v2.5 you can call subscribeAction to register a callback function which will be called after each dispatched action in the store.
The callback will receive an action descriptor (object with type and payload properties) as its first argument and the store's state as the second argument.
The documentation for this is on the Vuex API Reference page.
For example:
const store = new Vuex.Store({
plugins: [(store) => {
store.subscribeAction((action, state) => {
console.log("Action Type: ", action.type)
console.log("Action Payload: ", action.payload)
console.log("Current State: ", state)
})
}],
state: {
foo: 1
},
mutations: {
INCREASE_FOO(state) {
state.foo++;
},
},
actions: {
increaseFoo({ commit }) {
commit('INCREASE_FOO');
}
}
});
new Vue({
el: '#app',
store,
methods: {
onClick() {
this.$store.dispatch('increaseFoo');
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.min.js"></script>
<div id="app">
Foo state: {{ $store.state.foo }}
<button #click="onClick">Increase Foo</button>
</div>
Since 3.1.0, subscribeAction can also specify whether the subscribe handler should be called before or after an action dispatch (the default behavior is before):
store.subscribeAction({
before: (action, state) => {
console.log(`before action ${action.type}`)
},
after: (action, state) => {
console.log(`after action ${action.type}`)
}
})
documentation: https://vuex.vuejs.org/api/#subscribeaction

Resources